0.前言

上一篇文章,我们已经找到了我们的作案对象.接下来我们就要开始下手了~

Java SDK埋点如何实现 android埋点原理_Java SDK埋点如何实现


完整依赖

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.类型描述符,如下图所示:

Java SDK埋点如何实现 android埋点原理_android_02


我们最常见的自定义引用类型为“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

字节码指令,还有什么操作栈什么的。请自己学习

Java SDK埋点如何实现 android埋点原理_字节码_03


以上是基础,下面这个是操作码列表。这是ASM封装出来的,能让你更快的去插入和修改其字节码。

instructions,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码。
ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中。

对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点 AbstractInsnNode。其所有的派生类使用详情如下所示。

Java SDK埋点如何实现 android埋点原理_java_04


Java SDK埋点如何实现 android埋点原理_android_05


以上所有的知识,都是基础…这还只是一部分,所以ASM的学习成本虽然已经下降了很少,但是我感觉还是不友好0-0,如果看不下去,慢慢来吧咋们可以边看边学嘛.

3.Show You Code

别BB,赶紧操作起来,拿代码说话。

Java SDK埋点如何实现 android埋点原理_gradle_06


来了来了,我们在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类。你也可以自定义一个。快去实验一个吧.

在当你进行编译后.当当当当~去到编译后的文件去看自己的代码行是否有被插入。

Java SDK埋点如何实现 android埋点原理_gradle_07


好了…你会发现ASM才是重点,如何操作字节码,以及字节码的知识才能让你在这时候开发更多的骚操作。

此时你就通过编译这个一个操作。给自己的代码添加一行神奇的代码。

当然本文还有很多的点可以说.但是这里只是一个入门,能让你最快的能在代码中实现这么一个效果.虽然这里不包括增量编译的过滤,去修改binding的。第三方包也添加等.

4.总结

好不容易将所有的模块都分解出来,并且每一部分的作用都基本记录下来了…有demo的情况下还能慢慢学习。但是没有的情况下…就可能真的就是真正的折磨了…其实看到这里也已经差不多了,但是剩下的文章我会继续记录一些扩展的东西,如transform的替代,和利用AutoService去对Plugins进行组合。

Java SDK埋点如何实现 android埋点原理_Java SDK埋点如何实现_08