前言
本文主要讨论在AGP8.0以上路由插件应该如何提速,至于怎样织入注册代码就不着重介绍了,官方其实已经给出了解决方案了Android Gradle插件更新,相信来看本文的已经具备了这样的知识了。
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
ModifyClassesTask::allJars,
ModifyClassesTask::allDirectories,
ModifyClassesTask::output,
)
适配过AGP8.0插件的小伙伴应该都有同样的感受,就是用上边这个新的api去插入注册代码的时候虽然也简单方便了许多,但同时引发了新的问题,那就是最终有一个前缀名为 dexBuilder
的任务每次打包时时间都会变得非常的长,导致影响开发效率,不用这个api就没事。
看过官方文档的也发现了还有以下的一个新方式,可以避免dexBuilder
任务时间变长的问题,但是同时也引发了一个新的问题,就是api只是提供了修改字节码的方法,当访问到需要插入注册代码的时候,一般都没有扫描完所有的类,导致插入的注册代码缺少相关信息
androidComponents {
onVariants(selector().all(), {
instrumentation.transformClassesWith(AsmClassVisitorFactoryImpl.class,
InstrumentationScope.Project) { params ->
params.x = "value"
}
instrumentation.setAsmFramesComputationMode(
COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
})
}
到这貌似进入了一个死胡同,有着全盘扫描的api会使打包变慢,不变慢的api又不能扫描完所有的代码。我也看了很多博主发了很多这方面探索的文章,可谓是八仙过海各显神通啦 ,但始终没看到一个“最好”的方案
解决方案
我的解决方案自然也离不开官方的这些api,但是其实还是要看怎么用
- 1、绝对不可以在加速开发时再用
toTransform
这个api了,可以在release包时使用 - 2、使用
transformClassesWith
去修改注册位置的代码,插入注册代码 - 3、使用
toAppend
新增类去添加注册信息(稍后解释)
满足以上三点使用 transformClassesWith
之后就面临两个问题
- 1、没有全盘扫描的信息
- 2、到了注册所在类时没有全盘的信息应该插入什么代码呢?
要想解决这两个问题,肯定是有办法的,其实焦点只有一个就是在扫描到注册代码所在类时应该插入什么代码,我的办法就是插入一个新的类的静态方法,这个静态方法和插入位置的方法参数类型保持一致,并且将参数传入这个静态方法,做一个类似于 “钩子” 的东西,当然插入的这个类和其静态方法在插入代码的时候是不存在的。
这个新的类及其方法要等到扫描完所有的类之后再去生成,静态方法的里边就是原来你想要插入的代码,最后把这个新的类打入包中。
总结一下就是:
- 1、提前在注册位置插入一个新的类的静态方法
- 2、然后将包含注册信息的新的类打入包中
下边以 ARouter 为例介绍下
以下是 ARouter 中注册代码所在类,它的插件会将路由信息注册进 loadRouterMap
方法中
package com.alibaba.android.arouter.core;
public class LogisticsCenter {
private static Context mContext;
static ThreadPoolExecutor executor;
private static boolean registerByPlugin;
private static void loadRouterMap() {
registerByPlugin = false;
//在这插入注册信息,调用了下边的register方法
}
//不难发现调用此方法的参数就是IRouteRoot、IProviderGroup、IInterceptorGroup三种类型
private static void register(String className) {
if (!TextUtils.isEmpty(className)) {
try {
Class<?> clazz = Class.forName(className);
Object obj = clazz.getConstructor().newInstance();
if (obj instanceof IRouteRoot) {
registerRouteRoot((IRouteRoot) obj);
} else if (obj instanceof IProviderGroup) {
registerProvider((IProviderGroup) obj);
} else if (obj instanceof IInterceptorGroup) {
registerInterceptor((IInterceptorGroup) obj);
} else {
logger.info(TAG, "register failed, class name: " + className
+ " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
}
} catch (Exception e) {
logger.error(TAG,"register class error:" + className, e);
}
}
}
}
以下是官方的插件将 IRouteRoot
IProviderGroup
IInterceptorGroup
三种类以字符串的形式调用 register
注册进去
public class LogisticsCenter {
static ThreadPoolExecutor executor;
private static Context mContext;
private static boolean registerByPlugin;
private static void loadRouterMap() {
registerByPlugin = false;
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
}
下边是采用我提到的方案注册进去的代码,代码中也已经给出了说明
public class LogisticsCenter {
static ThreadPoolExecutor executor;
private static Context mContext;
private static boolean registerByPlugin;
//第二步生成注册的类及其方法
public class Wovendcb5740d5c3df72df60cb94faba460db {
public static void init() {
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
LogisticsCenter.register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
}
}
private static void loadRouterMap() {
registerByPlugin = false;
//第一步提前埋入进去一个类的静态方法
Wovendcb5740d5c3df72df60cb94faba460db.init();
}
}
下边将这两步进行解释,请注意一下两步我都是在app所在module注册的插件
- 第一步使用
transformClassesWith
注册进去一个类的静态方法(注意这个任务是在app所在module引入的)
class EasyRegisterPlugin : Plugin<Project> {
override fun apply(project: Project) {
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
variant.instrumentation.transformClassesWith(
MyClassVisitorFactory::class.java,
scope
) { params ->
}
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COPY_FRAMES
)
}
}
}
abstract class MyClassVisitorFactory : AsmClassVisitorFactory<MyParameters> {
override fun createClassVisitor(
classContext: ClassContext,
nextVisitor: ClassVisitor
): ClassVisitor {
return RegisterClassVisitor(nextVisitor)
}
override fun isInstrumentable(classData: ClassData): Boolean {
//在这如果是你要注册代码的类就返回true
return RegisterClassUtils.isWovenClass(classData.className)
}
}
interface MyParameters : InstrumentationParameters {
@get:Input
val myConfig: Property<String>
}
class RegisterClassVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM9, cv) {
companion object{
const val INVOKE_METHOD = "init"
}
private lateinit var className:String
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
className = name
}
override fun visitMethod(
access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?
): MethodVisitor {
val wovenClass = RegisterClassUtils.getWovenClass(className,name,descriptor)
return if (wovenClass != null){
//如果是你要插入注册代码的方法进入这里
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
MyMethodAdapter(mv, access, name, descriptor)
}else{
super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
inner class MyMethodAdapter(mv: MethodVisitor, access: Int, private val mName: String, private val mDesc: String) :
AdviceAdapter(Opcodes.ASM9, mv, access, mName, mDesc) {
override fun visitInsn(opcode: Int) {
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
val argTypes = Type.getArgumentTypes(mDesc)
for ((index,_) in argTypes.withIndex()) {
mv.visitVarInsn(Opcodes.ALOAD, index)
}
// 此处是关键所在:插入一个静态方法,类名是根据插入位置的类和方法及其参数签名生成的,
// 方法名是固定的,插入的方法签名和插入位置的方法签名保持一致,并且将插入位置的参数
// 传入此静态方法,如此就完成了“钩子”的功能
mv.visitMethodInsn(
INVOKESTATIC,
getWovenClassName(className,mName, mDesc),
INVOKE_METHOD,
mDesc,
false
)
}
super.visitInsn(opcode)
}
}
}
- 第二步使用
toAppend
生成新增要插入的类及其方法(注意这个任务是在app所在module引入的)
class EasyRegisterPlugin : Plugin<Project> {
override fun apply(project: Project) {
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val taskProvider = project.tasks.register("${variant.name}EasyRegisterAddClasses",
AddClassesTask::class.java){
it.variant = variant.name
}
taskProvider.configure {
//为什么这个加注册信息的任务要在compileJavaWithJavac任务结束后再执行稍后解释
it.dependsOn("compile${variant.name.capitalized()}JavaWithJavac")
//这块返回false是为了增量编译可以修改信息,否则就一直是第一次的信息
it.outputs.upToDateWhen { return@upToDateWhen false }
}
variant.artifacts
.forScope(ScopedArtifacts.Scope.PROJECT)
.use(taskProvider)
.toAppend(
ScopedArtifact.CLASSES,
AddClassesTask::output
)
}
}
}
abstract class AddClassesTask : DefaultTask() {
@get:Input
abstract var variant :String
@get:OutputDirectory
abstract val output: DirectoryProperty
@TaskAction
fun taskAction() {
addClass()
}
private fun addClass() {
//将你原本注册的代码写在此处
}
}
相信你看到此处大脑中已经有一个比较清晰的轮廓了:
就是我提前在本该插入注册代码的位置埋入了一个静态方法,只是还有一个疑问就是该怎么收集所有module甚至包括aar包中的类的信息呢?
不卖关子就是使用一个名字大概是 compileDebugJavaWithJavac
这样的一个任务,这个任务是将我们写的 java 代码转化为 class 字节的任务,在这个任务结束之后我们可以得到所在module的class字节码文件以及一些依赖的jar包,所以我们在这个任务执行结束之后,就可以去收集所在module的注册信息了。
class SearchCodePlugin(private val root:Boolean): Plugin<Project> {
companion object{
const val ANDROID_EXTENSION_NAME = "android"
}
override fun apply(project: Project) {
val isApp = project.plugins.hasPlugin(AppPlugin::class.java)
val isDynamicLibrary = project.plugins.hasPlugin(DynamicFeaturePlugin::class.java)
val androidObject: Any = project.extensions.findByName(ANDROID_EXTENSION_NAME) ?: return
val android = androidObject as BaseExtension
val variants = if (isApp or isDynamicLibrary) {
(android as AppExtension).applicationVariants
} else {
(android as LibraryExtension).libraryVariants
}
variants.all { variant ->
val javaCompile: AbstractCompile =
if (DefaultGroovyMethods.hasProperty(variant, "javaCompileProvider") != null) {
//gradle 4.10.1 +
variant.javaCompileProvider.get()
} else if (DefaultGroovyMethods.hasProperty(variant, "javaCompiler") != null) {
variant.javaCompiler as AbstractCompile
} else {
variant.javaCompile as AbstractCompile
}
javaCompile.doLast{
//在此去查找你要注册的代码
}
}
}
}
这个任务是每个module都必须执行的,哪个module修改代码都会走到这里,并且app所在module的这个任务是最后一个执行的,因此上边提到下文讲解的以下代码这里就有了解释,就是要使生成的类的任务在app所在module的compileDebugJavaWithJavac
的任务结束以后就可以插入代码了,这样就可以保证所有的注册信息都被收集到了
taskProvider.configure {
it.dependsOn("compile${variant.name.capitalized()}JavaWithJavac")
it.outputs.upToDateWhen { return@upToDateWhen false }
}
至此,仿佛就完事了,但是还有几个小小的问题
- 这样做收集代码的任务就需要在每一个module中引入插件才可以
- 我们可以在根目录的build.gradle引入插件时引入插件,然后递归寻找每一个module,去设置这样的扫描任务
- 如果是kotlin代码貌似收集不到?
- kotlin代码的字节码目录其实我们可以找到,而且java转class时,kotlin的代码已经转换完毕了~
- 可能还有人有个疑问就是aar这样的能不能找到要注册的类?
compileDebugJavaWithJavac
这个任务可以拿到当前moduleimplementation
和api
的依赖包以及依赖包中通过api
引入的包,反正你在写代码时能访问到的包都可以拿到。只要每一个module都有这个任务就可以保证所有的类都能被找到,只是有一个小小的缺点就是可能会多次扫描同一个包,这里你只要做好去重工作即可
结束语
至此其实整个一个优化流程就基本展现出来了,以上只是介绍了大体上的一个框架,很多细节没有介绍到,例如上边我提到的几个小问题,有兴趣的可以去我的项目EasyRegister去查看。
按照我的思路优化完之后,速度能够有显著的提升。并且项目中我还给出了现在有的几个框架的agp8的适配,欢迎star~