From cae215cab9de5b63a731793048c23ffc53050ecb Mon Sep 17 00:00:00 2001 From: lisonge Date: Mon, 21 Oct 2024 17:44:06 +0800 Subject: [PATCH] fix: cache node (#752) --- .../li/songe/gkd/service/A11yContext.kt | 90 +++++++++++-------- .../li/songe/gkd/service/A11yService.kt | 1 + .../kotlin/li/songe/gkd/service/NodeExt.kt | 20 ++++- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt index b33cf6a..0e03c1c 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yContext.kt @@ -26,6 +26,17 @@ private fun List.getInt(i: Int = 0) = get(i) as Int private const val MAX_CACHE_SIZE = MAX_DESCENDANTS_SIZE +private val AccessibilityNodeInfo?.notExpiredNode: AccessibilityNodeInfo? + get() { + if (this != null) { + val expiryMillis = if (text == null) 2000L else 1000L + if (isExpired(expiryMillis)) { + return null + } + } + return this + } + class A11yContext( private val disableInterrupt: Boolean = false ) { @@ -35,6 +46,7 @@ class A11yContext( private var parentCache = LruCache(MAX_CACHE_SIZE) var rootCache: AccessibilityNodeInfo? = null + private var lastClearTime = 0L private fun clearNodeCache(t: Long = System.currentTimeMillis()) { if (META.debuggable) { val sizeList = listOf(childCache.size(), parentCache.size(), indexCache.size()) @@ -59,18 +71,13 @@ class A11yContext( } } - private var lastClearTime = 0L private var lastAppChangeTime = appChangeTime - private fun clearNodeCacheIfTimeout() { + fun clearOldAppNodeCache() { if (appChangeTime != lastAppChangeTime) { lastAppChangeTime = appChangeTime clearNodeCache() return } - val t = System.currentTimeMillis() - if (t - lastClearTime > 30_000L) { - clearNodeCache(t) - } } var currentRule: ResolvedRule? = null @@ -84,6 +91,7 @@ class A11yContext( if (interruptInnerKey == interruptKey) return interruptInnerKey = interruptKey val rule = currentRule ?: return + if (!activityRuleFlow.value.activePriority) return if (!activityRuleFlow.value.currentRules.any { it === rule }) return if (rule.isPriority()) return if (META.debuggable) { @@ -99,12 +107,12 @@ class A11yContext( private fun getA11Child(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { guardInterrupt() - return node.getChild(index) + return node.getChild(index)?.apply { setGeneratedTime() } } private fun getA11Parent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { guardInterrupt() - return node.parent + return node.parent?.apply { setGeneratedTime() } } private fun getA11ByText( @@ -112,7 +120,9 @@ class A11yContext( value: String ): List { guardInterrupt() - return node.findAccessibilityNodeInfosByText(value) + return node.findAccessibilityNodeInfosByText(value).apply { + forEach { it.setGeneratedTime() } + } } private fun getA11ById( @@ -120,7 +130,9 @@ class A11yContext( value: String ): List { guardInterrupt() - return node.findAccessibilityNodeInfosByViewId(value) + return node.findAccessibilityNodeInfosByViewId(value).apply { + forEach { it.setGeneratedTime() } + } } private fun getFastQueryNodes( @@ -135,20 +147,45 @@ class A11yContext( } private fun getCacheRoot(node: AccessibilityNodeInfo? = null): AccessibilityNodeInfo? { - if (rootCache == null) { + if (rootCache.notExpiredNode == null) { rootCache = getA11Root() } if (node == rootCache) return null return rootCache } + private fun getCacheParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { + if (getCacheRoot() == node) { + return null + } + parentCache[node].notExpiredNode?.let { return it } + return getA11Parent(node).apply { + if (this != null) { + parentCache[node] = this + } else { + rootCache = node + } + } + } + + private fun getCacheChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { + if (index !in 0 until node.childCount) { + return null + } + return childCache[node to index].notExpiredNode ?: getA11Child(node, index)?.also { child -> + indexCache[child] = index + parentCache[child] = node + childCache[node to index] = child + } + } + private fun getPureIndex(node: AccessibilityNodeInfo): Int? { return indexCache[node] } private fun getCacheIndex(node: AccessibilityNodeInfo): Int { indexCache[node]?.let { return it } - getCacheParent(node)?.let(::getCacheChildren)?.forEachIndexed { index, child -> + getCacheChildren(getCacheParent(node)).forEachIndexed { index, child -> if (child == node) { indexCache[node] = index return index @@ -157,20 +194,6 @@ class A11yContext( return 0 } - private fun getCacheParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { - if (rootCache == node) { - return null - } - parentCache[node]?.let { return it } - return getA11Parent(node).apply { - if (this != null) { - parentCache[node] = this - } else { - rootCache = node - } - } - } - /** * 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知 * @@ -191,18 +214,8 @@ class A11yContext( return depth } - private fun getCacheChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? { - if (index !in 0 until node.childCount) { - return null - } - return childCache[node to index] ?: getA11Child(node, index)?.also { child -> - indexCache[child] = index - parentCache[child] = node - childCache[node to index] = child - } - } - - private fun getCacheChildren(node: AccessibilityNodeInfo): Sequence { + private fun getCacheChildren(node: AccessibilityNodeInfo?): Sequence { + if (node == null) return emptySequence() return sequence { repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index -> val child = getCacheChild(node, index) ?: return@sequence @@ -491,7 +504,6 @@ class A11yContext( rule: ResolvedRule, node: AccessibilityNodeInfo, ): AccessibilityNodeInfo? { - clearNodeCacheIfTimeout() currentRule = rule try { val queryNode = if (rule.matchRoot) { diff --git a/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt index 8657d16..7754490 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/A11yService.kt @@ -255,6 +255,7 @@ private fun A11yService.useMatchRule() { } } var lastNodeUsed = false + a11yContext.clearOldAppNodeCache() for (rule in activityRule.priorityRules) { // 规则数量有可能过多导致耗时过长 if (delayRule != null && delayRule !== rule) continue val statusCode = rule.status diff --git a/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt b/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt index 2107d7b..1a8ed04 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/NodeExt.kt @@ -23,6 +23,8 @@ val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo? // java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed. rootInActiveWindow.apply { a11yContext.rootCache = this + }?.apply { + setGeneratedTime() } // 在主线程调用会阻塞界面导致卡顿 } catch (e: Exception) { @@ -40,7 +42,9 @@ val AccessibilityEvent.safeSource: AccessibilityNodeInfo? } else { try { // 原因未知, 仍然报错 Cannot perform this action on a not sealed instance. - source + source?.apply { + setGeneratedTime() + } } catch (_: Exception) { null } @@ -64,6 +68,20 @@ fun AccessibilityNodeInfo.getVid(): CharSequence? { const val MAX_CHILD_SIZE = 512 const val MAX_DESCENDANTS_SIZE = 4096 +private const val A11Y_NODE_TIME_KEY = "generatedTime" +fun AccessibilityNodeInfo.setGeneratedTime() { + extras.putLong(A11Y_NODE_TIME_KEY, System.currentTimeMillis()) +} + +fun AccessibilityNodeInfo.isExpired(expiryMillis: Long): Boolean { + val generatedTime = extras.getLong(A11Y_NODE_TIME_KEY, -1) + if (generatedTime == -1L) { + setGeneratedTime() + return false + } + return (System.currentTimeMillis() - generatedTime) > expiryMillis +} + private val typeInfo by lazy { initDefaultTypeInfo().globalType } fun Selector.checkSelector(): String? {