Skip to content

Commit

Permalink
将APK解析时间优化到原先的三分之一
Browse files Browse the repository at this point in the history
重写apk parser,移除了大量不必要的计算,将~70M的APK解析时间从1500~1700ms降低到大约500ms。
  • Loading branch information
Em3rs0n authored and Em3rs0n committed Nov 24, 2018
1 parent 6613c8e commit 5bf7804
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 19 deletions.
6 changes: 0 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.dokka-android'

kotlin {
experimental {
coroutines "enable"
}
}

android {
compileSdkVersion 28
defaultConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import com.gh0u1l5.wechatmagician.spellbook.base.Version
import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorClasses
import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorFields
import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorMethods
import com.gh0u1l5.wechatmagician.spellbook.parser.ApkFile
import com.gh0u1l5.wechatmagician.spellbook.util.FileUtil
import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil
import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil
import dalvik.system.PathClassLoader
import net.dongliu.apk.parser.ApkFile
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.lang.ClassLoader.getSystemClassLoader
import kotlin.system.measureTimeMillis

@ExperimentalUnsignedTypes
@RunWith(AndroidJUnit4::class)
class MirrorUnitTest {
companion object {
Expand All @@ -34,8 +36,13 @@ class MirrorUnitTest {
}

private fun verifyPackage(apkPath: String) {
val cacheDir = context!!.cacheDir
// Parse the version of the apk
val regex = Regex("wechat-v(.*)\\.apk")
val match = regex.find(apkPath) ?: throw Exception("Unexpected path format")
val version = match.groupValues[1]

// Store APK file to cache directory.
val cacheDir = context!!.cacheDir
val apkFile = File(cacheDir, apkPath)
try {
javaClass.classLoader!!.getResourceAsStream(apkPath).use {
Expand All @@ -46,25 +53,36 @@ class MirrorUnitTest {
return // ignore if the apk isn't accessible
}

// Ensure the apk is presented, and start the test
assertTrue(apkFile.exists())
ApkFile(apkFile).use {
// Benchmark the APK parser
val timeParseDex = measureTimeMillis { it.classTypes }
Log.d("MirrorUnitTest", "Benchmark: Parse DexClasses takes $timeParseDex ms.")

// Initialize WechatGlobal
WechatGlobal.wxUnitTestMode = true
WechatGlobal.wxVersion = Version(it.apkMeta.versionName)
WechatGlobal.wxPackageName = it.apkMeta.packageName
WechatGlobal.wxVersion = Version(version)
WechatGlobal.wxPackageName = "com.tencent.mm"
WechatGlobal.wxLoader = PathClassLoader(apkFile.absolutePath, getSystemClassLoader())
WechatGlobal.wxClasses = it.dexClasses.map { clazz ->
ReflectionUtil.ClassName(clazz.classType)
}
WechatGlobal.wxClasses = it.classTypes

// Clear cached lazy evaluations
val objects = MirrorClasses + MirrorMethods + MirrorFields
ReflectionUtil.clearClassCache()
ReflectionUtil.clearMethodCache()
objects.forEach { instance ->
MirrorUtil.clearUnitTestLazyFields(instance)
}

MirrorUtil.generateReportWithForceEval(objects).forEach {
Log.d("MirrorUnitTest", "Verified ${it.first} -> ${it.second}")
// Test each lazy evaluation and generate result.
var result: List<Pair<String, String>>? = null
val timeSearch = measureTimeMillis {
result = MirrorUtil.generateReportWithForceEval(objects)
}
Log.d("MirrorUnitTest", "Benchmark: Search over classes takes $timeSearch ms.")
result?.forEach { entry ->
Log.d("MirrorUnitTest", "Verified: ${entry.first} -> ${entry.second}")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import android.widget.BaseAdapter
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.util.BasicUtil.tryAsynchronously
import de.robv.android.xposed.callbacks.XC_LoadPackage
import net.dongliu.apk.parser.ApkFile
import java.lang.ref.WeakReference

/**
Expand Down Expand Up @@ -38,7 +38,7 @@ object WechatGlobal {
*
* Example: "Ljava/lang/String;"
*/
@Volatile var wxClasses: List<String>? = null
@Volatile var wxClasses: Array<String>? = null

/**
* A flag indicating whether the codes are running under unit test mode.
Expand Down Expand Up @@ -114,7 +114,7 @@ object WechatGlobal {
wxLoader = lpparam.classLoader

ApkFile(lpparam.appInfo.sourceDir).use {
wxClasses = it.dexClasses.map { it.classType }
wxClasses = it.classTypes
}
} finally {
initializeChannel.done()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.gh0u1l5.wechatmagician.spellbook.parser

import java.io.Closeable
import java.io.File
import java.nio.ByteBuffer
import java.util.zip.ZipEntry
import java.util.zip.ZipFile

@ExperimentalUnsignedTypes
class ApkFile(apkFile: File) : Closeable {
companion object {
const val DEX_FILE = "classes.dex"
const val DEX_ADDITIONAL = "classes%d.dex"
}

constructor(path: String) : this(File(path))

private val zipFile: ZipFile = ZipFile(apkFile)

private fun readEntry(entry: ZipEntry): ByteArray =
zipFile.getInputStream(entry).use { it.readBytes() }

override fun close() =
zipFile.close()

val classTypes: Array<String> by lazy {
var ret = emptyArray<String>()
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()
}
return@lazy ret
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.gh0u1l5.wechatmagician.spellbook.parser

@ExperimentalUnsignedTypes
class DexHeader {
var version: Int = 0

var checksum: UInt = 0u

var signature: ByteArray = ByteArray(kSHA1DigestLen)

var fileSize: UInt = 0u

var headerSize: UInt = 0u

var endianTag: UInt = 0u

var linkSize: UInt = 0u

var linkOff: UInt = 0u

var mapOff: UInt = 0u

var stringIdsSize: Int = 0

var stringIdsOff: UInt = 0u

var typeIdsSize: Int = 0

var typeIdsOff: UInt = 0u

var protoIdsSize: Int = 0

var protoIdsOff: UInt = 0u

var fieldIdsSize: Int = 0

var fieldIdsOff: UInt = 0u

var methodIdsSize: Int = 0

var methodIdsOff: UInt = 0u

var classDefsSize: Int = 0

var classDefsOff: UInt = 0u

var dataSize: Int = 0

var dataOff: UInt = 0u

companion object {
const val kSHA1DigestLen = 20
const val kSHA1DigestOutputLen = kSHA1DigestLen * 2 + 1
}
}
Loading

0 comments on commit 5bf7804

Please sign in to comment.