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

前言

之前写过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 getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection inputs, Collection 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 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 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 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 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 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 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 ':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 二))