Repository navigation
优化无障碍服务事件处理性能并补充单元测试 - #108
Merged
Merged
Conversation
Profiling the service (simpleperf on an API 36 emulator, 16 cold app launches) showed 39% of CPU in GC, 21% in the node-tree traversal and ~23% in binder threads deserializing AccessibilityNodeInfo parcels. Service (TouchHelperServiceImpl): - merge widget and keyword matching into a single traversal per event instead of two traversals of the same sub-tree - return early from TYPE_WINDOW_CONTENT_CHANGED while no skip-ad process is running, before any allocation - bound the traversal backlog with a small queue of our own: drop events whose source sub-tree is already queued, evict the oldest sub-tree when the queue is full, and make up for evictions with one rate-limited full-window scan once the queue drains - keep immutable snapshots of widget/position rules and a copy-on-write keyword list so the worker never iterates a collection the UI mutates; make cross-thread fields volatile and clickedWidgets concurrent - fix NPE when node.getParent() is null, tolerate rules imported from JSON with missing fields, shut down the executor and clear pending handler messages on unbind Settings / UI / manifest: - only query PackageManager for the default whitelist on first run - split keywords on whitespace and drop blanks (a blank keyword matched every short label) - whitelist dialog: reuse ResolveInfo.applicationInfo and decode icons lazily while scrolling instead of all up-front on the UI thread - describeAccessibilityNode: StringBuilder instead of String.format - remove largeHeap and the declared but non-existent TouchHelperServiceReceiver Tests: - extract the matching rules into SkipAdRules (pure Java) with JUnit tests, and add Robolectric tests that drive TouchHelperServiceImpl with real AccessibilityEvent / AccessibilityNodeInfo trees (start/stop, clicks, dedupe, back-pressure, eviction, rescan, unbind, node recycling); 31 tests in total
- dedupe by source view now replaces the queued AccessibilityNodeInfo with the newest snapshot instead of dropping it: node text/bounds are cached at fetch time, so a label that changed from "3s" to "跳过" while the worker was busy was scanned with its stale text. The same collapsing now also applies to repeated full-window scans of one window - the drain processes one sub-tree per executor task and re-submits itself, so scheduled position clicks are no longer starved by a continuous stream of content-changed events - the full-window rescan is scheduled (rate limited) when a sub-tree is evicted rather than when the queue happens to drain - widget rules keep priority over keywords inside the single traversal: a keyword hit is only remembered while widget rules are active and is clicked at the end if no rule matched anywhere in the tree, matching the former two-pass order Adds regression tests for each case; 35 unit tests in total.
Owner
Author
|
已按审查意见修复,见提交
35 个单元测试全部通过;模拟器冒烟(8 次冷启动)无异常。 |
Full-window scans (on window-state changes and the rescan after evicted sub-trees) are requested asynchronously, so by the time the root is fetched the active window may be an IME, the launcher or a whitelisted app. IME window-state events deliberately keep the skip-ad process running, so the rescan could traverse the keyboard window and click a suggestion matching a keyword. The root's package is now checked against the same package set that filters content-changed events, and windows of excluded packages are never scanned. Adds tests for the IME window, a stopped process, an evicted sub-tree being covered by the rescan, and a window-state scan hitting another package's window; 39 unit tests in total.
Owner
Author
|
已修复,见提交 补扫和 STATE_CHANGED 的全窗口扫描都改为经 回归测试(4 条新增):输入法弹出触发补扫时不点击键盘窗口;补扫覆盖被淘汰的目标应用子树;流程停止后不再读取活动窗口;STATE_CHANGED 处理时活动窗口已切到其他包则不扫描。另外用您的 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
对无障碍服务的事件处理路径做了架构分析、真机(模拟器)profiling,并针对热点和设计问题做了优化,同时补上了项目此前完全缺失的自动化测试。
架构分析摘要
事件流:系统以 50ms 节流推送
TYPE_WINDOW_STATE_CHANGED/TYPE_WINDOW_CONTENT_CHANGED→onAccessibilityEvent(主线程)→ 单线程执行器对节点树做 BFS,每个getChild()一次 Binder IPC。simpleperf 采样(debug 包,8 个应用冷启动 ×1 轮,60s)显示 baseline 的 CPU 分布:GC 线程 39%、遍历线程 21%(其中
describeAccessibilityNode占 9%)、Binder 反序列化线程约 23%、主线程 10%。发现的设计问题(详见提交内的注释):
event.getSource()子树被 widget / keyword 两个任务各遍历一次,getSource()也调两次skipAdRunning == false时每个 CONTENT_CHANGED 仍做toString()+HashSet.containsclear()+addAll()直接修改服务线程正在遍历的同一个ArrayList/TreeMap实例clickedWidgets、keyWordList、setPackages等跨线程共享但非线程安全 / 非 volatileConcurrentHashMap.newKeySet()+ volatilenode.getParent()可能为 null 直接调用;规则 JSON 缺字段时e.idName.isEmpty()NPEonUnbind未关闭执行器、未清理 Handler 延迟消息Settings每次初始化都同步查 PackageManager 计算默认白名单getApplicationInfoIPCResolveInfo.applicationInfo,图标在列表滚动时按需加载largeHeap=true让常驻进程更易被 LMK 回收;Manifest 声明的.TouchHelperServiceReceiver类不存在describeAccessibilityNode用String.format+ 多次拼接StringBuilderProfiling 与结果
环境:Pixel 9 Pro XL 模拟器(API 36 / arm64),白名单清空,8 个系统应用冷启动 × 2 轮(16 次启动,与「打开 App → 开屏广告」路径一致),统计服务进程
/proc/<pid>/statCPU 时间与 debug 日志计数。debug 包(含诊断日志)
release 包(R8,AOT
-m speed,同一负载)baseline 与优化后的进程 CPU 都在 560–700 ms 区间波动,多轮交错测量差异在噪声内。这个负载下服务每 100 秒只消耗约 0.6 秒 CPU,事件投递与
getSource()IPC 等每事件固定成本两版相同且占主导,所以 release 下 CPU 总量没有可测量的变化;优化收益体现在遍历/IPC 次数、可控的队列深度与延迟,以及上表中的正确性修复。期间否决的一个方案:仅靠「超限丢弃 + 全窗口重扫」做背压,在 release 下反而略慢(全窗口重扫要拉取上百个节点,代价高于省下的小子树)。最终版用去重与淘汰最旧子树减少重扫,全窗口补扫限频 1 次/秒,只在确实丢弃过子树时触发。
审查后修复的三处回归(见第 3 个提交):去重曾保留旧节点快照导致文字更新后漏点;drain 独占执行器导致坐标点击被饿死;关键字命中曾提前结束遍历、抢占了控件规则的优先级。
测试
新增 39 个 JVM 单元测试(
./gradlew :app:testDebugUnitTest,全部通过):SkipAdRulesTest(17):关键字解析、关键字匹配(长度上限、排除自身名称、优先级、空关键字)、控件规则匹配(坐标 / id / 描述 / 文本、JSON 缺字段)。TouchHelperServiceImplTest(22,Robolectric):用真实AccessibilityEvent/AccessibilityNodeInfo树驱动服务:跳过流程的启动 / 停止 / 超时,关键字点击、同一控件只点一次、非目标包忽略、控件规则命中后停止控件匹配,背压(同源去重保留最新快照、同窗口全扫合并、淘汰最旧、全窗口扫描不被淘汰、淘汰后触发补扫、执行器在两次扫描间让出)、控件规则优先于更早出现的关键字、全窗口扫描仅限目标包窗口(输入法/桌面/白名单窗口不扫描),onUnbind关闭执行器,遍历不泄漏AccessibilityNodeInfo。构建配置:JUnit 4.13.2、Robolectric 4.14.1、
android.jetifier.ignorelist=bcprov(Robolectric 依赖的 bcprov 为 Java 21 字节码,Jetifier 无法处理也无需处理)。兼容性说明
🤖 Generated with Claude Code