這幾天一直在元件化架構方面的知識點,下面主要分析一下“得到”的元件化方案和Arouter實現元件間路由的功能。
元件化涉及到的知識點
得到的方案
最近一會在探索元件化的實現方案,得到是在每個元件的build.gradle給annotationProcessorOptions設定host引數,這個引數就是我們當前元件的Group,apt拿到這個Group名稱拼接需要生成的路由表類的全路徑(不同的module都會生成不同的路由表類),然後掃描當前module被註釋了RouteNode的類,將path和類資訊儲存到生成的類,類的生成主要通過javapoet框架實現
下面是App模組的路由表
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註冊這個路由表類,
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 類
當然,得到的元件化不僅僅這些,還有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'
外掛中做了不少東西,具體的大家可以去看看,我大致說說,子模組生成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去拿這個變數
大致思路程式碼:
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解決不了地方
Arouter的方案
去年CC元件化的作者向Arouter提交了一個pr,auto-register為Arouter提供一個在編譯階段自動註冊路由的功能,以前Arouter是通過反射的方式註冊路由表,現在是通過transfrom插入位元組碼實現。
Arouter不同於“得到”元件化,Arouter的元件模組是不能單獨執行的,需要開發著自行解決,Arouter只提供了路由的解決方案
Arouter主要提供了三個註解處理器
- RouteProcessor : 處理註解的路由,作用在類上面
- InterceptorProcessor : 路由攔截器,作用在類上面
- AutowiredProcessor : 注入上個頁面傳遞過來的值,作用在欄位上面
配置方面,還是一樣,每個元件都必須依賴註解處理器,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具體的分析也說完了,最後來說個總結吧
總結
涉及到的知識點
- apt的使用
- transfrom 的使用
在我看的幾款元件化實施方案上面,上面這兩個知識點必須要了解,如果想一起探討的話,可以加QQ群492386431
,畢竟一個人的想法會有侷限性,transfrom方面還需要知道gradle plugin外掛的知識。
這裡非常感謝CC元件化的作者billy,也是auto-register的作者,他真的是一位很棒的開發者,有什麼問題,他都會在群裡一一講解,幫助開發者解決困惑,畢竟現在很多群發圖和閒扯淡的多,他的群號是686844583
,大家可以一起探討,愛奇藝開源的跨程式元件化方案Andromeda的作者王龍海也在,希望大家能一起學習交流