在阅读源码前,请先下载源码:ARouter
最近阅读ARouter源码,发现这真的是一个非常优秀的框架。激发出兴趣来读一下他的源码,实际上,这个框架的结构非常简单。这个框架可以分为主流程和辅助流程来拆开分析。
主流程包含编译时和运行时两个部分,其中编译时主要做的是路由路径表的构建,运行时主要做的是路由路径表的加载;
辅助流程主要就是做启动优化。
主流程
1. 编译时
这部分主要涉及到的是路由路径表的构建,其实现原理是APT,即注解处理器。
使用ARouter时候,需要在目标类上,通过@Route注解进行标记,注解处理器处理的就是这个注解。打开源码路径下的arouter-compiler这个module,找到RouteProcessor,这个类就是用来处理@Route注解的类。这里需要了解的知识,除了APT,还有java-poet,请自行了解这些。
Processor类的入口方法是process方法,这个方法返回true,则这个处理器已经完成了自己的任务,不会被重复调用。其他比较重要的方法有getSupportedSourceVersion,getSupportedAnnotationTypes等。
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
通过这个方法,获取所有被@Route标记的元素。
获取到routeElements后,在parseRoutes方法进行处理。我们以最常用的Activity为例,进行分析。
rootMap.clear(); //用来分类存储标记元素
// 用来检测元素是否为对应的类型,通过Types.isSubtype()方法来检测。
TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType();
TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType();
TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType();
TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType();
最后将分类号的元素信息,存储在成员变量groupMap中去。
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();
然后再通过这个groupMap,借助java-poet,来生成真实的类。如下:
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(), -1, -2147483648));
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(), -1, -2147483648));
atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(), -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", new java.util.HashMap<String, Integer>(), -1, -2147483648));
atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
}
}
2. 运行时
这部分主要做的是,在ARouter.init()时候,将上过程生成的路径表加载到内存中。
如果你以官方demo程序验证这一步,需要将app/build.gradle中的
apply plugin: 'com.alibaba.arouter'
这一行代码注释掉。
我们以ARouter.init方法为入口,实际上最终实现init流程的是LogisticsCenter类的init方法。
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
...
if (registerByPlugin) {
//这是在辅助流程需要去讲的
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
}
}
这里需要着重看的是这一句:
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
我们查看这个方法:
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths) {
Log.v(TAG, "getFileNameByPackageName path=" + path);
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
Log.v(TAG, "find CLASS NAME " + className);
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
注意其中的getSourcePaths方法,这是从代码目录,来获取所有代码目录,然后在
getFileNameByPackageName找出以com.alibaba.android.arouter.routes
为开头包名的类,这些就是我们在步骤1中生成的辅助类。
这个方法结束后,回到LogisticsCenter#init方法,接下来要做的就是,把加载到的辅助类,通过反射生成对象,再调用其loadTo方法,将路由路径表加载到Warehouse类中去,方便以后的查询。
辅助流程
在以上的流程中,有一个严重的问题,那就是执行ARouter#init方法的时间过长,以源码的demo为例,在InstantRun的情况下,OnePlus5T需要100多毫秒才能初始化完,这对于程序启动优化来说,是一个不可忽视的时间了。那么如何解决这个问题呢?
MainActivity: init cost 134
这就是在上一步中,要求你注释掉的代码起作用了,将apply plugin: 'com.alibaba.arouter'
解除注释,让其发挥作用。
这里需要关注的module是arouter-gradle-plugin
。
先来看一下,使用了apply plugin: 'com.alibaba.arouter'
的神奇效果。
MainActivity: init cost 19
通过优化,让ARouter#init消耗时间直接降低了一个数量级,那么arouter-gradle-plugin
是怎么做到的呢?
这需要你先了解一下ASM。简单来说,这是一种字节码编程技术,通过修改编译后的字节码的方式,来对原始逻辑增强。
我们再来看ARouter#init的最终实现类和方法LogisticsCenter#init:
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
....
try {
long startInit = System.currentTimeMillis();
//billy.qi modified at 2017-12-06
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
....
}
.....
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}
我们可以看到,当registerByPlugin
为true的时候,则只是打印了一句日志,我们再看loadRouterMap()这个方法:
private static void loadRouterMap() {
registerByPlugin = false;
//auto generate register code by gradle plugin: arouter-auto-register
// looks like below:
// registerRouteRoot(new ARouter..Root..modulejava());
// registerRouteRoot(new ARouter..Root..modulekotlin());
}
这里的逻辑非常简单,到底在哪里去加载的路由路径表呢?我们看这个方法的注释,发现,这个方法是被arouter-auto-register
自动生成的。
我们打开arouter-gradle-plugin/resources/META-INF/gradle-plugins
这个目录,可以看到,有一个com.alibaba.arouter.properties文件,查看其内容:
implementation-class=com.alibaba.android.arouter.register.launch.PluginLaunch
这个PluginLaunch便是此gradle plugin的入口类。查看此类:
public class PluginLaunch implements Plugin<Project> {
@Override
public void apply(Project project) {
def isApp = project.plugins.hasPlugin(AppPlugin)
//only application module needs this plugin to generate register code
if (isApp) {
Logger.make(project)
Logger.i('Project enable arouter-register plugin')
def android = project.extensions.getByType(AppExtension)
def transformImpl = new RegisterTransform(project)
//init arouter-auto-register settings
ArrayList<ScanSetting> list = new ArrayList<>(3)
list.add(new ScanSetting('IRouteRoot'))
list.add(new ScanSetting('IInterceptorGroup'))
list.add(new ScanSetting('IProviderGroup'))
RegisterTransform.registerList = list
//register this plugin
android.registerTransform(transformImpl)
}
}
}
我们查看其代码,可以发现,这里一共做了三件事:
- 判断是否为app module,如果不是,则不做任何事,在app module下做2和3两步;
- 生成了一个RegisterTransform,并为其静态变量registerList赋值,注意此处赋值的registerList中包含的三个对象;
- 注册此RegisterTransform。
接下来,就轮到RegisterTransform来执行了。
Transform类,简单来说,就是可以在编译时,扫描所有的jar和class,包括引用类库中的。在扫描过程中,就可以借助ASM技术对目标类进行更改。
我们看其入口方法transform:
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
inputs.each { TransformInput input ->
// scan all jars
input.jarInputs.each { JarInput jarInput ->
...
if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
ScanUtil.scanJar(src, dest)
}
...
}
}
input.directoryInputs.each { DirectoryInput directoryInput ->
...
directoryInput.file.eachFileRecurse { File file ->
...
if(file.isFile() && ScanUtil.shouldProcessClass(path)){
ScanUtil.scanClass(file)
}
}
...
}
}
省去了一些细节,只保留了主线逻辑,我们可以看到,其扫描到的jar和class都经过了ScanUtils的方法来处理,我们继续跟踪下去,会发现,scanJar也是循环调用的scanClass,这样我们直接看scanClass方法:
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 {
ScanClassVisitor(int api, ClassVisitor cv) {
super(api, cv)
}
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
RegisterTransform.registerList.each { ext ->
if (ext.interfaceName && interfaces != null) {
interfaces.each { itName ->
if (itName == ext.interfaceName) {
//fix repeated inject init code when Multi-channel packaging
if (!ext.classList.contains(name)) {
ext.classList.add(name)
}
}
}
}
}
}
}
这里就又涉及到了ASM的知识,ScanClassVisitor是访问某个类的内部结构。
version:类的版本;
access:表示类的访问权限,public,private,protected等;
name:类的名字;
signature:有无泛型;
superName:其父类;
interfaces:其实现的接口;
在ScanClassVisitor中,并没有对类做修改,只是从遍历过的类中,把我们关心的类挑出来。那么,我们关心哪些类呢?
在PluginLaunch类中,我们注册了三个ScanSettings类,分别是IRouteRoot、IInterceptorGroup和IProviderGroup,也就是说,我们把实现了这三个接口的类,挑出来,加入到各自对应的ScanSettings类中记录起来。这三个接口是不是很熟悉?就是通过APT生成的用来记录路由路径表的类。
等收集好了这些记录的路径表信息后,就可以对LogisticsCenter通过ASM进行修改了。我们接着看RegisterTransform#transform方法中剩下的逻辑。
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 {
ext.classList.each {
Logger.i(it)
}
RegisterCodeGenerator.insertInitCodeTo(ext)
}
}
}
注意此处的insertInitCodeTo方法,这就是ASM修改的入口了。这里不对修改过程进行详细解释了。我们直接对比看LogisticsCenter修改前后关键代码的对比。
在app/build目录下,找到生成的apk文件,通过AndroidStudio来查看其中的class,找到关键LogisticsCenter关键方法loadRouterMap。
具体过程如下:
app/build/outputs/apk/debug/app-debug.apk -> classes.dex(双击) -> 找到LogisticsCenter#loadRouterMap方法 -> 右键: show Bytecode。
// 不使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
.registers 1
.line 64
const/4 v0, 0x0
sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z
.line 69
return-void
.end method
// 使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
.registers 1
.line 64
const/4 v0, 0x0
sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z
.line 69
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$app"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$app"
invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V
return-void
.end method
对比发现,使用apply plugin: 'com.alibaba.arouter'
后,这个方法增加了很多代码,基本上就是在加载路由路径表。使用这个gradle插件的基本思想就是,将查找路由路径表的过程,从运行时提前到了编译时,这算是一种AOT(Ahead of time)思想。
将最耗时的查找过程提前,也就解决了ARouter初始化时间过长的问题。