Gradle插件 扫描class文件,使用Asm完成字节码插桩功能

849 阅读9分钟

首先借助一张网图了解apk的编译流程

image.png

我们的字节码插桩就发生在javac编译生成class文件后,修改内容,合成.dex文件

ScanAllFileTransform 文件解读----- 1.其实主要看 transform 方法 ,在该方法中我们可以扫描所有javac编译后生成的class文件。 2.主要分为两个循环,一个是当前模块下的内容,一个是它所依赖的所有模块文件,或jar包等 3.根据匹配规则,找寻我们需要被插桩使用的class文件

AppAroutCodeInjector 文件解读----- 1.获取将要进行修改的文件,在我这个例子里是:ARouterUtils.class 2.通过ASM动态注入字节码,自定义AppAroutClassVisitor类继承ClassVisitor,找寻插桩位置 3.找到插桩点后 定义LoadAppLikeMethodAdapter类继承AdviceAdapter方法,进行Asm的字节码规则操作

我们的难点在于ASM如何使用,对于这一点官方提供了jar包工具,帮助我们将java文件转化为asm的字节码,这时候就根据规则编写就行

Asm字节码插桩工具的使用:

通过编写代码然后生成class文件 ,利用jar包将class文件生成所需的asm文件代码:

//Asm的使用:https://www.jianshu.com/p/905be2a9a700
* 使用asm提供的通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,我下载的是最新的 asm-all-5.2.jar,然后使用如下命令,即可生成
*  命令:
* java -classpath E:\googleDowmload\asm-all-5.1.jar org.objectweb.asm.util.ASMifier E:\MyLearing\MyAptUseLearing\base-arouter\build\tmp\kotlin-classes\debug\com\example\base_arouter\ARouterUtils.class
*
* jar包下载地址:
* http://nexus.neeveresearch.com/nexus/content/repositories/public/org/ow2/asm/asm-all/5.1/

后续是所有代码:详细解释看代码

MyPlugin

package com.example.arouterplugin;

import com.android.build.gradle.AppExtension;

import org.gradle.api.Plugin;
import org.gradle.api.Project;



/**
 * @author tgw
 * @date 2021/10/16
 * @describe
 *
 * //Asm的使用:https://www.jianshu.com/p/905be2a9a700
 * 使用asm提供的通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,我下载的是最新的 asm-all-5.2.jar,然后使用如下命令,即可生成
 *  命令:
 * java -classpath E:\googleDowmload\asm-all-5.1.jar org.objectweb.asm.util.ASMifier E:\MyLearing\MyAptUseLearing\base-arouter\build\tmp\kotlin-classes\debug\com\example\base_arouter\ARouterUtils.class
 *
 * jar包下载地址:
 * http://nexus.neeveresearch.com/nexus/content/repositories/public/org/ow2/asm/asm-all/5.1/
 */
class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
            AppExtension android = (AppExtension) project.getExtensions().getByType(AppExtension.class);
            android.registerTransform(new ScanAllFileTransform(project));
            System.out.println("MyPlugin自定义独立插件2.................");
    }
}

ScanUtil

package com.example.arouterplugin

import java.util.jar.JarEntry
import java.util.jar.JarFile


class ScanUtil {

    //扫描directoryInput用到的 这两个条件用于判断是否是我们apt生成的文件,
    // 这里排除了前面的路径 eg:
    static final PROXY_CLASS_PREFIX ="/com/example/myaptuselearing/routes/"
    static final PROXY_CLASS_SUFFIX = "RouteInterfaceImp.class"


    //扫描jar文件所用到的 注意class文件名中的包名是以“/”分隔开,而不是“.”分隔的,这个包名是我们通过APT生成的代理类的包名
    static final PROXY_CLASS_PACKAGE_NAME = "com/example/myaptuselearing/routes/"


    //ARouterUtils是应用生命周期框架初始化方法调用类
    static final REGISTER_CLASS_FILE_NAME = "com/example/base_arouter/ARouterUtils.class"

    //包含生命周期管理初始化类的文件,即包含  com.example.arouterb
    // 扫描结束后,我们会生成注册代码到这个文件ARouterUtils类的class文件或者jar文件
    static File FILE_CONTAINS_INIT_CLASS

    /**
     * 判断该class是否是我们的apt生成的目标类
     *
     * @param file
     * @return
     */
    static boolean isTargetProxyClass(String filePath) {
        if (filePath.startsWith(PROXY_CLASS_PREFIX) && filePath.endsWith(PROXY_CLASS_SUFFIX)) {
            return true
        }
        return false
    }

    /**
     * 扫描jar包里的所有class文件:
     * 1.通过包名识别所有需要注入的类名
     * 2.找到AppLifeCycleManager类所在的jar包,后面我们会在该jar包里进行代码注入
     *
     * @param jarFile
     * @param destFile
     * @return
     */
    static List<String> scanJar(File jarFile, File destFile) {
        def file = new JarFile(jarFile)
        Enumeration<JarEntry> enumeration = file.entries()
        List<String> list = null
        while (enumeration.hasMoreElements()) {
            //遍历这个jar包里的所有class文件项
            JarEntry jarEntry = enumeration.nextElement()
            //class文件的名称,这里是全路径类名,包名之间以"/"分隔
            String entryName = jarEntry.getName()
            println "tgw2 file scanJar 包中的文件名: = "+entryName
            if (entryName == REGISTER_CLASS_FILE_NAME) {
                //标记这个jar包包含 ARouterUtils.class
                //扫描结束后,我们会生成注册代码到这个文件里
                FILE_CONTAINS_INIT_CLASS = destFile
            } else {
                //通过包名来判断,严谨点还可以加上类名前缀、后缀判断
                //通过APT生成的类,都有统一的前缀、后缀
                if (entryName.startsWith(PROXY_CLASS_PACKAGE_NAME)) {
                    if (list == null) {
                        list = new ArrayList<>()
                    }
                    String classPath = entryName.substring(entryName.lastIndexOf("/") + 1)
                    println "tgw2 file scanJar APT生成的类: = "+classPath
                    list.addAll(classPath)
                }
            }
        }
        return list
    }

    static boolean shouldProcessPreDexJar(String path) {
        //&& !path.contains("androidx.appcompat.")
        return !path.contains("com.android.support")  && !path.contains("/android/m2repository")
    }

}

ScanAllFileTransform

package com.example.arouterplugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.apache.commons.io.FileUtils
import org.apache.commons.codec.digest.DigestUtils

/**
 * @author tgw* @date 2021/10/13
 * @describe Gradle Transform
 * <p>
 * 然而前面这个插件MyPlugin并没有什么卵用,它仅仅只是在编译时,在
 * 控制台打印一句话而已。那么怎么通过插件在打包前去扫描所有的class文件呢
 * ,幸运的是官方给我们提供了 Gradle Transform技术,
 * 简单来说就是能够让开发者在项目构建阶段即由class到dex转换期间修改class文件,
 * Transform阶段会扫描所有的class文件和资源文件,具体技术我这里不详细展开,下面通过伪代码部分说下我的思路。
 * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 * <p>
 * //要继承Transform 需要在本 模块下的build.gradle中依赖
 * implementation "com.android.tools.build:gradle:4.1.0"
 */
class ScanAllFileTransform extends Transform {


    Project project

    ScanAllFileTransform(Project project) {
        this.project = project
        println "tgw start to tScanAllFileTransform"
        System.out.println("MyPlugin自定义独立插件1>>>>>>>>>>>>>>>>")
    }

    //该Transform的名称,自定义即可,只是一个标识
    @Override
    String getName() {
        return ScanAllFileTransform.getSimpleName()
    }

    //该Transform支持扫描的文件类型,分为class文件和资源文件,我们这里只处理class文件的扫描
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    //Transfrom的扫描范围,我这里扫描整个工程,包括当前module以及其他jar包、aar文件等所有的class
    /**
     * transform输入的文件所属的范围
     * 值Transform 的作用范围,有一下7种类型:
     * 1.EXTERNAL_LIBRARIES        只有外部库
     * 2.PROJECT                   只有项目内容
     * 3.PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4.PROVIDED_ONLY             只提供本地或远程依赖项
     * 5.SUB_PROJECTS              只有子项目
     * 6.SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)
     * 7.TESTED_CODE               由当前变量(包括依赖项)测试的代码
     */

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    //是否增量扫描
    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        //是否增量编译
        boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
        println "\ntgw1 是否增量编译-------------->>>>>>>" + isIncremental
        //输入类型为消费类型的,需要设置输出,作为下个transform的输入
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
        assert outputProvider != null
        //不需要增量编译,先清除全部
        if (!isIncremental)
            outputProvider.deleteAll()

//        URLClassLoader urlClassLoader = ClassLoaderHelper.getClassLoader(transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), project);
//        if (getAbstractProcessor() != null)
//            getAbstractProcessor().setClassLoader(urlClassLoader);

        def appLikeProxyClassList = []
        //inputs就是所有扫描到的class文件或者是jar包,一共2种类型
        for (TransformInput input : transformInvocation.getInputs()) {

            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                println (" tgw1 目录扫描开始")
                //Transform扫描的class文件是输入文件(input),有输入必然会有输出(output),处理完成后需要将输入文件拷贝到一个输出目录下去,
                //后面打包将class文件转换成dex文件时,直接采用的就是输出目录下的class文件了。
                //必须这样获取输出路径的目录名称
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

                //用于检测路径是否为检测斜杠
                boolean  leftSlash = File.separator == '/'

                //递归扫描该目录下所有的class文件
                if (directoryInput.file.isDirectory()) {
                    String root = directoryInput.file.absolutePath
                    println "tgw directoryInput 的路径: = " + root
                    directoryInput.file.eachFileRecurse { File file ->
                        //排除E:\MyLearing\MyAptUseLearing\app\build\intermediates\javac\debug\classes\com\example\myaptuselearing\VerifyCustomAnnotationActivity$$BindAdapterImp.class
                        //像上面路径的前一段 只保留和包名相关的 com\example\myaptuselearing\VerifyCustomAnnotationActivity$$BindAdapterImp.class
                        def path = file.absolutePath.replace(root, '')
                        //如果不是斜杠(/) 则将反斜杠(\)替换为斜杠(/)
                        if (!leftSlash) {
                            path = path.replaceAll("\\", "/")
                        }
                        //形如 Heima$$****$$Proxy.class 的类,是我们要找的目标class,直接通过class的名称来判断,也可以再加上包名的判断,会更严谨点
                        println "tgw1 directoryInput 目录下的文件的路径: = " + file.absolutePath
                        println "tgw1 directoryInput 目录下的文件的名称: = " + file.name
                        println "tgw1 directoryInput 筛选后的路径: = " + path

                        if (file.isFile() && ScanUtil.isTargetProxyClass(path)) {
                            println "tgw1 file name: 生产的自己代理类文件全路径: " + file.absolutePath
                            println "tgw1 file name: 生产的自己代理类文件包名路径: " + path
                            //如果是我们自己生产的代理类,保存该类的类名
                            appLikeProxyClassList.add(file.name)
                        }
                    }
                }

                // copy to dest
                FileUtils.copyDirectory(directoryInput.file, dest)

            }

            for (JarInput jarInput : input.getJarInputs()) {
                println (" tgw2 JarInput jar 包扫描: "+jarInput.getFile().getAbsolutePath() )
                //与处理class文件一样,处理jar包也是一样,最后要将inputs转换为outputs
                def jarName = jarInput.name
                def md5 = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //获取输出路径下的jar包名称,必须这样获取,得到的输出路径名不能重复,否则会被覆盖
                def dest = outputProvider.getContentLocation(jarName + md5, jarInput.contentTypes, jarInput.scopes, Format.JAR)

                if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                    File src = jarInput.file
                    println (" tgw2 JarI 文件路径: "+src.getAbsolutePath())
                    //先简单过滤掉 support-v4 之类的jar包,只处理有我们业务逻辑的jar包
                    if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                        //扫描jar包的核心代码在这里,主要做2件事情:
                        //1.扫描该jar包里有没有实现IAppLike接口的代理类;
                        //2.扫描AppLifeCycleManager这个类在哪个jar包里,并记录下来,后面需要在该类里动态注入字节码;
                        List<String> list = ScanUtil.scanJar(src, dest)
                        if (list != null) {
                            appLikeProxyClassList.addAll(list)
                        }
                    }
                }
                //将输入文件拷贝到输出目录下
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

        println ""
        appLikeProxyClassList.forEach({ fileName ->
            println "tgw file name = " + fileName
        })
        println "\n包含AppLifeCycleManager类的jar文件"
        println "需要进行插桩的文件:"+ScanUtil.FILE_CONTAINS_INIT_CLASS.getAbsolutePath()
        println "开始自动注册"

        //1.通过前面的步骤,我们已经扫描到所有实现了 IAppLike接口的代理类;
        //2.后面需要在 AppLifeCycleManager 这个类的初始化方法里,动态注入字节码;
        //3.将所有 IAppLike 接口的代理类,通过类名进行反射调用实例化
        //这样最终生成的apk包里,AppLifeCycleManager调用init()方法时,已经可以加载所有组件的生命周期类了
        new AppAroutCodeInjector(appLikeProxyClassList).execute()

        println "transform finish0----------------<<<<<<<\n"


    }


}

AppAroutCodeInjector

package com.example.arouterplugin

import org.objectweb.asm.*
import org.apache.commons.io.IOUtils
import org.objectweb.asm.commons.AdviceAdapter

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class AppAroutCodeInjector {

    //扫描出来的所有 IAppLike 类
    List<String> proxyAppLikeClassList

    AppAroutCodeInjector(List<String> list) {
        proxyAppLikeClassList = list
    }

    void execute() {
        println("开始执行ASM方法======>>>>>>>>")

        File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS
        println("tgw3  开始执行ASM方法 操作的文件路径:"+srcFile.getAbsoluteFile())
        //创建一个临时jar文件,要修改注入的字节码会先写入该文件里
        def optJar = new File(srcFile.getParent(), srcFile.name + ".opt")
        if (optJar.exists())
            optJar.delete()
        def file = new JarFile(srcFile)
        Enumeration<JarEntry> enumeration = file.entries()
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = enumeration.nextElement()
            String entryName = jarEntry.getName()
            ZipEntry zipEntry = new ZipEntry(entryName)
            InputStream inputStream = file.getInputStream(jarEntry)
            jarOutputStream.putNextEntry(zipEntry)

            //找到需要插入代码的class,通过ASM动态注入字节码
            if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) {
                println "tgw3 insert register code to class >> " + entryName

                ClassReader classReader = new ClassReader(inputStream)
                // 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
                ClassVisitor classVisitor = new AppAroutClassVisitor(classWriter)
                //开始扫描class文件
                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                byte[] bytes = classWriter.toByteArray()
                //将注入过字节码的class,写入临时jar文件里
                jarOutputStream.write(bytes)
            } else {
                //不需要修改的class,原样写入临时jar文件里
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
            }
            inputStream.close()
            jarOutputStream.closeEntry()
        }

        jarOutputStream.close()
        file.close()

        //删除原来的jar文件
        if (srcFile.exists()) {
            srcFile.delete()
        }
        //重新命名临时jar文件,新的jar包里已经包含了我们注入的字节码了
        optJar.renameTo(srcFile)
    }

    //插入字节码的逻辑,都在这个类里面
    class AppAroutClassVisitor extends ClassVisitor {
        AppAroutClassVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor)
        }

        @Override
        MethodVisitor visitMethod(int access, String name,
                                  String desc, String signature,
                                  String[] exception) {
            println "tgw3 visit method: " + name
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exception)
            //找到 AppLifeCycleManager里的loadAppLike()方法,我们在这个方法里插入字节码
            if ("loadRouterMap" == name) {
                mv = new LoadAppLikeMethodAdapter(mv, access, name, desc)
            }

            return mv
        }
    }

    class LoadAppLikeMethodAdapter extends AdviceAdapter {

        LoadAppLikeMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc)
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            println "tgw3 -------onMethodEnter------"
            mv.visitCode();
            //遍历插入字节码,其实就是在 loadAppLike() 方法里插入类似registerAppLike("");的字节码
            proxyAppLikeClassList.forEach({ proxyClassName ->
                println "tgw3 开始注入代码:${proxyClassName}"
                def fullName = ScanUtil.PROXY_CLASS_PACKAGE_NAME + proxyClassName.substring(0, proxyClassName.length() - 6)
                println "tgw3 注入代码 full classname = ${fullName}"
                mv.visitTypeInsn(NEW, fullName);
                mv.visitInsn(DUP);
                mv.visitMethodInsn(INVOKESPECIAL, fullName, "<init>", "()V", false);
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, ScanUtil.REGISTER_CLASS_FILE_NAME.substring(0, ScanUtil.REGISTER_CLASS_FILE_NAME.length() - 6), "mRouterMap", "Ljava/util/Map;");
                mv.visitMethodInsn(INVOKEVIRTUAL, fullName, "register", "(Ljava/util/Map;)V", false);
            })
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
//


//            mv.visitCode();
//            mv.visitTypeInsn(NEW, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp");
//            mv.visitInsn(DUP);
//            mv.visitMethodInsn(INVOKESPECIAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "<init>", "()V", false);
//            mv.visitVarInsn(ALOAD, 0);
//            mv.visitFieldInsn(GETFIELD, "com/example/base_arouter/ARouterUtils", "mRouterMap", "Ljava/util/Map;");
//            mv.visitMethodInsn(INVOKEVIRTUAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "register", "(Ljava/util/Map;)V", false);
//            mv.visitInsn(RETURN);
//            mv.visitMaxs(2, 1);
//            mv.visitEnd();
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode)
            println "-------onMethodEnter------"
        }
    }

}