“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM—动态代码注入

news/2024/5/20 1:02:02 标签: android, gradle, android studio

ARouter系列文章:

“终于懂了” 系列:组件化框架 ARouter 完全解析(一)原理全解

“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT—帮助类生成

“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform—动态代码注入

在本系列前两篇中,介绍了ARouter的核心原理、ARouter中使用的APT技术,了解了帮助类的作用和编译期生成帮助类的原理。其中原理篇中提到了AGP方式加载路由——在编译时进行扫描并动态在LogisticsCenter#loadRouterMap()中插入了代码。本篇就来介绍ARouter是如何搜集帮助类信息如何动态注入代码,以及涉及到的AGPTrasformASM

一、Arouter中的核心技术

1.1 Gradle

先来了解Android开发必定要接触到的Gradle

  • 一个自动化构建框架
  • 在Android开发中,用来编译打包输出apk,还可以用来添加三方依赖

Gradle官网中的介绍:
Gradle是一种开源构建自动化工具,足够灵活,可以构建几乎任何类型的软件。Gradle对您要构建什么或如何构建它几乎没有任何限制

也就是说,Gradle这个构建工具,可以被用来构建任何东西,具体如何构建也是由使用者制定。 在Android领域,AndroidStudio中的项目默认会引入Gradle,就是被用来构建Android应用包——apk文件。

那么Google是如何制定Apk构建流程的呢? 通过上面了解知道,AndroidStudio中只有Gradle还不够,还得告诉它如何构建。我们知道Apk打包流程包括很多环节:源码文件经过JavaCompiler转为class文件、class经过dex操作变为dex文件、dex和资源等打包成apk、签名 等等一系列步骤,这就需要Google使用Gradle把这些操作都串联起来。

image.png

在项目根目录的build.gradle文件中的这一句,就会引入「Google使用Gradle制定的这一系列打包流程」,也就是Google自定义的各种Gradle 的任务。每个任务负责某个具体的事情,按照依赖关系排好执行顺序,先后执行,最后输出Apk文件。

在AndroidStudio的Terminal中,输入打包命令:./gradlew app:assembleDebug --console=plain,输出如下:

:app:preBuild UP-TO-DATE
...
:app:compileDebugJavaWithJavac
...
:app:transformClassesWithDexBuilderForDebug
...
:app:packageDebug
:app:assembleDebug

这些就是Google官方使用Gradle定义的相互有依赖关系的各种任务,这些任务执行完就完成了Apk的打包。

这些任务是如何被创建的呢?在app模块和arouter-api模块的build.gradle文件中,第一句分别是

//app的build.gradle
apply plugin: 'com.android.application'
//arouter-api的build.gradle
apply plugin: 'com.android.library'

意思是,app引入了名为com.android.applicationgradle插件、arouter-api引入的是名为com.android.librarygradle插件。上面介绍的各种任务就是在这两种插件中创建的,引入了插件,也就引入了插件对应的任务。

定义了各种gradle任务,为啥还要定义gradle插件呢?一是可以对任务进行管理,例如排好任务的依赖和执行顺序;二是可以复用,模块引入某个插件,就拥有了对应的能力。即gradle插件就是 将构建逻辑的可重用部分封装起来,可以应用到不同的项目和构建中。

gradle中,除了任务(Task)、插件(Plugin),还有一个概念——Project,共同组成了gradle的核心内容:

  • Project:Gradle工程,Project包含一个构建脚本,通常名为build.gradle。 构建脚本为该
    Project定义Task、依赖项、plugin和其他配置。一个Android工程以及每个Android module都是一个Project (都有一个build.gradle文件),一个build.gradle文件编译后就是一个Project对象
  • Task:Gradle任务,包含执行某些工作的逻辑,Task本身包括:
    • 动作(Actions):具体某中的操作,例如复制文件或编译源代码
    • 输入(Inputs):操作使用的值、文件、目录
    • 输出(Outputs):操作修改或生成的文件、目录
  • Plugin:Gradle插件,用于创建和封装Task,介入编译构建过重要达到扩展功能目的。Plugin使得相同逻辑和配置可以被多个Project复用

再来看下Gradle的生命周期,有三个阶段:

  • 初始化:确定哪些Project将参与构建。即读取settings.gradle文件中的include的所有模块
  • 配置:确认参与构建的所有任务及其顺序。读取参与构建的Project的build.gradle文件,确定需要运行哪些Task以及确定Task间依赖关系和执行顺序(包含直接在build.gradle中定义的Task以及引入的Plugin中定义的Task)
  • 执行:运行在配置阶段确定的Task,这里是真正执行所有的打包操作

以上对Gradle的基本认知非常重要,足以理解本篇内容。至于Gradle脚本编写、Task和Plugin详细用法这种固定知识点可自行到Gradle官网学习即可,这里不再搬运。

1.2 Android Gradle Plugin

AGP(Android Gradle Plugin),即 Android Gradle 插件,是Android开发中使用的Gradle插件。

上面提到的,在项目根目录的build.gradle文件中引入了com.android.tools.build:gradle:4.0.0,也就引入了 Google官方写好的两个Gradle插件:com.android.applicationcom.android.library,这两个插件内部会创建各种Task用于完成Apk编译打包流程。

来看一个形象的比喻:Gradle编译打包流程和工厂通过自动化流水线来生产流程:

工厂(玩具工厂等)开发工具(AndroidStudio等)
厂长程序员
流水线gradle
流水线中的一个机器gradle中的一个插件
原材料java/kotln文件、资源文件等
各工作台的操作:清洗、组装、包装打包流程:javac、APT、转dex、打包apk
流水线新加入一个机器:给原材料贴标签gradle中新增一个APG:遍历dex并动态插入代码

image.png

Gradle就像是工厂的流水线一样,负责定义流程和规则,而具体的工作都是通过插件(及其包含的Task)实现的。如果想在流水线上增加一个操作,例如给原材料贴标签,那么就在最开头的位置增加一个贴标签的机器即可。对应的,想要在编译过程中动态插入代码,我们就需要新增一个AGP。

原本只有Google官方定义好的AGP,现在需要做的就是我们自定义一个插件,并且插在编译过程中合适的位置上。这就是本篇中要介绍的重点内容——ARouter用来在编译阶段搜集帮助类信息/动态注入代码的AGP是如何实现的。

1.3 Transform

搜集帮助类信息、动态注入代码,要在编译过程的什么时机执行呢? 这个时机要能够搜集到所有帮助类(包括依赖的三方AAR中的)并且还可以向目标文件中插入代码。

在官方定义的AGP中提供了这样一个时机,如下打包流程图中箭头位置:

apk.png

在class文件转为dex文件之前,这个节点可以拿到参与构建的所有class文件,包括本项目java文件/kotln文件经javac生产的class文件和依赖的第三方库中的class文件。

代码上如何使用拿到这个时机呢?Android官方的AGP中提供了一个API——Transform

image.png

我们需要自定义Transform,在Transform的transform方法中就可以获取到所有的class文件。

  • transform方法被调用的时机就是在class被打进dex文件之前
  • 需要在创建的gradle插件中注册自定义的Transform
  • 事实上每添加一个Transform,在Grade的配置阶段就会创建一个gradle Task

显然,在Transform的transform方法中,就可以拿到所有的class文件,我们关心的是所有的帮助类和LogisticsCenter类。

1.4 ASM

在Transform的中拿到所有class文件后,如何识别帮助类和LogisticsCenter类呢?识别后如何在LogisticsCenter类中插入代码呢? 如下图这样:

image.png

上图是class文件反编译后的样子,我们可以看懂。 但如果使用文本软件直接打开class文件,基本就是看不懂的十六进制内容:

image.png

如果想要识别出是哪个类、或是要向中间某个位置插入一段代码,这是基本不可能了。我们知道class文件又称为字节码文件,可以使用javap命令把class文件转为大概能看懂的字节码文件(或者直接使用AndroidStudio插件ASM Bytecode Viewer来查看):

image.png

右侧就是字节码指令以及Class文件版本号、常量池、访问标识、变量表、指令代码行号表等信息(可以参考字节码,查看每个指令的含义),这里我们肉眼可以识别类名和方法名。

在Transform的中拿到了class文件后,想要识别类信息并且还能修改内容,这个在代码上如何实现先呢?方法就是ASM。

ASM:字节码操作框架,是一种基于java字节码层面的代码分析和修改工具,ASM的目标是生成、转换、分析已编译的java class文件,可使用ASM工具读、写、转换JVM指令集。通俗点讲就是来处理javac编译之后的class文件。

ASM通过访问者模式依次遍历class字节码中的各个部分(属性、属性、注解等),并不断回调到上层(这有点像SAX解析xml的过程),上层可在业务关心的某个访问点,修改原有逻辑。之所以可以这么做,是因为java字节码是按照严格的JVM规范生成的二进制字节流,ASM是照这个规范对java字节码的一次解释,将晦涩难懂的字节码背后对应的JVM指令一条条的转换成ASM API。其中核心类如下:

  • ClassReader:用于读取已经编译好的class文件,读取到内容(属性/方法等)后会调用ClassWriter对应的方法
  • ClassWriter:是ClassVisitor的子类,主要负责将ClassVisitor处理好后的class转换成byte数组,有了byte数组就能通过OutputStream写入文件,也就完成了对class文件的修改
  • ClassVisitor:class文件访问者,可用于修改类型西,如修改类名、属性、方法,也可生成类的class文件。对于字节码文件中不同的区域有不同的Visitor:用于访问方法的 MethodVisitor、用于访问类属性的FieldVisitor、用于访问注解的AnnotationVisitor
  • MethodVisitor:是ASM中最为重要的类,它是对帧栈中「操作数栈」进行操作的核心类,无论实现什么功能都离不开对操作数栈的操作

使用ASM识别一个类,具体怎么操作呢?

/**
 * 扫描class
 * @param inputStream class文件的输入流(从transform中获取的)
 */
static void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    inputStream.close()
}

static class ScanClassVisitor extends ClassVisitor {
    ...
    //该方法主要提供对java版本,类权限访问符、全限定名、泛型、父类、接口相关信息
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        ...
    }
}

以上是读取/扫描 class文件的固定写法,重点是自定的ClassVisitor——ScanClassVisitor类,在它的visit()方法中可以获取到 类名、类实现的接口等等。通过对比 类名、接口等信息,就能确认是否是我们要找的帮助类了。

使用ASM在类方法插入代码,具体怎么操作呢?

/**
 * 修改class方法
 * @param inputStream class文件的输入流(从transform中获取的)
 */
private byte[] handleClassMethod(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    return cw.toByteArray()//这里是
}

class MyClassVisitor extends ClassVisitor {
    ...
    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
        //要插入代码的目标方法method 被访问到
        if (name == method) {
            mv = new MyMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}

class MyMethodVisitor extends MethodVisitor {
    ...
    //visitInsn,执行零操作指令,意味着操作码后无需跟任何字节的操作数,例如下面if种判断的return指令
    @Override
    void visitInsn(int opcode) {
        //确保在方法return之前插入代码
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
            ...//此处是需要写自己的插入代码的逻辑
        }
        super.visitInsn(opcode)//插入代码之后再走return
    }
    ...
}

以上是在class文件方法中插入代码的固定写法,重点是自定的MethodVisitor——MyMethodVisitor,visitInsn()方法中确保在方法return之前插入代码。那么如何写插入代码的逻辑呢? 有三种方法:

  1. 如果你对字节码指令比较熟,可以直接调用MethodVisitor的API来写字节码指令
  2. 也可以使用javap命令查看要插入的代码的字节码形式,然后使用ASM的API来写字节指令。
  3. 最简单的,使用AndroidStudio插件ASM Bytecode Viewer,可以帮助我们查看字节码,并直接生成ASM代码。这样 要插入的代码 通过 AndroidStudio插件 就可以直接生成ASM API代码了,我们把ASM代码复制到MyMethodVisitor中即可(可以对比插入代码前后的ASM代码,复制新增的部分即可):

LogisticsCenter的loadRouterMap()查看字节码文件和对应的ASM字节码文件:

字节码文件

ASM字节码文件

在方法return前插入代码register("hufeiyang");,再来查看不同:

插入代码后的ASM字节码文件

插入代码后的字节码文件.png

可见ASM字节码的区别就是这两句:

methodVisitor.visitLdcInsn("hufeiyang");
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/alibaba/android/arouter/core/LogisticsCenter", "register", "(Ljava/lang/String;)V", false);

这两句就是register("hufeiyang");的ASM代码的翻译了,就可以在MyMethodVisitor的visitInsn()方法中使用了,也就实现了 在类方法插入代码 这一操作了。

好了,到这里就完成了所有的铺垫,介绍了Gradle、AGP、Transform、ASM,每个知识点都是以理解主,要能清楚的每个概念和作用。更深入地掌握需要针对性的去学习,本篇是以理解和看懂ARouter为目标进行介绍的。

二、帮助类的搜集

Arouter项目中,有一个Gradle插件模块,就是使用了上面那介绍的技术点来实现 帮助类的收集路由加载代码插入

Arouter的Gradle插件模块.png

核心类作用如下:

  • PluginLaunch,自定义Gradle 插件,并注册自定义的Transform
  • RegisterTransform,自定义Transform,在class文件被打进dex文件之前拿到所有的class文件
  • ScanUtil,使用ASM对源码生产class文件和三方jar中的class文件进行扫描,找到所有帮助类
  • RegisterCodeGenerator,找到类 LogisticsCenter,使用ASM实现路由加载代码的插入

先来看帮助类的搜集过程,也就是PluginLaunch、RegisterTransform、ScanUtil这三个类。

//ARouter自动注册插件
public class PluginLaunch implements Plugin<Project> {

    //apply()方法,对应着 "apply plugin:",即在gradle的配置阶段就会执行,把RegisterTransform这个task挂接到编译流程中
    @Override
    public void apply(Project project) {
        def isApp = project.plugins.hasPlugin(AppPlugin)
        //只有application模块才需要本插件去生成注册代码
        if (isApp) {
            Logger.make(project)
            Logger.i('Project enable arouter-register plugin')

            def android = project.extensions.getByType(AppExtension)
            def transformImpl = new RegisterTransform(project)
            
            //要扫描的接口:根帮助类接口、拦截器帮助类接口、服务帮助类接口
            ArrayList<ScanSetting> list = new ArrayList<>(3)
            list.add(new ScanSetting('IRouteRoot'))
            list.add(new ScanSetting('IInterceptorGroup'))
            list.add(new ScanSetting('IProviderGroup'))
            RegisterTransform.registerList = list
            //注册transform
            android.registerTransform(transformImpl)
        }
    }
}

PluginLaunch,就是自定义的Gradle插件(第一篇中有介绍过在使用ARouter框架的app模块中需要引入此插件),然后注册了自定义的RegisterTransform。RegisterTransform中的registerList保存着的是各个帮助类的接口。

//1. 扫描class找到 实现了指定接口的class
//2. 生成[帮助类注册代码]插入到 LogisticsCenter
class RegisterTransform extends Transform {

    Project project
    //用于存各个帮助类接口及对应的所有帮助类
    static ArrayList<ScanSetting> registerList
    //包含LogisticsCenter.class的jar文件
    static File fileContainsInitClass;
    ...
    //transform方法,会在gradle的执行阶段执行(Transform本身会被封装在一个Task中-TransformTask)
    //执行阶段具体什么时候呢?--源码生成class文件之后,class被打进dex文件之前
    @Override
    void transform(Context context, Collection<TransformInput> inputs
                   , Collection<TransformInput> referencedInputs
                   , TransformOutputProvider outputProvider
                   , boolean isIncremental) throws IOException, TransformException, InterruptedException {
        ...
        inputs.each { TransformInput input ->
            //一、扫描全部jar(三方库中的class文件)
            input.jarInputs.each { JarInput jarInput ->
                String destName = jarInput.name
                //重命名jar文件
                def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
                if (destName.endsWith(".jar")) {
                    destName = destName.substring(0, destName.length() - 4)
                }
                File src = jarInput.file
                File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

                if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                    //-----扫描jar以找到目标class文件 begin----
                    ScanUtil.scanJar(src, dest)
                    //-----扫描jar以找到目标class文件 end ----
                }

                FileUtils.copyFile(src, dest)
            }

            //二、扫描全部目录(源码生成的class文件)
            input.directoryInputs.each { DirectoryInput directoryInput ->
                File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

                String root = directoryInput.file.absolutePath
                if (!root.endsWith(File.separator))
                    root += File.separator
                directoryInput.file.eachFileRecurse { File file ->
                    def path = file.absolutePath.replace(root, '')
                    if (!leftSlash) {
                        path = path.replaceAll("\\", "/")
                    }

                    //-----扫描目录以找到目标包路径的class文件 begin----
                    //类是在arouter APT生成类的包,'com/alibaba/android/arouter/routes/'
                    if(file.isFile() && ScanUtil.shouldProcessClass(path)){
                        ScanUtil.scanClass(file)
                    }
                    //-----扫描目录以找到目标包路径的class文件 end----
                }

                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }

        //扫描结束,拿到生成的所有目标帮助类(APT生成的IRouteRoot、IInterceptorGroup、IProviderGroup的实现类)
        if (fileContainsInitClass) {
            registerList.each { ext ->
                Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath)

                if (ext.classList.isEmpty()) {
                    Logger.e("No class implements found for interface:" + ext.interfaceName)
                } else {
                    //在 LogisticsCenter#loadRouterMap()中插入代码
                    RegisterCodeGenerator.insertInitCodeTo(ext)
                }
            }
        }
    }
}

RegisterTransform是自定义的Transform,transform方法会在gradle的执行阶段执行——源码生成class文件之后、class被打进dex文件之前。具体逻辑就是 扫描全部jar(三方库中的class文件)、扫描全部目录(源码生成的class文件),拿到所有目标帮助类,在 LogisticsCenter#loadRouterMap()中插入代码。具体的扫描过程是使用ScanUtil类,插入代码是RegisterCodeGenerator类。

class ScanUtil {
    //扫描jar
    static void scanJar(File jarFile, File destFile) {
        if (jarFile) {
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            while (enumeration.hasMoreElements()) { //遍历jar里面的所有class文件
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) {//找到包名是'com/alibaba/android/arouter/routes/'的class
                    InputStream inputStream = file.getInputStream(jarEntry)
                    scanClass(inputStream) //也是走scanClass方法
                    inputStream.close()
                } else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {//LogisticsCenter,物流中心,是在jar中
                    // 记录包含 LogisticsCenter.class 的jar文件,扫描完成后会向其中插入代码
                    RegisterTransform.fileContainsInitClass = destFile
                }
            }
            file.close()
        }
    }
...

    //扫描class文件
    static void scanClass(File file) {
        scanClass(new FileInputStream(file))
    }

    //扫描class,inputStream class文件的输入流(从transform中获取的)
    static void scanClass(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        inputStream.close()
    }

    static class ScanClassVisitor extends ClassVisitor {
        ...
        //该方法主要提供对java版本,类权限访问符、全限定名、泛型、父类、接口相关信息
        void visit(int version, int access, String name, String signature,String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
            //按类实现的接口,分别存到 RegisterTransform.registerList对应ScanSetting#classList中
            RegisterTransform.registerList.each { ext ->
                if (ext.interfaceName && interfaces != null) {
                    interfaces.each { itName ->
                        if (itName == ext.interfaceName) {
                            if (!ext.classList.contains(name)) {
                                ext.classList.add(name)
                            }
                        }
                    }
                }
            }
        }
    }
}

无论是jar还是目录,都是找到包名是com/alibaba/android/arouter/routes/的class文件,然后通过class文件的输入流,使用ASM读取class文件内容,再自定义ScanClassVisitor在visit()方法,按类实现的接口找到所有的帮助类,存到 RegisterTransform.registerList对应的ScanSetting.classList中。到这里,所有的帮助类都已经找到了。

三、动态代码注入

最后来看看代码插入的过程:

class RegisterCodeGenerator {
    ...
    static void insertInitCodeTo(ScanSetting registerSetting) {
        if (registerSetting != null && !registerSetting.classList.isEmpty()) {
            RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
            //包含LogisticsCenter.class的jar文件
            File file = RegisterTransform.fileContainsInitClass
            if (file.getName().endsWith('.jar'))
                processor.insertInitCodeIntoJarFile(file)
        }
    }

    //插入代码到jar
    private File insertInitCodeIntoJarFile(File jarFile) {
        if (jarFile) {
            ...
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))

            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = file.getInputStream(jarEntry)
                jarOutputStream.putNextEntry(zipEntry)

                //找到类 LogisticsCenter
                if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
                    //拿到LogisticsCenter.class文件的输入流,开始插代码!!!
                    def bytes = referHackWhenInit(inputStream)
                    
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                ...
            }
            ...
            optJar.renameTo(jarFile)
        }
        return jarFile
    }

    //使用ASM插入代码
    private byte[] referHackWhenInit(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

    class MyClassVisitor extends ClassVisitor {
        ...
        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
            //找到要插入代码的方法-loadRouterMap
            if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
                mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
            }
            return mv
        }
    }

    class RouteMethodVisitor extends MethodVisitor {
    ..
        @Override
        void visitInsn(int opcode) {
            //在return之前插入代码
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                extension.classList.each { name ->
                    name = name.replaceAll("/", ".")//把帮助类路径中的"/"换成"."
                    mv.visitLdcInsn(name)//帮助类类名
                    //在LogisticsCenter.loadRouterMap()中添加代码:register(name),name就是 根帮助类/拦截器帮助类/服务帮助类 类名
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                            , ScanSetting.GENERATE_TO_CLASS_NAME
                            , ScanSetting.REGISTER_METHOD_NAME
                            , "(Ljava/lang/String;)V"
                            , false)
                }
            }
            super.visitInsn(opcode)
        }
        ...
    }
}

首先是拿到jar中LogisticsCenter.class文件的输入流,接着使用ASM自定义MyClassVisitor在visitMethod()方法中找到要插入代码的方法-loadRouterMap()。然后在自定义RouteMethodVisitor的visitInsn()方法中,确保在return之前插入代码。遍历所有帮助类,把帮助类路径中的"/“换成”." 。最后插入代码的方式就是前面介绍的使用AndroidStudio插件查看ASM代码的方法。

到这里,源码也就是梳理完了。

四、总结

本篇内容较多,前半部分都是在介绍铺垫知识,只有完全理解了才能掌握ARouter框架的核心。

其中Gradle、AGP是Android开发中必定使用到的技术点,希望通过对ARouter的学习能更加深刻的理解Android编译构建的过程,以及使用Gradle插件能完成哪些事情。

Transform是官方Gradle 插件中提供的API,它能获取和处理被打进dex文件之前的class文件。不过Transform在最新的Gradle插件版本中被废弃了,不过这不影响我们我们对它的学习。

ASM是字节码操作框架,结合AndroidStudio中的ASM代码查看插件,使得我们可以便捷的修改class文件内容。

在以上内容完全掌握后,你会发现很多著名框架的实现基本都是这些技术点的应用。并且,如果在平时开发中遇到类似的问题,也可以通过这些技术进行解决。例如在我的组件化文章《“终于懂了” 系列:Android组件化,全面掌握!》中,提到的AppLifeCycle插件,使用的也是这些技术。所以一个好的框架,深入学习后不仅知道了它的原理,也能掌握涉及的到所有技术点,这对技术视野和深度都是一个很大的提高。

到这里这一系列也要结束了,中间隔了很久才完成这最后一篇,好在还是有始有终。

你的 点赞、评论,是对我的巨大鼓励!

欢迎关注我的 公众号  胡飞洋 ,文章更新可第一时间收到;

如果有问题或者想进群,号内有加我微信的入口,我拉你进技术讨论群。在技术学习、个人成长的道路上,我们一起前进!


http://www.niftyadmin.cn/n/84198.html

相关文章

Python字符串处理 -- 内附蓝桥题:门牌制作,卡片

字符串处理 ~~不定时更新&#x1f383;&#xff0c;上次更新&#xff1a;2023/02/20 &#x1f5e1;常用函数&#xff08;方法&#xff09; 1. s.count(str) --> 计算字符串 s 中有多少个 str 举个栗子&#x1f330; s "1354111" print(s.count(1)) # 答案为…

关于服务器时区修改我们需要做哪些操作

前一阵在客户现场遇到一个操作系统时区采用默认 UTC 时区的问题&#xff0c;正常来说安装完服务器我们首先要确保当前时区在东八区 PRC、CST 或 Asia/Shanghai&#xff0c;但是如果已经运行了一段时间系统&#xff0c;再修改可能就比较麻烦了。 项目上用的比较多的是 pgsql 和…

【一文速通】五个主流过拟合解决方法

过拟合是一个需要解决的问题&#xff0c;因为它会让我们无法有效地使用现有数据。有时我们也可以在构建模型之前&#xff0c;预估到会出现过拟合的情况。通过查看数据、收集数据的方式、采样方式&#xff0c;错误的假设&#xff0c;错误表征能够发现过拟合的预兆。为避免这种情…

ClickHouse SQL 语法详解

一、ClickHouse SQL 语句语句表示可以使用 SQL 查询执行的各种操作。每种类型的语句都有自己的语法和用法详细信息&#xff0c;这些语法和用法详细信息单独描述如下所示:1、SELECTSELECT 查询执行数据检索。 默认情况下&#xff0c;请求的数据返回给客户端&#xff0c;同时结合…

山东大学教授团畅谈ChatGPT革命座谈会,探讨ChatGPT发展趋势

2月18日&#xff0c;由山东大学多院系教授学者组成的山东大学教授团在济南福瑞达自贸创新产业园举行了“畅谈ChatGPT革命”座谈会&#xff0c;诸位教授学者就ChatGPT出现的影响进行了探讨。产业园首席顾问李铁岗教授向大家介绍产业园区山东大学经济学院教授、济南福瑞达自贸创新…

蓝桥杯C++练习系统全记录

本文是在学校开题了练习系统之后&#xff0c;可以免费白嫖这个系统&#xff0c;然后根据这个系统的习题顺序而写的答案和个人解析。 目录 基础练习 十六进制转八进制 十六进制转十进制 十进制转十六进制 特殊回文数 回文数 特殊的数字 杨辉三角形 查找整数 数列特征…

C++进阶:二叉搜索树

文章目录1 二叉搜索树概念2 二叉搜索树的实现2.1 结点的定义2.2 二叉搜索树的插入2.2 二叉搜索树的查找2.3 二叉搜索树的删除2.4 二叉搜索树的默认成员函数2.4.1 拷贝构造2.4.2 析构函数2.4.3 赋值重载3 二叉搜索树的应用3.1 k模型3.2 kv模型4 二叉搜索树的性能分析1 二叉搜索树…

【牛客刷题专栏】0x0C:JZ4 二维数组中的查找(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…