问题
所有IOC系统,都不可避免的要进行实现的注册,包括很多初始化相关的事情。在Android上,随便一个多module的App,多多少少都有相同的问题。
Android冷启动App,IOC系统启动时,基本都要反射来突破Module间依赖的限制(如果这个能解决,也就不需要IOC了)。此时,性能一定会有一些问题,而且理解上不太容易描述清楚。
解决方法
提高性能的方法有不少种:
- 每个Module中的所有内容都聚合到一起,构成一个大的初始化器。则最多只需要反射n(n = Module数)次,即可完成初始化。
- 整个Project聚合到一起。不论如何,总归有一个Module(最终输出APK)会依赖所有子Module。这样,可以以apt的方法生成出一个大的聚合类,只需反射一次即可完成初始化。
- 让子Module主动注册自己,这样理论上能解决反射问题。
方法中的细节
第一种方法
其实就是大家用的方案。并没有什么特别的,稳定无公害,只是Module个数收到了一些限制。
第二种方法
方案具体来说是这样的,App分为三层(以解决依赖问题):
- 最下层是Plugin interface的声明层,这一层是PluginManager所在的层级。运行时反射最上层生成的类来初始化。
- 中间层是Plugin实现所在层,可以是任意多的module,依赖最下层。在编译期,用apt的方法,输出本Module中共插件信息的config文件。
- 最上层依赖一切。在编译期,仍然是apt的方法,读取每个中间层生成的config文件,生成一个初始化类,所有Impl都可以在这里依赖,正常new出来。
第三种方法
并没听说,有哪个大厂用这个办法。这里有一个catch,Java的运行周期中,并没有一个能既不反射、又不引用类就能loadClass的方式,连Class都没load,什么都玩不了。
不过,这里是Android,实际上是可以通过AndroidManifest来做一些不用loadClass就注册上的组件。用来初始化,Receiver + Action的方式就很合适,在任何Class都不在的情况下,系统会找到Receiver,调用onReceive。那么onReceive中做主动的注册就很好了。沿着这个思路,研究了两天,发现我想多了。
这个办法有一些问题:
- Receiver要做一套permission来保证Action不会唤起外部App(监听大厂App绝对是保活利器)
- Receiver的执行都被post到了主线程中,所以并不能保证顺序、不能保证及时性
- Receiver里都是垃圾代码,第二种方法中的apt生成可以搞定这一点
- Receiver需要注册,这个是口头约定。最好是能够用生成的办法去自动修改AndroidManifest。这个就是症结,暂时无解
为什么Android原生的Manifest要手写
不知道有谁想过没,为什么Android原生的Manifest要手写?Activity在编译期加几个Annotation,apt做注册不好吗?答案是,做不到!
想这样做,一定要求apt发生在resource处理之前。前提条件是不成立的!!!gradle中的processManifest是发生在generateSource之前的。所以,apt并不能有效的修改Manifest。
上代码:
task genManifest(type: Exec) {
doLast {
System.out.println("gradle logging after compile")
project.android.libraryVariants.all { variant ->
variant.outputs.each { output ->
//这里用来读apt生成的东西
File file = new File(project.projectDir.absolutePath + "/build/generated/source/apt/" + variant.getBuildType().getName() + "/test_file")
System.out.println("gradle logging file " + file.absolutePath)
if (file.exists()) {
System.out.println("gradle logging exist")
}
}
}
}
}
afterEvaluate {
// 可以保证genManifest执行
project.tasks.findByName('assembleDebug').dependsOn genManifest
genManifest.mustRunAfter ':generateDebugSources'
}
但是,根据stackOverflow,修改后的manifest要加到resourceSet中,而这个早已在processResource时用过了…
结论
要么用一坨反射,受不了就做一下apt时的merge,并没有办法做成Receiver形式,看起来很美的脑洞而已。
后续解决
使用了一个很简单的方案:javassist在合包的时候修改二进制。代码可以参考autoregister只是把ASM改成javassist(容易读一些)。比较好的拆分是:
- 有一个Config类,负责被javassist插入代码。这样安排的目的在于:
- 降低debug断点难度,因为是个纯数据类,所以基本不需要断点
- 解除掉类间依赖(在做首个dex方法数优化的时候,非常有用)
- 一个Manager类,负责管理所有的IOC实现。其中对于Config的引用是靠反射的,这样Manager在编译期是不依赖Config的,不需要打入同一个dex