Markdown版本筆記 | 個人GitHub首頁 | 個人博客 | 個人微信 | 個人郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
組件化 獲得 DDComponent JIMU 模塊 插件 MDjava
原始的獲得官網的項目(2.7K):DDComponentForAndroid
項目做者離職後維護的項目(1.4K):JIMU
我的實踐Demo
官方Demo簡化android
已實現的功能git
參考:淺談Android組件化github
模塊化、插件化和組件化的關係
在技術開發領域,模塊化
是指分拆代碼,即當咱們的代碼特別臃腫的時候,用模塊化將代碼分而治之、解耦分層。具體到 android 領域,模塊化的具體實施方法分爲插件化和組件化
。編程
插件化和組件化的區別
一套完整的插件化或組件化都必須可以實現單獨調試、集成編譯、數據傳輸、UI 跳轉、生命週期和代碼邊界
這六大功能。插件化和組件化最重要並且是惟一的區別的就是:插件化能夠動態
增長和修改線上的模塊,組件化的動態能力相對較弱,只能對線上已有模塊進行動態的加載和卸載
,不能新增和修改
.。api
如何取捨插件化和組件化?
在插件化和組件化取捨的一個重要原則是:APP 是否有動態增長或修改
線上模塊的需求,若是這種動態性的需求很弱,就不須要考慮插件化,通常說來,電商類或廣告類產品對這個需求比較強烈,而相似「獲得 APP」這類的知識服務產品,每一個功能的推出都是通過精細打磨的,對這種即時的動態性要求不高,因此不須要採用插件化。微信
若是你的產品對動態性的要求比較高,那麼在選擇插件化以前也須要從兩個方面權衡一下:網絡
所以,對大多數產品來講,組件化都是一個不錯甚至最佳的選擇,它沒有兼容性,能夠更方便地拆分,而且幾乎沒有技術障礙,能夠更順利地去執行。特別是對急需拆分的產品來講,組件化是一個可退可守的方案,能夠更快地執行下去,而且未來要是遷移到插件化,組件化拆分也是必經的一步。架構
何爲「完全」組件化
之因此稱這個方案是完全的組件化,主要是爲了更好地強調組件之間代碼邊界
的問題,組件之間的直接引用(compile)是要堅定避免的,一旦這麼作了,就不免會致使直接使用其餘組件的具體實現類,這樣針對接口編程的要求就成了一句空話。更嚴重的是,一旦決定對組件進行動態地加載或卸載,就會致使嚴重地崩潰。因此只有作到了代碼隔離,這個組件化方案才能夠稱之爲「完全」的。app
在如今的方案中能夠作到代碼編寫期間組件之間是徹底不可見的,所以杜絕了直接使用具體的實現類的狀況,可是在編譯打包的時候,又會自動把依賴的組件打包進去。該方案是一個集合了六大功能的完整方案,覆蓋了組件化中須要考慮的所有狀況。
既然是「完全」組件化,那麼代碼解耦以後,怎樣才能讓主項目間接引用
各個獨立的組件呢?
方案採用的是一個配置文件,每一個組件聲明本身所須要的其餘組件,配置分爲 debug 和 release 兩種,能夠在平常開發和正式打包之間更靈活的切換。
方案自定義了一個 gradle 插件,它去讀取每一個組件的配置文件,構建出組件之間的依賴關係
。這個插件更「智能」的地方在於,它分析運行的 task 命令,判斷是不是打包命令
,是的話(例如 assembleRelease)自動根據配置引入,不是(例如正常的 sync /build)等則不引入,也就是在代碼編寫期間
組件之間是徹底不可見的,所以杜絕了直接使用具體的實現類的狀況。可是在編譯打包
的時候,又會自動把依賴的組件打包進去。固然這裏面還會涉及到組件之間如何經過「接口 + 實現」的方式進行數據傳輸,每一個組件若是進行加載等問題,這些在 方案 中都有成熟的解決方式。
方案中自定義的 gradle 插件還有一個比較好的功能就是能夠自動的識別和修改組件的屬性,它能夠識別出當前調試的是哪一個組件,而後把這個組件修改成 application 項目,而其餘組件則默默的修改爲 library 項目。所以不管是要單獨編譯一個組件仍是要把這個組件集成到其餘組件中調試,都是不須要作任何的手動修改,使用起來至關的方便。
在剛開始對「獲得 APP」Android 端的代碼進行組件化拆分的時候,「獲得 APP」已是一個千萬用戶級的產品。通過那麼長時間的積累,幾十萬行代碼堆積在一塊兒,編譯一次大約須要 10 分鐘的時間,這嚴重影響了開發效率。
因爲業務複雜,代碼交織在一塊兒,可謂牽一髮而動全身,所以每一個人在寫新需求的時候都有嚴重的代碼包袱,瞻前顧後,花費在熟悉以前的代碼的時間甚至大於新需求的開發時間。而且每一個改動都須要測試人員進行大範圍的迴歸,因此整個開發團隊的效率都受到了影響。在這種狀況下,實施組件化是迫在眉睫了。
因爲國內對插件化
的研究是比較火爆的,而對組件化的研究熱情就相對淡了不少。在設計「獲得 APP」組件化方案的時候,幾乎查閱了所有的組件化文章,都沒有找到一個完整的支持上面說的六大功能的方案,因此不得不從頭開始設計,「完全組件化」的方案可跳轉閱讀 Android 完全組件化方案實踐 和 Android 完全組件化 demo
讓方案從紙上運用到實際,是一個比較困難的過程,這期間要注意兩個方面的問題:一是技術細節上的不斷完善,二是團隊的共識建設問題。
技術上的問題主要是如何讓方案更靈活,需求老是比預期要複雜,遇到特殊的需求,以前的設計可能就無法實現,或者必須得突破以前肯定的拆分原則。這時候就須要回過頭再審視整個方案,看看可否在某些方面作一些調整。方案中數據傳輸和 UI 跳轉是分開的兩個功能,這是在實際拆分中才作出的選擇,由於數據傳輸更爲頻繁,且交互形式更多樣,使用一個標準化的路由協議難以知足,所以把數據傳輸改爲了接口 + 實現的形式,針對接口編程就能夠更加靈活地處理這些狀況。
除了技術上的,更重要的是團隊的共識問題。要執行一個組件化拆分這樣的大工程,須要團隊的每一個人達成共識,不管是在方案仍是在技術的實現細節上,你們都能有一個統一的方向。
爲此,在拆分以前多作幾回組內的分享討論,從方案的制定到每一次的實施,都讓團隊的大部分紅員參與進來。正所謂磨刀不誤砍柴工,在這種前提下,團隊的共識建設會對後期工做效率的提升產生很大的價值。確立了共識,還須要確立統一的規則,雖然說條條大路通羅馬,可是在一個產品裏,仍是須要選擇統一的道路,不然即使作了拆分,效果也會大打折扣。
不管是 Android 仍是 iOS,要解決的問題都是同樣的,所以在組件化方案上要實現的功能(即上面所說的上面六種功能)也都是同樣的,因此二者的組件化大致上來講是基本相同的。
有一個微小的區別在於技術實現方式的不一樣,因爲兩個平臺用到的開發技術是不一樣的,Android 的組件化可能須要考慮向插件化的遷移,後期一旦有動態變更功能的強需求,能夠快速地切換。而目前蘋果官方是不容許這種動態性的,因此這方面的考慮就會少一點。可是 iOS 一樣能夠作到動態地加載和卸載組件的,所以在諸如生命週期、代碼邊界等問題上也須要格外注意,只是目前一些 iOS 組件化方案在這方面可能考慮的相對少一點。
組件化後的代碼結構很是清晰,分層結構以及之間的交互很明瞭,團隊中的任何一我的均可以很輕鬆的繪製出代碼結構圖,這個在以前是無法作到的,而且每一個組件的編譯時間從 10 分鐘降到了幾十秒,工做效率有了很大地提高,最關鍵的仍是解耦以後,每次開發需求的時候,面對的代碼愈來愈少,不用揹負那麼重的代碼包袱,能夠說達到了「代碼越寫越少」的理想狀況。
其實組件化對外輸出也是很可觀的,如今一個版本開發完成後,咱們能夠跟測試說這期就回歸「天天聽本書」組件,其餘的不須要回歸。這種自信在以前是絕對沒有的,測試的壓力也能夠小不少。更重要的是咱們的組件能夠複用,「獲得 APP」會上線新的產品,他們能夠直接使用已有的組件,省去了不少重複造輪子的工做,這點對整個公司效率的提高也是頗有幫助的。
項目發展到必定程度,隨着人員的增多,代碼愈來愈臃腫,這時候就必須進行模塊化的拆分。在我看來,模塊化是一種指導理念,其核心思想就是分而治之、下降耦合。而在Android工程中如何實施,目前有兩種途徑,也是兩大流派,一個是組件化,一個是插件化。
提起組件化和插件化的區別,有一個很形象的圖:
上面的圖看上去彷佛比較清晰,其實容易致使一些誤解,有下面幾個小問題,圖中說的就不太清楚:
本文主要集中講的是組件化的實現思路,對於插件化的技術細節不作討論,咱們只是從上面的問答中總結出一個結論:組件化和插件化的最大區別(應該也是惟一區別)就是組件化在運行時不具有動態添加和修改組件的功能
,可是插件化是能夠的。
暫且拋棄對插件化「道德」上的批判,我認爲對於一個Android開發者來說,插件化的確是一個福音,這將使咱們具有極大的靈活性。可是苦於目前尚未一個徹底合適、完美兼容的插件化方案,特別是對於已經有幾十萬代碼量的一個成熟產品來說,套用任何一個插件化方案都是很危險的工做。因此咱們決定先從組件化作起,本着作一個最完全的組件化方案的思路去進行代碼的重構,下面是最近的思考結果,歡迎你們提出建議和意見。
要實現組件化,不論採用什麼樣的技術路徑,須要考慮的問題主要包括下面幾個:
把龐大的代碼進行拆分,AndroidStudio可以提供很好的支持,使用IDE中的multiple module
這個功能,咱們很容易把代碼進行初步的拆分。在這裏咱們對兩種module進行區分:
基礎庫
library,這些代碼被其餘組件直接引用
(是次方案中很是核心的設計理念),好比網絡庫module能夠認爲是一個library。完整的功能模塊
,好比讀書或者分享module就是一個Component。爲了方便,咱們統一把library稱之爲依賴庫,而把Component稱之爲組件,咱們所講的組件化也主要是針對Component這種類型。而負責拼裝這些組件以造成一個完成app的module,通常咱們稱之爲主項目、主module或者Host
,方便起見咱們也統一稱爲主項目。
通過簡單的思考,咱們可能就能夠把代碼拆分紅下面的結構:
這種拆分都是比較容易作到的,從圖上看,讀書、分享等都已經拆分組件,並共同依賴於公共的依賴庫(簡單起見只畫了一個),而後這些組件都被主項目所引用。讀書、分享等組件之間沒有直接的聯繫,咱們能夠認爲已經作到了組件之間的解耦。
可是這個圖有幾個問題須要指出:
主項目對組件的直接引用是不能夠的
,可是咱們的讀書組件最終是要打到apk裏面,不只代碼要和併到claases.dex裏面,資源也要通過meage操做合併到apk的資源裏面,怎麼避免這個矛盾呢?這些問題咱們後面一個個來解決,首先咱們先看代碼解耦要作到什麼效果,像上面的直接引用並使用其中的類確定是不行的了。因此咱們認爲代碼解耦的首要目標就是組件之間的徹底隔離
,咱們不只不能直接使用其餘組件中的類,最好能根本不瞭解其中的實現細節。只有這種程度的解耦纔是咱們須要的。
其實單獨調試比較簡單,只須要把 apply plugin: 'com.android.library'
切換成 apply plugin: 'com.android.application'
就能夠,可是咱們還須要修改一下AndroidManifest
文件,由於一個單獨調試須要有一個入口的actiivity。
咱們能夠設置一個變量isRunAlone
,標記當前是否須要單獨調試,根據isRunAlone的取值,使用不一樣的gradle插件和AndroidManifest文件,甚至能夠添加Application等Java文件,以即可以作一下初始化的操做。
爲了不不一樣組件之間資源名重複,在每一個組件的build.gradle中增長resourcePrefix "xxx_"
,從而固定每一個組件的資源前綴。下面是讀書組件的build.gradle
的示例:
if (isRunAlone.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } //... .. resourcePrefix "readerbook_" sourceSets { main { if (isRunAlone.toBoolean()) { manifest.srcFile 'src/main/runalone/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/runalone/java'] res.srcDirs = ['src/main/res', 'src/main/runalone/res'] } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } }
經過這些額外的代碼,咱們給組件搭建了一個測試Host
,從而讓組件的代碼運行在其中,因此咱們能夠再優化一下咱們上面的框架圖。
上面咱們講到,主項目和組件、組件與組件之間不能直接使用類的相互引用來進行數據交互。那麼如何作到這個隔離呢?
在這裏咱們採用接口+實現
的結構。每一個組件聲明本身提供的服務Service(抽象類或者接口),組件負責將這些Service實現並註冊到一個統一的路由Router中去。若是要使用某個組件的功能,只須要向Router請求這個Service的實現,具體的實現細節咱們全然不關心(也沒有暴露),只要能返回咱們須要的結果就能夠了。這與Binder的C/S架構很相像。
由於咱們組件之間的數據傳遞都是基於接口編程的,接口和實現是徹底分離的,因此組件之間就能夠作到解耦,咱們能夠對組件進行替換、刪除等動態管理。
這裏面有幾個小問題須要明確:
componentservice
(另外一個核心設計)的依賴庫,裏面定義了每一個組件向外提供的service和一些公共model。將全部組件的service整合在一塊兒,是爲了在拆分初期操做更爲簡單,後面須要改成自動化的方式來生成。這個依賴庫須要嚴格遵循開閉原則,以免出現版本兼容等問題。下面就是加上數據傳輸功能以後的架構圖:
能夠說UI的跳轉也是組件提供的一種特殊的服務,能夠歸屬到上面的數據傳遞中去。不過通常UI的跳轉咱們會單獨處理,通常經過短鏈
的方式來跳轉到具體的Activity。每一個組件能夠註冊本身所能處理的短鏈的scheme和host
,並定義傳輸數據的格式。而後註冊到統一的UIRouter
中,UIRouter經過scheme和host的匹配關係負責分發路由。
UI跳轉部分的具體實現是經過在每一個Activity上添加註解,而後經過apt(Annotation Processing Tool, 註解處理器)
造成具體的邏輯代碼。這個也是目前Android中UI路由的主流實現方式。
具體的功能介紹和使用規範,請你們參見文章:android完全組件化—UI跳轉升級改造
因爲咱們要動態的管理組件,因此給每一個組件添加幾個生命週期狀態:加載、卸載和降維
。爲此咱們給每一個組件增長一個ApplicationLike
類,裏面定義了onCreate和onStop
兩個生命週期函數。
一個小的細節是,主項目負責加載組件,因爲主項目和組件之間是隔離的,那麼主項目如何調用組件ApplicationLike的生命週期方法呢?
目前咱們採用的是基於編譯期字節碼插入
的方式,掃描全部的ApplicationLike
類(其有一個共同的父類),而後經過Javassist
(Java assist ,一個開源的分析、編輯和建立Java字節碼的類庫)在主項目的onCreate中插入調用 ApplicationLike.onCreate 的代碼
。
咱們再優化一下組件化的架構圖:
每一個組件單獨調試經過並不意味着集成在一塊兒沒有問題,所以在開發後期咱們須要把幾個組件機集成到一個app裏面去驗證。因爲咱們上面的機制保證了組件之間的隔離,因此咱們能夠任意選擇幾個組件參與集成。這種按需索取的加載機制能夠保證在集成調試中有很大的靈活性,而且能夠極大的加快編譯速度。
咱們的作法是這樣的,每一個組件開發完成以後,發佈一個relaese的aar
到一個公共倉庫,通常是本地的maven庫。而後主項目經過參數配置要集成的組件就能夠了。
因此咱們再稍微改動一下組件與主項目之間的鏈接線,造成的最終組件化架構圖以下:
此時再回顧咱們在剛開始拆分組件化時提出的三個問題,應該說都找到了解決方式,可是還有一個隱患沒有解決,那就是咱們可使用compile project(xxx:reader.aar)
來引入組件嗎?雖然咱們在數據傳輸章節使用了接口+實現
的架構,組件之間必須針對接口編程,可是一旦咱們引入了reader.aar,那咱們就徹底能夠直接使用到其中的實現類啊,這樣咱們針對接口編程的規範就成了一紙空文。千里之堤毀於蟻穴,只要有代碼(不管是有意仍是無心)是這麼作了,咱們前面的工做就白費了。
咱們但願只在assembleDebug或者assembleRelease
的時候把aar引入進來(也就是在執行打包
命令的時候,首先經過compile引入組件),而在開發階段,全部組件都是看不到的(由於開發階段咱們並無經過compile引入組件),這樣就從根本上杜絕了引用實現類的問題。
爲了實現這個目的,咱們是這麼作的:咱們把這個問題交給gradle
來解決,咱們建立一個gradle插件,而後每一個組件都apply這個插件,插件的配置代碼也比較簡單,就是在執行打包命令的時候,根據配置添加各類組件依賴,而且自動化生成組件加載代碼:
apply plugin: 'com.dd.comgradle' //根據配置添加各類組件依賴,而且自動化生成組件加載代碼 if (project.android instanceof AppExtension) { AssembleTask assembleTask = getTaskInfo(project.gradle.startParameter.taskNames) if (assembleTask.isAssemble && (assembleTask.modules.contains("all") || assembleTask.modules.contains(module))) { project.dependencies.add("compile","xxx:reader-release@aar") //添加組件依賴 //字節碼插入的部分也在這裏實現 } } //獲取正在執行的Task的信息 private AssembleTask getTaskInfo(List<String> taskNames) { AssembleTask assembleTask = new AssembleTask(); for (String task : taskNames) { if (task.toUpperCase().contains("ASSEMBLE")) { assembleTask.isAssemble = true; String[] strs = task.split(":") assembleTask.modules.add(strs.length > 1 ? strs[strs.length - 2] : "all"); } } return assembleTask }
拆分原則(只是建議)
組件化的拆分是個龐大的工程,特別是從幾十萬行代碼的大工程拆分出去,所要考慮的事情千頭萬緒。爲此我以爲能夠分紅三步:
組件化的動態需求(然並不支持)
最開始咱們講到,理想的代碼組織形式是插件化的方式,屆時就具有了完備的運行時動態化。在向插件化遷徙的過程當中,咱們能夠經過下面的集中方式來實現編譯速度的提高和動態更新。
獲得組件化改造大流程
本文是筆者在設計"獲得app"的組件化中總結一些想法(目前已經離職加入頭條),在設計之初參考了目前已有的組件化和插件化方案,站在巨人的肩膀上又加了一點本身的想法,主要是組件化生命週期以及徹底的代碼隔離方面。特別是最後的代碼隔離,不只要有規範上的約束(針對接口編程),更要有機制保證開發者不犯錯,我以爲只有作到這一點才能認爲是一個完全的組件化方案。
2018-4-22