Android全埋點解決方案(ASM+Transform 二)

前言

之前寫過Android全埋點解決方案(ASM 一 Transform),但是這個實際上只有transform,沒有asm相關(guān)的。它只是使用transform遍歷下文件而已。今天會使用到ASM做插樁。

一、ASM

是一個功能比較齊全的java字節(jié)碼操作與分析框架。通過使用ASM框架,我們可以動態(tài)生產(chǎn)類或者增強既有類的功能。ASM可以直接生成二進制.class文件,也可以在類被jvm加載前,動態(tài)的改變現(xiàn)有類的行為。Java的二進制被存儲在嚴格格式定義.class文件里面,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息用來表示類中的所有元素,包括名稱、方法、屬性以及java字節(jié)碼指令。ASM從字節(jié)碼文件中讀入這些信息后,能夠改變類的行為、分析類的信息,甚至能夠根據(jù)具體的要求生成新的類。

二、簡單介紹ASM幾個核心類
  • ClassReader. 改類主要用來解析編譯過的.class字節(jié)碼文件
  • ClassWriter 該類用來構(gòu)建重新編譯后的類,比如修改類的類名、方法、屬性,甚至是生成新的類字節(jié)碼文件。
  • ClassVisitor. 主要負責“拜訪”類成員信息。其中包括表記在類上面的注解、類的構(gòu)造、類的字段、類的方法、靜態(tài)代碼塊等。
  • AdviceAdapter 實現(xiàn)了MethodVisitor接口,主要負責拜訪方法的信息,用來進行具體的方法字節(jié)碼操作。
三、ASM+Transform 點擊事件插樁原理

我們可以自定義一個Gradle Plugin,然后注冊一個Transform對象。在transform方法里面可以分別遍歷目標和jar包,然后我們就可以遍歷當前應用程序所有的.class文件。然后再利用ASM框架的相關(guān)API,去加載相應的.class文件,就可以找到特定滿足特定條件的.class文件和相關(guān)方法,最后去修改相應的方法以動態(tài)插入埋點字節(jié)碼,從而達到自動埋點的效果。

四、實現(xiàn)
  • 把埋點做成一個sdk,代碼在https://github.com/yangzai100/ASTDemo/tree/master 里面master分支的sdk中。然后依賴到主app中,并初始化。
  • 創(chuàng)建一個android Library module,名稱叫:plugin
  • 清空plugin.gradle,修改成如下內(nèi)容
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"

    }
}
  • 創(chuàng)建groovy目錄
    清空plugin/src/main目錄下所有的文件。然后在plugin/src下面創(chuàng)建groovy目錄,在里面創(chuàng)建一個package,比如com.sensorsdata.analytics.android.plugin

  • 新建Transform類. 代碼關(guān)鍵地方都有注釋

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<>()
                            /** 遍歷以某一擴展名結(jié)尾的文件*/
                            dir.traverse(type: FileType.FILES,nameFilter : ~/.*\.class/){
                                File classFile ->
                                    /**排除sdk和support系統(tǒng)包 R相關(guān)的和系統(tǒng)相關(guān)的 提高編譯速度*/
                                    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文件字節(jié)數(shù)組,調(diào)用modifyClass進行修改,再將修改后的byte數(shù)組生成.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 掃描類的第一個調(diào)用的方法
    @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 代表類的名稱  字節(jié)碼是以/表示路徑的:a/b/c/MyClass  也不需要寫.class
        //signature 表示泛型,如果類沒有定義泛型,表示為null
        //supername  表示當前類所繼承的父類。普通類我們雖然沒有寫父類,但是jdk編譯的時候會加上去
        //interfaces  表示類所實現(xiàn)的接口列表
        //visitorMethod 剛方法是當掃描器掃描到方法的時候調(diào)用
        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  表示泛型相關(guān)的信息



        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
    }

    /**
     * 獲取方法參數(shù)下標為 index 的對應 ASM index
     * @param types 方法參數(shù)類型數(shù)組
     * @param index 方法中參數(shù)下標,從 0 開始
     * @param isStaticMethod 該方法是否為靜態(tài)方法
     * @return 訪問該方法的 index 位參數(shù)的 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文件 讓系統(tǒng)找到Plugin
    在plugin/src/main目錄下依次新建目錄resources/META-INF/gradle-plugins,然后在改目錄下新建文件com.sensorsdata.android.properties,其中com.sensorsdata.android就是我們的插件名稱。文件內(nèi)容如下:
implementation-class=com.sensorsdata.analytics.android.plugin.SensorsAnalyticsPlugin
  • 構(gòu)建插件 ./gradlew uploadArchives命令構(gòu)建或者點擊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后再去斷點。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容