Android通过字节码插桩(Gradle + ASM)实现自动埋点

前言

字节码插桩,看起来挺牛皮,实际上是真的很牛皮。
但是牛皮不代表难学,只需要一点前置知识就能轻松掌握。
对Gradle和ASM不太了解的同学,强烈推荐下面的文章:

【Android】函数插桩(Gradle + ASM)
字节码插桩–你也可以轻松掌握

正所谓:”工欲善其事,必先利其器“。
开始之前,建议在Android Studio中安装”ASM Bytecode Viewer Support Kotlin”
代码右键 ASM Bytecode Viewer 便能自动生成ASM插桩代码,效果如下:

实战:

既然准备工作已经做好,那就正式进入实战,先看下目录结构:

1、StatisticPlugin (注册插件和获取配置的埋点信息)
class StatisticPlugin implements Plugin<Project> {

    public final static HashMap<String, BuryPointCell> HOOKS = new HashMap<>()

    @Override
    void apply(Project project) {
        def android = project.extensions.findByType(AppExtension)
        // 注册BuryPointTransform
        android.registerTransform(new BuryPointTransform())
        // 获取gradle里面配置的埋点信息
        def extension = project.extensions.create('buryPoint', BuryPointExtension)
        project.afterEvaluate {
            // 遍历配置的埋点信息,将其保存在HOOKS方便调用
            extension.hooks.each { Map<String, Object> map ->
                BuryPointCell cell = new BuryPointCell()
                boolean isAnnotation = map.get("isAnnotation")
                cell.isAnnotation = isAnnotation
                cell.agentName = map.get("agentName")
                cell.agentDesc = map.get("agentDesc")
                cell.agentParent = map.get("agentParent")
                if (isAnnotation) {
                    cell.annotationDesc = map.get("annotationDesc")
                    cell.annotationParams = map.get("annotationParams")
                    HOOKS.put(cell.annotationDesc, cell)
                } else {
                    cell.methodName = map.get("methodName")
                    cell.methodDesc = map.get("methodDesc")
                    cell.methodParent = map.get("methodParent")
                    cell.methodParams = map.get("methodParams")
                    HOOKS.put(cell.methodName + cell.methodDesc, cell)
                }
            }
        }
    }
}
复制代码
2、BuryPointTransform (实现Transform进行.class文件遍历拿到所有方法)
class BuryPointTransform extends Transform {

    @Override
    String getName() {
        return "BuryPoint"
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASS->处理的java的class文件
     * RESOURCES->处理java的资源
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 是否增量编译
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     *
     * @param context
     * @param inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
     * @param outputProvider 输出路径
     */
    @Override
    void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental
    ) throws IOException, TransformException, InterruptedException {
        if (!incremental) {
            //不是增量更新删除所有的outputProvider
            outputProvider.deleteAll()
        }
        inputs.each { TransformInput input ->
            //遍历目录
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            // 遍历jar 第三方引入的 class
            input.jarInputs.each { JarInput jarInput ->
                handleJarInput(jarInput, outputProvider)
            }
        }
    }

    /**
     * 处理文件目录下的class文件
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否是目录
        if (directoryInput.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (filterClass(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
        // 获取output目录
        def dest = outputProvider.getContentLocation(
                directoryInput.name,
                directoryInput.contentTypes,
                directoryInput.scopes,
                Format.DIRECTORY)
        //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    /**
     * 处理jar文件,一般是第三方依赖库jar文件
     */
    static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插桩class
                if (filterClass(entryName)) {
                    //class文件处理
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] bytes = classWriter.toByteArray()
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //结束
            jarOutputStream.close()
            jarFile.close()
            //生成输出路径 + md5Name
            def dest = outputProvider.getContentLocation(
                    jarName + md5Name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

    /**
     * 检查class文件是否需要处理
     * @param fileName
     * @return
     */
    static boolean filterClass(String name) {
        return (name.endsWith(".class")
                && !name.startsWith("R\$")
                && "R.class" != name
                && "BuildConfig.class" != name)
    }

}
复制代码
3、BuryPointVisitor (访问class的类)
class BuryPointVisitor extends ClassVisitor {

    BuryPointVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor)
    }

    /**
     * @param version 类版本
     * @param access 修饰符
     * @param name 类名
     * @param signature 泛型信息
     * @param superName 父类
     * @param interfaces 实现的接口
     */
    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
    }

    @Override
    void visitEnd() {
        super.visitEnd()
    }

    /**
     * 扫描类的方法进行调用
     * @param access 修饰符
     * @param name 方法名字
     * @param descriptor 方法签名
     * @param signature 泛型信息
     * @param exceptions 抛出的异常
     * @return
     */
    @Override
    MethodVisitor visitMethod(int methodAccess, String methodName, String methodDescriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
        return new BuryPointMethodVisitor(methodVisitor, methodAccess, methodName, methodDescriptor)
    }

}
复制代码
4、BuryPointMethodVisitor (访问方法的类,在这里进行代码插入)
class BuryPointMethodVisitor extends AdviceAdapter {

    String methodName
    String methodDescriptor

    BuryPointMethodVisitor(MethodVisitor methodVisitor, int access, String name, String desc) {
        super(Opcodes.ASM7, methodVisitor, access, name, desc)
        this.methodName = name
        this.methodDescriptor = desc
    }

    /**
     * 扫描类的注解时调用
     * @param descriptor 注解名称
     * @param visible
     * @return
     */
    @Override
    AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible)
        // 通过descriptor判断是否是需要扫描的注解
        BuryPointCell cell = StatisticPlugin.HOOKS.get(descriptor)
        if (cell != null) {
            BuryPointCell newCell = cell.clone()
            return new BuryPointAnnotationVisitor(annotationVisitor) {
                @Override
                void visit(String name, Object value) {
                    super.visit(name, value)
                    // 保存注解的参数值
                    newCell.annotationData.put(name, value)
                }

                @Override
                void visitEnd() {
                    super.visitEnd()
                    newCell.methodName = methodName
                    newCell.methodDesc = methodDescriptor
                    StatisticPlugin.HOOKS.put(newCell.methodName + newCell.methodDesc, newCell)
                }
            }
        }
        return annotationVisitor
    }

    /**
     * lambda表达式时调用
     * @param name
     * @param descriptor
     * @param bootstrapMethodHandle
     * @param bootstrapMethodArguments
     */
    @Override
    void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments)
        String desc = (String) bootstrapMethodArguments[0]
        BuryPointCell cell = StatisticPlugin.HOOKS.get(name + desc)
        // 通过name + desc判断是否是插入的方法
        if (cell != null) {
            String parent = Type.getReturnType(descriptor).getDescriptor()
            if (parent == cell.methodParent) {
                Handle handle = (Handle) bootstrapMethodArguments[1]
                BuryPointCell newCell = cell.clone()
                newCell.methodName = handle.getName()
                newCell.methodDesc = handle.getDesc()
                StatisticPlugin.HOOKS.put(newCell.methodName + newCell.methodDesc, newCell)
            }
        }
    }

    /**
     * 进入方法时调用
     */
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter()
        BuryPointCell cell = StatisticPlugin.HOOKS.get(methodName + methodDescriptor)
        if (cell != null) {
            if (cell.isAnnotation) { // 遍历注解参数并赋值给采集方法
                def entrySet = cell.annotationParams.entrySet()
                def size = entrySet.size()
                for (int i = 0; i < size; i++) {
                    def load = entrySet[i].getValue()
                    def store = getVarInsn(load)
                    mv.visitLdcInsn(cell.annotationData.get(entrySet[i].getKey()))
                    mv.visitVarInsn(store, i + 10)
                    mv.visitVarInsn(load, i + 10)
                }
                mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)
            } else { // 将扫描方法参数赋值给采集方法
                for (int key : cell.methodParams.keySet()) {
                    mv.visitVarInsn(cell.methodParams.get(key), key)
                }
                mv.visitMethodInsn(INVOKESTATIC, cell.agentParent, cell.agentName, cell.agentDesc, false)
            }
        }
    }

    /**
     * 推断类型
     * int ILOAD = 21; int ISTORE = 54;
     * 33 = ISTORE - ILOAD
     *
     * @param load
     * @returno
     */
    private static int getVarInsn(int load) {
        return load + 33
    }

}
复制代码
5、 如何使用?
5.1、 先打包一下插件到本地仓库进行引用

5.2、 在项目的根build.gradle加入插件的依赖
    repositories {
        google()
        mavenCentral()
        jcenter()
        maven{
            url uri('repos')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.meituan.android.walle:plugin:1.1.7'
        // 使用自定义插件
        classpath 'com.example.plugin:statistic:1.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
复制代码
5.3、 在app的build.gradle中使用并配置参数
plugins {
    id 'com.android.application'
    id 'statistic'
}

import org.objectweb.asm.Opcodes

buryPoint {
    hooks = [
            [
                    'agentName'   : 'viewOnClick',                                             //采集数据的方法名
                    'agentDesc'   : '(Landroid/view/View;)V',                                  //采集数据的方法描述
                    'agentParent' : 'com/example/fragment/project/utils/StatisticHelper',      //采集数据的方法的路径
                    'isAnnotation': false,
                    'methodName'  : 'onClick',                                                 //插入的方法名
                    'methodDesc'  : '(Landroid/view/View;)V',                                  //插入的方法描述
                    'methodParent': 'Landroid/view/View$OnClickListener;',                     //插入的方法的实现接口
                    'methodParams': [
                            1: Opcodes.ALOAD                                                   //采集数据的方法参数起始索引(从1开始) : 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
                    ]
            ],
            [
                    'agentName'       : 'testAnnotation',
                    'agentDesc'       : '(Ljava/lang/String;Z)V',
                    'agentParent'     : 'com/example/fragment/project/utils/StatisticHelper',
                    'isAnnotation'    : true,
                    'annotationDesc'  : 'Lcom/example/fragment/project/utils/TestAnnotation;', //扫描的方法注解名称
                    'annotationParams': [
                            'message': Opcodes.ALOAD,                                          //方法注解的值名称 : 参数类型对应的ASM指令,加载不同类型的参数需要不同的指令
                            'sb'     : Opcodes.ILOAD
                    ]
            ],
    ]
}
复制代码
6、 运行项目查看输出日志
2021-06-28 20:04:49.544 25211-25211/com.example.fragment.project.debug I/----------自动埋点:注解: MainActivity.onCreate:false
2021-06-28 20:05:03.535 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:menu ViewText:null
2021-06-28 20:05:06.085 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:coin ViewText:我的积分
2021-06-28 20:05:08.039 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:black ViewText:null
2021-06-28 20:05:11.616 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:username ViewText:去登录
2021-06-28 20:05:16.816 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:login ViewText:登录
复制代码

项目地址

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享