CC:基於組件匯流排的Android組件化開源框架

CC:Component Caller,一個android組件化開發框架, 已開源,github地址:github.com/luckybilly/C

本文主要講解框架實現原理,如果只是想了解一下如何使用,可直接到github上查看README文檔

前言

首先說明一下,本文將講述的組件化與業內的插件化(如:Atlas, RePlugin等)不是同一個概念

【圖片來源於網路】

組件化開發:就是將一個app分成多個Module,每個Module都是一個組件(也可以是一個基礎庫供組件依賴),開發的過程中我們可以單獨調試部分組件,組件間不需要互相依賴,但可以相互調用,最終發布的時候所有組件以lib的形式被主app工程依賴並打包成1個apk。

插件化開發:和組件化開發略有不用,插件化開發時將整個app拆分成很多模塊,這些模塊包括一個宿主和多個插件,每個模塊都是一個apk(組件化的每個模塊是個lib),最終打包的時候將宿主apk和插件apk(或其他格式)分開或者聯合打包。

本文將主要就以下幾個方面進行介紹:

一、為什麼需要組件化?

二、CC的功能介紹

三、CC技術要點

四、CC執行流程詳細解析

五、使用方式介紹

六、老項目進行組件化改造的成本有多高?

一、為什麼需要組件化?

關於使用組件化的理由,上網能搜到很多,如業務隔離、單獨以app運行能提高開發及調試效率等等這裡就不多重複了,我補充一條:組件化之後,我們能很容易地實現一些組件層面的AOP,例如:

  • 輕易實現頁面數據(網路請求、I/O、資料庫查詢等)預載入的功能
  • 組件被調用時,進行頁面跳轉的同時非同步執行這些耗時邏輯
  • 頁面跳轉並初始化完成後,再將這些提前載入好的數據展示出來
  • 在組件功能調用時進行登錄狀態校驗
  • 藉助攔截器機制,可以動態給組件功能調用添加不同的中間處理邏輯

二、CC的功能介紹

  1. 支持組件間相互調用(不只是Activity跳轉,支持任意指令的調用/回調)
  2. 支持組件調用與Activity、Fragment的生命周期關聯
  3. 支持app間跨進程的組件調用(組件開發/調試時可單獨作為app運行)
  • 在獨立運行組件時非常有用,比如:一個組件的某個功能要用到用戶的登錄信息,若未登錄則調起登錄組件的登錄頁面,若已登錄則獲取當前用戶信息。此時可以直接使用主app中的登錄組件及用戶在主app中的登錄狀態,該組件作為app獨立運行時無需依賴登錄組件,可以始終保持獨立運行狀態進行開發。
  1. 支持app間調用的開關及許可權設置(滿足不同級別的安全需求,默認打開狀態且不需要許可權)
  2. 支持同步/非同步方式調用
  3. 支持同步/非同步方式實現組件
  4. 調用方式不受實現方式的限制(例如:可以非同步調用另一個組件的同步實現功能。註:不要在主線程同步調用耗時操作)
  5. 支持添加自定義攔截器(按添加的先後順序執行)
  6. 支持超時設置
  7. 支持手動取消
  8. 編譯時自動註冊組件(IComponent),無需手動維護組件註冊表(使用ASM修改位元組碼的方式實現)
  9. 支持動態註冊/反註冊組件(IDynamicComponent)
  10. 支持組件間傳遞Fragment等非基礎類型的對象(組件在同一個app內時支持、跨app傳遞非基礎類型的對象暫不支持)
  11. 儘可能的解決了使用姿勢不正確導致的crash:

  • 組件調用處、回調處、組件實現處的crash全部在框架內部catch住
  • 同步返回或非同步回調的CCResult對象一定不為null,避免空指針,同時可以根據CCResult的結果進行降級處理。

demo效果演示

組件A打包在主app中,組件B為單獨運行的組件app,下圖演示了在主app中調用兩者的效果,並將結果以Json的格式顯示在下方。demo下載地址):

三、 CC技術要點

實現CC組件化開發框架主要需要解決的問題有以下幾個方面:

  • 組件如何自動註冊?
  • 如何兼容同步/非同步方式調用組件?
  • 如何兼容同步/非同步方式實現組件?
  • 如何跨app調用組件?
  • 組件如何更方便地在application和library之間切換?
  • 如何實現startActivityForResult?
  • 如何阻止非法的外部調用?
  • 如何與Activity、Fragment的生命周期關聯起來

3.1 組件如何自動註冊?

為了減少後期維護成本,想要實現的效果是:當需要添加某個組件到app時,只需要在gradle中添加一下對這個module的依賴即可(通常都是maven依賴,也可以是project依賴)

最初想要使用的是annotationProcessor通過編譯時註解動態生成組件映射表代碼的方式來實現。但嘗試過後發現行不通,因為編譯時註解的特性只在源碼編譯時生效,無法掃描到aar包里的註解(project依賴、maven依賴均無效),也就是說必須每個module編譯時生成自己的代碼,然後要想辦法將這些分散在各aar種的類找出來進行集中註冊。

ARouter的解決方案是:

  • 每個module都生成自己的java類,這些類的包名都是com.alibaba.android.arouter.routes
  • 然後在運行時通過讀取每個dex文件中的這個包下的所有類通過反射來完成映射表的註冊,詳見ClassUtils.java源碼

    運行時通過讀取所有dex文件遍歷每個entry查找指定包內的所有類名,然後反射獲取類對象。這種效率看起來並不高。

ActivityRouter的解決方案是(demo中有2個組件名為app和sdk):

  • 在主app module中有一個@Modules({"app", "sdk"})註解用來標記當前app內有多少組件,根據這個註解生成一個RouterInit類
  • 在RouterInit類的init方法中生成調用同一個包內的RouterMapping_app.map
  • 每個module生成的類(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包內(在不同的aar中,但包名相同)
  • 在RouterMapping_sdk類的map()方法中根據掃描到的當前module內所有路由註解,生成了調用Routers.map(...)方法來註冊路由的代碼
  • 在Routers的所有api介面中最終都會觸發RouterInit.init()方法,從而實現所有路由的映射表註冊

    這種方式用一個RouterInit類組合了所有module中的路由映射表類,運行時效率比掃描所有dex文件的方式要高,但需要額外在主工程代碼中維護一個組件名稱列表註解: @Modules({"app", "sdk"})

    還有沒有更好的辦法呢?

Transform API: 可以在編譯時(dex/proguard之前)掃描當前要打包到apk中的所有類,包括: 當前module中java文件編譯後的class、aidl文件編譯後的class、jar包中的class、aar包中的class、project依賴中的class、maven依賴中的class。

ASM: 可以讀取分析位元組碼、可以修改位元組碼

二者結合,可以做一個gradle插件,在編譯時自動掃描所有組件類(IComponent介面實現類),然後修改位元組碼,生成代碼調用掃描到的所有組件類的構造方法將其註冊到一個組件管理類(ComponentManager)中,生成組件名稱與組件對象的映射表。

此gradle插件被命名為:AutoRegister,現已開源,並將功能升級為編譯時自動掃描任意指定的介面實現類(或類的子類)並自動註冊到指定類的指定方法中。只需要在app/build.gradle中配置一下掃描的參數,沒有任何代碼侵入,原理詳細介紹傳送門

3.2 如何兼容同步/非同步方式調用組件?

通過實現java.util.concurrent.Callable介面同步返回結果來兼容同步/非同步調用:

  • 同步調用時,直接調用CCResult result = Callable.call()來獲取返回結果
  • 非同步調用時,將其放入線程池中運行,執行完成後調用回調對象返回結果: IComponentCallback.onResult(cc, result)

ExecutorService.submit(callable)

3.3 如何兼容同步/非同步方式實現組件?

調用組件的onCall方法時,可能需要非同步實現,並不能同步返回結果,但同步調用時又需要返回結果,這是一對矛盾。

此處用到了Object的wait-notify機制,當組件需要非同步返回結果時,在CC框架內部進行阻塞,等到結果返回時,通過notify中止阻塞,返回結果給調用方

注意,這裡要求在實現一個組件時,必須確保組件一定會回調結果,即:需要確保每一種導致調用流程結束的邏輯分支上(包括if-else/try-catch/Activity.finish()-back鍵-返回按鈕等等)都會回調結果,否則會導致調用方一直阻塞等待結果,直至超時。類似於向伺服器發送一個網路請求後伺服器必須返回請求結果一樣,否則會導致請求超時。

3.4 如何跨app調用組件?

為什麼需要跨app進行組件調用呢?

  1. 對現有項目進行組件化改造的過程,肯定不是一蹴而就,而是一個個組件逐步從主工程中抽離,這就涉及到主工程與組件間的通信。如果不能跨app進行組件調用,開發時就需要跟主工程一起打包,失去了組件化開發的一個非常大的優勢:組件單獨編譯運行提高開發&測試效率。
  2. 當獨立運行的組件需要調用到其他組件的功能時,不需要將其他組件編譯進來一起打包,可以調用主app中的組件,可以始終保持單module編譯運行的狀態進行開發。

目前,常見的組件化框架採用的跨app通信解決方案有:

  • URLScheme(如:ActivityRouter、阿里ARouter等)

    • 優點:

      • 基因中自帶支持從webview中調用
      • 不用互相註冊(不用知道需要調用的app的進程名稱等信息)

    • 缺點:

      • 只能單向地給組件發送信息,適用於啟動Activity和發送指令,不適用於獲取數據(例如:獲取用戶組件的當前用戶登錄信息)
      • 需要有個額外的中轉Activity來統一處理URLScheme,然後進行轉發
      • 如果設備上安裝了多個使用相同URLScheme的app,會彈出選擇框(多個組件作為app同時安裝到設備上時會出現這個問題)
      • 無法進行許可權設置,無法進行開關設置,任意app都可調用,存在安全性風險

  • AIDL (如:ModularizationArchitecture)

    • 優點:

      • 可以傳遞Parcelable類型的對象
      • 效率高
      • 可以設置跨app調用的開關

    • 缺點:

      • 調用組件之前需要提前知道該組件在那個進程,否則無法建立ServiceConnection
      • 組件在作為獨立app和作為lib打包到主app時,進程名稱不同,維護成本高

設計此功能時,我的出發點是:作為組件化開發框架基礎庫,想盡量讓跨進程調用與在進程內部調用的功能一致,對使用此框架的開發者在切換app模式和lib模式時盡量簡單,另外需要盡量不影響產品安全性。因此,跨組件間通信實現的同時,應該滿足以下條件:

  • 每個app都能給其它app調用
  • app可以設置是否對外提供跨進程組件調用的支持
  • 組件調用的請求發出去之後,能自動探測當前設備上是否有支持此次調用的app
  • 支持超時、取消

基於這些需求,我最終選擇了BroadcastReceiver + Service + LocalSocket來作為最終解決方案:

*如果appA內發起了一個當前app內不存在的組件:Component1,則建立一個LocalServerSocket,同時發送廣播給設備上安裝的其它同樣使用了此框架的app,同時,若某個appB內支持此組件,則根據廣播中帶來的信息與LocalServerSocket建立連接,並在appB內調用組件Component1,並將結果通過LocalSocket發送給appA。

BroadcastReceiver是android四大組件之一,可以設置接收許可權,能避免外部惡意調用。並且可以設置開關,接收到此廣播後決定是否響應(假裝沒接收到...)。

之所以建立LocalSocket鏈接,是為了能繼續給這次組件調用請求發送超時和取消的指令。*

用這種方式實現時,遇到了3個問題:

  • 由於廣播接收器定義在基礎庫中,所有app內都有,當用戶在主線程中同步調用跨app的組件時,調用方主線程被阻塞,廣播接收器也在需要主線程中運行,導致廣播接收器無法運行,直至timeout,組件調用失敗。

    • 將廣播接收器放到子進程中運行問題得到解決

  • 被調用的app未啟動或被手動結束進程,遇到廣播接收不到的問題

    • 這個問題暫時未很好的解決,但考慮到組件化開發只在開發期間需要用到跨進程通信,開發者可以通過手動在系統設置中給對應的app賦予自啟動許可權來解決問題

  • 跨進程調用時,只能傳遞基本數據類型,無法獲取Fragment等java對象

    • 這個問題在app內部調用時不存在,app內部來回傳遞的都是Map,可以傳遞任何數據類型。但由於進程間通信是通過字元串來回發送的,暫時支持不了非基本數據類型,未來可以考慮支持Serializable

3.5 組件如何更方便地在application和library之間切換?

關於切換方式在網路上有很多文章介紹,基本上都是一個思路:在module的build.gradle中設置一個變數來控制切換apply plugin: com.android.applicationapply plugin: com.android.library以及sourceSets的切換。

為了避免在每個module的build.gradle中配置太多重複代碼,我做了個封裝,默認為library模式,提供2種方式切換為application模式:在module的build.gradle中添加ext.runAsApp = true或在工程根目錄中local.properties中添加module_name=true

使用這個封裝只需一行代碼:

// 將原來的 apply plugin: com.android.application或apply plugin: com.android.library
//替換為下面這一行
apply from: https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle

註:cc-settings.gradle源碼傳送門

3.6 如何實現startActivityForResult?

android的startActivityForResult的設計也是為了頁面傳值,在CC組件化框架中,頁面傳值根本不需要用到startActivityForResult,直接作為非同步實現的組件來處理(在原來setResult的地方調用CC.sendCCResult(callId, ccResult)另外需要注意:按back鍵及返回按鈕的情況也要回調結果)即可。

如果是原來項目中存在大量的startActivityForResult代碼,改造成本較大,可以用下面這種方式來保留原來的onActivityResult(...)及activity中setResult相關的代碼:

  • 在原來調用startActivityForResult的地方,改用CC方式調用,將當前context傳給組件

    CC.obtainBuilder("demo.ComponentA")

    .setContext(context)

    .addParams("requestCode", requestCode)

    .build()

    .callAsync();

  • 在組件的onCall(cc)方法中用startActivityForResult的方式打開Activity

    @Override

    public boolean onCall(CC cc) {

    Context context = cc.getContext();

    Object code = cc.getParams().get("requestCode");

    Intent intent = new Intent(context, ActivityA.class);

    if (!(context instanceof Activity)) {

    //調用方沒有設置context或app間組件跳轉,context為application

    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    }

    if (context instanceof Activity && code != null && code instanceof Integer) {

    ((Activity)context).startActivityForResult(intent, (Integer)code);

    } else {

    context.startActivity(intent);

    }

    CC.sendCCResult(cc.getCallId(), CCResult.success());

    return false;

    }

3.7 如何阻止非法的外部調用?

為了適應不同需求,有2個安全級別可以設置:

  • 許可權驗證(給進程間通信的廣播設置許可權,一般可設置為簽名級許可權校驗),步驟如下:

    • 新建一個module
    • 在該module的build.gradle中添加對基礎庫的依賴,如: compile com.billy.android:cc:0.3.0
    • 在該module的src/main/AndroidManifest.xml中設置許可權及許可權的級別,參考component_protect_demo
    • 其它每個module都額外依賴此module,或自定義一個全局的cc-settings.gradle,參考cc-settings-demo-b.gradle

  • 外部調用是否響應的開關設置(這種方式使用起來更簡單一些)

    • 在Application.onCreate()中調用CC.enableRemoteCC(false)可關閉響應外部調用

為了方便開發者接入,默認是開啟了對外部組件調用的支持,並且不需要許可權驗證。app正式發布前,建議調用CC.enableRemoteCC(false)來關閉響應外部調用本app的組件。

3.8 如何與Activity、Fragment的生命周期關聯起來

背景:在使用非同步調用時,由於callback對象一般是使用匿名內部類,會持有外部類對象的引用,容易引起內存泄露,這種內存泄露的情況在各種非同步回調中比較常見,如Handler.post(runnable)、Retrofit的Call.enqueue(callback)等。

為了避免內存泄露及頁面退出後取消執行不必要的任務,CC添加了生命周期關聯的功能,在onDestroy方法被調用時自動cancel頁面內所有未完成的組件調用

  • Activity生命周期關聯

    在api level 14 (android 4.0)以上可以通過註冊全局activity生命周期回調監聽,在`onActivityDestroyed`方法中找出所有此activity關聯且未完成的cc對象,並自動調用取消功能:

    application.registerActivityLifecycleCallbacks(lifecycleCallback);
  • android.support.v4.app.Fragment生命周期關聯

    support庫從 [25.1.0][support25] 開始支持給fragment設置生命周期監聽:

    FragmentManager.registerFragmentLifecycleCallbacks(callback)

    可在其`onFragmentDestroyed`方法中取消未完成的cc調用

  • andorid.app.Fragment生命周期關聯(暫不支持)

四、 CC執行流程詳細解析

組件間通信採用了組件匯流排的方式,在基礎庫的組件管理類(ComponentMananger)中註冊了所有組件對象,ComponentMananger通過查找映射表找到組件對象並調用。

當ComponentMananger接收到組件的調用請求時,查找當前app內組件清單中是否含有當前需要調用的組件

  • 有: 執行App內部CC調用的流程:

  • 沒有:執行App之間CC調用的流程

    ![App之間組件調用匯流排](user-gold-cdn.xitu.io/2)

4.1 組件的同步/非同步實現和組件的同步/非同步調用原理

  • 組件實現時,當組件調用的相關功能結束後,通過CC.sendCCResult(callId, ccResult)將調用結果發送給框架
  • IComponent實現類(組件入口類)onCall(cc)方法的返回值代表是否非同步回調結果:

    • true: 將非同步調用CC.sendCCResult(callId, ccResult)
    • false: 將同步調用CC.sendCCResult(callId, ccResult)。意味著在onCall方法執行完之前會調用此方法將結果發給框架

  • 當IComponent.onCall(cc)返回false時,直接獲取CCResult並返回給調用方
  • 當IComponent.onCall(cc)返回true時,將進入wait()阻塞,知道獲得CCResult後通過notify()中止阻塞,繼續運行,將CCResult返回給調用方
  • 通過ComponentManager調用組件時,創建一個實現了java.util.concurrent.Callable介面ChainProcessor類來負責具體組件的調用

    • 同步調用時,直接執行ChainProcessor.call()來調用組件,並將CCResult直接返回給調用方
    • 非同步調用時,將ChainProcessor放入線程池中執行,通過IComponentCallback.onResult(cc, ccResult)將CCResult回調給調用方

執行過程如下圖所示:

4.2 自定義攔截器(ICCInterceptor)實現原理

  • 所有攔截器按順序存放在調用鏈(Chain)中
  • 在自定義攔截器之前有1個CC框架自身的攔截器:

    • ValidateInterceptor

  • 在自定義攔截器之後有2個CC框架自身的攔截器:

    • LocalCCInterceptor(或RemoteCCInterceptor)
    • Wait4ResultInterceptor

  • Chain類負責依次執行所有攔截器interceptor.intercept(chain)
  • 攔截器intercept(chain)方法通過調用Chain.proceed()方法獲取CCResult

4.3 App內部CC調用流程

當要調用的組件在當前app內部時,執行此流程,完整流程圖如下:

CC的主體功能由一個個攔截器(ICCInterceptor)來完成,攔截器形成一個調用鏈(Chain),調用鏈由ChainProcessor啟動執行,ChainProcessor對象在ComponentManager中被創建。

因此,可以將ChainProcessor看做一個整體,由ComponentManager創建後,調用組件的onCall方法,並將組件執行後的結果返回給調用方。

ChainProcessor內部的Wait4ResultInterceptor

ChainProcessor的執行過程可以被timeout和cancel兩種事件中止。

4.4 App之間CC調用流程

當要調用的組件在當前app內找不到時,執行此流程,完整流程圖如下:

五、使用方式介紹

CC的集成非常簡單,僅需4步即可完成集成:

>>>>閱讀全文


推薦閱讀:

TAG:Android | ReactNative | 開源 |