From 388e25f904e73633f6639ae1b9e1aa1d959607cb Mon Sep 17 00:00:00 2001 From: Em3rs0n Date: Sat, 24 Nov 2018 04:30:38 -0500 Subject: [PATCH] =?UTF-8?q?=E5=88=A9=E7=94=A8Trie=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=B0=86=E6=80=A7=E8=83=BD=E6=8F=90=E5=8D=87=E4=BA=8610?= =?UTF-8?q?=E5=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 借助新的并行计算工具构造了一个Trie结构,在保持APK Parser性能基本不变的情况下,将 自动适配表达式的性能提升了10倍。原先一个表达式求值需要耗费将近1秒,现在只需20ms不到。 旧的Benchmark记录 BM: Parse DexClasses takes 758 ms. BM: Search android.support.v4.app takes 747 ms. BM: Search com.tencent.mm.ui.contact takes 1239 ms. BM: Search com.tencent.mm.plugin.sns.ui takes 1393 ms. BM: Search com.tencent.mm takes 1635 ms. BM: Search com.tencent.mm.ui.conversation takes 761 ms. BM: Search com.tencent.mm.sdk.platformtools takes 860 ms. BM: Search com.tencent.mm.sdk.platformtools takes 669 ms. BM: Search com.tencent.mm.booter.notification takes 561 ms. BM: Search com.tencent.mm.storage takes 871 ms. BM: Search com.tencent.mm.booter.notification.queue takes 913 ms. BM: Search com.tencent.mm.ui takes 518 ms. BM: Search com.tencent.mm.modelsfs takes 626 ms. BM: Search com.tencent.mm.ui.chatting takes 470 ms. BM: Search over classes takes 4414 ms. 数据结构升级后的Benchmark记录 BM: Parse DexClasses takes 779 ms. BM: Search android.support.v4.app takes 42 ms. BM: Search com.tencent.mm takes 167 ms. BM: Search com.tencent.mm.sdk.platformtools takes 5 ms. BM: Search com.tencent.mm.booter.notification takes 1 ms. BM: Search com.tencent.mm.booter.notification.queue takes 0 ms. BM: Search com.tencent.mm.modelsfs takes 0 ms. BM: Search com.tencent.mm.plugin.sns.ui takes 66 ms. BM: Search com.tencent.mm.storage takes 1 ms. BM: Search com.tencent.mm.ui takes 2 ms. BM: Search com.tencent.mm.ui.chatting takes 24 ms. BM: Search com.tencent.mm.ui.contact takes 19 ms. BM: Search com.tencent.mm.ui.conversation takes 14 ms. BM: Search over classes takes 558 ms. --- .../wechatmagician/spellbook/WechatGlobal.kt | 3 +- .../spellbook/parser/ApkFile.kt | 30 ++++++-- .../spellbook/parser/ClassTrie.kt | 71 +++++++++++++++++++ .../spellbook/util/MirrorUtil.kt | 5 +- .../spellbook/util/ReflectionUtil.kt | 32 ++------- 5 files changed, 105 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ClassTrie.kt diff --git a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatGlobal.kt b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatGlobal.kt index 013e7fd..0178ae3 100644 --- a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatGlobal.kt +++ b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatGlobal.kt @@ -6,6 +6,7 @@ import com.gh0u1l5.wechatmagician.spellbook.SpellBook.getApplicationVersion import com.gh0u1l5.wechatmagician.spellbook.base.Version import com.gh0u1l5.wechatmagician.spellbook.base.WaitChannel import com.gh0u1l5.wechatmagician.spellbook.parser.ApkFile +import com.gh0u1l5.wechatmagician.spellbook.parser.ClassTrie import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.tryAsynchronously import de.robv.android.xposed.callbacks.XC_LoadPackage import java.lang.ref.WeakReference @@ -75,7 +76,7 @@ object WechatGlobal { * 这些类名使用的是 JVM 标准中规定的类名格式, 例如 String 的类名会被表示为 "Ljava/lang/String;" * Refer: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 */ - @Volatile var wxClasses: Array? = null + @Volatile var wxClasses: ClassTrie? = null get() { if (!wxUnitTestMode) { initChannel.wait(INIT_TIMEOUT) diff --git a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ApkFile.kt b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ApkFile.kt index 1b347a1..57e5c95 100644 --- a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ApkFile.kt +++ b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ApkFile.kt @@ -1,5 +1,7 @@ package com.gh0u1l5.wechatmagician.spellbook.parser +import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelForEach + import java.io.Closeable import java.io.File import java.nio.ByteBuffer @@ -23,13 +25,27 @@ class ApkFile(apkFile: File) : Closeable { override fun close() = zipFile.close() - val classTypes: Array by lazy { - var ret = emptyArray() - for (i in 1 until 1000) { - val path = if (i == 1) DEX_FILE else String.format(DEX_ADDITIONAL, i) - val entry = zipFile.getEntry(path) ?: break - val buffer = ByteBuffer.wrap(readEntry(entry)) - ret += DexParser(buffer).parseClassTypes() + private fun getDexFilePath(idx: Int) = + if (idx == 1) DEX_FILE else String.format(DEX_ADDITIONAL, idx) + + private fun isDexFileExist(idx: Int): Boolean { + val path = getDexFilePath(idx) + return zipFile.getEntry(path) != null + } + + val classTypes: ClassTrie by lazy { + var last = 2 + while (isDexFileExist(last)) last++ + + val ret = ClassTrie() + (1..last).parallelForEach { idx -> + val path = getDexFilePath(idx) + val entry = zipFile.getEntry(path) + val data = readEntry(entry) + val buffer = ByteBuffer.wrap(data) + DexParser(buffer).parseClassTypes().forEach { type -> + ret += type + } } return@lazy ret } diff --git a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ClassTrie.kt b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ClassTrie.kt new file mode 100644 index 0000000..3fe97e7 --- /dev/null +++ b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ClassTrie.kt @@ -0,0 +1,71 @@ +package com.gh0u1l5.wechatmagician.spellbook.parser + +import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelForEach +import java.util.concurrent.ConcurrentHashMap + +class ClassTrie { + companion object { + private fun convertJVMTypeToClassName(type: String) = + type.substring(1, type.length - 1).replace('/', '.') + } + + private val head: TrieNode = TrieNode() + + operator fun plusAssign(type: String) { + head.add(convertJVMTypeToClassName(type)) + } + + operator fun plusAssign(types: Array) { + types.asList().parallelForEach { this += it } + } + + fun search(packageName: String, depth: Int): List { + return head.search(packageName, depth) + } + + private class TrieNode { + val classes: MutableList = ArrayList(50) + + val children: MutableMap = ConcurrentHashMap() + + fun add(className: String) { + add(className, 0) + } + + private fun add(className: String, pos: Int) { + val delimiterAt = className.indexOf('.', pos) + if (delimiterAt == -1) { + synchronized(this) { + classes.add(className) + } + return + } + val pkg = className.substring(pos, delimiterAt) + if (pkg !in children) { + children[pkg] = TrieNode() + } + children[pkg]!!.add(className, delimiterAt + 1) + } + + fun get(depth: Int = 0): List { + if (depth == 0) { + return classes + } + return children.flatMap { it.value.get(depth - 1) } + } + + fun search(packageName: String, depth: Int): List { + return search(packageName, depth, 0) + } + + private fun search(packageName: String, depth: Int, pos: Int): List { + val delimiterAt = packageName.indexOf('.', pos) + if (delimiterAt == -1) { + val pkg = packageName.substring(pos) + return children[pkg]?.get(depth) ?: emptyList() + } + val pkg = packageName.substring(pos, delimiterAt) + return children[pkg]?.search(packageName, depth, delimiterAt + 1) ?: emptyList() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/MirrorUtil.kt b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/MirrorUtil.kt index 6b7a683..3bca461 100644 --- a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/MirrorUtil.kt +++ b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/MirrorUtil.kt @@ -1,7 +1,6 @@ package com.gh0u1l5.wechatmagician.spellbook.util import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal -import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelMap /** * 封装了一批用于检查“自动适配表达式”的函数 @@ -57,7 +56,7 @@ object MirrorUtil { * WARN: 仅供单元测试使用 */ fun generateReportWithForceEval(instances: List): List> { - return instances.parallelMap { instance -> + return instances.map { instance -> collectFields(instance).map { val value = it.second if (value is Lazy<*>) { @@ -67,6 +66,6 @@ object MirrorUtil { } "${instance::class.java.canonicalName}.${it.first}" to it.second.toString() } - }.flatten() + }.flatten() // 为了 Benchmark 的准确性, 不对结果进行排序 } } \ No newline at end of file diff --git a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/ReflectionUtil.kt b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/ReflectionUtil.kt index 92ccd14..0301cd8 100644 --- a/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/ReflectionUtil.kt +++ b/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/ReflectionUtil.kt @@ -3,6 +3,7 @@ package com.gh0u1l5.wechatmagician.spellbook.util import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal import com.gh0u1l5.wechatmagician.spellbook.base.Classes import com.gh0u1l5.wechatmagician.spellbook.parser.ApkFile +import com.gh0u1l5.wechatmagician.spellbook.parser.ClassTrie import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge.hookMethod import java.lang.reflect.Field @@ -66,40 +67,21 @@ object ReflectionUtil { * 里面其他包拥有的类. * * @param loader 用于取出 [Class] 对象的加载器 - * @param classes 所有已知的类名, 由于 Java 的 [ClassLoader] 对象不支持读取所有类名, 我们必须先通过其他手段 - * 获取一个类名列表, 详情请参见 [ApkFile] 和 [WechatGlobal] + * @param trie 整个 APK 的包结构, 由于 Java 的 [ClassLoader] 对象不支持读取所有类名, 我们必须先通过其他手段 + * 解析 APK 结构, 然后才能检索某个包内的所有类, 详情请参见 [ApkFile] 和 [WechatGlobal] * @param packageName 包名 * @param depth 深度 */ - @JvmStatic fun findClassesFromPackage(loader: ClassLoader, classes: Array, packageName: String, depth: Int = 0): Classes { + @JvmStatic fun findClassesFromPackage(loader: ClassLoader, trie: ClassTrie, packageName: String, depth: Int = 0): Classes { val key = "$depth-$packageName" val cached = classCache[key] if (cached != null) { return cached } - - val packageLength = packageName.count { it == '.' } + 1 - val packageDescriptor = "L${packageName.replace('.', '/')}" - val result = Classes(classes.filter { clazz -> - val currentPackageLength = clazz.count { it == '/' } - if (currentPackageLength < packageLength) { - return@filter false - } - // Check depth - val currentDepth = currentPackageLength - packageLength - if (depth != -1 && depth != currentDepth) { - return@filter false - } - // Check prefix - if (!clazz.startsWith(packageDescriptor)) { - return@filter false - } - return@filter true - }.mapNotNull { - findClassIfExists(it.substring(1, it.length - 1).replace('/', '.'), loader) + val classes = Classes(trie.search(packageName, depth).mapNotNull { name -> + findClassIfExists(name, loader) }) - - return result.also { classCache[key] = result } + return classes.also { classCache[key] = classes } } /**