Android 組件化之路

首先先分清楚兩個概念:android

模塊化

模塊化編程是將一個程序按照功能拆分紅相互獨立的若干模塊,它強調將程序的功能分離成獨立的、可替換的模塊。每一個模塊內只有與其相關功能的內容。web

模塊化編程和結構化編程與面向對象編程是密切相關的,它們的目的都是將大型軟件程序劃分紅一個個更小的部分。模塊化編程的粒度會更「粗」一些。在Java9中也在編譯器層面提供了模塊化的支持:Java Platform Module System 。面試

組件是一個相似的概念,但一般指更高的級別;組件是整個系統的一部分,而模塊是單個程序的一部分。「模塊」一詞因語言而有很大差別;在Python中,它很是小,每一個文件都是一個模塊,而在Java 9中,它是很是大的,其中模塊是包的集合,包又是文件的集合。編程

在面向對象編程中,一般使用接口做爲模塊間通訊的橋樑,也就是基於接口的編程。安全

組件化

組件化開發是軟件工程的一個分支,它強調對給定軟件系統中普遍可用的功能進行分割。基於可重用的目的將一個大的軟件系統拆分紅多個獨立的組件,減小系統耦合度。性能優化

組件化開發中認爲組件做爲系統的一部分,是可獨立運行的服務,維基百科中舉了一個例子:在web服務中,有一種面向服務的架構設計--service-oriented architectures (SOA),這種架構設計從業務角度出發,利用企業現有的各類軟件體系,從新整合並構建起一套新的軟件架構。這套軟件架構能夠隨着業務的變化,隨時靈活地結合現有服務,組成一個新的軟件。增長應用系統的靈活性。服務器

組件能夠產生或者消費事件,也能夠應用於事件驅動架構。網絡

  • 組件之間經過接口進行通訊
  • 組件是可替換的,若是後續組件知足初始組件的需求(經過接口表示),則組件能夠替換另外一個組件(在設計時或運行時),所以能夠用更新的版本或替代的版本替換組件,而不會破壞系統的運行。
  • 一個判斷可替換組件的經驗法則是:若是組件B至少提供了A提供的組件,而且使用的組件不超過A使用的組件,那麼組件B能夠當即替換組件A
  • 當組件直接與用戶交互時,應該考慮基於組件的可用性測試。
  • 組件須要是徹底文檔化、全面測試、具備全面的輸入效度檢查的。

模塊化 or 組件化

無論是模塊化仍是組件化,都不是一個新的設計思想,它們最先都是在20世紀60年代就已經被提出了,可是早期的移動應用因爲相對簡單,自己邏輯功能也很少,因此在移動端的應用反而沒那麼普遍。(雖然Java最開始的模塊化是針對在移動和嵌入式設備上的應用設計的)。數據結構

從上面的概述來看其實組件化跟模塊化沒有明顯的區別;一個登陸功能能夠是一個模塊也能夠是一個組件,一個日期選擇控件能夠是一個模塊,也能夠是一個組件,由於無論是模塊化仍是組件化,它們都有一個共同的目標:將一個大的軟件系統細化成一個個模塊或者組件,都是爲了重用和解耦。所以沒有一個明確的界線去區分它們。架構

網上不少文章對於組件和模塊的定義也是不盡相同的,一些人認爲組件的粒度更細,它只是具有單一功能與業務無關的組件,好比一個日曆選擇控件就認爲是一個組件。而模塊他們認爲就是業務模塊,顧名思義,就是按業務劃分而成的模塊。而另外一部分人則相反。

在維基百科對模塊化的解釋中有這麼一句:

A component is a similar concept, but typically refers to a higher level; a component is a piece of a whole system, while a module is a piece of an individual program

也就是認爲組件粒度較模塊要更大,因此本文對組件和模塊作出如下定義:

  • 組件:側重於業務,可編譯成單獨的app,通常只負責單一業務,具有自身的生命週期(一般包含Android四大組件的一個或多個,因此稱之爲組件也更加貼切)
  • 模塊:側重於功能,與業務無關,好比自定義控件、網絡請求庫、圖片加載庫等

而從Android Studio推出以後,咱們在開發項目時也會有意識的將一些可重用的代碼邏輯抽離成一個個的Module,這也就是模塊化開發的雛形。固然,組件化開發也不是就盡善盡美的,下面列舉了它的一些優缺點:

優勢:一個複雜的系統能夠由一個個組件集合而成,甚至於不一樣的組合能夠構建出不一樣的系統。每一個組件有獨立的版本,可獨立編譯、打包,大大提升了系統的靈活性以及開發人員的開發效率。應用的更新能夠精細到組件,組件的升級替換不會影響到其它組件,也不會受其它組件的限制。

基於組件化架構設計的應用比傳統的「單片」設計可重用性高得多,由於這些組件能夠在其餘項目中重用,並且開發人員無需瞭解整個應用,能夠只專一於分配給他們的較小的任務,提升開發效率。

缺點:組件化的實施對開發人員和團隊管理人員提出了更高水平的要求,項目管理難度更大。組件間如何進行通訊也是須要慎重考慮的。萬事開頭難,在對一個項目進行組件化分解時就好像庖丁解牛通常,你須要瞭解項目的「肌理筋骨」,才知道從何處下「刀」,才能更輕易的去分解項目,這就要求架構師對於項目的總體需求瞭如指掌。

下面就來談談個人組件化之路。。。

首先我負責的項目相似於一個遠程控制應用,它與服務器創建Socket鏈接,接收服務器發送過來的指令,針對這些指令對當前Android設備執行關機、安裝應用等操做。應用自己也會收集一些設備信息如應用運行日誌,使用時長等,在某個指定的時間點上傳至服務器。理想的組件間依賴關係是這樣的:

其中基礎模塊不能脫離主工程獨立運行,組件之間不能直接依賴,組件間通訊方式能夠是接口也能夠是事件總線。團隊中的開發人員只須要關注自身負責的組件(在開發模式下各組件會轉化爲可單獨運行的App,說白了就是在build.gradle文件中將apply plugin: 'com.android.library'改成apply plugin: 'com.android.application',網上有不少相關資料,在此就不贅述了)。

如今來了個開發需求須要改動組件Component1內部的邏輯,團隊中的小A是負責該組件的開發人員,在接到需求後,小A啪啪啪一頓猛如虎的操做完成需求後,對該組件進行單元測試,檢查組件輸入輸出,測試經過後提交代碼,審覈經過後構建平臺構建、打包、發佈,整個過程徹底沒有「驚動」其餘組件,Perfect!

然而現實是殘酷的。。

因爲組件間不可能徹底不通訊,因此現實狀況組件之間的依賴關係有多是這樣的:

對比上圖,組件之間顯得更加「親密無間」了,並且這還不是糟糕的狀況,當組件愈來愈多,各類相互依賴,循壞依賴的問題會讓你痛不欲生。

由於組件之間不可避免的存在須要通訊的狀況,好比 Component1須要調用Component2的方法通常狀況下咱們都是直接經過類名或對象引用的方式去調用相應的方法。可是這種通訊方式正是致使組件之間高度耦合的罪魁禍首,因此必須杜絕這種通訊方式。

那麼問題來了,怎麼作到既能讓組件間通訊又高度解耦呢?這就須要用到文章開頭提到的面向接口編程思想和依賴注入(或者叫依賴查找)技術。舉個🌰:

組件A中的Foo1類依賴組件BFoo2類中的bar方法,一種比較low的實現方式是:

//ComponentA
class Foo1 {
    private Foo2 mFoo2;
    public void main() {
        mFoo2 = new Foo2();
        mFoo2.bar();
    }
}

//ComponentB
class Foo2 {
    public void bar() {
        //nop
    }
}

這種實現方式違反了控制反轉設計原則,耦合度高,假如這時需求變動了,須要使用組件C的Foo3類中的bar()方法去替換原來的實現,那這下樂子就大了。

而經過面向接口編程以及依賴注入技術咱們能很好的遵循控制反轉設計原則:

//Common Component
interface IBar {
    void bar()
}

//ComponentA
class Foo1 {
    private IBar mBar;

    public void main() {
        if (mBar != null) {
            mBar.bar();
        }
    }

    public void setBar(IBar bar) {
        mBar = bar;
    }
}

//ComponentB
class Foo2 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

//ComponentC
class Foo3 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

這就是經典的實現了控制反轉的示例代碼,Foo1類只知道本身須要一個實現了IBar接口的實例,而後調用接口的bar()方法,至因而誰去實現的這個接口,很差意思,它壓根不關心。

雖然你Foo1類是舒服了,把依賴關係交給外部去解決了,可是總要有人去負責這部分的工做吧。這時候依賴注入容器(IOC容器)就登場了,若是對web開發有所瞭解的同窗確定不會感到陌生,Spring就是一個IOC容器,這個容器把依賴查找,類實例化(其實就是根據類的路徑名稱經過反射進行實例化)這些髒活累活攬在身上,這樣既實現了控制反轉又極大提升了應用的靈活性和可維護性。

正由於依賴注入能有效地下降代碼之間的耦合度,因此基於依賴注入實現的組件化框架(路由框架)也就應運而生了,目前主流的Android組件化框架有ARouter、CC、DDComponentForAndroid、ActivityRouter等等,我本身也使用Kotlin基於kapt技術實現了一個路由框架KRouter。

雖然相關的框架有不少,可是它們實現原理不外乎兩種:

  • 一種是將分佈在各個組件的類按照必定的規則在內部生成映射表,這個映射表的數據結構一般是一個Map,Key是一個字符串,Value是一個類或者是類的路徑名稱(用於經過反射進行類的實例化)。通俗來講就是類的查找,這種實現方式要求調用方和被調用方都持有接口類,一般這些共同持有的接口類會被定義在一個Common基礎模塊中,並且在運行時這些相互通訊的組件必須打包到同一個APK中。這種實現方式致使沒法真正實現代碼隔離(須要通訊的兩個組件仍然是存在依賴關係的),基於這種原理實現的組件化架構「自約束能力」很弱,由於沒法約束開發人員經過直接引用的方式進行通訊的行爲,雖然一開始設計人員想的很美好,可是開發人員在實現時作出來的產品卻不是那樣,由於「自約束能力」弱的架構設計是經過「編碼規範」、「測試驅動」甚至是「人員熟練度」來保證開發人員實現的代碼符合設計人員的設計初衷,並且這種架構也沒法保證後續接手維護項目的開發人員可以貫徹本來的設計思想,隨着時間推移,項目往愈來愈糟糕的方向演進(解決這個問題最好的方案就是從編譯器層面進行約束,也就是把問題攔截在編碼階段,然而Java9才支持模塊化開發,Android目前還處於支持部分Java8的特性的階段,路還很長)。
  • 另外一種方案是基於事件總線的方式實現組件之間的通訊,再也不是面向接口編程,而是面向通訊協議編程,能夠理解爲組件間的調用相似http請求。這些框架會在內部創建跨進程通訊的鏈接(也就是事件總線),這條事件總線負責分發路由請求以及返回執行結果。這種實現方式的好處是真正能夠實現代碼隔離,組件能夠運行在獨立的進程中,可是隻支持基本類型參數的轉發。實現跨進程通訊有不少方案,好比Android原生的四大組件、Socket、FileObserver、MemoryFile、基於AIDL的Messager等等,使用Android原生的好處是安全性方面的工做由Android幫咱們完成了,而使用Socket則須要本身實現加密Socket。

第一種方案適合小型的項目,由於這些項目一般都是單進程的,雖然這樣設計的架構「自約束能力」弱,可是目前大多數Android項目團隊開發人數也不會太多,因此管理難度較小,而第二種實現方案則更適合跨進程組件化的項目(組件通常運行在獨立的進程中甚至一個組件就是一個APP)。

在我看來Android的組件化是存在3個階段的,第一個是從單工程項目過分到多模塊的階段;第二個是從多模塊過分到多組件的階段;第三個就是多組件獨立進程的階段。而目前大多數應用其實都是在第二個階段或者介於第二和第三個階段之間,因此對於這樣的項目,選擇一個既支持類查找方式,又支持事件總線的組件化框架是最合適的(這也是一開始設計KRouter想要達到的效果,雖然目前暫時不支持跨進程組件。。。)

在項目實施組件化過程當中,其實真正耗費時間、精力的不是編碼,而是一開始組件的劃分以及組件單元測試的代碼的編寫。有可能由於一開始對業務的不熟悉,致使後期開發時發現組件劃分的不夠準確,須要加以調整;或者是對接口抽象的不夠好,致使維護時頻繁修改接口;還有可能在編寫單元測試時以爲枯燥乏味而選擇放棄。咱們不能由於遇到這些困難就半途而廢,或者是質疑本身的架構設計能力,沒有哪個架構設計是放之四海皆準的,有可能一個項目的架構設計放在另外一個項目中就顯得不那麼合適了。

因此好的架構設計還須要設計人員「因地制宜」的對一個比較通用的架構骨架進行查漏補缺,最後使其與實際項目更加契合。

祝你們都能成爲一個優秀的架構設計師。

免費獲取安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於android面試的題目彙總能夠加:936332305 / 連接:點擊連接加入【安卓開發架構】

相關文章
相關標籤/搜索