单独组件_阿里P8年薪百万大牛-教你打造一个Android组件化开发框架
作者簡介
本篇來自 lucky_billy 的投稿,分享了他的開源組件化框架,詳細地講解框架形成的思路,希望對大家有所幫助。
lucky_billy 的博客地址:
http://blog.csdn.net/cdecde111解讀開源框架設計思想B站學習視頻
滿滿誠意:【實戰MVVM和Jetpack的完美結合讓頁面開發不再煩惱】
【1.什么是插件化】
【2.插件化能解決的問題及與組件化的區別】
【3.常用插件化框架對比】
。
。
。
【13.手寫實現插件的資源加載】
點擊【設計思想解讀開源框架】學習筆記,學習路線獲取!
前言
CC:Component Caller,一個android組件化開發框架, 已開源,github 地址:
https://github.com/luckybilly/CC本文主要講解框架實現原理,如果只是想了解一下如何使用,可直接到 github上查看README文檔
首先說明一下,本文將講述的組件化與業內的插件化(如:Atlas, RePlugin等)不是同一個概念
[圖片上傳失敗...(image-cd3f7b-1607780116731)]
組件化開發:就是將一個app分成多個Module,每個Module都是一個組件(也可以是一個基礎庫供組件依賴),開發的過程中我們可以單獨調試部分組件,組件間不需要互相依賴,但可以相互調用,最終發布的時候所有組件以lib的形式被主app工程依賴并打包成1個apk。
插件化開發:和組件化開發略有不用,插件化開發時將整個app拆分成很多模塊,這些模塊包括一個宿主和多個插件,每個模塊都是一個apk(組件化的每個模塊是個lib),最終打包的時候將宿主apk和插件apk(或其他格式)分開或者聯合打包。
本文將主要就以下幾個方面進行介紹:
一、為什么需要組件化?
二、CC的功能介紹
三、CC技術要點
四、CC執行流程詳細解析
為什么需要組件化?
關于使用組件化的理由,上網能搜到很多,如業務隔離、單獨以app運行能提高開發及調試效率等等這里就不多重復了,我補充一條:組件化之后,我們能很容易地實現一些組件層面的AOP,例如:
- 輕易實現頁面數據(網絡請求、I/O、數據庫查詢等)預加載的功能組件被調用時,進行頁面跳轉的同時異步執行這些耗時邏輯頁面跳轉并初始化完成后,再將這些提前加載好的數據展示出來
- 在組件功能調用時進行登錄狀態校驗
- 借助攔截器機制,可以動態給組件功能調用添加不同的中間處理邏輯
CC的功能介紹
demo效果演示
組件A 打包在主app中,組件B 為單獨運行的組件app,下圖演示了在主app中調用兩者的效果,并將結果以Json的格式顯示在下方:
CC技術要點
實現CC組件化開發框架主要需要解決的問題有以下幾個方面:
- 組件如何自動注冊?
- 如何兼容同步/異步方式調用組件?
- 如何兼容同步/異步方式實現組件?
- 如何進行跨進程組件任意功能的調用(不只是啟動Activity)?
- 組件如何更方便地在application和library之間切換?
- 如何實現startActivityForResult?
- 如何阻止非法的外部調用?
- 如何與Activity、Fragment的生命周期關聯起來
組件如何自動注冊?
為了減少后期維護成本,想要實現的效果是:當需要添加某個組件到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中配置一下掃描的參數,沒有任何代碼侵入,原理詳細介紹:
http://blog.csdn.net/cdecde111/article/details/78074692如何兼容同步/異步方式調用組件?
通過實現 java.util.concurrent.Callable 接口同步返回結果來兼容同步/異步調用:
- 同步調用時,直接調用 CCResult result = Callable.call() 來獲取返回結果
- 異步調用時,將其放入線程池中運行,執行完成后調用回調對象返回結果: IComponentCallback.onResult(cc, result)
ExecutorService.submit(callable)
如何兼容同步/異步方式實現組件?
調用組件的 onCall方法 時,可能需要異步實現,并不能同步返回結果,但同步調用時又需要返回結果,這是一對矛盾。
此處用到了 Object 的wait-notify機制,當組件需要異步返回結果時,在CC框架內部進行阻塞,等到結果返回時,通過notify中止阻塞,返回結果給調用方
注意,這里要求在實現一個組件時,必須確保組件一定會回調結果,即:需要確保每一種導致調用流程結束的邏輯分支上(包括if-else/try-catch/Activity.finish()-back鍵-返回按鈕等等)都會回調結果,否則會導致調用方一直阻塞等待結果,直至超時。類似于向服務器發送一個網絡請求后服務器必須返回請求結果一樣,否則會導致請求超時。
如何進行跨進程組件任意功能的調用(不只是啟動Activity)?
市面上常見的組件化框架采用的通信解決方案有:
URLScheme(例如:ActivityRouter、ARouter等)
- 優勢有:基因中自帶支持從webview中調用不用互相注冊(不用知道需要調用的app的進程名稱等信息)
- 劣勢有:只能單向地給組件發送信息,適用于啟動Activity和發送指令,不適用于獲取數據(例如:獲取用戶組件的當前用戶登錄信息)需要有個額外的中轉Activity來統一處理URLScheme
如果設備上安裝了多個使用相同URLScheme的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
組件如何更方便地在application和library之間切換?
關于切換方式在網絡上有很多文章介紹,基本上都是一個思路:在 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'
如何實現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; }
如何阻止非法的外部調用?
為了適應不同需求,有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的組件。
如何與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開始支持給fragment設置生命周期監聽:
FragmentManager.registerFragmentLifecycleCallbacks(callback)
可在其 onFragmentDestroyed 方法中取消未完成的cc調用
- andorid.app.Fragment生命周期關聯(暫不支持)
CC執行流程詳細解析
組件間通信采用了組件總線的方式,在基礎庫的組件管理類(ComponentMananger)中注冊了所有組件對象,ComponentMananger通過查找映射表找到組件對象并調用。
當ComponentMananger接收到組件的調用請求時,查找當前app內組件清單中是否含有當前需要調用的組件
有:執行App內部CC調用的流程:
沒有:執行App之間CC調用的流程
組件的同步/異步實現和組件的同步/異步調用原理
組件實現時,當組件調用的相關功能結束后,通過CC.sendCCResult(callId, ccResult)將調用結果發送給框架
IComponent實現類(組件入口類)onCall(cc)方法的返回值代表是否異步回調結果:
- true: 將異步調用CC.sendCCResult(callId, ccResult)
- false: 將同步調用CC.sendCCResult(callId, ccResult)。意味著在onCal方法執行完之前會調用此方法將結果發給框架
當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回調給調用方
執行過程如下圖所示:
自定義攔截器(ICCInterceptor)實現原理
所有攔截器按順序存放在調用鏈(Chain)中
在自定義攔截器之前有1個CC框架自身的攔截器:
- ValidateInterceptor
在自定義攔截器之后有2個CC框架自身的攔截器:
- LocalCCInterceptor(或RemoteCCInterceptor)
- Wait4ResultInterceptor
Chain類負責依次執行所有攔截器interceptor.intercept(chain)
攔截器intercept(chain)方法通過調用Chain.proceed()方法獲取CCResult
App內部CC調用流程
當要調用的組件在當前app內部時,執行此流程,完整流程圖如下:
CC的主體功能由一個個攔截器(ICCInterceptor)來完成,攔截器形成一個調用鏈(Chain),調用鏈由ChainProcessor啟動執行,ChainProcessor對象在ComponentManager中被創建。
因此,可以將ChainProcessor看做一個整體,由ComponentManager創建后,調用組件的onCall方法,并將組件執行后的結果返回給調用方。
ChainProcessor內部的Wait4ResultInterceptor
ChainProcessor的執行過程可以被timeout和cancel兩種事件中止。
App之間CC調用流程
當要調用的組件在當前app內找不到時,執行此流程,完整流程圖如下:
結語
本文比較詳細地介紹了android組件化開發框架《CC》的主要功能、技術方案及執行流程,并給出了使用方式的簡單示例。
大家如果感興趣的話可以從GitHub上clone源碼來進行具體的分析,如果有更好的思路和方案也歡迎貢獻代碼進一步完善CC。
轉載請注明:Android開發中文站 ? 教你打造一個Android組件化開發框架
總結
以上是生活随笔為你收集整理的单独组件_阿里P8年薪百万大牛-教你打造一个Android组件化开发框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: servlet学习--Cookie小应用
- 下一篇: JDBC小应用