美文网首页Android GradleAndroid进阶
最通俗易懂的字节码插桩实战(Gradle + ASM)—— 自

最通俗易懂的字节码插桩实战(Gradle + ASM)—— 自

作者: miaowmiaow | 来源:发表于2021-06-28 20:03 被阅读0次

    前言

    字节码插桩,看起来挺牛皮,实际上是真的很牛皮。
    但是牛皮不代表难学,只需要一点前置知识就能轻松掌握。

    Gradle Transform

    Google在Android Gradle的1.5.0 版本以后提供了 Transfrom API,允许开发者在项目的编译过程中操作 .class 文件。Transfrom需要介绍的地方不多,唯一的难点就是要熟悉API,我会在文尾推荐相关文章,这里就不过多介绍,影响大家的阅读体验。

    ASM

    ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。
    刚去了解ASM的时候,我是真的差点被字节码吓退,字节码这东西根本就不是给人读的,在我认知里能去读字节码的都是大神。就在我准备放弃时,ASM Bytecode Viewer从天而降拯救了我。

    ASM Bytecode Viewer

    ASM Bytecode Viewer是一款能 查看字节码生成ASM代码 的插件,帮助我们打败了ASM学习路上最大的拦路虎,剩下就是对ASM的熟悉和使用可以说是so easy。
    1.在Android Studio中搜索 ASM Bytecode Viewer Support Kotlin 找到并安装
    2.代码右键 ASM Bytecode Viewer 便能自动生成ASM插桩代码,效果如下:

    实战:

    前面介绍了 Gradle TransformASMASM Bytecode Viewer,现在就正式进入实战,先看下目录结构:

    1、StatisticPlugin

    顾名思义就是我们本次编写的插件,在apply 方法的注册 BuryPointTransform,读取 build.gradle 里面配置的需要埋点的方法和注解。(Gradle Transform属实没啥好介绍,后面我就不过多哔哔,直接上代码和注释。熟悉并觉得无聊可直接跳到 BuryPointMethodVisitor

    class StatisticPlugin implements Plugin<Project> {
    
        public static Map<String, BuryPointEntity> BURY_POINT_MAP
    
        @Override
        void apply(Project project) {
            def android = project.extensions.findByType(AppExtension)
            // 注册BuryPointTransform
            android.registerTransform(new BuryPointTransform())
            // 获取gradle里面配置的埋点信息
            def statisticExtension = project.extensions.create('statistic', StatisticExtension)
            project.afterEvaluate {
               // 遍历配置的埋点信息,将其保存在BURY_POINT_MAP方便调用
                BURY_POINT_MAP = new HashMap<>()
                def buryPoint = statisticExtension.getBuryPoint()
                if (buryPoint != null) {
                    buryPoint.each { Map<String, Object> map ->
                        BuryPointEntity entity = new BuryPointEntity()
    
                        ...省略中间非关键代码,详细请到github中查看...
    
                        if (entity.isAnnotation) {
                            if (map.containsKey("annotationDesc")) {
                                entity.annotationDesc = map.get("annotationDesc")
                            }
                            if (map.containsKey("annotationParams")) {
                                entity.annotationParams = map.get("annotationParams")
                            }
                            BURY_POINT_MAP.put(entity.annotationDesc, entity)
                        } else {
                            if (map.containsKey("methodOwner")) {
                                entity.methodOwner = map.get("methodOwner")
                            }
                            if (map.containsKey("methodName")) {
                                entity.methodName = map.get("methodName")
                            }
                            if (map.containsKey("methodDesc")) {
                                entity.methodDesc = map.get("methodDesc")
                            }
                            BURY_POINT_MAP.put(entity.methodName + entity.methodDesc, entity)
                        }
                    }
                }
            }
        }
    }
    
    2、BuryPointTransform

    通过transform 方法的 Collection<TransformInput> inputs 对 .class文件遍历拿到所有方法

    class BuryPointTransform extends Transform {
    
        ...省略中间非关键代码,详细请到github中查看...
    
        /**
         *
         * @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)
                }
            }
        }
    
    }
    
    3、BuryPointClassVisitor

    通过visitMethod拿到方法进行修改

    class BuryPointVisitor extends ClassVisitor {
    
        ...省略中间非关键代码,详细请到github中查看...
    
        /**
         * 扫描类的方法进行调用
         * @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)
            if ((methodAccess & Opcodes.ACC_INTERFACE) == 0 && "<init>" != methodName && "<clinit>" != methodName) {
                methodVisitor = new BuryPointAdviceAdapter(api, methodVisitor, methodAccess, methodName, methodDescriptor)
            }
            return methodVisitor
        }
    
    }
    
    4、BuryPointAdviceAdapter

    终于到了本次文章的核心代码了。

    • visitAnnotation在扫描到注解时调用。我们通过 descriptor 来判断是否是需要埋点的注解,如果是则保存注解参数和对应的方法名称,等到onMethodEnter时进行代码插入。
    • visitInvokeDynamicInsn在描到lambda表达式时调用,bootstrapMethodArguments[0] 得到方法描述,通过 name + desc 判断当前lambda表达式是否是需要的埋点的方法,如果是则保存lambda方法名称,等到onMethodEnter时进行代码插入。
    • onMethodEnter在进入方法时调用,这里就是我们插入代码的地方了。通过 methodName + methodDescriptor 判断当前方法是否是需要的埋点的方法,如果是则插入埋点方法。
    ——重点,要考,画起来——
    1. mv.visitVarInsn(type.getOpcode(ISTORE), slotIndex)slotIndex 是怎么的来的呢?
      答:因为我们要通过visitVarInsn把注解参数压入到局部变量表中,而局部变量表(Local Variable Table)是一组变量值存储空间,用于存放 方法参数和方法内定义的局部变量。具体的顺序是 this-方法接收的参数-方法内定义的局部变量。因此我们要通过newLocal(type)来获取 slotIndex 按顺序把注解参数压入到局部变量表中。

    2. isStatic(methodAccess) ? 0 : 1 为什么 static 方法是0开始计算?
      答:对于非静态方法(non-static method)来说,索引位置为0的位置存放的是this变量,所以要加1;对于静态方法(static method)来说,索引位置为0的位置则不需要存储this变量。

    class BuryPointAdviceAdapter extends AdviceAdapter {
    
        ...省略中间非关键代码,详细请到github中查看...
    
        /**
         * 扫描类的注解时调用
         * @param descriptor 注解名称
         * @param visible
         * @return
         */
        @Override
        AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible)
            // 通过descriptor判断是否是需要扫描的注解
            BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(descriptor)
            if (entity != null) {
                BuryPointEntity newEntity = entity.clone()
                return new BuryPointAnnotationVisitor(api, annotationVisitor) {
                    @Override
                    void visit(String name, Object value) {
                        super.visit(name, value)
                        // 保存注解的参数值
                        newEntity.annotationData.put(name, value)
                    }
    
                    @Override
                    void visitEnd() {
                        super.visitEnd()
                        newEntity.methodName = methodName
                        newEntity.methodDesc = methodDesc
                        StatisticPlugin.BURY_POINT_MAP.put(newEntity.methodName + newEntity.methodDesc, newEntity)
                    }
                }
            }
            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]
            BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(name + desc)
            if (entity != null) {
                String parent = Type.getReturnType(descriptor).getDescriptor()
                if (parent == entity.methodOwner) {
                    Handle handle = (Handle) bootstrapMethodArguments[1]
                    BuryPointEntity newEntity = entity.clone()
                    newEntity.isLambda = true
                    newEntity.methodName = handle.getName()
                    newEntity.methodDesc = handle.getDesc()
                    StatisticPlugin.BURY_POINT_MAP.put(newEntity.methodName + newEntity.methodDesc, newEntity)
                }
            }
        }
    
        /**
         * 方法进入时调用
         */
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(methodName + methodDesc)
            if (entity != null && !entity.isMethodExit) {
                onMethod(entity)
            }
        }
    
        /**
         * 方法退出前调用
         */
        @Override
        protected void onMethodExit(int opcode) {
            BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(methodName + methodDesc)
            if (entity != null && entity.isMethodExit) {
                onMethod(entity)
            }
            super.onMethodExit(opcode)
        }
    
        private void onMethod(BuryPointEntity entity) {
            if (entity.isAnnotation) {
                // 遍历注解参数并赋值给采集方法
                for (Map.Entry<String, String> entry : entity.annotationParams.entrySet()) {
                    String key = entry.getKey()
                    if (key == "this") {
                        //所在方法的当前对象的引用
                        mv.visitVarInsn(ALOAD, 0)
                    } else {
                        mv.visitLdcInsn(entity.annotationData.get(key))
                        Type type = Type.getType(entry.getValue())
                        int slotIndex = newLocal(type)
                        mv.visitVarInsn(type.getOpcode(ISTORE), slotIndex)
                        mv.visitVarInsn(type.getOpcode(ILOAD), slotIndex)
                    }
                }
                mv.visitMethodInsn(INVOKESTATIC, entity.agentOwner, entity.agentName, entity.agentDesc, false)
                // 防止其他类重名方法被插入
                StatisticPlugin.BURY_POINT_MAP.remove(methodName + methodDesc, entity)
            } else {
                // 获取方法参数
                Type methodType = Type.getMethodType(methodDesc)
                Type[] methodArguments = methodType.getArgumentTypes()
                // 采集数据的方法参数起始索引( 0:this,1+:普通参数 ),如果是static,则从0开始计算
                int slotIndex = (methodAccess & ACC_STATIC) != 0 ? 0 : 1
                // 获取采集方法参数
                Type agentMethodType = Type.getMethodType(entity.agentDesc)
                Type[] agentArguments = agentMethodType.getArgumentTypes()
                List<Type> agentArgumentList = new ArrayList<Type>(Arrays.asList(agentArguments))
                // 将扫描方法参数赋值给采集方法
                for (Type methodArgument : methodArguments) {
                    int size = methodArgument.getSize()
                    int opcode = methodArgument.getOpcode(ILOAD)
                    String descriptor = methodArgument.getDescriptor()
                    Iterator<Type> agentIterator = agentArgumentList.iterator()
                    // 遍历采集方法参数
                    while (agentIterator.hasNext()) {
                        Type agentArgument = agentIterator.next()
                        String agentDescriptor = agentArgument.getDescriptor()
                        if (agentDescriptor == descriptor) {
                            mv.visitVarInsn(opcode, slotIndex)
                            agentIterator.remove()
                            break
                        }
                    }
                    slotIndex += size
                }
                if (agentArgumentList.size() > 0) { // 无法满足采集方法参数则return
                    return
                }
                mv.visitMethodInsn(INVOKESTATIC, entity.agentOwner, entity.agentName, entity.agentDesc, false)
                if (entity.isLambda) {
                    StatisticPlugin.BURY_POINT_MAP.remove(methodName + methodDesc, entity)
                }
            }
        }
    
    }
    
    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'
    }
    
    statistic {
        buryPoint = [
                [
                        //注解标识
                        'isAnnotation'    : true,
                        //方式插入时机,true方法退出前,false方法进入时
                        'isMethodExit'    : true,
                        //采集数据的方法的路径
                        'agentOwner'      : 'com/example/fragment/library/common/utils/StatisticHelper',
                        //采集数据的方法名
                        'agentName'       : 'testAnnotation',
                        //采集数据的方法描述(对照annotationParams,注意参数顺序)
                        'agentDesc'       : '(Ljava/lang/Object;ILjava/lang/String;)V',
                        //扫描的注解名称
                        'annotationDesc'  : 'Lcom/example/fragment/library/common/utils/TestAnnotation;',
                        //扫描的注解的参数
                        'annotationParams': [
                                //参数名 : 参数类型(对应的ASM指令,加载不同类型的参数需要不同的指令)
                                //this  : 所在方法的当前对象的引用(默认关键字,按需可选配置)
                                'this'   : 'Ljava/lang/Object;',
                                'code'   : 'I',
                                'message': 'Ljava/lang/String;',
                        ]
                ],
        ]
    }
    
    6、 运行项目查看输出日志
    2021-06-28 20:04:49.544 25211-25211/com.example.fragment.project.debug I/----------自动埋点:注解: MainActivity.onCreate:false
    2021-06-28 20:05:06.085 25211-25211/com.example.fragment.project.debug I/----------自动埋点:  ViewId:coin ViewText:我的积分
    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:登录
    

    参考

    在AndroidStudio中自定义Gradle插件
    史上最通俗易懂的ASM教程
    Android函数插桩(Gradle + ASM)

    Thanks

    以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
    如果喜欢的话希望点个赞吧,您的鼓励是我前进的动力。
    谢谢~~

    项目地址

    相关文章

      网友评论

        本文标题:最通俗易懂的字节码插桩实战(Gradle + ASM)—— 自

        本文链接:https://www.haomeiwen.com/subject/vikcultx.html