談談組件化-從源碼到理解

這幾天一直在組件化架構方面的知識點,下面主要分析一下「獲得」的組件化方案和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,具體能夠看 BaseCompRouterbash

固然,獲得的組件化不只僅這些,還有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解決不了地方


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的做者王龍海也在,但願你們能一塊兒學習交流

相關文章
相關標籤/搜索