*本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈html
CC:Component Caller,一個android組件化開發框架, 已開源,github地址:github.com/luckybilly/… 本文主要講解框架實現原理,若是隻是想了解一下如何使用,可直接到github上查看README文檔java
首先說明一下,本文將講述的組件化與業內的插件化(如:Atlas, RePlugin等)不是同一個概念android
【圖片來源於網絡】組件化開發:就是將一個app分紅多個Module,每一個Module都是一個組件(也能夠是一個基礎庫供組件依賴),開發的過程當中咱們能夠單獨調試部分組件,組件間不須要互相依賴,但能夠相互調用,最終發佈的時候全部組件以lib的形式被主app工程依賴並打包成1個apk。git
插件化開發:和組件化開發略有不用,插件化開發時將整個app拆分紅不少模塊,這些模塊包括一個宿主和多個插件,每一個模塊都是一個apk(組件化的每一個模塊是個lib),最終打包的時候將宿主apk和插件apk(或其餘格式)分開或者聯合打包。github
本文將主要就如下幾個方面進行介紹:web
1、爲何須要組件化?數據庫
2、CC的功能介紹api
3、CC技術要點安全
4、CC執行流程詳細解析服務器
5、使用方式介紹
6、老項目進行組件化改造的成本有多高?
關於使用組件化的理由,上網能搜到不少,如業務隔離、單獨以app運行能提升開發及調試效率等等這裏就很少重複了,我補充一條:組件化以後,咱們能很容易地實現一些組件層面的AOP,例如:
組件A打包在主app中,組件B爲單獨運行的組件app,下圖演示了在主app中調用二者的效果,並將結果以Json的格式顯示在下方。demo下載地址):
實現CC組件化開發框架主要須要解決的問題有如下幾個方面:
爲了減小後期維護成本,想要實現的效果是:當須要添加某個組件到app時,只須要在gradle中添加一下對這個module的依賴便可(一般都是maven依賴,也能夠是project依賴)
最初想要使用的是annotationProcessor經過編譯時註解動態生成組件映射表代碼的方式來實現。但嘗試事後發現行不通,由於編譯時註解的特性只在源碼編譯時生效,沒法掃描到aar包裏的註解(project依賴、maven依賴均無效),也就是說必須每一個module編譯時生成本身的代碼,而後要想辦法將這些分散在各aar種的類找出來進行集中註冊。
ARouter的解決方案是:
運行時經過讀取全部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中配置一下掃描的參數,沒有任何代碼侵入,原理詳細介紹傳送門
經過實現java.util.concurrent.Callable
接口同步返回結果來兼容同步/異步調用:
CCResult result = Callable.call()
來獲取返回結果ExecutorService.submit(callable)
複製代碼
調用組件的onCall方法時,可能須要異步實現,並不能同步返回結果,但同步調用時又須要返回結果,這是一對矛盾。 此處用到了Object的wait-notify機制,當組件須要異步返回結果時,在CC框架內部進行阻塞,等到結果返回時,經過notify停止阻塞,返回結果給調用方
注意,這裏要求在實現一個組件時,必須確保組件必定會回調結果,即:須要確保每一種致使調用流程結束的邏輯分支上(包括if-else/try-catch/Activity.finish()-back鍵-返回按鈕等等)都會回調結果,不然會致使調用方一直阻塞等待結果,直至超時。相似於向服務器發送一個網絡請求後服務器必須返回請求結果同樣,不然會致使請求超時。
爲何須要跨app進行組件調用呢?
目前,常見的組件化框架採用的跨app通訊解決方案有:
設計此功能時,個人出發點是:做爲組件化開發框架基礎庫,想盡可能讓跨進程調用與在進程內部調用的功能一致,對使用此框架的開發者在切換app模式和lib模式時儘可能簡單,另外須要儘可能不影響產品安全性。所以,跨組件間通訊實現的同時,應該知足如下條件:
基於這些需求,我最終選擇了BroadcastReceiver + Service + LocalSocket來做爲最終解決方案:
若是appA內發起了一個當前app內不存在的組件:Component1,則創建一個LocalServerSocket,同時發送廣播給設備上安裝的其它一樣使用了此框架的app,同時,若某個appB內支持此組件,則根據廣播中帶來的信息與LocalServerSocket創建鏈接,並在appB內調用組件Component1,並將結果經過LocalSocket發送給appA。 BroadcastReceiver是android四大組件之一,能夠設置接收權限,能避免外部惡意調用。而且能夠設置開關,接收到此廣播後決定是否響應(僞裝沒接收到...)。 之因此創建LocalSocket連接,是爲了能繼續給此次組件調用請求發送超時和取消的指令。
用這種方式實現時,遇到了3個問題:
關於切換方式在網絡上有不少文章介紹,基本上都是一個思路:在module的build.gradle中設置一個變量來控制切換apply plugin: 'com.android.application'
或apply 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'
複製代碼
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;
}
複製代碼
爲了適應不一樣需求,有2個安全級別能夠設置:
權限驗證(給進程間通訊的廣播設置權限,通常可設置爲簽名級權限校驗),步驟以下:
compile 'com.billy.android:cc:0.3.0'
外部調用是否響應的開關設置(這種方式使用起來更簡單一些)
CC.enableRemoteCC(false)
可關閉響應外部調用爲了方便開發者接入,默認是開啓了對外部組件調用的支持,而且不須要權限驗證。app正式發佈前,建議調用CC.enableRemoteCC(false)來關閉響應外部調用本app的組件。
背景:在使用異步調用時,因爲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 開始支持給fragment設置生命週期監聽:
FragmentManager.registerFragmentLifecycleCallbacks(callback)
複製代碼
可在其onFragmentDestroyed
方法中取消未完成的cc調用
andorid.app.Fragment生命週期關聯(暫不支持)
組件間通訊採用了組件總線的方式,在基礎庫的組件管理類(ComponentMananger)中註冊了全部組件對象,ComponentMananger經過查找映射表找到組件對象並調用。
當ComponentMananger接收到組件的調用請求時,查找當前app內組件清單中是否含有當前須要調用的組件
沒有:執行App之間CC調用的流程
java.util.concurrent.Callable
接口ChainProcessor
類來負責具體組件的調用
ChainProcessor.call()
來調用組件,並將CCResult直接返回給調用方ChainProcessor
放入線程池中執行,經過IComponentCallback.onResult(cc, ccResult)
將CCResult回調給調用方執行過程以下圖所示:
ICCInterceptor
)實現原理全部攔截器按順序存放在調用鏈(Chain)中
在自定義攔截器以前有1個CC框架自身的攔截器:
ValidateInterceptor
在自定義攔截器以後有2個CC框架自身的攔截器:
LocalCCInterceptor
(或RemoteCCInterceptor
)Wait4ResultInterceptor
Chain類負責依次執行全部攔截器interceptor.intercept(chain)
攔截器intercept(chain)
方法經過調用Chain.proceed()
方法獲取CCResult
當要調用的組件在當前app內部時,執行此流程,完整流程圖以下:
CC的主體功能由一個個攔截器(ICCInterceptor
)來完成,攔截器造成一個調用鏈(Chain
),調用鏈由ChainProcessor啓動執行,ChainProcessor對象在ComponentManager中被建立。 所以,能夠將ChainProcessor看作一個總體,由ComponentManager建立後,調用組件的onCall方法,並將組件執行後的結果返回給調用方。 ChainProcessor內部的Wait4ResultInterceptor
ChainProcessor的執行過程能夠被timeout和cancel兩種事件停止。
當要調用的組件在當前app內找不到時,執行此流程,完整流程圖以下:
CC的集成很是簡單,僅需4步便可完成集成:
添加自動註冊插件
buildscript {
dependencies {
classpath 'com.billy.android:autoregister:1.0.4'
}
}
複製代碼
引用apply cc-settings.gradle文件代替 'app plugin ...'
apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
複製代碼
實現IComponent接口建立一個組件類
public class ComponentA implements IComponent {
@Override
public String getName() {
//組件的名稱,調用此組件的方式:
// CC.obtainBuilder("demo.ComponentA").build().callAsync()
return "demo.ComponentA";
}
@Override
public boolean onCall(CC cc) {
Context context = cc.getContext();
Intent intent = new Intent(context, ActivityComponentA.class);
if (!(context instanceof Activity)) {
//調用方沒有設置context或app間組件跳轉,context爲application
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
//發送組件調用的結果(返回信息)
CC.sendCCResult(cc.getCallId(), CCResult.success());
return false;
}
}
複製代碼
使用CC.obtainBuilder("component_name").build().call()
調用組件
//同步調用,直接返回結果
CCResult result = CC.obtainBuilder("demo.ComponentA").build().call();
//或 異步調用,不須要回調結果
CC.obtainBuilder("demo.ComponentA").build().callAsync();
//或 異步調用,在子線程執行回調
CC.obtainBuilder("demo.ComponentA").build().callAsync(new IComponentCallback(){...});
//或 異步調用,在主線程執行回調
CC.obtainBuilder("demo.ComponentA").build().callAsyncCallbackOnMainThread(new IComponentCallback(){...});
複製代碼
更多用法請看github上的README
PS:配合個人另外一個庫(PreLoader)一塊兒食用味道更佳:AOP實如今打開頁面以前預加載頁面所需的數據,而且這個預加載功能徹底在組件內部實現,與外部無耦合。
有些同窗很想嘗試組件化開發,但僅僅停留在瞭解的階段,緣由是擔憂在老項目上進行改造的工程量太大,不敢大改。
CC框架自己就是在老項目進行組件化改造的需求下設計出來的,考慮到了組件化過程當中的一些痛點:
apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'
便可module名稱=true
便可。幾乎零門檻進行組件化開發本文比較詳細地介紹了android組件化開發框架《CC》的主要功能、技術方案及執行流程,並給出了使用方式的簡單示例。 你們若是感興趣的話能夠從GitHub上clone源碼來進行具體的分析,若是有更好的思路和方案也歡迎貢獻代碼進一步完善CC。