这几天一直在组件化架构方面的知识点,下面主要分析一下“获得”的组件化方案和Arouter实现组件间路由的功能。java
最近一会在探索组件化的实现方案,获得是在每一个组件的build.gradle给annotationProcessorOptions设置host参数,这个参数就是咱们当前组件的Group,apt拿到这个Group名称拼接须要生成的路由表类的全路径(不一样的module都会生成不一样的路由表类),而后扫描当前module被注释了RouteNode的类,将path和类信息存储到生成的类,类的生成主要经过javapoet框架实现android
下面是App模块的路由表git
public class AppUiRouter extends BaseCompRouter {
public AppUiRouter() {
}
public String getHost() {
return "app";
}
public void initMap() {
super.initMap();
this.routeMapper.put("/main", MainActivity.class);
this.paramsMapper.put(MainActivity.class, new HashMap() {
{
this.put("name", Integer.valueOf(8));
}
});
this.routeMapper.put("/test", TestActivity.class);
}
}
复制代码
这些都是在编译期间实现的,那么,运行期呢?在运行的时候,经过在Application注册这个路由表类,api
UIRouter.getInstance().registerUI("app");
复制代码
这个app参数就是咱们在build.gradle设置的host的值,也就是Group值,而后经过UIRouter的fetch方法,拼接apt以前生成的注册表类所在的路径,而后经过反射,将这个类拿到,存档到map集合里面缓存
private IComponentRouter fetch(@NonNull String host) {
//经过host拼接apt生成的类的路径
String path = RouteUtils.genHostUIRouterClass(host);
if (routerInstanceCache.containsKey(path))
return routerInstanceCache.get(path);
try {
Class cla = Class.forName(path);
IComponentRouter instance = (IComponentRouter) cla.newInstance();
routerInstanceCache.put(path, instance);
return instance;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
复制代码
这样,在下次发起openUri打开其余组件的Activity的时候,就能够经过openUri的方式,拿到host值,而后拿到IComponentRouter,而后拿到path,取出注册表对应的Activity.class,而后就跟日常同样startActivity打开对应的Activity,具体能够看 BaseCompRouter 类bash
固然,获得的组件化不只仅这些,还有application的注册,由于组件模块有些须要在application中初始化,可是一个app中不容许多个application的存在,因此,获得提供了两个方案,反射的方式,将组件的application路径交给主app,由主app的application统一反射注册,另外一种方案就是经过gradle插件的方式,在组件的build.gradle设置combuild参数,主要是为了向插件提供参数,以下:架构
combuild {
applicationName = 'com.luojilab.share.runalone.application.ShareApplication'
isRegisterCompoAuto = true
}
复制代码
而后module统一依赖 apply plugin: 'com.dd.comgradle'app
插件中作了很多东西,具体的你们能够去看看,我大体说说,子模块生成aar,移动到componentrelease文件夹,主模块去componentrelease文件夹中compile依赖这些aar,若是组件是单独调试模块,也给模块设置了sourceSet,设置不一样路径的AndroidManifest,而后注册了transform,transform主要是将combuild设置的applicationName,拿到类路径,而后经过javassist插入字节码插入到主Application的onCreate方法中去,看一看生成后是什么样的框架
public class AppApplication extends Application {
public AppApplication() {
}
public void onCreate() {
super.onCreate();
UIRouter.getInstance().registerUI("app");
Object var2 = null;
(new ReaderAppLike()).onCreate();
(new ShareApplike()).onCreate();
(new KotlinApplike()).onCreate();
}
}
复制代码
获得的方案仍是有点诟病的,在build.grdle中设置了moudle的名称,这个名称是要与application注册的名称是必需要一致的,这两个名称没有一个统一的来源,很容易致使集成的开发者弄错,致使找不到注册表,我建议的方案是,在组件的build.gradle设置一个ext扩展变量,为咱们模块的名字,而后apt的host去拿这个扩展变量,buildTypes里面设置一个buildConfigField,指向的也是这个变量,那么在组件中注册组件的时候,就能够经过BuildConfig去拿这个变量maven
大体思路代码:
apply plugin: 'com.dd.comgradle'
ext.moduleName = [
host: "share"
]
android {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [host: moduleName.host]
}
}
...
buildTypes {
debug {
buildConfigField "String", "HOST", "\"${moduleName.host}\""
}
release {
buildConfigField "String", "HOST", "\"${moduleName.host}\""
}
}
//-------------------------------------------
子组件ShareApplike.class
@Override
public void onCreate() {
//子组件包名+BuildConfig拿到host的值
uiRouter.registerUI(com.luojilab.share.BuildConfig.HOST);
Log.i("ShareApplike","ShareApplike-----");
new ShareApplike().onCreate();
}
复制代码
这样能够确保注册的组件和生成的组件是一致的
还有一个我以为很差的就是application那个用transform插入字节码的功能,须要在build.gradle中去配置comBuild对应的application路径,对于集成者来讲,配置越少,实现功能越强大是最好的方法,transform实现的功能就是对各个组件的application插入字节码,实际上是彻底能够抛弃使用transform,虽然用transfrom插入字节码能够避免了反射,但毕竟组件的application比较少,反射的话,也就那几个类,影响不了多大的性能,反而是注册表,若是组件注册的路由特别多,那么这个路由表就会特别大,反射会影响很大的性能,我以为比较好的方法是,定义一个和RouteNode同样的注解叫RouteApplication,而后将组件须要执行的application都标上RouteApplication注解,apt解析拿到这些类,生成对应的moudle名称+Application的类,而后在运行阶段,openUri打开其余组件的时候,拼接路径类,而后反射,和路由表方式同样,这样,能够彻底摒弃transfrom的存在,少了一些配置
还有一个就是,若是为了性能着想,仍是不要用apt的方式,apt总会遇到反射,建议全用transfrom插入字节码的方式,将路由所有插入到一个路由表管理类里面,这个路由表管理类是咱们本身写好的,只是里面啥都没有,都是在编译阶段经过transform插入,transform使用javassist或是asm均可以操做字节码,只不过一个好用,但耗时,一个很差用,速度快,但用谁都可有可无,并都是在编译阶段,只要不影响运行阶段就行
还有就是apt只能对当前module的类进行扫描拿到class信息,而且是扫描不了jar包、maven、aar里面的类,因此,仍是比较有局限性的,transfrom能够扫描apt解决不了地方
去年CC组件化的做者向Arouter提交了一个pr,auto-register为Arouter提供一个在编译阶段自动注册路由的功能,之前Arouter是经过反射的方式注册路由表,如今是经过transfrom插入字节码实现。
Arouter不一样于“获得”组件化,Arouter的组件模块是不能单独运行的,须要开发着自行解决,Arouter只提供了路由的解决方案
Arouter主要提供了三个注解处理器
配置方面,仍是同样,每一个组件都必须依赖注解处理器,Arouter和“获得”提供的参数做用是不同的,获得提供的参数直接就是路由表的分组Group,而Arouter提供的module参数主要是生成收集当前module全部的分组,而后收集的分组对应各个路由表
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
复制代码
RouteProcessor中主要是扫描被Route注解的类,而后拿到当前Route注解类的path、group和被注入值Autowired字段。这些信息都存储在RouteMeta类中,主要是方便管理,这个地方说一下group这个字段,举个例子:
/**
* 那么test就是这个group字段
*/
@Route(path = "/test/activity1")
复制代码
这个group字段是何时赋值给RouteMeta的呢,那就是在调用categories方法的时候,经过routeVerify方法进行校验是否符合path路径的时候赋值的,具体能够看RouteProcessor类的routeVerify方法。
而后能够看categories方法,这个方法看下groupMap这个集合,他是一个Map<String, Set<RouteMeta>>
类型,主要功能仍是分拣,以Group为key,将Group同样的RouteMeta放在一个set集合里面,为后面生成注册表类作基础
分拣好分组的信息以后,就会开始遍历这个groupMap集合,这个地方主要功能就是经过javapoet来建立类文件,来看下一段生成类的代码,稍微比较核心一点。
// 拼接 Arouter$$Group$$<test>类(groupName)
String groupFileName = NAME_OF_GROUP + groupName;
//生成对应的类
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()).build().writeTo(mFiler);
//将生成类存储到一个rootMap集合,这个是找到Group对应的路由表的关键
rootMap.put(groupName, groupFileName);
}
复制代码
在遍历循环结束后,rootMap的做用来了,首先是填充字段,拼接字段信息添加到MethodSpec.Builder中
if (MapUtils.isNotEmpty(rootMap)) {
// Generate root meta by group name, it must be generated before root, then I can find out the class of group.
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
}
复制代码
这个地方就是咱们在build.gradle中javaCompileOptions设置moduleName的缘由,主要功能就是生成以当前module名字为结尾的Arouter$$Root$$类,而后将Group的类信息存储在这个moduleName类中
// 拼接 Arouter$$Root$$<moduleName>类
String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(elements.getTypeElement(ITROUTE_ROOT)))
.addModifiers(PUBLIC)
//添加拼接好的字段
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()).build().writeTo(mFiler);
复制代码
下面我贴一下生成的两个类
ARouter$$Root$$app.java :收集app module中全部Group对应的路由表类路径
public class ARouter$$Root$$app implements IRouteRoot {
public ARouter$$Root$$app() {
}
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", service.class);
routes.put("test", test.class);
routes.put("test2", test2.class);
}
}
复制代码
ARouter$$Group$$test2.java : app module中test分组的路由表
public class ARouter$$Group$$test2 implements IRouteGroup {
public ARouter$$Group$$test2() {
}
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test2/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test2/activity2", "test2", new HashMap<String, Integer>() {
{
this.put("key1", Integer.valueOf(8));
}
}, -1, -2147483648));
}
}
复制代码
Arouter生成的路由表和“获得”的方案不同,而后咱们来对比一下,“获得”的方案是给当前组件定死了这个Group分组,好比Reader组件设置的host为reader,那么,这个Reader组件中,全部生成的路由表的Group分组都是reader,好处就是提早作好了分组的概念,生成的路由表类也是根据host的名称生成出来,很直观,反观Arouter,首先生成的是一个关于module的类,这个module类中存储了当前module全部的group分组的类信息,若是当前module有不少的group,那么就会生成不少的类,很差的地方看起来不太直观,生成的类信息太多,不过都差很少,Arouter反射的对象是module,“获得”反射的对象是Group。
Group分组在Arouter并非一个很重的概念,跟“获得”的方案不同,每一个组件都规定了group,而Arouter能够随意定义group,可能一个组件里面有不少的group。
路由表信息都生成了,接下来就是反注册了,Arouter以前的方案是采用遍历Dex文件取出类信息并将这些类信息进行反射,拿到注册表,放到一个缓存里面,后来引入auto-register以后,采用注入字节码的方式,主要逻辑来看LogisticsCenter
类。
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
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 {
...
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
...
}
}
复制代码
loadRouterMap这个方法主要是设置是否使用自动注册的功能,默认registerByPlugin的值为false,仍是采用ClassUtils的方式去反射注册表,若是想采用auto-register的方,设置registerByPlugin为true,并在build.gradle中依赖插件 apply plugin: 'com.alibaba.arouter'
,具体的依赖能够看arouter-api模块
auto-register的好处是什呢么?刚和做者聊了下
AutoRegister插件从根本上解决了找不到dex文件的问题:经过编译时进行字节码扫描对应3个接口的实现类,生成注册代码到ARouter的LogisticsCenter类中,运行时无需再读取dex文件,从而避免加固的兼容性问题。
复制代码
大体意思就是,Arouter原来要遍历apk的dex来找到注册表类信息,可是,因为加固问题,会致使找不到dex文件,遍历dex文件是一个耗时的操做,在初始化应用的时候速度没有自动注册的好。
这个地方还有一个好玩的事情,咱们仍是来看下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());
}
复制代码
刚开始看的时候,我一直觉得auto-register所作的插入的字节码就是插入registerRouteRoot(new ARouter..Root..modulejava())
,咱们在前面分析的时候就说过,注册表的Group分组是放在每一个module的类信息中,若是直接将module类找到,拿出他的Group map集合,根据map集合就能够找到Route路由集合,而且,一点也不会用到反射,确实优化的不错,但看完auto-register的源码后,发现并非插入的这段代码,而是插入register("ARouter$$Root$$moduleName类路径");
,就是将各个module存储分组的类进行了注册,来看下regiter方法
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);
}
}
}
复制代码
跟registerRouteRoot(new ARouter..Root..modulejava())
相比,多了一步反射,我很好奇,明明transform能找到存储Group分组的module类,经过插入字节码就能解决,为啥还要多作一步反射呢?摆脱反射不是能更好的优化性能吗?后来我去问了auto-register的做者,他跟我说,故事是这样的:
我提交PR后,ARouter的做者反馈说增长了首个dex的大小,要改为类名反射建立对象的方式注册(须要配置混淆规则)。
可是我测试下来没发现这个注册对首个dex的影响有多大,因此autoregister中继续保持以对象方式注册
复制代码
最终,Arouter仍是采用了反射的方式
最后来讲下auto-register作了啥,auto-register主要利用transform遍历全部模块的class信息,寻找class的全路径起始部分是不是com/alibaba/android/arouter/routes/
,是的话,加入到一个缓存的registerList集合里面,等待被插入字节码
插入字节码部分,咱们来看看吧,大体贴一点代码
@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)//存储group分组的module类名
// generate invoke register method into LogisticsCenter.loadRouterMap()
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, ScanSetting.GENERATE_TO_CLASS_NAME//com/alibaba/android/arouter/core/LogisticsCenter
, ScanSetting.REGISTER_METHOD_NAME//register
, "(Ljava/lang/String;)V"
, false)
}
}
super.visitInsn(opcode)
}
复制代码
这段代码是用asm来插入字节码,asm寻找类路径是采用斜杠的方式,但插入字节码的类,须要是点号,这段代码就是向LogisticsCenter类的loadRouterMap方法,插入一段register("存储group分组的module类名");
代码
Arouter具体的分析也说完了,最后来讲个总结吧
涉及到的知识点
在我看的几款组件化实施方案上面,上面这两个知识点必需要了解,若是想一块儿探讨的话,能够加QQ群492386431
,毕竟一我的的想法会有局限性,transfrom方面还须要知道gradle plugin插件的知识。
这里很是感谢CC组件化的做者billy,也是auto-register的做者,他真的是一位很棒的开发者,有什么问题,他都会在群里一一讲解,帮助开发者解决困惑,毕竟如今不少群发图和闲扯淡的多,他的群号是686844583
,你们能够一块儿探讨,爱奇艺开源的跨进程组件化方案Andromeda的做者王龙海也在,但愿你们能一块儿学习交流