0.前言
上一篇文章,我们已经找到了我们的作案对象.接下来我们就要开始下手了~
完整依赖
dependencies {
implementation gradleApi()
api "com.android.tools.build:gradle-api:$apgVersion"
api "com.android.tools.build:gradle:$apgVersion"
implementation 'commons-io:commons-io:2.6'
implementation "commons-codec:commons-codec:1.15"
api 'org.ow2.asm:asm:9.2'
api 'org.ow2.asm:asm-tree:9.2'
implementation 'org.javassist:javassist:3.27.0-GA'
}
1.字节码框架ASM
要知道JAVA跨平台的原因就是所有的JAVA文件都会被编译成.class文件通过JVM虚拟机执行。而.class文件则是由字节码组成的文件。那我们最终需要的是操作字节码,而正常人类是不会直接编写字节码代码
,就好像你写C总不能去写汇编吧。
那么自然而然就出现了一个字节码框架----->ASM
ASM则是可以从类文件中读入信息后,能改变类行为,分析类信息。
当然你也可以选择Javassist,不过性能上还是ASM比较好,不过需要更多的学习成本…
从这里开始可能就和其它文章不太一样了,因为插入操作已经开始变得奇怪了,但是确实比FieldVisitor之类的好一些
网上文章:
ClassVisitor
ClassReader
ClassWriter
MethodVisitor & AdviceAdapter
FieldVisitor
这里:
classNode
classReader
classNode.outerClass
classNode.interfaces
classNode.methods–>MethodNode
InsnList
classWriter
2.字节码基础知识
如果你不了解字节码的基础在干嘛…那还修改个毛.(当然,我也不懂.摘抄点资料,让大家不用到处找…)
先上大佬链接
http://www.zyiz.net/tech/detail-125513.htmlhttps://www.jianshu.com/p/c202853059b4#5.2 建议大家下载一个ASM Bytecode Outline插件,可以在build文件夹中找到.class去通过这个插件去看字节码长什么样子…
关于字节码,有以下概念定义比较重要:
全限定名(Internal names):
全限定名即为全类名中的“.”,换为“/”,举例:
类android.widget.AdapterView.OnItemClickListener的全限定名为:
android/widget/AdapterView$OnItemClickListener
描述符(descriptors):
1.类型描述符,如下图所示:
我们最常见的自定义引用类型为“L全限定名”.例如:
Android中的android.view.View类,
描述符为“Landroid/view/View;”
方法描述符的组织结构为:
(参数类型描述符)返回值描述符
其中无返回值void用“V”代替,举例:
方法boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)
描述符如下:
(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z
字节码指令,还有什么操作栈什么的。请自己学习
以上是基础,下面这个是操作码列表。这是ASM封装出来的,能让你更快的去插入和修改其字节码。
instructions,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码。
ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中。
对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点 AbstractInsnNode。其所有的派生类使用详情如下所示。
以上所有的知识,都是基础…这还只是一部分,所以ASM的学习成本虽然已经下降了很少,但是我感觉还是不友好0-0,如果看不下去,慢慢来吧
咋们可以边看边学嘛.
3.Show You Code
别BB,赶紧操作起来,拿代码说话。
来了来了,我们在transform中找到了我们“作案对象”,传入了我们的字节码,那么我们仔细看我们的modifyClass方法是什么东西.
val injectHelper = DoubleTapClassNodeHelper()
if(dirFile.isDirectory){
FileUtils.copyDirectory(dirFile, dest)
dirFile.walkTopDown().filter { it.isFile }
.forEach {classFile ->
if (classFile.name.endsWith(".class")) {
Log.info("-------------classFile.inpus ${classFile}----------------------")
val bytes = IOUtils.toByteArray(FileInputStream(classFile))
injectHelper.modifyClass(bytes)
}
}
}
这里全是虾哥代码摘抄
package com.aoto.onclickhelp
import com.aoto.base.asm.lambdaHelper
import com.aoto.base.base.AsmHelper
import com.aoto.base.base.Log
import com.aoto.onclickplugin.DoubleTabConfig
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Label
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.tree.*
import java.io.IOException
class DoubleTapClassNodeHelper : AsmHelper {
private val classNodeMap = hashMapOf<String, ClassNode>()
@Throws(IOException::class)
override fun modifyClass(srcClass: ByteArray): ByteArray {
val classNode = ClassNode(ASM5)
val classReader = ClassReader(srcClass)
//1 将读入的字节转为classNode
classReader.accept(classNode, 0)
classNodeMap[classNode.name] = classNode
// 判断当前类是否实现了OnClickListener接口
val hasAnnotation = classNode.hasAnnotation()
val className = classNode.outerClass
val parentNode = classNodeMap[className]
Log.info("Find the classNode ${classNode.name}")
val hasKeepAnnotation = if (hasAnnotation) {
true
} else {
parentNode?.hasAnnotation() ?: false
}
if (!hasKeepAnnotation) {
// interfaces!=null,To find onclicklistener
classNode.interfaces?.forEach {
if(it == "android/view/View\$OnClickListener"){
classNode.methods?.forEach { method ->
//找到该Interface 构造方法
if(method.name == "<init>"){
// initFunction(classNode, method)
}
//desc is the method Type
if(method.name =="onClick"&& method.desc =="(Landroid/view/View;)V"){
// insertTrack(classNode, method)
Log.info("Find the method ${method.name} and the desc ${method.desc}")
}
}
}
}
//Modify the Higher order function,lambda method
classNode.lambdaHelper {
(it.name == "onClick" && it.desc.contains(")Landroid/view/View\$OnClickListener;"))
}.apply {
if (isNotEmpty()) {
classNode.methods?.forEach { method ->
if (method.name == "<init>") {
// initFunction(classNode, method)
return@forEach
}
}
}
}.forEach { method ->
// insertTrack(classNode, method)
}
}
//调用Fragment的onHiddenChange方法
val classWriter = ClassWriter(0)
//3 将classNode转为字节数组
classNode.accept(classWriter)
return classWriter.toByteArray()
}
private fun insertLambda(node: ClassNode, method: MethodNode) {
// 根据outClassName 获取到外部类的Node
}
private fun initFunction(node: ClassNode, method: MethodNode) {
var hasLog = false
node.fields?.forEach {
if (it.name == "textLog") {
hasLog = true
}
}
if (!hasLog) {
node.visitField(ACC_PRIVATE + ACC_FINAL, "textLog", String.format("L%s;",
"com/kotlin/aop/Buried_point/TestLog"), node.signature, null)
val instructions = method.instructions
method.instructions?.iterator()?.forEach {
if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
instructions.insertBefore(it, VarInsnNode(ALOAD, 0))
instructions.insertBefore(it, TypeInsnNode(NEW, "com/kotlin/aop/Buried_point/TestLog") )
instructions.insertBefore(it, InsnNode(DUP))
instructions.insertBefore(it, MethodInsnNode(INVOKESPECIAL, "com/kotlin/aop/Buried_point/TestLog",
"<init>", "()V", false))
instructions.insertBefore(it, FieldInsnNode(PUTFIELD, node.name, "textLog",
String.format("L%s;", "com/kotlin/aop/Buried_point/TestLog")))
}
}
}
}
private fun insertTrack(node: ClassNode, method: MethodNode) {
// 判断方法名和方法描述
val instructions = method.instructions
val firstNode = instructions.first
//Insert the code
instructions?.insertBefore(firstNode, VarInsnNode(ALOAD, 0))
instructions?.insertBefore(firstNode, FieldInsnNode(GETFIELD, node.name,
"textLog", String.format("L%s;", "com/kotlin/aop/Buried_point/TestLog")))
instructions?.insertBefore(firstNode, MethodInsnNode(INVOKEVIRTUAL, "com/kotlin/aop/Buried_point/TestLog",
"textLog", "()V", false))
}
// 判断Field是否包含注解
private fun ClassNode.hasAnnotation(): Boolean {
var hasAnnotation = false
this.visibleAnnotations?.forEach { annotation ->
// Log.info("name:$name visibleAnnotations:${annotation.desc} ")
if (annotation.desc == "com/kotlin/aop/Buried_point/TestLog/Test;") {
hasAnnotation = true
}
}
this.invisibleAnnotations?.forEach { annotation ->
// Log.info("name:$name visibleAnnotations:${annotation.desc} ")
if (annotation.desc == "com/kotlin/aop/Buried_point/TestLog/Test;") {
hasAnnotation = true
}
}
return hasAnnotation
}
}
来吧,解释来吧.
我们先介绍几个类
ClassNode类
类似于我们的class,类的构造里面包括这个类的节点的许多信息,比如version,access,interfaces,fields…ClassReader 类
这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法。ClassWriter 类
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将
.class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出
步骤:
新建classNode(看做类文件)
通过ClassReader读入字节码
将读入的字节码转入到classNode中.去判断是否有我们要扩展的注解。
判断该类(类文件)是否有实现OnClickListener这个接口
判断在该接口下的构造方法下执行initFunction(classNode, method)
判断在该接口下的onClick方法执行
insertTrack(classNode, method)
通过ClassWriter将classNode写入.输出字节码。
我们最终的目的是将
view.findViewById<Button>(R.id.buriedpoint_Butotn).setOnClickListener(object:View.OnClickListener{
override fun onClick(v: View?) {
Log.d("TAG", "onClick: You know this not good")
}
变成
view.findViewById<Button>(R.id.buriedpoint_Butotn).setOnClickListener(object:View.OnClickListener{
private final TestLog testLog = new TestLog();
override fun onClick(v: View?) {
this.testLog.textLog()
Log.d("TAG", "onClick: You know this not good")
}
所以我们对字节码的操控都在initFunction和insertTrack中
private fun initFunction(node: ClassNode, method: MethodNode) {
var hasLog = false
node.fields?.forEach {
if (it.name == "textLog") {
hasLog = true
}
}
if (!hasLog) {
node.visitField(ACC_PRIVATE + ACC_FINAL, "textLog", String.format("L%s;",
"com/kotlin/aop/Buried_point/TestLog"), node.signature, null)
val instructions = method.instructions
method.instructions?.iterator()?.forEach {
if ((it.opcode >= Opcodes.IRETURN && it.opcode <= Opcodes.RETURN) || it.opcode == Opcodes.ATHROW) {
instructions.insertBefore(it, VarInsnNode(ALOAD, 0))
instructions.insertBefore(it, TypeInsnNode(NEW, "com/kotlin/aop/Buried_point/TestLog") )
instructions.insertBefore(it, InsnNode(DUP))
instructions.insertBefore(it, MethodInsnNode(INVOKESPECIAL, "com/kotlin/aop/Buried_point/TestLog",
"<init>", "()V", false))
instructions.insertBefore(it, FieldInsnNode(PUTFIELD, node.name, "textLog",
String.format("L%s;", "com/kotlin/aop/Buried_point/TestLog")))
}
}
}
}
这一行只单纯的表示
private final TestLog test
node.visitField(ACC_PRIVATE + ACC_FINAL, "textLog", String.format("L%s;",
"com/kotlin/aop/Buried_point/TestLog"), node.signature, null)
如果不信,你可以在加入代码后直接编译,去到build/intermediates/transform/YourFile 去看.class文件。你可以看到很神奇的事情发生。最好的学习当然就是多去看修改前和修改后的代码啦
这个为
new TestLog
instructions.insertBefore(it, TypeInsnNode(NEW, "com/kotlin/aop/Buried_point/TestLog") )
这个是调用该构造函数,连起来为
new TestLog()
instructions.insertBefore(it, MethodInsnNode(INVOKESPECIAL, "com/kotlin/aop/Buried_point/TestLog",
"<init>", "()V", false))
你以为大功告成了?,nonono.你还没赋值呢…
instructions.insertBefore(it, FieldInsnNode(PUTFIELD, node.name, "textLog",
String.format("L%s;", "com/kotlin/aop/Buried_point/TestLog")))
是的。。这么一大串,才是表示
private final TestLog testlog = new TestLog()
而中间的判断则是代表我们不会在所有地方进行插入,只是在遍历过后在特定的地方进行插入.
so,要具备实现一个复杂 ASM 插桩的能力,我们需要对 JVM 字节码、ASM 字节码以及 ASM 源码中的核心工具类的实现 做到了然于心
再看insertTrack,其实就很简单了,就是调用this.testlog.testlog()方法
private fun insertTrack(node: ClassNode, method: MethodNode) {
// 判断方法名和方法描述
val instructions = method.instructions
val firstNode = instructions.first
//Insert the code
instructions?.insertBefore(firstNode, VarInsnNode(ALOAD, 0))
instructions?.insertBefore(firstNode, FieldInsnNode(GETFIELD, node.name,
"textLog", String.format("L%s;", "com/kotlin/aop/Buried_point/TestLog")))
instructions?.insertBefore(firstNode, MethodInsnNode(INVOKEVIRTUAL, "com/kotlin/aop/Buried_point/TestLog",
"textLog", "()V", false))
}
当然。所有的一切你都要在com.kotlin.aop.Buried_point包下有一个TestLog类。你也可以自定义一个。快去实验一个吧.
在当你进行编译后.当当当当~去到编译后的文件去看自己的代码行是否有被插入。
好了…你会发现ASM才是重点,如何操作字节码,以及字节码的知识才能让你在这时候开发更多的骚操作。
此时你就通过编译这个一个操作。给自己的代码添加一行神奇的代码。
当然本文还有很多的点可以说.但是这里只是一个入门,能让你最快的能在代码中实现这么一个效果.虽然这里不包括增量编译的过滤,去修改binding的。第三方包也添加等.
4.总结
好不容易将所有的模块都分解出来,并且每一部分的作用都基本记录下来了…有demo的情况下还能慢慢学习。但是没有的情况下…就可能真的就是真正的折磨了…其实看到这里也已经差不多了,但是剩下的文章我会继续记录一些扩展的东西,如transform的替代,和利用AutoService去对Plugins进行组合。