Android字节码插桩实现(Gradle + ASM)

在Android编译过程中,往字节码里插入自定义的字节码,称为字节码插桩或函数插桩。

函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。应用到在Android中,可以用来做用行为统计、方法耗时统计等功能。

字节码实战

需求分析

需求:在Android应用中,记录每个页面的打开\关闭

一般来说就是记录Activity的创建和销毁(这里以Activity区分页面)。所以只要在Activity的onCreate()onDestroy()中插入对应的代码即可。

如何为Activity插入代码?
一个个写?不可能!毕竟我们是高(懒)效(惰)的程序员
写在BaseActivity中?好像可以,不过项目中如果有第三方的页面就显得有些无力了,而且不通用

我们希望实现一个可以自动在Activity的onCreate()onDestroy()中插入代码的工具,可以在任意工程中使用。

于是,自定义Gradle插件 + ASM(Java字节码操纵框架)便成为一个不错的选择

实现思路

Android打包过程自定义Gradle插件了解后发现,java文件会先转化为class文件,然后在转化为dex文件。而通过Gradle插件提供的Transform API,可以在编译成dex文件之前得到class文件。得到class文件之后,便可以通过ASM对字节码进行修改,即可完成字节码插桩

步骤如下:

(1)了解Android打包过程,找到插入点即class转换成dex过程

(2)了解自定义Gradle插件Transform API,在Transform#transform()中得到class文件

(3)找到FragmentActivityclass文件,通过ASM库,在onCreate()中插入代码(为什么是FragmentActivity而不是Activity后面会说到)

(4)将原文件替换为修改后的class文件

Android打包插桩点

class文件:java源文件经过javac后生成一种紧凑的8位字节的二进制流文件

插入点dex节点,表示将class文件打包到dex文件的过程,其输入包括class文件以及第三方依赖的class文件

关于Transform API:从1.5.0-beta1开始,Gradle插件包含一个Transform API,允许第三方插件在将编译后的类文件转换为dex文件之前对其进行操作

关于混淆:关于混淆可以不用当心。混淆其实是个ProguardTransform,在自定义的Transform之后执行。

动手实现

主要实现以下功能:

  • 自定义Gradle插件
  • 处理class文件
  • 替换

自定义Gradle插件

在Android Studio中自定义Gradle插件
Android Gradle Plugin打包Apk过程中的Transform API

目录结构

目录结构分为两部分:插件部分src/main/groovy中)、ASM部分src/main/java中)

lifecycle-plugin
  src
    main
      groovy
        cn.appblog.plugin.lifecycle
          LifecyclePlugin.groovy
      java
        cn.appblog.plugin.lifecycle
          LifecycleClassVisitor.java
          LifecycleOnCreateMethodVisitor.java
          LifecycleOnDestroyMethodVisitor.java
      resources
        META-INF
          gradle-plugins
            cn.appblog.gradle.properties
  build.gradle
build.gradle

build.gradle内容为

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()

    implementation 'com.android.tools.build:gradle:3.5.3'
}

//指定编译的编码
tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}

//group和version在后面使用自定义插件的时候会用到
group='cn.appblog.plugin'
version='0.0.1'

uploadArchives {
    repositories {
        mavenDeployer {
            //提交到远程服务器:
            // repository(url: "http://www.xxx.com/repos") {
            //    authentication(userName: "admin", password: "admin")
            // }
            //本地的Maven地址:当前工程下
            repository(url: uri('D:/repos'))
        }
    }
}
gradle-plugins

properties文件内容为

implementation-class=cn.appblog.plugin.lifecycle.LifecyclePlugin
LifecyclePlugin.groovy

继承Transform,实现Plugin接口,通过Transform#transform()得到Collection<TransformInput> inputs,里面有我们想要的class文件。

package cn.appblog.plugin.lifecycle

import com.android.annotations.NonNull
import com.android.build.api.transform.*
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
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.zip.ZipEntry

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES

class LifecyclePlugin extends Transform implements Plugin<Project> {

    @Override
    void apply(Project project) {
        //registerTransform
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(this)
    }

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

    @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(@NonNull TransformInvocation transformInvocation) {
        println '--------------- LifecyclePlugin visit start --------------- '
        def startTime = System.currentTimeMillis()
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //删除之前的输出
        if (outputProvider != null)
            outputProvider.deleteAll()
        //遍历inputs
        inputs.each { TransformInput input ->
            //遍历directoryInputs
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInputs(directoryInput, outputProvider)
            }

            //遍历jarInputs
            input.jarInputs.each { JarInput jarInput ->
                handleJarInputs(jarInput, outputProvider)
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
        println '--------------- LifecyclePlugin visit end --------------- '
        println "LifecyclePlugin cost : $cost s"
    }

    /**
     * 处理文件目录下的class文件
     */
    static void handleDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否是目录
        if (directoryInput.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (checkClassFile(name)) {
                    println '----------- deal with "class" file <' + name + '> -----------'
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new LifecycleClassVisitor(classWriter)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                } else {
                    println '----------- directory "class" file <' + name + '> need not deal -----------'
                }
            }
        }
        //处理完输入文件之后,要把输出给下一个任务
        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes,
                Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    /**
     * 处理Jar中的class文件
     */
    static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插桩class
                if (checkClassFile(entryName)) {
                    //class文件处理
                    println '----------- deal with "jar" class file <' + entryName + '> -----------'
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new LifecycleClassVisitor(classWriter)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    println '----------- jar "class" file <' + entryName + '> need not deal -----------'
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //结束
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

    /**
     * 检查class文件是否需要处理
     * @param fileName
     * @return
     */
    static boolean checkClassFile(String name) {
        //只处理需要的class文件
        return (name.endsWith(".class") && !name.startsWith("R\$")
                && !"R.class".equals(name) && !"BuildConfig.class".equals(name)
                && ("android/support/v4/app/FragmentActivity.class".equals(name) || "androidx/appcompat/app/AppCompatActivity.class".equals(name)))
    }

}

主要看方法transform(),通过参数inputs可以拿到所有的class文件。inputs中包括directoryInputsjarInputsdirectoryInputs为文件夹中的class文件,而jarInputs为jar包中的class文件。

对应两个处理方法LifecyclePlugin#handleDirectoryInput()LifecyclePlugin#handleJarInputs()

这两个方法都在做同一件事,就是遍历directoryInputsjarInputs,得到对应的class文件,然后交给ASM处理,最后覆盖原文件。

发现:在input.jarInputs中并没有android.jar,因为android.jar不在编译期,操作不了。本想在Activity中做处理,因为找不到android.jar,只好退而求其次选择android.support.v4.app中的FragmentActivity

处理class文件

handleDirectoryInputshandleJarInputs中,可以看到ASM的部分代码。这里以handleDirectoryInputs为例。

handleDirectoryInputs中ASM代码:

ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)

其中,关键处理类LifecycleClassVisitor

LifecycleClassVisitor

用于访问class的工具,在visitMethod()里对类名方法名进行判断是否需要处理。若需要,则交给MethodVisitor

package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleClassVisitor extends ClassVisitor implements Opcodes {

    private String mClassName;

    public LifecycleClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        //System.out.println("LifecycleClassVisitor : visit -----> started :" + name);
        this.mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        //System.out.println("LifecycleClassVisitor : visitMethod : " + name);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        //匹配FragmentActivity
        if ("android/support/v4/app/FragmentActivity".equals(this.mClassName) || "androidx/appcompat/app/AppCompatActivity".equals(this.mClassName)) {
            if ("onCreate".equals(name) ) {
                //处理onCreate
                System.out.println("LifecycleClassVisitor : change method ----> " + name);
                return new LifecycleOnCreateMethodVisitor(mv);
            } else if ("onDestroy".equals(name)) {
                //处理onDestroy
                System.out.println("LifecycleClassVisitor : change method ----> " + name);
                return new LifecycleOnDestroyMethodVisitor(mv);
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        //System.out.println("LifecycleClassVisitor : visit -----> end");
        super.visitEnd();
    }
}

visitMethod()中判断是否为FragmentActivity,且为方法onCreateonDestroy,然后交给LifecycleOnDestroyMethodVisitorLifecycleOnCreateMethodVisitor处理。

提示:ClassVisitor#visitMethod()只能访问当前类定义的method(一开始想访问父类的方法,陷入误区)
如,在MainActivity中只重写了onCreate(),没有重写onDestroy()。那么在visitMethod()中只会出现onCreate(),不会有onDestroy()

回到需求,我们希望在onCreate()中插入对应的代码,来记录页面被打开(这里通过Log代替)

Log.i("TAG", "-------> onCreate : " + this.getClass().getSimpleName());

于是,在LifecycleOnCreateMethodVisitor中如下处理(LifecycleOnDestroyMethodVisitorLifecycleOnCreateMethodVisitor相似)

LifecycleOnCreateMethodVisitor
package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleOnCreateMethodVisitor extends MethodVisitor {

    public LifecycleOnCreateMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM4, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        //方法执行前插入
        mv.visitLdcInsn("yezhou");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn("-------> onCreate : ");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }

    @Override
    public void visitInsn(int opcode) {
        //方法执行后插入
        /*
        if (opcode == Opcodes.RETURN) {
            mv.visitLdcInsn("TAG");
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("-------> onCreate : end :");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
        */
        super.visitInsn(opcode);
    }
}

只需要在visitCode()中插入上面的代码,即可实现onCreate()内容执行之前,先执行我们插入的代码。

如果想在onCreate()内容执行之后插入代码,该怎么做?和上面相似,只要在visitInsn()方法中插入对应的代码即可。代码如下:

@Override
public void visitInsn(int opcode) {
    //判断RETURN
    if (opcode == Opcodes.RETURN) {
        //在这里插入代码
        ...
    }
    super.visitInsn(opcode);
}

如果对字节码不是很了解,看到上面visitCode()中的代码可能会觉得既熟悉又陌生,那是ASM插入字节码的用法。
如果写不来,没关系,这里介绍一个插件ASM Bytecode Outline

LifecycleOnDestroyMethodVisitor
package cn.appblog.plugin.lifecycle;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LifecycleOnDestroyMethodVisitor extends MethodVisitor {

    public LifecycleOnDestroyMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM4, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        //方法执行前插入
        mv.visitLdcInsn("yezhou");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn("-------> onDestroy : ");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}
替换

class文件的插桩已经说完,剩下最后一步替换。眼尖的同学应该发现,代码上面已经出现过了。还是以LifecyclePlugin#handleDirectoryInputs()中的代码为例:

byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
      file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()

classWriter得到class修改后的byte流,然后通过流的写入覆盖原来的class文件(Jar包的覆盖会稍微复杂一点,这里就不细说了)

File.separator:文件的分隔符。不同系统分隔符可能不一样。如:同样一个文件,Windows下是C:\tmp\test.txt;Linux 下却是/tmp/test.txt

插件使用

工程目录下的build.gradle配置插件

buildscript {
    repositories {
        maven {  //本地Maven仓库地址
            url uri('D:/repos')
        }
    }
    dependencies {
        classpath 'cn.appblog.plugin:lifecycle-plugin:0.0.1'
    }
}

创建一个Android项目app,在app.gradle中引用插件

apply plugin: 'cn.appblog.gradle'

运行后,按步骤操作:打开MainActivity —> 打开SecondActivity —> 返回MainActivity

查看效果:

cn.appblog.asmdemo I/yezhou: -------> onCreate : MainActivity
cn.appblog.asmdemo I/yezhou: -------> onCreate : SecondActivity
cn.appblog.asmdemo I/yezhou: -------> onDestroy : SecondActivity

可以发现,页面打开\关闭都会打印对应的log。说明我们插入的代码被执行了,而且,使用时对项目没有任何“入侵”。

结语

本文内容涉及知识较多,在熟悉Android打包过程字节码Gradle Transform APIASM等之前,阅读起来会很困难。不过,在了解并学习这些知识的之后,相信你对Android会有新的认识。

本文转载至:https://www.jianshu.com/p/16ed4d233fd1

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/28/android-bytecode-instrumentation-implementation-gradle-asm/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android字节码插桩实现(Gradle + ASM)
在Android编译过程中,往字节码里插入自定义的字节码,称为字节码插桩或函数插桩。 函数插桩可以帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量……
<<上一篇
下一篇>>
文章目录
关闭
目 录