美文网首页
Android全埋点解决方案(ASM+Transform 二)

Android全埋点解决方案(ASM+Transform 二)

作者: 旺仔_100 | 来源:发表于2021-09-12 15:54 被阅读0次
    前言

    之前写过Android全埋点解决方案(ASM 一 Transform),但是这个实际上只有transform,没有asm相关的。它只是使用transform遍历下文件而已。今天会使用到ASM做插桩。

    一、ASM

    是一个功能比较齐全的java字节码操作与分析框架。通过使用ASM框架,我们可以动态生产类或者增强既有类的功能。ASM可以直接生成二进制.class文件,也可以在类被jvm加载前,动态的改变现有类的行为。Java的二进制被存储在严格格式定义.class文件里面,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括名称、方法、属性以及java字节码指令。ASM从字节码文件中读入这些信息后,能够改变类的行为、分析类的信息,甚至能够根据具体的要求生成新的类。

    二、简单介绍ASM几个核心类
    • ClassReader. 改类主要用来解析编译过的.class字节码文件
    • ClassWriter 该类用来构建重新编译后的类,比如修改类的类名、方法、属性,甚至是生成新的类字节码文件。
    • ClassVisitor. 主要负责“拜访”类成员信息。其中包括表记在类上面的注解、类的构造、类的字段、类的方法、静态代码块等。
    • AdviceAdapter 实现了MethodVisitor接口,主要负责拜访方法的信息,用来进行具体的方法字节码操作。
    三、ASM+Transform 点击事件插桩原理

    我们可以自定义一个Gradle Plugin,然后注册一个Transform对象。在transform方法里面可以分别遍历目标和jar包,然后我们就可以遍历当前应用程序所有的.class文件。然后再利用ASM框架的相关API,去加载相应的.class文件,就可以找到特定满足特定条件的.class文件和相关方法,最后去修改相应的方法以动态插入埋点字节码,从而达到自动埋点的效果。

    四、实现
    • 把埋点做成一个sdk,代码在https://github.com/yangzai100/ASTDemo/tree/master 里面master分支的sdk中。然后依赖到主app中,并初始化。
    • 创建一个android Library module,名称叫:plugin
    • 清空plugin.gradle,修改成如下内容
    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
    
        implementation 'org.ow2.asm:asm:7.1'
        implementation 'org.ow2.asm:asm-commons:7.1'
    //    compile 'org.ow2.asm:asm-analysis:7.1'
    //    compile 'org.ow2.asm:asm-util:7.0'
    //    compile 'org.ow2.asm:asm-tree:7.1'
        compileOnly 'com.android.tools.build:gradle:3.4.1'
    
    }
    
    repositories {
        jcenter()
    }
    
    uploadArchives{
        repositories.mavenDeployer{
            //本地仓库路径,以放到项目根目录下的repo的文件夹为列子
            repository(url:uri('../repo'))
            //groupId 自定定义
            pom.groupId = "com.sensorsdata"
            //artifactId
            pom.artifactId = "autotrack.android"
            //插件版本号
            pom.version = "1.1.5"
    
        }
    }
    
    • 创建groovy目录
      清空plugin/src/main目录下所有的文件。然后在plugin/src下面创建groovy目录,在里面创建一个package,比如com.sensorsdata.analytics.android.plugin

    • 新建Transform类. 代码关键地方都有注释

    package com.sensorsdata.analytics.android.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.QualifiedContent
    import com.android.build.api.transform.TransformException
    import com.android.build.api.transform.TransformInput
    import com.android.build.api.transform.TransformOutputProvider
    import com.android.build.gradle.internal.pipeline.TransformManager
    import groovy.io.FileType
    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.FileUtils
    import org.gradle.api.Project
    import com.android.build.api.transform.Transform
    
    public class SensorAnalyticsTransform extends Transform{
            private static Project project;
    
        public SensorAnalyticsTransform(Project project) {
            this.project = project;
        }
    
    
    
        @Override
        String getName() {
            return "sensorsAnalytics"
        }
    
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        @Override
        boolean isIncremental() {
            return false
        }
    
        @Override
        void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                       TransformOutputProvider outputProvider, boolean isIncremental) throws IOException,
                TransformException, InterruptedException {
            super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
            print("我开始transform了")
    
            if (!incremental){
                outputProvider.deleteAll()
            }
    
            inputs.each {
                TransformInput input ->
                    //遍历目录
                    input.directoryInputs.each {
                        DirectoryInput directoryInput ->
                            /** 当前这个Transform 输出目录 */
                            File dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes,directoryInput.scopes, Format.DIRECTORY)
    
                            File dir = directoryInput.file
    
                            if (dir){
                                HashMap<String,File> modifyMap = new HashMap<>()
                                /** 遍历以某一扩展名结尾的文件*/
                                dir.traverse(type: FileType.FILES,nameFilter : ~/.*\.class/){
                                    File classFile ->
                                        /**排除sdk和support系统包 R相关的和系统相关的 提高编译速度*/
                                        if (SensorsAnalyticsClassModifier.isShouldModify(classFile.name)){
                                            /**
                                             * 修改.class文件,将修改后的.class文件放到一个HashMap中,然后将输入目录下的所有.class文件拷贝到输出目录,最后将
                                             * HashMap中修改的.class文件拷贝到输出目录,覆盖之前拷贝的.class文件(原.class文件)。*/
                                            File modified = SensorsAnalyticsClassModifier.modifyClassFile(dir,classFile,context.getTemporaryDir())
                                            if(modified != null){
                                                /**key 为包名+类名
                                                 * 如:/cn/sensorsdata/autotrack/android/app/MainActivity.class
                                                 */
                                                String key = classFile.absolutePath.replace(dir.absolutePath,"")
                                                modifyMap.put(key,modified)
                                            }
                                        }
                                }
    
                                FileUtils.copyDirectory(directoryInput.file,dest)
                                modifyMap.entrySet().each {
                                    Map.Entry<String,File> en ->
                                        File target = new File(dest.absolutePath + en.getKey())
                                        if(target.exists()){
                                            target.delete()
                                        }
                                        FileUtils.copyFile(en.getValue(),target)
                                        en.getValue().delete()
                                }
                            }
    
                    }
                    input.jarInputs.each {
                        String destName = it.file.name
                        /**截取文件路径对md5值重命名输出文件,因为可能同名,会覆盖*/
                        def hexName = DigestUtils.md5Hex(it.file.absolutePath).substring(0,8);
                        /*获取jar名字*/
                        if(destName.endsWith(".jar")){
                            destName = destName.substring(0,destName.length() - 4)
    
                        }
                        /**获取输出文件*/
                        File dest = outputProvider.getContentLocation(destName + "_" + hexName,
                        it.contentTypes,it.scopes,Format.JAR)
    
                        def modifiedJar = SensorsAnalyticsClassModifier.modifyJar(it.file,
                                context.getTemporaryDir(),true)
                        if (modifiedJar == null){
                            modifiedJar = it.file
                        }
                        FileUtils.copyFile(modifiedJar,dest)
                    }
            }
        }
    }
    

    会用到SensorsAnalyticsClassModifier类

    package com.sensorsdata.analytics.android.plugin
    
    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.IOUtils
    import org.objectweb.asm.ClassReader
    import org.objectweb.asm.ClassVisitor
    import org.objectweb.asm.ClassWriter
    
    import java.util.jar.JarEntry
    import java.util.jar.JarFile
    import java.util.jar.JarOutputStream
    import java.util.regex.Matcher
    
    class SensorsAnalyticsClassModifier {
        private static HashSet<String> exclude = new HashSet<>();
    
        static {
            exclude = new HashSet<>();
            exclude.add("android.support")
            exclude.add("com.sensorsdata.analytics.android.sdk")
        }
    
    
        static File modifyJar(File jarFile, File tempDir, boolean nameHex) {
            /**
             * 读取原 jar
             */
            def file = new JarFile(jarFile, false)
    
            /**
             * 设置输出到的 jar
             */
            def hexName = ""
            if (nameHex) {
                hexName = DigestUtils.md5Hex(jarFile.absolutePath).substring(0, 8)
            }
            def outputJar = new File(tempDir, hexName + jarFile.name)
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(outputJar))
            Enumeration enumeration = file.entries()
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                InputStream inputStream = null
                try {
                    inputStream = file.getInputStream(jarEntry)
                } catch (Exception e) {
                    return null
                }
                String entryName = jarEntry.getName()
                if (entryName.endsWith(".DSA") || entryName.endsWith(".SF")) {
                    //ignore
                } else {
                    String className
                    JarEntry jarEntry2 = new JarEntry(entryName)
                    jarOutputStream.putNextEntry(jarEntry2)
    
                    byte[] modifiedClassBytes = null
                    byte[] sourceClassBytes = IOUtils.toByteArray(inputStream)
                    if (entryName.endsWith(".class")) {
                        className = entryName.replace(Matcher.quoteReplacement(File.separator), ".").replace(".class", "")
                        if (isShouldModify(className)) {
                            modifiedClassBytes = modifyClass(sourceClassBytes)
                        }
                    }
                    if (modifiedClassBytes == null) {
                        modifiedClassBytes = sourceClassBytes
                    }
                    jarOutputStream.write(modifiedClassBytes)
                    jarOutputStream.closeEntry()
                }
            }
            jarOutputStream.close()
            file.close()
            return outputJar
        }
    
        protected static boolean isShouldModify(String className) {
            Iterator<String> iterator = exclude.iterator()
            while (iterator.hasNext()) {
                String packageName = iterator.next()
                if (className.startsWith(packageName)) {
                    return false
                }
            }
    
            if (className.contains('R$') || className.contains('R2$')
                    || className.contains('R.class') || className.contains('R2.class')
                    || className.contains('BuildConfig.class')) {
                return false
            }
    
            return true
        }
    
        private static byte[] modifyClass(byte[] srcClass) {
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
            ClassVisitor classVisitor = new SensorsAnalyticsClassVisitor(classWriter)
            ClassReader cr = new ClassReader(srcClass)
            cr.accept(classVisitor, ClassReader.SKIP_FRAMES)
            return classWriter.toByteArray()
        }
    
        /**
         * 先获取包名和类名,再获取.class文件字节数组,调用modifyClass进行修改,再将修改后的byte数组生成.class文件
         * @param dir
         * @param classFile
         * @param tempDir
         * @return
         */
        static File modifyClassFile(File dir, File classFile, File tempDir) {
            File modify = null
            try {
                String className = path2className(classFile.absolutePath.replace(dir.absolutePath + File.separator, ""))
                byte[] sourceClassBytes = IOUtils.toByteArray(new FileInputStream(classFile))
                byte[] modifiedClassBytes = modifyClass(sourceClassBytes)
                if (modifiedClassBytes) {
                    modify = new File(tempDir, className.replace(".", "") + ".class")
                    if (modify.exists())
                        modify.delete()
                }
                modify.createNewFile()
                new FileOutputStream(modify).write(modifiedClassBytes)
            } catch (Exception e) {
                e.printStackTrace()
                modify = classFile
            }
    
            return modify
        }
    
        static String path2className(String pathName) {
            pathName.replace(File.separator, ".").replace(".class", "")
        }
    }
    

    又会用到SensorsAnalyticsClassVisitor类

    package com.sensorsdata.analytics.android.plugin
    import org.objectweb.asm.AnnotationVisitor
    import org.objectweb.asm.ClassVisitor
    import org.objectweb.asm.Handle
    import org.objectweb.asm.MethodVisitor
    import org.objectweb.asm.Opcodes
    import org.objectweb.asm.Type
    
    
    class SensorsAnalyticsClassVisitor extends ClassVisitor implements Opcodes{
    
        private final
        static String SDK_API_CLASS = "com/sensorsdata/analytics/android/sdk/SensorsDataAutoTrackHelper"
        private ClassVisitor classVisitor
        private String[] mInterfaces
    
    
        private HashMap<String, SensorsAnalyticsMethodCell> mLambdaMethodCells = new HashMap<>()
        SensorsAnalyticsClassVisitor( ClassVisitor cv) {
            super(Opcodes.ASM6, cv)
            this.classVisitor = cv
    
        }
    
        ///Classvisitor 扫描类的第一个调用的方法
        @Override
        void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
            //version 表示jdk版本 例如51代码jdk1.7
            //access ACC_PUBLIC ACC_ 开头是常量
            //name 代表类的名称  字节码是以/表示路径的:a/b/c/MyClass  也不需要写.class
            //signature 表示泛型,如果类没有定义泛型,表示为null
            //supername  表示当前类所继承的父类。普通类我们虽然没有写父类,但是jdk编译的时候会加上去
            //interfaces  表示类所实现的接口列表
            //visitorMethod 刚方法是当扫描器扫描到方法的时候调用
            mInterfaces = interfaces
        }
    
        private
        static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List<Integer> paramOpcodes) {
            for (int i = start; i < start + count; i++) {
                methodVisitor.visitVarInsn(paramOpcodes[i - start], i)
            }
            methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false)
        }
    
    
    
        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
          //accss方法修饰符
            //name 表示方法名
            //desc  表示方法签名  举例  String[]    [Ljava/lang/String;      Class<?> Ljava/lang/Class
            //signature  表示泛型相关的信息
    
    
    
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
    
            String nameDesc = name + desc
    
            methodVisitor = new SensorsAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) {
                boolean isSensorsDataTrackViewOnClickAnnotation = false
    
                @Override
                void visitEnd() {
                    super.visitEnd()
    
                    if (mLambdaMethodCells.containsKey(nameDesc)) {
                        mLambdaMethodCells.remove(nameDesc)
                    }
                }
    
                @Override
                void visitInvokeDynamicInsn(String name1, String desc1, Handle bsm, Object... bsmArgs) {
                    super.visitInvokeDynamicInsn(name1, desc1, bsm, bsmArgs)
    
                    try {
                        String desc2 = (String) bsmArgs[0]
                        SensorsAnalyticsMethodCell sensorsAnalyticsMethodCell = SensorsAnalyticsHookConfig.LAMBDA_METHODS.get(Type.getReturnType(desc1).getDescriptor() + name1 + desc2)
                        if (sensorsAnalyticsMethodCell != null) {
                            Handle it = (Handle) bsmArgs[1]
                            mLambdaMethodCells.put(it.name + it.desc, sensorsAnalyticsMethodCell)
                        }
                    } catch (Exception e) {
                        e.printStackTrace()
                    }
                }
    
                /**
                 * 在原有的方法前面进行插桩
                 * 和他对应的有onMethodExit  在原有的方法后插桩
                 */
                @Override
                protected void onMethodEnter() {
                    super.onMethodEnter()
    
                    /**mLambdaMethodCells
                     * 在 android.gradle 的 3.2.1 版本中,针对 view 的 setOnClickListener 方法 的 lambda 表达式做特殊处理。
                     */
                    SensorsAnalyticsMethodCell lambdaMethodCell = mLambdaMethodCells.get(nameDesc)
                    if (lambdaMethodCell != null) {
                        Type[] types = Type.getArgumentTypes(lambdaMethodCell.desc)
                        int length = types.length
                        Type[] lambdaTypes = Type.getArgumentTypes(desc)
                        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
                        }
                    }
    
                    /**
                     * 包含OnclickListener 接口就做插桩 ,插入trackViewOnClick方法
                     */
                    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)
                        }
                    }
                }
    
                @Override
                AnnotationVisitor visitAnnotation(String s, boolean b) {
                    if (s == 'Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick;') {
                        isSensorsDataTrackViewOnClickAnnotation = true
                    }
    
                    return super.visitAnnotation(s, b)
                }
            }
            return methodVisitor
        }
    
        /**
         * 获取方法参数下标为 index 的对应 ASM index
         * @param types 方法参数类型数组
         * @param index 方法中参数下标,从 0 开始
         * @param isStaticMethod 该方法是否为静态方法
         * @return 访问该方法的 index 位参数的 ASM index
         */
        int getVisitPosition(Type[] types, int index, boolean isStaticMethod) {
            if (types == null || index < 0 || index >= types.length) {
                throw new Error("getVisitPosition error")
            }
            if (index == 0) {
                return isStaticMethod ? 0 : 1
            } else {
                return getVisitPosition(types, index - 1, isStaticMethod) + types[index - 1].getSize()
            }
        }
    }
    
    • 自定义plugin来注册transform,源码如下
    package com.sensorsdata.analytics.android.plugin
    
    import com.android.build.gradle.AppExtension
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    public class SensorsAnalyticsPlugin implements Plugin<Project>{
    //project ':app'
        @Override
        void apply(Project project) {
            AppExtension appExtension = project.extensions.findByType(AppExtension.class)
            appExtension.registerTransform(new SensorAnalyticsTransform(project))
        }
    }
    
    • 新建proprties文件 让系统找到Plugin
      在plugin/src/main目录下依次新建目录resources/META-INF/gradle-plugins,然后在改目录下新建文件com.sensorsdata.android.properties,其中com.sensorsdata.android就是我们的插件名称。文件内容如下:
    implementation-class=com.sensorsdata.analytics.android.plugin.SensorsAnalyticsPlugin
    
    • 构建插件 ./gradlew uploadArchives命令构建或者点击android studio右边的uploadArchives

    • 添加对插件的依赖
      在根gradle下面添加


      根gradle.png

    然后在app的gradle中

    apply plugin: 'com.sensorsdata.android'
    

    OK,到这里就全部弄完了。
    build一下,在app下的build中查看


    image.png

    当然自己动手会遇到很多问题,例如groovy代码编辑器根本不会提示,只能在upload和编译时候才会报错。
    然后debug可以通过android stuido


    image.png

    断点不进去可以通过clean后再去断点。

    相关文章

      网友评论

          本文标题:Android全埋点解决方案(ASM+Transform 二)

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