我们都知道ARouter可以用于组件化各个模块之间的通信和跳转,在使用ARouter过程中,我产生了几个问题:
1、如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path,那么编译通得过吗?如果通得过的话,通过path获取的又是哪一个Activity呢?
2、如果不同的module下,有两个Activity是相同的组会怎么样?即module1有一个SecondActivity使用/a/c的path,而module2也有一个ThirdActivity也使用/a/d的path,编译得过吗?
3、ARouter也可用于获取服务,假设采用通过接口的方式发现服务的话,如果接口不止一个实现,会怎样,会报错吗?
4、ARouter服务,为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口?
5、每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?
6、arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?
于是闲得无聊的我,看了一下ARouter的源码,写了一个Demo:XRouter。求一波star。
友情提示:文章较长,可以收藏慢慢看,关于问题文末有总结和回答。
ARouter源码分为几个部分:
1、编译期,即从.java文件到.class文件期间:利用apt框架生成各类,主要逻辑在arouter-compiler模块
2、transform时期(可选,可以加快第一次进入应用速度):作用于class到dex文件时期,用ASM框架插入字节码,模块是arouter-gradle-plugin
3、运行时期:在应用运行时,使用ARouter.init()和navigation进行路由跳转,逻辑在arouter-api模块
接下来我们一一分析:
arouter-compiler:编译期生成类文件。
在ARouter源码中,arouter-compiler模块用于处理注解。
ARouter在编译期,即由.java到.class之前,通过提取注解,生成了我们想要的.java文件。
主要的代码是在arouter-compiler这个module中。我们引入ARouter时用到了annotationProcessor 'com.alibaba:arouter-compiler:latestversion'即是引入的这个module,annotationProcessor的作用是在编译期处理注解,并不会打包进apk。
这个module中,我们主要分析RouteProcessor的代码,其他xxxProcessor原理也是类似,不一一赘述。
RouteProcessor间接继承AbstractProcessor,init方法获取了各类工具,process方法才是重头戏。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
try {
this.parseRoutes(routeElements); //1
} catch (Exception e) {
}
return true;
}
return false;
}
都是做一些准备工作,重点在注释1的parseRoutes方法中。
parseRoutes方法,由于比较长,我只截取了一部分:
...//前面经过了很多的工作之后
if (types.isSameType(tm, iProvider)) { // Its implements iProvider interface himself.
// This interface extend the IProvider, so it can be used for mark provider
loadIntoMethodOfProviderBuilder.addStatement(
"providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
(routeMeta.getRawType()).toString(),
routeMetaCn,
routeTypeCn,
className,
routeMeta.getPath(),
routeMeta.getGroup());
} else if (types.isSubtype(tm, iProvider)) {
// This interface extend the IProvider, so it can be used for mark provider
loadIntoMethodOfProviderBuilder.addStatement(
"providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
tm.toString(), // So stupid, will duplicate only save class name.
routeMetaCn,
routeTypeCn,
className,
routeMeta.getPath(),
routeMeta.getGroup());
}
...
截取的这部分代码和ARouter接口发现服务有关,在types.isSameType(tm, iProvider)的这句话中,types是一个Type对象,在RouteProcessor的init方法中从环境中拿到,我们可以用它来比较两个接口是否相同或者一个是另一个的子类。
通过调用types的isSameType方法,我们可以比较tm和iProvider是否是同样的接口。tm变量是class文件所实现的接口,这个class文件则是有注解修饰的class文件,iProvider则是com\alibaba\android\arouter\facade\template\IProvider.java这个接口。
如果types是IProvider接口,即该类直接实现了IProvider,那么ARouter就利用javapoet生成相应的代码。types.isSubtype(tm, iProvider)的意思是如果types是IProvider的子类,那么ARouter同样就利用javapoet生成相应的代码。
javapoet是什么?JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。ARouter利用javapoet生成了各种路由文件。这样就可以在运行的时候去扫描路由文件所在的路径,获取路由文件的类名,调用相关方法初始化路由表。
这里可能很难理解,再来梳理一下,ARouter发现服务是这样的,如果有一个类HelloService继承IProvider,并且HelloService有path的注解的话,比如:
@Route(path = "/module1/hello")
public class HelloService implements IProvider {
@Override
public void init(Context context) {
}
}
那么根据上面的情况,ARouter会生成这样的类文件,module1是模块名,可以看到往传进来的一个Map中放入了一个以HelloService为key,对应的RouteMeta为value的键值对。RouteMeta可以理解为跳转的元信息,可以通过RouteMeta获取跳转的path,group以及对应的目标Class等:
public class ARouter$Providers$module1 implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.example.module1.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloService.class, "/module1/hello", "module1", null, -1, -2147483648));
}
}
然而ARouter还提供了另一种方式,即用一个类比如IUserService继承IProvider接口,然后编写类UserServiceImpl实现IUserService,同时UserServiceImpl用path注解,这时ARouter会生成这样的类文件,可以看到这时候放入Map的key就不再是实现的UserServiceImpl类名,而是其所实现的接口IUserService,这就是ARouter通过接口发现服务的规则,用来实现解耦:
public class ARouter$Providers$module1 implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648));
}
}
现在考虑文章开头提到的第3和4个问题。第3个问题是如果接口不止一个实现,会怎样。还是以上面的UserServiceImpl为例子,那么如果还有一个类比如UserServiceImpl2同样集成IUserService,并且UserServiceImpl2同样也有path注解,那么ARouter会生成这样类文件。
public class ARouter$Providers$module1 implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648));
providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl2.class, "/u/2", "u", null, -1, -2147483648));
}
}
显然,同样实现IUserService会使它们有相同的key,后面put的元素会覆盖掉之前put的元素。经过上面的推论,可以得出如果接口不止一个实现,字母表排在后面的接口会覆盖掉排在前面的接口
第4个问题:为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口,这是因为ARouter在RouteProcessor的parseRoutes方法中只处理了接口的情况。
那么第1个和第2个问题是怎样呢?
问题1:如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。
我们知道apt框架是分module来进行处理的,因此我们也把问题分为在同一个module下和在不同module下:
1、如果SecondActivity和ThirdActivity在同一个module下:
RouteProcessor有一个成员变量groupMap,groupMap对生成ARouter$Group$group_name文件起到了非常重要的作用。private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();
groupMap的key是string,即group的名字,value是一个Set,我们都知道Set的一个特性,当试图放入一个Set中已有的元素时,会放入不了,并且不会抛异常。由此我们猜测,如果我们在同一个module中注解相同的path,那么排在字母表后面的元素会无效。即如果有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。那么ARouter生成的group类文件将会是下面这样,ThirdActivity由于没有被添加到Set中因此不会再生成的文件中出现:
public class ARouter$Group$a implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/a/b", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/a/b", "a", null, -1, -2147483648));
}
}
2、如果SecondActivity和ThirdActivity在不同的module下:
由于apt框架是分module编译,并且每个module都会生成ARouter$Root$module_name,ARouter$Group$group_name等文件,那么毫无疑问,会生成两个相同的文件。还是以上面的SecondActivity和ThirdActivity为例子,那么它们都会生成ARouter$Group$a的文件,那么在合并到dex的时候肯定会出错,事实上也是这样的。
总结一下:
对于第1个问题:如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path。
- 如果SecondActivity和ThirdActivity在同一个module中,那么字母表排在后面的元素会因为无法添加到Set中而无效。即只生成了SecondActivity的相关路由。
- 如果在不同的module中,由于apt框架是分module编译,并且每个module都会生成ARouterRoot$module_name,ARouterGroup$group_name等文件,那么毫无疑问,会生成两个相同的group的路由文件,这时候根本就编译过不了。
这也同样解释了第2个问题,因此不再赘述。
对于第5个问题:每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?
则需要在arouter-api模块中去找答案了。
arouter-api:跳转的实现模块
ARouter在运行期间需要用到的类比如ARouter.java,LogisticsCenter.java等都在arouter-api这个模块中。
对于第5个问题:每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?
我们先来考虑通过接口获取服务的情况:
在使用ARouter的时候,我们一般是通过这样的方式通过接口获取服务:
val service2 = ARouter.getInstance().navigation(IUserService::class.java)
ARouter.getInstance()显然是通过DCL获取ARouter单例,然后调用ARouter的navigation方法,ARouter的navigation如下:
public <T> T navigation(Class<? extends T> service) {
return _ARouter.getInstance().navigation(service);
}
_ARouter是真正实现逻辑的一个类,它的navigation如下:
protected <T> T navigation(Class<? extends T> service) {
try {
Postcard postcard = LogisticsCenter.buildProvider(service.getName()); //1
....
LogisticsCenter.completion(postcard);
return (T) postcard.getProvider();
} catch (NoRouteFoundException ex) {
return null;
}
}
注释1,让我们进去看看干了什么:
public static Postcard buildProvider(String serviceName) {
RouteMeta meta = Warehouse.providersIndex.get(serviceName); //1
if (null == meta) {
return null;
} else {
return new Postcard(meta.getPath(), meta.getGroup());
}
}
注释1处,可见是从Warehouse的providersIndex获取了serviceName对应的RouteMeta,providersIndex是一个Map,是在ARouter初始化的时候得到的一个以ClassName作为key,RouteMeta作为value的Map。
Warehouse可以立即为数据仓库,是一个包含众多静态Map的一个类。ARouter在初始化的时候将路由表加载到内存,就是将路由表加载到Warehouse的众多Map中。
RouteMeta可以理解为一个的bean,里面存储跳转的path,以及最终跳转的目标className。
由于显然在通过接口获取服务之前ARouter已经初始化完毕,所以这里的meta不为null。通过接口的类名拿到了对应的RouteMeta后,然后组成了一个Postcard(跳卡)。
回到_ARouter的navigation方法,之后调用了LogisticsCenter.completion(postcard);。看一下completion方法:
public synchronized static void completion(Postcard postcard) {
......
switch (routeMeta.getType()) {
case PROVIDER: // if the route is provider, should find its instance
// Its provider, so it must implement IProvider
Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta); //1
if (null == instance) { // There's no instance of this provider
IProvider provider;
try {
provider = providerMeta.getConstructor().newInstance();
provider.init(mContext);
Warehouse.providers.put(providerMeta, provider);
instance = provider;
} catch (Exception e) {
throw new HandlerException("Init provider failed! " + e.getMessage());
}
}
postcard.setProvider(instance);
postcard.greenChannel(); // Provider should skip all of interceptors
break;
....
}
}
}
省略了一下代码,省略的代码的逻辑就是先去Warehouse的routes这个Map去找有没有相应的RouteMeta,如果没有的话,就去Warehouse的groupsIndex加载该group,然后这个时候routes这个map肯定有相应的RouteMeta了。
然后看注释1,从Warehouse的providers这个map中去获取class对应的对象,如果有的话,就直接使用,如果没有的话,就通过反射创建一个实例,并添加到Warehouse的providers这个map中。所以每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。
而如果是通过path的方式呢?由于通过path的方式最终调用的也是completion方法,因此结论和通过接口的方式发现服务相同。
至此第5个问题的结论是:每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。
还有一个问题,arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?
arouter-gradle-plugin:作用于class到dex文件时期,用ASM框架插入字节码
在ARouter的源码中,arouter-gradle-plugin模块的作用是使用ASM的方式往LogisticsCenter的loadRouterMap方法中插入字节码:
LogisticsCener:
private static void loadRouterMap() {
registerByPlugin = false;
//ASM框架往下面插入字节码
//比如:register("com.alibaba.android.arouter.routes.ARouter$Root$arouterapi");
//比如:register("com.alibaba.android.arouter.routes.ARouter$Providers$arouterapi")
}
那么什么时候调用loadRouterMap方法的呢?在LogisticsCenter的init方法中。
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
loadRouterMap(); //1
if (registerByPlugin) { //2
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); //3
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 {
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
}
} catch (Exception e) {
...
}
}
LogisticsCenter的init方法会在ARouter初始化的时候调用,用于将路由表加载到内存当中。
注释1调用了loadRouterMap方法,loadRouterMap方法被插入字节码,那么会调用register方法,与此同时registerByPlugin这个boolean值会被设置为true。因此,在注释2处,如果registerByPlugin为false,则说明没有被插入字节码,将进入注释3。那么则会通过扫描指定包名下面的所有className,去进行相关初始化,具体的逻辑在arouter-api模块的ClassUtils的getFileNameByPackageName方法中:
ClassUtils.getFileNameByPackageName:
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) {
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)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
return classNames;
}
可以看到,采用了一个闭锁CountDownLatch,开线程池去扫描各个dex文件指定包名下的className。主线程必须等待所有dex文件被扫描完毕,如果有多个dex文件的话可能会比较耗时。不过由于ARouter对此作了缓存,被找到的所有的className会被缓存到sp中,下一次加载的时候就直接从缓存中取,只有第一次进入应用会比较耗时。
而arouter-gradle-plugin的目的就是连这第一次进入应用的耗时都要减少。
如何分析着手arouter-gradle-plugin这个模块呢?arouter-gradle-plugin首先是一个插件,分析一个插件那么得从它的插件类着手。
arouter-gradle-plugin模块首先从PluginLaunch这个类入手:
public class PluginLaunch implements Plugin<Project> {
@Override
public void apply(Project project) {
def isApp = project.plugins.hasPlugin(AppPlugin)
if (isApp) {
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) //1
}
}
}
可以看到是一个典型的插件,并且判断了只有application 模块才需要运行相关的逻辑。
注释1注册了一个transformImpl,transformImpl的类型是RegisterTransform,继承了Transform。Transform会在gradle构建过程中,从class文件到dex文件期间,对class文件或资源文件做相关的修改。
RegisterTransform这个类关键点是在transform方法:
void transform(Context context, Collection<TransformInput> inputs
, Collection<TransformInput> referencedInputs
, TransformOutputProvider outputProvider
, boolean isIncremental) throws IOException, TransformException, InterruptedException {
boolean leftSlash = File.separator == '/'
inputs.each { TransformInput input -> //1
input.jarInputs.each { JarInput jarInput ->
String destName = jarInput.name
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)) {
ScanUtil.scanJar(src, dest) //2
}
FileUtils.copyFile(src, dest)
}
// scan class files
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("\\\\", "/")
}
if(file.isFile() && ScanUtil.shouldProcessClass(path)){
ScanUtil.scanClass(file) //3
}
}
// copy to dest
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
if (fileContainsInitClass) {
registerList.each { ext ->
if (ext.classList.isEmpty()) {
} else {
RegisterCodeGenerator.insertInitCodeTo(ext) //4
}
}
}
}
transform有两种inputs,一种是jar,一种是Directory,因此注释1对这两种inputs做了区分。接下来看注释2,调用ScanUtils的scanJar方法,让我们看看是什么:
static void scanJar(File jarFile, File destFile) {
if (jarFile) {
def file = new JarFile(jarFile)
Enumeration enumeration = file.entries()
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) { //1
InputStream inputStream = file.getInputStream(jarEntry)
scanClass(inputStream) //2
inputStream.close()
} else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
// mark this jar file contains LogisticsCenter.class
// After the scan is complete, we will generate register code into this file
RegisterTransform.fileContainsInitClass = destFile //3
}
}
file.close()
}
}
注释1通过判断class的className是否以对应ROUTER_CLASS_PACKAGE开头,ROUTER_CLASS_PACKAGE开头的话证明就是ARouter在注解编译期间产生的类文件,然后调用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)
}
}
}
}
}
}
}
scanClass主要是判断是否class实现了相关的接口,所谓的相关的接口则是IRouteRoot,IInterceptorGroup和IProviderGroup这几个接口,如果实现了这几个接口的任意一个接口(实际上不应该也不允许实现多个)就把它的className加入到相应的classList,用于之后插入字节码做准备。
让我们回到ScanUtils的scanJar方法,注释3找到了fileContainsInitClass,所谓的fileContainsInitClass即LogisticsCenter.java这个类,之后ARouter插入字节码便是往LogisticsCenter的loadRouterMap方法中插入字节码。
回到RegisterTransform类的transform方法,注释3:DirectoryInputs也是和jarInputs一样,需要扫描。
接着来到注释4,如果找到了fileContainsInitClass,即LogisticsCenter这个类,那么则调用RegisterCodeGenerator的insertInitCodeTo方法:
static void insertInitCodeTo(ScanSetting registerSetting) {
if (registerSetting != null && !registerSetting.classList.isEmpty()) {
RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
File file = RegisterTransform.fileContainsInitClass
if (file.getName().endsWith('.jar'))
processor.insertInitCodeIntoJarFile(file)
}
}
insertInitCodeTo进行了相关初始化和判空,调用了insertInitCodeIntoJarFile方法:
private File insertInitCodeIntoJarFile(File jarFile) {
if (jarFile) {
def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
if (optJar.exists())
optJar.delete()
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)
if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) { //1
def bytes = referHackWhenInit(inputStream)
jarOutputStream.write(bytes)
} else {
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()
if (jarFile.exists()) {
jarFile.delete()
}
optJar.renameTo(jarFile)
}
return jarFile
}
重点看注释1,如果类名为GENERATE_TO_CLASS_FILE_NAME,则说明是LogisticsCenter这个类,然后调用referHackWhenInit方法,往LogisticsCenter这个类中插入字节码:
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()
}
这里才是最终插入ASM字节码实现逻辑。可以看到是个典型的ASM代码,MyClassVisitor对输入流进行处理,MyClassVisitor如下:
class MyClassVisitor extends ClassVisitor {
MyClassVisitor(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)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
//generate code into this method
if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
}
return mv
}
}
class RouteMethodVisitor extends MethodVisitor {
RouteMethodVisitor(int api, MethodVisitor mv) {
super(api, mv)
}
@Override
void visitInsn(int opcode) {
//generate code before return
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
extension.classList.each { name ->
name = name.replaceAll("/", ".")
mv.visitLdcInsn(name)//类名
// generate invoke register method into LogisticsCenter.loadRouterMap()
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, ScanSetting.GENERATE_TO_CLASS_NAME
, ScanSetting.REGISTER_METHOD_NAME
, "(Ljava/lang/String;)V"
, false)
}
}
super.visitInsn(opcode)
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
RouteMethodVisitor关键在visitInsn方法,判断opCode是否是返回的操作码,如果是的话在这之前插入各类字节码,由于修改了操作数栈,因此也需要重写visitMaxs方法修改最大操作数栈。
至此,arouter-gradle-plugin的逻辑梳理完成。
总结
至此,我们回答了文章开头提出的几个问题:
1、如果我们注解相同的path会怎么样?即有一个SecondActivity使用/a/b的path,而另一个ThirdActivity也使用/a/b的path,那么编译通得过吗?如果通得过的话,通过path获取的又是哪一个Activity呢?
答:如果在相同的module中,由于ARouter源码中使用的是Set,那么获取的是字母表排在前面的元素。如果在不同的module中,编译不过(和问题2一样)。
2、如果不同的module下,有两个Activity是相同的组会怎么样?即module1有一个SecondActivity使用/a/c的path,而module2也有一个ThirdActivity也使用/a/d的path,编译得过吗?
答:编译不过。由于都生成了相同的group文件,合并dex的时候会报错。
3、ARouter也可用于获取服务,假设采用通过接口的方式发现服务的话,如果接口不止一个实现,会怎样,会报错吗?
答:如果接口不止一个实现,并且接口的实现都用path注释的话,字母表排在后面的接口会覆盖掉排在前面的接口。
4、ARouter服务,为什么不能用抽象类继承IProvider然后实现抽象类而只能用接口继承IProvider然后实现该接口?
答:ARouter只处理了接口的情况,没有处理抽象类。
5、每次通过ARouter获取相同的path的服务,获取的都是同一个对象还是不同的对象?
答:每次通过ARouter的接口的方式发现服务,每次获取的都是同一个对象。
6、arouter-gradle-plugin的作用是什么?网上说ARouter加入apk后第一次加载会耗时,又是怎么回事?
答:arouter-gradle-plugin是一个插件,被ARouter用来加快应用安装后第一次进入时的速度。如果使用插件的话,那么会ASM直接插入字节码,省去了运行时需要扫描指定包名下面的所有className所造成的耗时。网上说ARouter加入apk后第一次加载会耗时,这是指的是没有使用arouter插件的时候,在第一次进入apk时,主线程必须等待子线程去扫描指定包名下面的所有className,如果class比较多,会比较耗时。