美文网首页高级Android进阶gradle
Gradle+ASM实战——进阶篇

Gradle+ASM实战——进阶篇

作者: Peakmain | 来源:发表于2021-06-16 10:23 被阅读0次

    前言

    • 上篇文章我写了入门篇:Gradle 插件 + ASM 实战——入门篇,对gradle+ASM不熟的大家可以去上篇文章查看

    • github地址:https://github.com/Peakmain/AsmActualCombat

    • Gradle Transform
      Gradle Transform是Android官方提供给开发者在项目构建阶段(.class -> .dex转换期间)用来修改.class文件的一套标准API,即把输入的.class文件转变成目标字节码文件


      image.png
    • ClassVisitor
      访问类的成员信息

    模板搭建

    • 修改访问入口BuryPointPlugin
    package com.peakmain.analytics.plugin
    
    import com.android.build.gradle.AppExtension
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    class BuryPointPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            BuryPointExtension extension = project.extensions.create("peakmainPlugin", BuryPointExtension)
            boolean disableBuryPointPlugin = false
            Properties properties = new Properties()
            //gradle.properties是否存在
            if(project.rootProject.file('gradle.properties').exists()){
                 //gradle.properties文件->输入流
                properties.load(project.rootProject.file('gradle.properties').newDataInputStream())
                disableBuryPointPlugin=Boolean.parseBoolean(properties.getProperty("peakmainPlugin.disableAppClick","false"))
            }
            //如果disableBuryPointPlugin可用
            if(!disableBuryPointPlugin){
                AppExtension appExtension = project.extensions.findByType(AppExtension.class)
                appExtension.registerTransform(new BuryPointTransform(project,extension))
            }else{
                println("------------您已关闭了埋点插件--------------")
            }
        }
    }
    
    • BuryPointTransform
    package com.peakmain.analytics.plugin
    
    import com.android.build.api.transform.Context
    import com.android.build.api.transform.DirectoryInput
    import com.android.build.api.transform.Format
    import com.android.build.api.transform.JarInput
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.Transform
    import com.android.build.api.transform.TransformException
    import com.android.build.api.transform.TransformInput
    import com.android.build.api.transform.TransformInvocation
    import com.android.build.api.transform.TransformOutputProvider
    import com.android.build.gradle.internal.pipeline.TransformManager
    import org.objectweb.asm.ClassVisitor
    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.FileUtils
    import org.apache.commons.io.IOUtils
    import org.objectweb.asm.ClassReader
    import org.objectweb.asm.ClassWriter
    import org.gradle.api.Project
    import java.util.jar.JarEntry
    import java.util.jar.JarFile
    import java.util.jar.JarOutputStream
    import java.util.zip.ZipEntry
    
    class BuryPointTransform extends Transform {
        private static Project project
        private BuryPointExtension buryPointExtension
    
        BuryPointTransform(Project project, BuryPointExtension buryPointExtension) {
            this.project = project
            this.buryPointExtension = buryPointExtension
        }
    
        @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
        }
    
        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation)
            _transform(transformInvocation.context, transformInvocation.inputs, transformInvocation.outputProvider)
        }
        /**
         *
         * @param context
         * @param inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
         * @param outputProvider 输出路径
         */
        void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider) 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)
                }
            }
        }
    
        void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
            if (directoryInput.file.isDirectory()) {
                directoryInput.file.eachFileRecurse { File file ->
                    String name = file.name
                    if (filterClass(name)) {
                        // 用来读 class 信息
                        ClassReader classReader = new ClassReader(file.bytes)
                        // 用来写
                        ClassWriter classWriter = new ClassWriter(0 /* flags */)
                        //todo 改这里就可以了
                        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()
                    }
                }
            }
            // 把修改好的数据,写入到 output
            def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                    directoryInput.scopes, Format.DIRECTORY)
            FileUtils.copyDirectory(directoryInput.file, dest)
        }
    
        void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
            if (jarInput.file.absolutePath.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 + "classes_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(0)
                        //todo 改这里就可以了
                        ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
                        // 下面还可以包多层
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                        byte[] code = classWriter.toByteArray()
                        jarOutputStream.write(code)
                    } else {
                        jarOutputStream.putNextEntry(zipEntry)
                        jarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }
                    jarOutputStream.closeEntry()
                }
                //结束
                jarOutputStream.close()
                jarFile.close()
                def dest = outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(tmpFile, dest)
                tmpFile.delete()
            }
        }
    
        boolean filterClass(String className) {
            return (className.endsWith(".class") && !className.startsWith("R\$")
                    && "R.class" != className && "BuildConfig.class" != className)
        }
    }
    

    不同项目只需要TODO位置就可以了

    • BuryPointVisitor
    class BuryPointVisitor extends ClassVisitor {
    
        private ClassVisitor classVisitor
        private String[] mInterfaces
        BuryPointVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM6, classVisitor)
            this.classVisitor = 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) {
                println("name->"+name+",superName->"+superName)
            super.visit(version, access, name, signature, superName, interfaces)
            this.mInterfaces=interfaces
        }
    }
    
    • 编译结果


      image.png

    ClassVisitor方法详解

    • visit方法:扫描类的时候会进入这里,它的作用:可以替换一些类,比如ImageView
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            if(superName.equals("android/widget/ImageView")
                    && !name.equals("com/peakmain/PeakmainImageView")){
              superName = "com/peakmain/PeakmainImageView";
            }
            super.visit(version, access, name, signature, superName, interfaces);
        }
    
    • visitMethod:扫描到方法的时候调用
        /**
         * 扫描类的方法进行调用
         * @param access 修饰符
         * @param name 方法名字
         * @param descriptor 方法签名
         * @param signature 泛型信息
         * @param exceptions 抛出的异常
         * @return
         */
        @Override
        MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
            println("name->"+name+"----------------------descriptor->"+descriptor)
            return methodVisitor
        }
    
    • visitVarInsn
      aload 0 相当于字节码mv.visitVarInsn(ALOAD, 0);加载局部变量表下标0位置对象到操作数栈

    • visitMethodInsn
      执行方法

    private final static String SDK_API_CLASS = "com/peakmain/sdk/SensorsDataAutoTrackHelper"
      methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
    

    等价于:执行SensorsDataAutoTrackHelper的静态方法trackViewOnClick参数是View view

    • onMethodEnter
      方法执行之前插入字节码代码
                protected void onMethodEnter() {
                    super.onMethodEnter()
                    if(name == "sendMessageAtTime"){
                        println("进入sendMessageAtTime")
                        methodVisitor.visitLdcInsn("TAG");
                        methodVisitor.visitLdcInsn("PeakmainHandler->sendMessageAtTime")
                        methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
    
                    }
    }
    

    相当于在SendMessageAtTime方法之前插入了

     Log.e("TAG", "PeakmainHandler->sendMessageAtTime");
    
    image.png
    • visitInsn:方法return返回之前插入字节码,
    • visitCode:方法调用前插入字节码,并在onMethodEnter之后插入字节码

    实战

    注解方式获取方法消耗的时间

        private void getMessageStartCostTime(MethodVisitor methodVisitor) {
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 1)
            Label label1 = new Label()
            methodVisitor.visitLabel(label1)
        }
        private void getMessageEndCostTime(MethodVisitor methodVisitor, String name) {
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LLOAD, 1)
            methodVisitor.visitInsn(LSUB)
            methodVisitor.visitVarInsn(LSTORE, 2)
            Label label2 = new Label();
            methodVisitor.visitLabel(label2)
            methodVisitor.visitLdcInsn("LogMessageCostTime")
            methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
            methodVisitor.visitLdcInsn(name + "消耗的时间:")
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitVarInsn(LLOAD, 2)
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false)
            methodVisitor.visitInsn(POP);
            Label label3 = new Label()
            methodVisitor.visitLabel(label3)
        }
    

    编译之后结果:


    image.png

    实现点击事件的埋点
    这里只截取部分代码,完整代码可查看github

        protected void onMethodEnter() {
                    super.onMethodEnter()
    
                    /**
                     * 在 android.gradle 的 3.2.1 版本中,针对 view 的 setOnClickListener 方法 的 lambda 表达式做特殊处理。
                     */
                    BuryPointMethodCell lambdaMethodCell = mMethodCells.get(nameDesc)
                    if (lambdaMethodCell != null) {
                        Type[] types = Type.getArgumentTypes(lambdaMethodCell.desc)
                        int length = types.length
                        Type[] lambdaTypes = Type.getArgumentTypes(descriptor)
                        int paramStart = lambdaTypes.length - length
                        if (paramStart < 0) {
                            return
                        } else {
                            for (int i = 0; i < length; i++) {
                                if (lambdaTypes[paramStart + i].descriptor != types[i].descriptor) {
                                    return
                                }
                            }
                        }
                        boolean isStaticMethod = SensorsAnalyticsUtils.isStatic(access)
                        if (!isStaticMethod) {
                            if (lambdaMethodCell.desc == '(Landroid/view/MenuItem;)Z') {
                                methodVisitor.visitVarInsn(ALOAD, 0)
                                methodVisitor.visitVarInsn(ALOAD, getVisitPosition(lambdaTypes, paramStart, isStaticMethod))
                                methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, '(Ljava/lang/Object;Landroid/view/MenuItem;)V', false)
                                return
                            }
                        }
    
                        for (int i = paramStart; i < paramStart + lambdaMethodCell.paramsCount; i++) {
                            methodVisitor.visitVarInsn(lambdaMethodCell.opcodes.get(i - paramStart), getVisitPosition(lambdaTypes, i, isStaticMethod))
                        }
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, lambdaMethodCell.agentDesc, false)
                        return
                    }
    
                    if (nameDesc == 'onContextItemSelected(Landroid/view/MenuItem;)Z' ||
                            nameDesc == 'onOptionsItemSelected(Landroid/view/MenuItem;)Z') {
                        methodVisitor.visitVarInsn(ALOAD, 0)
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Ljava/lang/Object;Landroid/view/MenuItem;)V", false)
                    }
    
                    if (isSensorsDataTrackViewOnClickAnnotation) {
                        if (desc == '(Landroid/view/View;)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                            return
                        }
                    }
    
                    if ((mInterfaces != null && mInterfaces.length > 0)) {
                        if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V')) {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                        } else if (mInterfaces.contains('android/content/DialogInterface$OnClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;I)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ILOAD, 2)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;I)V", false)
                        } else if (mInterfaces.contains('android/content/DialogInterface$OnMultiChoiceClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;IZ)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ILOAD, 2)
                            methodVisitor.visitVarInsn(ILOAD, 3)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;IZ)V", false)
                        } else if (mInterfaces.contains('android/widget/CompoundButton$OnCheckedChangeListener') && nameDesc == 'onCheckedChanged(Landroid/widget/CompoundButton;Z)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ILOAD, 2)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/CompoundButton;Z)V", false)
                        } else if (mInterfaces.contains('android/widget/RatingBar$OnRatingBarChangeListener') && nameDesc == 'onRatingChanged(Landroid/widget/RatingBar;FZ)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                        } else if (mInterfaces.contains('android/widget/SeekBar$OnSeekBarChangeListener') && nameDesc == 'onStopTrackingTouch(Landroid/widget/SeekBar;)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                        } else if (mInterfaces.contains('android/widget/AdapterView$OnItemSelectedListener') && nameDesc == 'onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ALOAD, 2)
                            methodVisitor.visitVarInsn(ILOAD, 3)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
                        } else if (mInterfaces.contains('android/widget/TabHost$OnTabChangeListener') && nameDesc == 'onTabChanged(Ljava/lang/String;)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackTabHost", "(Ljava/lang/String;)V", false)
                        } else if (mInterfaces.contains('android/widget/AdapterView$OnItemClickListener') && nameDesc == 'onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ALOAD, 2)
                            methodVisitor.visitVarInsn(ILOAD, 3)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
                        } else if (mInterfaces.contains('android/widget/ExpandableListView$OnGroupClickListener') && nameDesc == 'onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ALOAD, 2)
                            methodVisitor.visitVarInsn(ILOAD, 3)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewGroupOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;I)V", false)
                        } else if (mInterfaces.contains('android/widget/ExpandableListView$OnChildClickListener') && nameDesc == 'onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z') {
                            methodVisitor.visitVarInsn(ALOAD, 1)
                            methodVisitor.visitVarInsn(ALOAD, 2)
                            methodVisitor.visitVarInsn(ILOAD, 3)
                            methodVisitor.visitVarInsn(ILOAD, 4)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewChildOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;II)V", false)
                        }
                    }
    
                }
    
    • 参考书籍:《Android全埋点解决方案》

    相关文章

      网友评论

        本文标题:Gradle+ASM实战——进阶篇

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