手把手教你通过 AGP + ASM 实现 Android 应用插桩
首先要了解一下 AGP
和 ASM
,AGP
的全称是 Android Gradle Plugin
,这是 Google
为 apk
和 aar
打包在 gradle
平台上开发的一款插件,简单来说你通过 Android Studio
打出的 apk
和 aar
包都是由它完成的,AGP
还为其他的插件提供了 transform
接口来实现 JVM
字节码或者其他资源的处理;ASM
是处理 JVM
字节码知名的库,它可以很简单的读取和修改 JVM
字节码。
在开始之前我们有必要了解一下我们的插桩是工作在打包的哪一步骤中,只从 Java
相关的代码来看,打包过程中 AIDL
和 Java
/Kotlin
的注解处理器都会生成 Java
/Kotlin
源码,他们会和我们应用中的源码一样交给 Java
/Kotlin
编译器编译成 JVM
字节码,AGP
的 transform
接口会把这些 class
字节码和 aar
/jar
依赖库 (包含所有的库中的字节码和其中的资源文件)交由我们添加的 transform
对象来处理,添加的 transfrom
对象就像一个个拦截器节点,input
就表示拦截器的输入,output
就表示拦截器的输出,外部输入的 class
/jar
文件都放在 input
中,修改完成后我们需要重新写入到 output
中,前一个拦截节点的 output
就是下一个节点的 input
,当所有的 transform
拦截器节点都处理完后,就需要把最终处理过的 JVM
字节码交给 D8
/R8
编译器,D8
/R8
会对这些 JVM
字节码做脱糖、混淆 、字节码优化和重新编译 dex
字节码等等一系列操作就得到了 Dex
字节码文件供 Android
虚拟机使用。
通过我们对上面打包过程的了解,我们能够知道 transform
接口能够处理注解处理器、AIDL
生成的代码,也能够处理我们应用所依赖 jar
/ aar
的库中的字节码。transform
的工作在 Kotlin
/Java
与 D8
/R8
编译器之间,所以我们拿到的字节码是没有混淆和脱糖等操作的。
创建一个 Gradle 插件项目
Gradle 配置
首先添加 AGP
和 ASM
依赖:
dependencies {
// ...
implementation 'com.android.tools.build:gradle:4.1.1'
api 'org.ow2.asm:asm:9.1'
api 'org.ow2.asm:asm-commons:9.1'
}
添加 java-gradle-plugin
的插件:
plugins {
// ...
id 'java-gradle-plugin'
// ...
}
指定我们的插件的 id 和实现类:
gradlePlugin {
plugins {
// 这个 `apmCore` 是随便填的
apmCore {
id = // 填写自己项目的插件 id
implementationClass = // 填写自己项目的插件实现类
}
}
}
Plugin 实现类
class ApmCorePlugin : Plugin<Project> {
override fun apply(project: Project) {
// ...
if (project.plugins.hasPlugin("com.android.application")) {
val appExtension = project.extensions.getByName("android") as AppExtension
appExtension.registerTransform(
ApmCoreTransform(
project = project,
apmCorePlugin = this
)
)
}
//...
}
}
这个 Plugin
实现的类要和 gradle
中的配置对应,apply()
函数为入口函数,通过 AGP
的插件我们能够拿到 AppExtension
对象,然后调用其 registerTransform
方法就可以完成自定义 transfrom
的添加。
Transform 接口
基础方法介绍
class ApmCoreTransform(val project: Project, val apmCorePlugin: ApmCorePlugin) : Transform() {
override fun getName(): String = "ApmTransform"
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
override fun isIncremental(): Boolean = true
override fun transform(transformInvocation: TransformInvocation?) {
// ...
}
}
getName()
: 当前 transform
的名字,能够在打包过程中看到它对应生成的 gradle task
。
getInputTypes()
: 指定要处理的输入类型,上面的设置就表示只处理字节码,不处理资源。
getScope()
: 表示处理输入的范围,上面的设置表示整个项目的所有 module
和所有的依赖。 isIncremetal()
: 表示是否支持增量更新,最好选择是。如果是否的话,每次打包都相当于清除缓存后重新打包;如果是是的话,只会处理修改后的文件。
transform()
: 方法为处理插桩的入口函数。
处理 transform 的 input 和 output
处理插桩的入口函数是 transform
,所有需要的参数也都是从 TransformInvocation
中获取的。
val outputProvider = transformInvocation.outputProvider
val isIncremental = transformInvocation.isIncremental
if (!isIncremental) {
outputProvider.deleteAll()
}
// 需要 Hook 的Jar文件和Classes文件, 供后续 Hook 使用。
val jarsInputMapOutput = ConcurrentHashMap<File, File>()
val classInputMapOutput = ConcurrentHashMap<File, File>()
// 全部的Jar文件和Classes 文件, 供后续 Pre Scan 使用
val jarsAllInputFiles = LinkedBlockingDeque<File>()
val classesAllInputFiles = LinkedBlockingDeque<File>()
开始做了一些准备工作,outputProvider
和后续的 output
文件/文件夹创建相关,jarsInputMapOutput
与 classInputMapOutput
分别表示要处理的 input
jar
/class
文件与 output
文件的映射。jarsAllInputFiles
与 classesAllInputFiles
表示所有输入的 input
的 jar
和 class
文件。
查找映射需要处理的 input
与 output
的 jar
与 class
文件:
// 1. 映射和创建Output文件
for (inputs in transformInvocation.inputs) {
// 处理jar文件
for (jarInput in inputs.jarInputs) {
// ...
}
// 处理class文件
for (dirInput in inputs.directoryInputs) {
// ...
}
}
由于处理 jar
和 class
的逻辑比较长,我把他们分为两段来看。
处理 jar
:
for (jarInput in inputs.jarInputs) {
val outputFile = outputProvider.getContentLocation(
jarInput.file.absolutePath,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
if (isIncremental) {
when (jarInput.status) {
Status.NOTCHANGED -> {
jarsAllInputFiles.add(jarInput.file)
}
Status.REMOVED -> {
if (outputFile.exists()) {
outputFile.delete()
}
}
Status.ADDED, Status.CHANGED, null -> {
if (!outputFile.exists()) {
outputFile.parentFile.mkdirs()
outputFile.createNewFile()
}
jarsInputMapOutput[jarInput.file] = outputFile
jarsAllInputFiles.add(jarInput.file)
}
}
} else {
if (!outputFile.exists()) {
outputFile.parentFile.mkdirs()
outputFile.createNewFile()
}
jarsInputMapOutput[jarInput.file] = outputFile
jarsAllInputFiles.add(jarInput.file)
}
}
首先通过 outputProvider#getContentnLocation()
方法获取到 output
的 jar
的文件路径。
首先会判断当前的构建是不是增量编译,如果不是那就处理简单,就认为所有的 input
都要处理,映射所有的 input
到 output
。
如果是增量编译,那么就要单独判断这个 input
的 jar
文件的状态。其中包括:Status.NOTCHANGED
(没有改变)、 Status.REMOVED
(被删除)、Status.ADDED
(添加) 和 Status.CHANGED
(改变) 等状态。只有当状态是 Status.ADDED
和 Status.CHANGED
时,我们才会处理这个这个文件,创建 input
和 output
的文件映射。
处理 class
:
for (dirInput in inputs.directoryInputs) {
val dirInputFile = dirInput.file
val dirOutputFile = outputProvider.getContentLocation(
dirInputFile.absolutePath,
dirInput.contentTypes,
dirInput.scopes,
Format.DIRECTORY
)
if (!dirOutputFile.exists()) {
dirOutputFile.mkdirs()
}
if (isIncremental) {
val changedFiles = dirInput.changedFiles
for ((f, status) in changedFiles) {
val name = f.absolutePath.replaceFirst(dirInputFile.absolutePath, "")
val outputFile = File("${dirOutputFile.absolutePath}$name")
when (status) {
Status.NOTCHANGED -> {
classesAllInputFiles.put(f)
}
Status.REMOVED -> {
if (outputFile.exists()) {
outputFile.delete()
}
}
Status.CHANGED, Status.ADDED, null -> {
if (!outputFile.exists()) {
outputFile.parentFile.mkdirs()
outputFile.createNewFile()
}
classInputMapOutput[f] = outputFile
classesAllInputFiles.put(f)
}
}
}
} else {
dirInputFile.scanFiles { f ->
val name = f.absolutePath.replaceFirst(dirInputFile.absolutePath, "")
val outputFile = File("${dirOutputFile.absolutePath}$name")
if (!outputFile.exists()) {
outputFile.parentFile.mkdirs()
outputFile.createNewFile()
}
classInputMapOutput[f] = outputFile
classesAllInputFiles.put(f)
}
}
}
这里是直接输入的一个文件夹,这个文件夹下面的文件就是对应的 class
文件,同样的我们借助 outputProvider#getContentLocation()
方法获取最终 output
的文件夹的路径。
如果不是增量编译,遍历输入的文件夹,通过输出的路径和文件名创建新的文件,然后添加到 class
的 input
与 output
映射。
如果是增量编译和 jar
的处理方式一样,只是映射新添加和已经修改的 input
的 class
文件到 output
。
如果你的插桩需要知道类的继承关系,那么你需要在插桩前把所有的输入都扫描一遍,这个过程中需要借助 ASM
, 我们后面再讲,当然也不仅仅是继承关系,别的你需要的信息也是可以的。
// 2. 执行PreScan
val scanParentsStartTime = System.currentTimeMillis()
ApmCorePlugin.removeClassPreScanInterceptor(PreScanClassInfoInterceptor::class.java)
findClasses.clear()
ApmCorePlugin.addClassPreScanInterceptor(PreScanClassInfoInterceptor())
preScanJars(transformInvocation, jarsAllInputFiles.toList())
preScanClasses(transformInvocation, classesAllInputFiles.toList())
val scanParentEndTime = System.currentTimeMillis()
Log.d(TAG, "PreScan 耗时: ${scanParentEndTime - scanParentsStartTime} ms, 总共 ${findClasses.size} Classes")
这里的 PreScan 只是读 input
中的数据,而不会有输出。
最后根据上面步骤构建的 input
和 output
映射来执行插桩处理了。
// 3. Hook
val hookStartTime = System.currentTimeMillis()
hookJars(transformInvocation, jarsInputMapOutput)
hookClasses(transformInvocation, classInputMapOutput)
val hookEndTime = System.currentTimeMillis()
Log.d(TAG, "Hook 耗时: ${hookEndTime - hookStartTime} ms")
其中 hookJars()
和 hookClasses()
方法就是我用来处理插桩的方法。
hookJars():
internal fun ApmCoreTransform.hookJars(transformInvocation: TransformInvocation, inputOutputFiles: Map<File, File>) {
val tasks = mutableListOf<Future<*>>()
for ((inputFile, outputFile) in inputOutputFiles) {
if (inputFile.name.endsWith(".jar") && outputFile.name.endsWith(".jar")) {
tasks.add(apmExecutor.submit {
val inputZipFile = ZipFile(inputFile)
val inputEntries = inputZipFile.entries()
ZipOutputStream(FileOutputStream(outputFile)).use { outputZipStream ->
while (inputEntries.hasMoreElements()) {
val entry = inputEntries.nextElement()
if (entry.name.endsWith(".class")) {
inputZipFile.getInputStream(entry).use { inputStream ->
val classReader = ClassReader(inputStream)
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
classReader.accept(
ApmCoreClassVisitor(
writer = classWriter,
transform = this,
transformInvocation = transformInvocation,
// 复制每个拦截器,解决多线程问题.
interceptors = ApmCorePlugin.getAllClassesHookInterceptors().map { it.clone() }
),
ClassReader.EXPAND_FRAMES
)
outputZipStream.putNextEntry(ZipEntry(entry.name))
outputZipStream.write(classWriter.toByteArray())
outputZipStream.flush()
outputZipStream.closeEntry()
}
} else {
inputZipFile.getInputStream(entry).use { inputStream ->
outputZipStream.putNextEntry(ZipEntry(entry.name))
inputStream.copyTo(outputZipStream)
outputZipStream.flush()
outputZipStream.closeEntry()
}
}
}
outputZipStream.flush()
outputZipStream.finish()
}
})
} else {
tasks.add(apmExecutor.submit {
FileUtils.copyFile(inputFile, outputFile)
})
}
}
for (t in tasks) {
t.get()
}
}
jar
文件是 zip
的压缩文件,我们可以直接用 jdk
中的处理 zip
的接口,不熟悉的同学可以去别的地方找找资料。 这里还开了一个线程池来处理,处理速度可以更快。 这里会读取 jar
中的 class
文件用 ASM
接口处理完后,把处理完的数据写入到 output
的 jar
文件中,如果不是 class
文件就直接复制到 output
中。
ApmCoreClassVisitor
就是我们通过 ASM
接口处理输入的 class
字节码的地方,最终的处理后的结果会写入到 ClassWriter
中,加入你不需要输出,这个 ClassWriter
也可以传递为空,比如上面说到的 PreScan 时只需要获取类的继承关系。
hookClasses:
internal fun ApmCoreTransform.hookClasses(transformInvocation: TransformInvocation, inputOutputFiles: Map<File, File>) {
val tasks = mutableListOf<Future<*>>()
for ((inputFile, outputFile) in inputOutputFiles) {
if (inputFile.name.endsWith(".class") && outputFile.name.endsWith(".class")) {
tasks.add(apmExecutor.submit {
FileInputStream(inputFile).use { inputStream ->
val classReader = ClassReader(inputStream)
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
classReader.accept(
ApmCoreClassVisitor(
writer = classWriter,
transform = this,
transformInvocation = transformInvocation,
// 复制每个拦截器,解决多线程问题.
interceptors = ApmCorePlugin.getAllClassesHookInterceptors().map { it.clone() }
),
ClassReader.EXPAND_FRAMES
)
FileOutputStream(outputFile).buffered().use { outputStream ->
outputStream.write(classWriter.toByteArray())
outputStream.flush()
}
}
})
} else {
tasks.add(apmExecutor.submit {
FileUtils.copyFile(inputFile, outputFile)
})
}
}
for (t in tasks) {
t.get()
}
}
class
文件的处理方式就更加简单了,没有 zip
文件的处理。
到这里就完成了一次插桩流程,不过我们还没有看 ASM
的接口,是如何修改字节码的,我们还需要继续。
ASM 修改字节码
通过 IDEA
中 ASM Viewer
插件可以将字节码转换成 ASM
的代码供你做插桩功能时的参考,强烈推荐大家学习一下 JVM
字节码,这会让你写 ASM
插桩时会得心应手
ClassVisitor
class ApmCoreClassVisitor(
writer: ClassVisitor?,
private val transform: ApmCoreTransform,
) : ClassVisitor(Opcodes.ASM8, writer) {
private val needHandleInterceptors: LinkedBlockingDeque<ClassHookInterceptor> by lazy {
LinkedBlockingDeque()
}
private var classInfoData: ClassInfoData? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor? {
val methodInfoData = MethodInfoData(
methodAccess = access,
methodName = name,
methodDescriptor = descriptor,
methodSignature = signature,
methodExceptions = exceptions?.toList(),
classInfoData = classInfoData!!
)
val methodInterceptor = needHandleInterceptors
.fold(emptyList<MethodHookInterceptor>()) { mi, ci -> mi + ci.methodInterceptors() }
.filter { it.needIntercept(methodInfoData, transform) }
val fixedInfo =
methodInterceptor.fold(methodInfoData) { mi, i -> i.methodInfoIntercept(mi) }
val mv = super.visitMethod(
fixedInfo.methodAccess,
fixedInfo.methodName,
fixedInfo.methodDescriptor,
fixedInfo.methodSignature,
fixedInfo.methodExceptions?.toTypedArray()
)
return ApmCoreMethodVisitor(
interceptors = methodInterceptor,
methodInfoData = fixedInfo,
methodVisitor = mv
)
}
override fun visitEnd() {
super.visitEnd()
}
override fun visitField(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
value: Any?
): FieldVisitor {
return super.visitField(access, name, descriptor, signature, value)
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
return super.visitAnnotation(descriptor, visible)
}
}
visit()
方法中会传递类的基本信息,包括类名,可见性,父类,实现的接口。我们也可以修改这些信息,比如继承的类和实现的接口,还可以收集基本的类的信息,比如我们前面说到的 PreScan 流程中需要找到所有的类的继承关系,就时通过 visit()
方法。
visitMethod()
方法是对方法的处理,这个方法是我们大部分时间插桩都要用到的,后面也会重点分析自定义 MethodVistor
, 这个方法中也会有方法的可见性、方法名、方法签名和抛出的异常等,也可以通过 super.visitMethod()
方法来修改,这个方法还会返回一个 MethodVistor
对象,如果你不需要再修改这个方法的字节码,就直接返回这个 MethodVistor
对象就好了,如果你需要对这个方法的字节码作出修改就需要添加一个自定义的 MethodVistor
,像我上面的 demo
中就添加了一个自定义的 ApmCoreMethodVistor
对象。
visitEnd()
表示当前 class
对象访问完了,在这个方法中也可以做一些逻辑的,例如在 visiMethod()
的回调中没有某个方法,我就可以在 visitEnd()
方法前手动添加这个方法。
visitField()
和 visitAnnotation()
分别表示访问成员变量和注解,他们和 visitMethod()
方法也是类似的,后续就不多讲他们了。
MethodVisitor
class ApmCoreMethodVisitor(
private val methodInfoData: MethodInfoData,
methodVisitor: MethodVisitor?
) : AdviceAdapter(
Opcodes.ASM8,
methodVisitor,
methodInfoData.methodAccess,
methodInfoData.methodName,
methodInfoData.methodDescriptor
) {
override fun onMethodEnter() {
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
}
override fun visitTypeInsn(opcode: Int, type: String?) {
super.visitTypeInsn(opcode, type)
}
override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, descriptor: String?) {
super.visitFieldInsn(opcode, owner, name, descriptor)
}
override fun visitMethodInsn(
opcodeAndSource: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface)
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
return super.visitAnnotation(descriptor, visible)
}
}
onMethodEnter()
和 onMethodExit()
表示一个方法的开始和结束,我们也可以根据需求做一些操作,加入我需要统计所有的方法耗时,我们就可以通过在 onMethodEnter()
中添加一个指令去调用我们准备好的方法通知方法进入了,通过 onMethodExit()
方法在调用我们准备好的另一个方法通知方法结束了。
visityTypeInsn()
表示执行 NEW, ANEWARRAY, CHECKCAST, INSTANCEOF
指令,熟悉字节码指令后这些应该非常熟悉。
visitFieldInsn()
表示执行 GETSTATIC, PUTSTATIC, GETFIELD, PUTFIELD
指令。
visitMethodInsn()
表示执行 INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC, INVOKEINTERFACE
指令,都是和方法调用有关,也是插桩常用的方法。
还有非常多的指令对应的方法,我这里就不再单独列出来了,点到为止,如果自己用到某个指令的方法,我没有列出来,自己再去 ASM
的文档中找就好了,都是类似的。
ASM 插桩实战
插桩有一个非常重要的原则,就是你所修改后的代码和修改前的代码在执行后他们的操作数栈和本地变量表是不会改变的,如果发生了大量的改变,处理起来将会非常麻烦,目前我还没有处理过这样的插桩。
解决 context.getColor() 在低版本上导致的崩溃
在低版本上调用 context
中的以下版本会导致崩溃:
@ColorInt
public final int getColor(@ColorRes int id) {
return getResources().getColor(id, getTheme());
}
通常需要使用 ContextCompat
中的以下方法替换:
@SuppressWarnings("deprecation")
@ColorInt
public static int getColor(@NonNull Context context, @ColorRes int id) {
if (Build.VERSION.SDK_INT >= 23) {
return Api23Impl.getColor(context, id);
} else {
return context.getResources().getColor(id);
}
}
这两个方法所需要的操作数栈都是一样的,而且返回值也是一样的,那 hook
起来简直不要太简单。
companion object {
const val ANDROID_CONTEXT_COLOR_METHOD_NAME = "getColor"
const val ANDROID_CONTEXT_COLOR_METHOD_DES = "(I)I"
const val ANDROID_CONTEXT_COMPACT_CLASS_NAME = "androidx/core/content/ContextCompat"
const val ANDROID_CONTEXT_COMPACT_COLOR_METHOD_NAME = "getColor"
const val ANDROID_CONTEXT_COMPACT_COLOR_METHOD_DES = "(Landroid/content/Context;I)I"
}
override fun visitMethodInsn(
opcodeAndSource: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
if (owner!!.isAndroidContext(owner)
&& name == ANDROID_CONTEXT_COLOR_METHOD_NAME
&& descriptor == ANDROID_CONTEXT_COLOR_METHOD_DES) {
super.visitMethodInsn(
Opcodes.INVOKESTATIC,
ANDROID_CONTEXT_COMPACT_CLASS_NAME,
ANDROID_CONTEXT_COMPACT_COLOR_METHOD_NAME,
ANDROID_CONTEXT_COMPACT_COLOR_METHOD_DES,
isInterface)
} else {
super.visitMethodInsn(
opcodeAndSource,
owner,
name,
descriptor,
isInterface
)
}
}
首先判断 owner
必须是 Android
中的 Context
方法,方法名必须是 getColor
,方法的描述必须是 (I)I
,如果满足上面的条件就可以把它替换成 ContextCompat#getColor()
。
我们也可以用同样的方法替换 Android
中的 Log
打日志相关的方法,把它替换成你需要的日志方法,同样的必须让其和 Android
中的 Log
消耗的操作数栈,返回值也要一样。
Dialog
,PopupWindow
的 badToken
问题也可以通过这种方式解决,同时也可以顺便监听 Dialog
/ PopupWindow
的显示与消失,拿 Dialog
举例,Dialog#show()
表示显示,Dialog#dismiss()
表示消失,我可以静态方法 DialogCompat#show(d: Dialog)
来代理显示,用 DialogCompat#dismiss(d: Dialog)
来代理消失,我在显示的时候就可以判断对应的 Activity
是否存活,如果存活我就显示,不存活就不显示,而且还可以监听他们的生命周期。
我还用这个方法 hook
过 U3d
库中的敏感权限导致上架失败的问题,这个方法很简单,但是很实用,能够解决很多的问题。
自动初始化库
假如我想要我们的库在在 Application#attachBaseContext()
的时候完成初始化,这个时候就有两种情况,一种是 Application
重写了 attachBaseContext()
方法,一种没有重写。
无论哪种情况都必须保证当前的 class
是 Application
的子类:
var isApplication: Boolean = false
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
isApplication = transform.hasTargetParentClass(name ?: "",
"android/app/Application"
)
super.visit(version, access, name, signature, superName, interfaces)
}
这也是为什么我要在 hook
前要先扫描一次类的继承关系。
我们先看简单的,重写了 attachBaseContext()
, 需要在 MethodVisitor#onMethodEnter()
添加以下代码:
override fun onMethodEnter() {
super.onMethodEnter()
if (isApplication) {
if (name == "attachBaseContext" && descriptor == "(Landroid/content/Context;)V") {
mv?.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/gmlive/common/apm/apmcore/baseplugins/startup/StartupHook",
"onApplicationStarted",
"()V",
false
)
}
}
}
当找到 attachbaseContext()
方法后就在方法的最开头调用了我们自己定义的 onApplicationStarted()
方法,这个静态方法没有参数也没有返回值,不会对原有方法的变量表和操作数栈照成影响。
假如没有找到 attachbaseContext()
方法,那就需要我们在 ClassVisitor
的 visitEnd()
回调中手动添加这个方法:
var donotFindAttachBase: Boolean = true
override fun visitEnd() {
if (isApplication && donotFindAttachBase) {
val mv = cv?.visitMethod(
Opcodes.ACC_PROTECTED,
"attachBaseContext",
"(Landroid/content/Context;)V",
null,
null
)
mv?.visitCode()
mv?.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/gmlive/common/apm/apmcore/baseplugins/startup/StartupHook",
"onApplicationStarted",
"()V",
false
)
mv?.visitVarInsn(Opcodes.ALOAD, 0)
mv?.visitVarInsn(Opcodes.ALOAD, 1)
mv?.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"android/app/Application",
"attachBaseContext",
"(Landroid/content/Context;)V",
false
)
mv?.visitInsn(Opcodes.RETURN)
mv?.visitMaxs(2, 2)
mv?.visitEnd()
}
super.visitEnd()
}
我这里还调用了我自己的 hook
方法后,还调用了 super.attachBaseContext()
方法。
计算方法的耗时
计算耗时的插桩不想贴代码了,有点累了。。。。。。
在 MethodVisitor#onMethodEnter()
方法中插入 hook
的 methodStart()
方法,在 MethodVistor#onMethodExit()
方法中插入 hook
的 methodEnd()
方法,那我们怎么判断调用的方法是哪个呢?在 Java
中非常简单,直接拿方法栈就好了,上一个方法就是调用的方法。
同一个线程中方法的调用就像是 xml
一样,方法的调用就是呈现一个树状。每一个线程都是一棵树,然后我们能够统计每一个方法节点的耗时。
总结
我当时第一次写出插桩的时候非常激动,感觉发现了新大陆,但是他也不能滥用,他出现了问题比较难找原因,推荐在 hook
类的时候加上本地日志,自己也好查,然后通过 jadx
反编译来看看插桩后的代码是否正确,然后大量测试慢慢验证。
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap