“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM—动态代码注入
ARouter系列文章:
“终于懂了” 系列:组件化框架 ARouter 完全解析(一)原理全解
“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT—帮助类生成
“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform—动态代码注入
在本系列前两篇中,介绍了ARouter的核心原理、ARouter中使用的APT技术,了解了帮助类的作用和编译期生成帮助类的原理。其中原理篇中提到了AGP方式加载路由——在编译时进行扫描并动态在LogisticsCenter#loadRouterMap()中插入了代码。本篇就来介绍ARouter是如何搜集帮助类信息、如何动态注入代码,以及涉及到的AGP、Trasform、ASM 。
一、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把这些操作都串联起来。
在项目根目录的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.application
的gradle插件、arouter-api引入的是名为com.android.library
的gradle插件。上面介绍的各种任务就是在这两种插件中创建的,引入了插件,也就引入了插件对应的任务。
定义了各种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.application
、com.android.library
,这两个插件内部会创建各种Task用于完成Apk编译打包流程。
来看一个形象的比喻:Gradle编译打包流程和工厂通过自动化流水线来生产流程:
工厂(玩具工厂等) | 开发工具(AndroidStudio等) |
---|---|
厂长 | 程序员 |
流水线 | gradle |
流水线中的一个机器 | gradle中的一个插件 |
原材料 | java/kotln文件、资源文件等 |
各工作台的操作:清洗、组装、包装 | 打包流程:javac、APT、转dex、打包apk |
流水线新加入一个机器:给原材料贴标签 | gradle中新增一个APG:遍历dex并动态插入代码 |
Gradle就像是工厂的流水线一样,负责定义流程和规则,而具体的工作都是通过插件(及其包含的Task)实现的。如果想在流水线上增加一个操作,例如给原材料贴标签,那么就在最开头的位置增加一个贴标签的机器即可。对应的,想要在编译过程中动态插入代码,我们就需要新增一个AGP。
原本只有Google官方定义好的AGP,现在需要做的就是我们自定义一个插件,并且插在编译过程中合适的位置上。这就是本篇中要介绍的重点内容——ARouter用来在编译阶段搜集帮助类信息/动态注入代码的AGP是如何实现的。
1.3 Transform
搜集帮助类信息、动态注入代码,要在编译过程的什么时机执行呢? 这个时机要能够搜集到所有帮助类(包括依赖的三方AAR中的)并且还可以向目标文件中插入代码。
在官方定义的AGP中提供了这样一个时机,如下打包流程图中箭头位置:
在class文件转为dex文件之前,这个节点可以拿到参与构建的所有class文件,包括本项目java文件/kotln文件经javac生产的class文件和依赖的第三方库中的class文件。
代码上如何使用拿到这个时机呢?Android官方的AGP中提供了一个API——Transform:
我们需要自定义Transform,在Transform的transform
方法中就可以获取到所有的class文件。
transform
方法被调用的时机就是在class被打进dex文件之前- 需要在创建的gradle插件中注册自定义的Transform
- 事实上每添加一个Transform,在Grade的配置阶段就会创建一个gradle Task
显然,在Transform的transform
方法中,就可以拿到所有的class文件,我们关心的是所有的帮助类和LogisticsCenter
类。
1.4 ASM
在Transform的中拿到所有class文件后,如何识别帮助类和LogisticsCenter
类呢?,识别后如何在LogisticsCenter
类中插入代码呢? 如下图这样:
上图是class文件反编译后的样子,我们可以看懂。 但如果使用文本软件直接打开class文件,基本就是看不懂的十六进制内容:
如果想要识别出是哪个类、或是要向中间某个位置插入一段代码,这是基本不可能了。我们知道class文件又称为字节码文件,可以使用javap命令把class文件转为大概能看懂的字节码文件(或者直接使用AndroidStudio插件ASM Bytecode Viewer
来查看):
右侧就是字节码指令以及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 {...@OverrideMethodVisitor 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指令@Overridevoid visitInsn(int opcode) {//确保在方法return之前插入代码if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {...//此处是需要写自己的插入代码的逻辑}super.visitInsn(opcode)//插入代码之后再走return}...
}
以上是在class文件方法中插入代码的固定写法,重点是自定的MethodVisitor——MyMethodVisitor,visitInsn()方法中确保在方法return之前插入代码。那么如何写插入代码的逻辑呢? 有三种方法:
- 如果你对字节码指令比较熟,可以直接调用MethodVisitor的API来写字节码指令
- 也可以使用javap命令查看要插入的代码的字节码形式,然后使用ASM的API来写字节指令。
- 最简单的,使用AndroidStudio插件
ASM Bytecode Viewer
,可以帮助我们查看字节码,并直接生成ASM代码。这样 要插入的代码 通过 AndroidStudio插件 就可以直接生成ASM API代码了,我们把ASM代码复制到MyMethodVisitor中即可(可以对比插入代码前后的ASM代码,复制新增的部分即可):
LogisticsCenter的loadRouterMap()查看字节码文件和对应的ASM字节码文件:
在方法return前插入代码register("hufeiyang");
,再来查看不同:
可见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插件模块,就是使用了上面那介绍的技术点来实现 帮助类的收集、路由加载代码插入。
核心类作用如下:
- 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挂接到编译流程中@Overridepublic 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//注册transformandroid.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文件之前@Overridevoid 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.fileFile 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.absolutePathif (!root.endsWith(File.separator))root += File.separatordirectoryInput.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 {//扫描jarstatic 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/'的classInputStream 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.fileContainsInitClassif (file.getName().endsWith('.jar'))processor.insertInitCodeIntoJarFile(file)}}//插入代码到jarprivate 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)//找到类 LogisticsCenterif (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 {...@OverrideMethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)//找到要插入代码的方法-loadRouterMapif (name == ScanSetting.GENERATE_TO_METHOD_NAME) {mv = new RouteMethodVisitor(Opcodes.ASM5, mv)}return mv}}class RouteMethodVisitor extends MethodVisitor {..@Overridevoid 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插件,使用的也是这些技术。所以一个好的框架,深入学习后不仅知道了它的原理,也能掌握涉及的到所有技术点,这对技术视野和深度都是一个很大的提高。
到这里这一系列也要结束了,中间隔了很久才完成这最后一篇,好在还是有始有终。
你的 点赞、评论,是对我的巨大鼓励!
欢迎关注我的 公众号 胡飞洋 ,文章更新可第一时间收到;
如果有问题或者想进群,号内有加我微信的入口,我拉你进技术讨论群。在技术学习、个人成长的道路上,我们一起前进!
相关文章:
“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM—动态代码注入
ARouter系列文章: “终于懂了” 系列:组件化框架 ARouter 完全解析(一)原理全解 “终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT—帮助类生成 “终于懂了” 系列:组件化框架…...

传闻腾讯引进Quest 2?我觉得可行性很低
根据36kr最新消息称,腾讯XR团队解散后,确定不碰XR硬件领域,但并未完全放弃XR规划,将转变思路和玩法,业内消息称腾讯计划引进Meta旗下Quest 2 VR一体机。消息称,该计划在2022年11月份XR部门负责人沈黎走后便…...
【数据集】CMIP6气候模式数据下载
1 CMIP6简介 目前,国际耦合模式比较计划(Coupled Model Intercomparison Project, CMIP) 已经发展到第六阶段(CMIP6)。CMIP6模式采用了较以往更加合理的参数化方案,综合考虑了大气中温室气体排放、气溶胶浓度及社会经济、科学技术发展及政府规划等多方面的综合影响,将提…...
华为OD机试 - 最长的元音字符串 | 机试题算法思路 【2023】
最近更新的博客 华为OD机试 - 简易压缩算法(Python) | 机试题算法思路 【2023】 华为OD机试题 - 获取最大软件版本号(JavaScript) 华为OD机试 - 猜字谜(Python) | 机试题+算法思路 【2023】 华为OD机试 - 删除指定目录(Python) | 机试题算法思路 【2023】 华为OD机试 …...

浅谈c++引用
浅谈c 在这里开设 <<浅谈C>> 系列专题,针对C重点内容展开探讨与观察底层,同时也是一个面试专栏,所选知识大多为面试常见问题.前期较为基础,难度会逐渐上升哦~ 本专栏采用经典的哲学三段论编写:是什么|为什么|怎么做 力图精简,高效. 第一章: 浅谈C函数重载 传送门…...

2023什么是分销商城?怎么搭建分销商城
当实体机构都接连探索线上营销模式的时候,分销也随着社交电商的兴起应运而生。 大家好,我是你们熟悉而又陌生的好朋友梦龙,一个创业期的年轻人 它借助裂变效率高的属性,建立更多用户触点,更好的提升企业运营的势能&am…...

408数据结构考点总结
文章目录第一章 绪论考点 1:时间复杂度与空间复杂度时间复杂度空间复杂度第二章 线性表考点 2:顺序表考点 3:单链表第三章 栈和队列考点 4:栈和队列的基本性质考点5:循环队列考点6:双端队列输出受限的双端队…...

虹科分享 | 网络流量监控 | 你的数据能告诉你什么:解读网络可见性的4种数据类型
要了解网络性能问题的原因,可见性是关键。而这四种数据类型(流、数据包、SNMP和API)都在增强网络可见性方面发挥着重要作用。 流 流是通过网络发送的数据的摘要。流类型不同,可以包括NetFlow, sFlow, jFlow和IPFIX。不同的流类型…...
SpringBoot日志框架使用详解
几种常见的日志级别由低到高分为:TRACE < DEBUG < INFO < WARN < ERROR < FATAL 。如何理解这个日志级别呢?很简单,如果项目中的日志级别设置为INFO ,那么比它更低级别的日志信息 就看不到了,即是TRACE…...

剑指offer-消失的数字、数组中出现的次数
消失的数字 解法一:求和相减 假设nums为[0,1,2,4],消失的数字为3,完整的数组应该是[0,1,2,3,4],则sum101247,sum20123410,我们很容易发现 sum2-sum1 01234 - 0124 3,即为消失的数字。因此,我们可以采用先…...
axios请求配置baseURL选项
在src同级目录创建 (1).env.delelopment : 开发模式时调用 (2).env.production :生产模式时调用 (3).env.testing : 测试模式时调用 # 页面标题 VITE_APP_TITLE 若依管理系统# 生产环境配…...

风储VSG-基于虚拟同步发电机的风储并网系统MATLAB仿真
MATLAB2021b版本仿真模型:风力发电机模块、储能控制模块、功率计算模块、VSG控制模块、电压电流双环控制模块。永磁同步风机输出功率、储能系统输出功率以及逆变器输出功率曲线。直流母线电压波动曲线。逆变器输出电压、电流曲线。完整模型见博主资源!&a…...

vim常用命令
vim常用三种模式 命令模式(Command mode) 插入模式(Insert mode) 末行模式(Last line mode) (一)进入命令模式 vi 或者 vim(二)命令模式 -> 插入模式 &…...
华为OD机试 - 最差产品奖 | 机试题算法思路 【2023】
最近更新的博客 华为OD机试 - 简易压缩算法(Python) | 机试题算法思路 【2023】 华为OD机试题 - 获取最大软件版本号(JavaScript) 华为OD机试 - 猜字谜(Python) | 机试题+算法思路 【2023】 华为OD机试 - 删除指定目录(Python) | 机试题算法思路 【2023】 华为OD机试 …...

HR:你会Python数据分析吗?
之前看到一个段子: 以前去面试,HR会问你“精通office吗?” 现在去面试,HR会问你“会Python数据分析吗?” 图片来源:网络 大数据时代,无论是数据分析师、研发,到运营、市场、产品经…...

算法18:LeetCode_链表相关算法题
链表无小事,只要是涉及到链表的算法题,边界值的设定尤为重要,而且及其容易出错误。这就要求我们平时多加练习。但是,我们在面试和笔试的过程中往往会碰到链表相关的题目,所以我们在笔试的时候一般都会借助系统提供的工…...

SpringBoot集成Tomcat服务
文章目录一、Tomcat集成1、依赖层级2、自动化配置二、Tomcat架构三、Tomcat配置1、基础配置2、属性配置类3、配置加载分析四、周期管理方法1、控制类2、核心方法使用的成本越低,内部封装越复杂; 一、Tomcat集成 1、依赖层级 在SpringBoot框架的web依赖…...

【机器学习】决策树-C4.5算法
1.C4.5算法 C4.5算法与ID3相似,在ID3的基础上进行了改进,采用信息增益比来选择属性。ID3选择属性用的是子树的信息增益,ID3使用的是熵(entropy, 熵是一种不纯度度量准则),也就是熵的变化值&…...

actipro-winforms-controls-23.1.0 Crack
actipro-winforms一组用于构建漂亮的 Windows 窗体桌面应用程序的 UI 控件,用于构建 IDE 的高级停靠窗口、MDI、属性网格、树控件和文件夹/文件浏览器,用于常见数据类型、自动完成、屏蔽编辑和代码编辑的强大编辑器,功能区、图表、微型图表、…...

适合打游戏用的蓝牙耳机有哪些?吃鸡无延迟的蓝牙耳机推荐
现在手游的兴起,让游戏市场变得更加火爆,各种可以提高玩家体验的外设也越来越多,除了提升操作的外置按键与手柄外,能带来更出色音质与舒心使用的游戏耳机,整体氛围感更好,让玩家在细节上占据优势࿰…...

股指期货波动一个点多少钱?
很多朋友在交易股指期货时,都会好奇一个问题:股指期货波动一个点,我的账户里到底是赚了还是亏了多少钱?要搞清楚这个问题,其实很简单,只需要了解两个关键信息:股指期货的“交易单位”࿰…...

Windows账户管理,修改密码,创建帐户...(无需密码)
前言 我们使用wWindows操作系统时,账户是非常重要的概念 它不仅能够帮助我们区分文档主题权限等等 嗯还有最重要的解锁电脑的作用! 但想要管理他,不仅需要原本的密码,而且设置中的管理项也非常的不全。 Windows有一款netplwi…...
学习记录aigc
1、DIT https://zhuanlan.zhihu.com/p/683612528 DiT最大的创新点是将Transformer引入到了扩散模型中,并完全抛弃了CNN。但是DiT并不是第一个引入Transformer的,例如之前的U-ViT,UniDiffuser等都尝试了将Transformer引入到扩散模型中。至于…...

Excel 模拟分析之单变量求解简单应用
正向求解 利用公式根据贷款总额、还款期限、贷款利率,求每月还款金额 反向求解 根据每月还款能力,求最大能承受贷款金额 参数: 目标单元格:求的值所在的单元格 目标值:想要达到的预期值 可变单元格:变…...
ADI硬件笔试面试题型解析下
本专栏预计更新60期左右。当前第17期-ADI硬件. ADI其硬件工程师岗位的招聘流程通常包括笔试和多轮技术面试,考察领域涵盖模拟电路设计、数字电路、半导体器件和信号处理等。 本文通过分析平台上的信息,汇总了ADI硬件工程师的典型笔试和面试题型,并提供详细解析和备考建议,…...
【学习笔记】构造函数+重载相关
【学习笔记】构造函数重载相关 一、构造函数 构造函数在创建对象的过程就会执行,带参数与不带参数,带参数的构造函数会默认将成员变量赋值传进去的参数。 class Layer { private:int layer_id; // 层IDstd::string layer_json; // 层的JSON配置…...
JVM——打开JVM后门的钥匙:反射机制
引入 在Java的世界里,反射机制(Reflection)就像一把万能钥匙,能够打开JVM的“后门”,让开发者在运行时突破静态类型的限制,动态操控类的内部结构。想象一下,传统的Java程序如同按菜单点菜的食客…...
git提交代码和解决冲突修复bug
提交到分支的步骤如下: 确保你当前在开发分支上,可以使用命令 git branch 来查看当前所在分支,并使用 git checkout 命令切换到开发分支。使用 git add 命令将修改的文件添加到暂存区。使用 git commit 命令提交代码到本地仓库。 解决合并冲…...
亚矩阵云手机实测体验:稳定流畅背后的技术逻辑
最近在测试一款云手机服务时,发现亚矩阵的表现出乎意料地稳定。作为一个经常需要多设备协作的开发者,我对云手机的性能、延迟和稳定性要求比较高。经过一段时间的体验,分享一下真实感受,避免大家踩坑。 1. 云手机能解决什么问…...

2025远离Deno和Fresh
原创作者:庄晓立(LIIGO) 原创时间:2025年6月6日 原创链接:https://blog.csdn.net/liigo/article/details/148479884 版权所有,转载请注明出处! 相识 Deno,是Nodejs原开发者Ryan Da…...