ASM之自动采集点击事件

1,998 阅读3分钟

比如自动采集点击事件:其实就是在transform中遍历当前应用的所有.class文件,然后用ASM相关API去加载和解析相应的.class文件,然后找到满足特定条件的.class文件和相关方法,最后就是动态修改相应的方法以动态插入埋点字节码。 1:创建transform

class DataTrackTransform extends Transform {
    private static Project project
    DataTrackTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "DataTrackTransform"
    }

    @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 {
        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 ->
                            if (SensorsAnalyticsClassModifier.isShouldModify(classFile.class.name)) {
                                File modified = SensorsAnalyticsClassModifier.modifyClassFile(dir, classFile, context.getTemporaryDir())
                                if (modified != null) {
                                    //key为包名+类名
                                    String ke = classFile.absolutePath.replace(dir.absolutePath, "")
                                    modifyMap.put(ke, 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 { JarInput jarInput ->
                String destName = jarInput.file.name
                //截取文件的md5值重命名输出文件,因为可能同名会覆盖
                def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
                //获取jar名字
                if (destName.endsWith(".jar")) {
                    destName = destName.substring(0, destName.length() - 4)
                }
                File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                def modifyJar = SensorsAnalyticsClassModifier.modifyJar(jarInput.file, context.getTemporaryDir(), true)
                if (modifyJar == null) {
                    modifyJar = jarInput.file
                }
                FileUtils.copyFile(modifyJar, dest)

            }
        }
    }
}

这里的关键点主要有以下几个: .isShouldModify:以类名为条件过滤掉一部分.class文件,主要是为了性能考虑.

modifyClassFile:开始修改class文件。它的具体实现如下

static File modifyClassFile(File dir, File classFile, File tempDir) {
    File modified = null
    try {
        //将绝对路径名中的/替换成.并且去掉文件名后缀.class
        String className = path2ClassName(classFile.absolutePath.replace(dir.absolutePath + File.separator, ""))
        //获取源文件的字节码
        byte[] sourceClassBytes = IOUtils.toByteArray(new FileInputStream(classFile))
        //修改字节码
        byte[] modifiedClassBytes = modifyClass(sourceClassBytes)
        if (modifiedClassBytes) {
            modified = new File(tempDir, className.replace('.', '') + '.class')
            if (modified.exists()) {
                modified.delete()
            }
            modified.createNewFile()
            new FileOutputStream(modified).write(modifiedClassBytes)
        }
    } catch (Exception e) {
        e.printStackTrace()
        modified = classFile
    }
    return modified
}

这里最重要的就是modifyClass方法了,它的实现也很简单,这里除了ClassVisitor需要定义之外其他的都是固定写法

private static byte[] modifyClass(byte[] srcClass) throws IOException {
    /**
     * ClassWriter.COMPUTE_MAXS:Flag to automatically compute the maximum stack size and the maximum
     * number of local variables of methods.
     */
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
    ClassVisitor classVisitor = new SensorsAnalyticsClassVisitor(classWriter)
    //解析源字节码
    ClassReader cr = new ClassReader(srcClass)
    cr.accept(classVisitor, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
}

在classVisitor中我们才开始真正的操作选定的类,它会首先执行visit方法,在这里我们会得到类的名字、修饰符(desc)、所实现的接口,父类的名字等等。然后会遍历类的变量和方法,这里重点说遍历方法,遍历到方法时会首先调用visitAnnotation(s,b)方法,在这里能得到注解类的修饰符(desc:)和是否运行时可见比如我定义了一个注解

那么它的参数s='Lplugin/custom/mwy/com/sdk/SensorsDataTrackViewOnClick;'而b=true,在visitAnnotation中如果想要拿到注解上的值,比如这里的value,还需要通过AnnotationVisitor来获取,

class MyAnnotationVisitor extends AnnotationVisitor {
    public String typeName
    public String id


    MyAnnotationVisitor(AnnotationVisitor av) {
        super(Opcodes.ASM6, av)
    }

    /**
     * 读取注解类型的值
     * @param name
     * @param value
     */
    @Override
    void visit(String name, Object value) {
        super.visit(name, value)
        if (name == "value")
            typeName = value
        else if (name == "id") {
            id = value
        }

    }

    /**
     * 注解枚举类型的值
     * @param name
     * @param desc
     * @param value
     */
    @Override
    void visitEnum(String name, String desc, String value) {
        super.visitEnum(name, desc, value)
    }

    @Override
    AnnotationVisitor visitAnnotation(String name, String desc) {
        return super.visitAnnotation(name, desc)
    }
    /**
     * 注解数组类型的值
     * @param name
     * @return
     */
    @Override
    AnnotationVisitor visitArray(String name) {
        return super.visitArray(name)
    }

    @Override
    void visitEnd() {
        super.visitEnd()
    }
}

从上面的代码我们可以看出如果想要获取之前定义的value的值,直接在visit方法中就可以获得。接下来我们一般会关注方法的结束和开始,比如这里在结束时打点,实现onMethodExit方法

@Override
protected void onMethodExit(int opcode) {
    super.onMethodExit(opcode)

    if (mInterfaces != null && mInterfaces.length > 0) {
        if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V') ||
                desc == '(Landroid/view/View;)V') {
            methodVisitor.visitVarInsn(ALOAD, 1)
            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
        } 
    }
}

这里的visitMethodInsn的第一个参数的取值解释如下:

指令 说明
invokeinterface 用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method)
invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class)
invokestatic 用以调用类方法(Invoke a class (static) method )
invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。(Invoke instance method; special handling for superclass, private, and instance initialization method invocations )
invokedynamic JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method)

比如下面的例子

public class JVMInstructionTest implements Runnable {
    public JVMInstructionTest() {
        System.out.println("constructor method");
    }

    private void s() {
        System.out.println("private method");
    }

    static void print() {
        System.out.println("static method");
    }

    void p() {
        System.out.println("instance method");
    }

    public void d(String str) {
        System.out.println("for method handle " + str);
    }

    static void ddd(String str) {
        System.out.println("static method for method handle " + str);
    }

    public static void main(String[] args) throws Throwable {
        /**
         * invoke special
         */
        JVMInstructionTest test = new JVMInstructionTest();
        /**
         * invoke special
         */
        test.s();
        /**
         * invoke virtual
         */
        test.p();
        /**
         * invoke static
         */
        print();
        /**
         * invoke interface
         */
        Runnable r = new JVMInstructionTest();
        r.run();
        /**
         * Java 8中,lambda表达式和默认方法时,底层会生成和使用invoke dynamic
         * invoke dynamic
         */
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        list.stream().forEach(System.out::println);

    }

    @Override
    public void run() {
        System.out.println("interface method");
    }
}

上面onMethodExit方法中的那些ifelse则是用于判断当前类所实现接口的方法,并且调用自己写的类中的相关方法,比如注解方式所调用的方法为

public static void trackViewOnClick(String value,String id) {
    try {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("$element_type", "Annotation");
        jsonObject.put("$element_id", id);
        if (!TextUtils.isEmpty(value))
        jsonObject.put("$element_content", value);
        SensorsDataAPI.getInstance().track("$AppClick", jsonObject);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这里是将注解的值用于埋点,其他同理。 2.创建plugin,步骤同之前 3.引入插件 4.构建应用实现埋点