Android 编译期的黑科技(三)- 字节码篇

字节码织入

可以绕过编译,直接操作字节码,从而实现代码注入。所以使用 Javassist 的时机就是在构建工具 Gradle 将源 文件编译成 .class 文件之后,在将 .class 打包成 .dex 文件之前。也有两个相当成熟的字节码框架可供使用。使用起来大同小异这节主要介绍ASM

ASM

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

Javaassist

java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。与ASM的主要区别在于不能生成新类

优点

  • 功能强大 几乎能完成所有需求

缺点

  • 需要对字节码有一定了解 有一定技术壁垒

使用

依赖

AMS 是以插件形式被引用到项目中的 最简单的方式莫过于写在 buildSrc文件夹中 会自动被Gradle识别为插件

具体使用

在这里会用一个简单的Hook View.onClickListener() 的例子来讲解用法 完整的demo请点这里 ASMDemo

Plugin

首先要注册到Gradle Transform的回调中

 class AsmTran implements Plugin<Project> {

    @Override
    void apply(Project project) {
        AppExtension appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new ClickTransform())
    }
}

Transform

确定要对哪些类进行处理 Android中通常要排除R文件以及androidSDK

class ClickTransform extends Transform {
 @Override
    void transform(TransformInvocation transformInvocation) {
        transformInvocation.inputs.each {
            TransformInput input ->
                input.directoryInputs.each {
                    DirectoryInput directoryInput ->
                   //                  dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                            File inputFile ->
                                waitableExecutor.execute(new Callable<Object>() {
                                    @Override
                                    Object call() throws Exception {
                                        File modified = modifyClassFile(dir, inputFile, context.getTemporaryDir())
                                        if (modified != null) {
                                            File target = new File(inputFile.absolutePath.replace(srcDirPath, destDirPath))
                                            if (target.exists()) {
                                                target.delete()
                                            }
                                            FileUtils.copyFile(modified, target)
                                            modified.delete()
                                        }
                                        return null
                                    }
                                })
                        }
                       
                        }}}
 
    }
}

ClassVisitor

重头戏来了 在这个类里会进行具体.class的解析 确定那个方法会被织入

class ClickClassVisitor extends ClassVisitor implements Opcodes {
...

...
//扫描方法
    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
        String nameDesc = name + descriptor

        if (nameDesc == 'onClick(Landroid/view/View;)V') {
            println("插入!")
            methodVisitor = new ClickMethodVisitor(methodVisitor, access, name, descriptor)
        }
        return methodVisitor
    }

    ...
    }

AdviceAdapter

具体织入的代码会在这里

class ClickMethodVisitor extends AdviceAdapter {
  ...
  //会在实际方法前织入代码
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            methodVisitor.visitVarInsn(ALOAD, 1)
            methodVisitor.visitLdcInsn("log hook")
            methodVisitor.visitLdcInsn("start")
            methodVisitor.visitMethodInsn(INVOKESTATIC, Log, "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
        }
  //会在实际方法后织入代码
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode)
            methodVisitor.visitVarInsn(ALOAD, 1)
            methodVisitor.visitMethodInsn(INVOKESTATIC, click, "trackViewOnClick", "(Landroid/view/View;)V", false)
            methodVisitor.visitVarInsn(ALOAD, 1)
            methodVisitor.visitLdcInsn("log hook")
            methodVisitor.visitLdcInsn("end")
            methodVisitor.visitMethodInsn(INVOKESTATIC, Log, "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)

        }
...
    }

字节码基础

要精通ASM使用最终还是要了解字节码的生成 这里推荐一个插件ASM ByteCode Outline 可以将java 直接转成字节码文件 可以根据需求直接ctrl-c+ctrl-v 是不是很轻松呢

总结

字节码织入相对使用AOP生成代码使用难度稍高,至少需要对字节码的转换有一定了解。但是经过短暂学习之后会发现这个方式几乎是无所不能的,非常好用。