架構師的思維,聊一聊APP組件化的那些事兒
- 2019 年 10 月 5 日
- 筆記

背景
我司之前一直採用MVP+Dagger2+Retrofit+Rxjava的項目結構。這種結構對於我們這種只有幾個人的團隊來說一直沒有什麼問題,因此使用了多年。直到18年初,公司決定擴展海外業務。我們海外的業務模式是這樣的:
- 採用擠牙膏的運營方式,前期只會有國內的部分業務,後期會慢慢把國內的業務移植過去。
- 不同地區有不同的APP,這些APP可能有不同的業務功能。
- 海外的APP會有UI、邏輯等細微的不同。
在這樣的背景下,我們決定實施組件化,從而實現組件的多APP復用。
項目結構
我們的項目結構經過了三次變更,最終它的樣子是這樣的。

我們將項目自上而下分為四層:殼工程、業務組件層、業務相關基礎組件層、業務無關組件層。這四層分別承擔的責任如下:
- 殼工程:殼工程主要用來做一些打包的配置和一些對全局都有影響的功能。打包配置這個很好理解,例如:渠道包配置、風味配置等。對全局有影響的功能主要是一些檢測APP運行狀態的功能。例如:我們會在Debug模式下開啟LeakCanary、會檢測UI渲染是否失幀等。另外,我們集成了Tinker,也是在這個模塊下完成的。
- 業務組件:業務層的代碼,每個組件單獨出去都是一個完整的功能,它們還是沿用之前的MVP架構。
- 業務相關基礎組件:這一層是為業務層服務的。原則上這一層不要有UI的邏輯處理,只提供業務能力。UI部分可以放到業務組件中。但是,這個原則並不是一成不變的。例如,我們為掃碼功能、分享、地圖等功能都提供了完整的UI。
- 業務無關組件:這一層就和業務完全無關了。
整個項目我們使用多倉庫的管理方式。我們的倉庫管理方式是這樣的:
- APP殼工程和每個業務組件都是一個單獨倉庫。
- 業務相關基礎組件整合到一個倉庫。
- 業務無關組件整合到一個倉庫。
這樣做的原因是:業務層的組件必然會在不同的APP中有所差異,我們的設計必須擁抱這種差異。 我們期望每個業務組件的改動都以版本的形式留下記錄,這樣我們可以儘可能的使組件在不同的APP間復用。因此我們需要每個業務組件是一個單獨的倉庫。但是,倉庫過多,勢必會造成倉庫、版本等管理上的困擾。因此我們把業務相關組件層和業務無關組件層分別整合到一個倉庫中。這樣做的另外一個原因是我們對於業務相關基礎組件和業務無關組件在不同APP的差異是拒絕的,我們希望不同的APP可以共用這些組件。
業務組件是沒有辦法單獨運行的,它必須集成到殼工程中。這種集成有兩種,一種是以aar的方式。另一種以module的方式。我們APP發版是通過一個APP管理系統來完成。在APP release打包時,我們可以選擇需要的組件aar即可,因此在打release包時,是沒有辦法依賴本地module的。在開發階段,所有的代碼都在本地,我們可以通過配置debug.properties來指定依賴哪些module和aar。
組件間通信
說到組件間通信,目前已經有了很成熟的輪子,ARouter、WMRouter等。WMRouter開源較晚,我們做組件化時還沒有出現。因此我們當時主要調研了ARouter,但是,最終放棄了。ARouter對於我們來說有點重量級了。因為它不僅提供了路由功能,還提供了參數注入、攔截等目前我們不需要的功能。如果我們要使用好它,項目改造較多。其次,它對內存有一定的消耗。它內部維護着一套路由表以及參數的對應表。這些都是常駐內存的。而我們一些東南亞國家的用戶他們的手機非常差,內存是我們不得不考慮的一個因素。最終我們做了一個輕量級的接口+實現的通信框架。
每個業務組件對外提供通信服務都需要兩部分完成:接口+實現。接口部分用來聲明該業務組件可以提供哪些服務,它們會被下沉到業務相關基礎組件的一個單獨Module中,按包名區分。實現部分是具體的服務實現,它們放在各自的業務組件中。我們可以像這樣使用它。
//註冊 ServiceManager.getInstance().register(IMainService.class,new MainService()); //使用 ServiceManager.getInstance().get(IMainService.class).invoke();
除此之外,我們在開發時,只會讓幾個module參與編譯。因此可能會出現ServiceManager.getInstance().get(IMainService.class)返回null的情況。為了不讓程序崩潰,我們這裡採用動態代理的方式進行了處理,通知開發者該組件沒有註冊。
組件初始化
在介紹組件間通信時,我故意忽略的一個細節,那就是接口和實現是在什麼時候注入到路由表中去的。要說清楚這個問題,就不得不提組件入口。我們為每個組件提供了初始化能力。
public interface IApplicationLike { void onCreate(Application application); }
每個組件都可以實現了IApplicationLike來初始化一些當前組件需要的功能。
public class MainApplication implements IApplicationLike { @Override public void onCreate(Application application) { ServiceManager.getInstance().register(IMainService.class,new MainService()); } }
除此之外,我們還在業務相關組件有一個GlobalApplicationLike,它用來初始化一些公共的功能。
public class GlobalApplicationLike implements IApplicationLike { static { //兼容21之前的svg。 AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); } @Override public void onCreate(Application application) { initServerApiUrl(getApplication()); NetWorkUtil.initNetWork(getApplication(),UserCache.getInstance(getApplication()).getSessionId()); initImageTool(getApplication()); initShare(getApplication()); initApplicationComponent(getApplication()); initLogTools(getApplication()); initPrintLogger(); initPrintService(getApplication()); initLanguage(getApplication()); initStatisticTool(getApplication()); initLoginOutService(); } }
這些實現IApplicationLike是通過位元組碼注入技術插入到Application.onCreate()中的。關於位元組碼技術,可以看一下這篇文章Android編譯期插樁,讓程序自己寫代碼(三)。
改進組件初始化
上套方案是我們最初的設計。它的缺點很明顯那就是GlobalApplicationLike會依賴太多的module,而且一旦新增需要初始化的公共庫那必須修改GlobalApplicationLike。直到我看到了知乎的組件化方案。它對初始化方案的描述是這樣的:
有些組件有在應用啟動時初始化服務的需求,而且很多服務還是有依賴關係的,最初我們為每個組件都添加了一個 init() 方法,但是並不能解決依賴順序問題,需要每個組件都在 app 工程中按順序添加初始化代碼才能正常運行,這使得不熟悉整套組件業務的人很難建立起一個可以獨立運行的組件 app。因此我們開發了一套多線程初始化框架,每個組件只要新建若干個啟動 Task 類,並在 Task 中聲明依賴關係即可。
我們根據這個思想,開發了我們自己的Task框架。我們去除了知乎方案中的多線程初始化功能,因為我們不知道怎麼處理依賴和多線程間的關係。同時,我們也增加了根據process name初始化的功能。
@Task(name="PluginAStart",depend = {"PluginAfter"},process = {"processName"}) public class PluginAStartTask extends Task { public PluginAStartTask(String name) { super(name); } @Override protected void run() { } }
組件化遇到Dagger2
我在前面提到過,我們的項目用到了Dagger2。Dagger2有兩種使用方式,一種是使用與Android平台無關的注入方式。
mainApplicationComponent = DaggerMainApplicationComponent.builder() .baseApplicationComponent(getBaseApplicationComponent()) .mainApplicationModule(new MainApplicationModule(getApplication())) .build();
這種方式比較靈活,與組件化並不衝突。具體的做法是我們可以在業務相關基礎組件層創建一個BaseComponent用來提供公共有的注入對象。以後各個模塊的Component都依賴BaseComponent。
還有一種方式是使用Dagger.Android,它的出現是為了解決上文中提到的模板代碼問題。這種方式提高了Dagger的易用性,但是它與組件化不兼容。不幸的是,我們的項目恰好採用了這種方式。因此我們必須解決掉它。
我們最終確定方案如下:整個方案分為兩部分,一部分是lib庫。另一部分是gradle插件。lib庫核心類有兩個,DaggerApplicationLike和ApplicationAndroidInjector。DaggerApplicationLike是為了在組件中替代DaggerApplication,它的代碼基本都是DaggerApplication的拷貝,就不貼出來了。ApplicationAndroidInjector實現了AndroidInjector,它是用來為Application提供注入的。ApplicationAndroidInjector中,有一個DaggerApplicationLike的集合。這個集合內的元素是通過gradle插件利用位元組碼注入技術在編譯期注入的。在inject時,我們合併DaggerApplicationLike集合,生成全新的activityInjector、serviceInjector等注入給Application。
public class ApplicationAndroidInjector<T extends Application> implements AndroidInjector<T> { private final List<DaggerApplicationLike> mDaggerApplicationLikes = new ArrayList<>(); public ApplicationAndroidInjector(){ //在這裡gradle插件會在編譯期通過位元組碼注入技術將所有實現了DaggerApplicationLike的類注入到mDaggerApplicationLikes中 } @Override public void inject(T instance) { //combineActivityInjector()用來合併mDaggerApplicationLikes中的activityInjector。生成一個全新的AndroidInjector注入到Application中去。 injectActivity(instance, DispatchingAndroidInjector_Factory.newDispatchingAndroidInjector(combineActivityInjector())); injected(instance); } protected void injectActivity(T instance, DispatchingAndroidInjector<Activity> dispatchingAndroidInjector) { if (instance instanceof DaggerApplication) { DaggerApplication application = (DaggerApplication) instance; DaggerApplication_MembersInjector.injectActivityInjector(application, dispatchingAndroidInjector); } } }
ApplicationAndroidInjector使用如下:
public class TestApplication extends DaggerApplication { @Override protected AndroidInjector<? extends DaggerApplication> applicationInjector() { return new ApplicationAndroidInjector<>(); } }
拆分過程
- 規劃。我個人把規劃分為技術規劃和產品規劃。
- 技術規劃主要是結合我們當前的項目做一些技術選型。我上面提到的項目結構、組件間通信、組件初始化等都屬於技術規劃。
- 產品規劃是指我們要了解產品未來的發展方向以及近期可能的迭代需求。我們做設計時要有一定的前瞻性,了解產品未來的發展方向可以幫助我們拆分出更合理的業務組件。為什麼這麼說呢?我舉個例子。現在很多企業都忙於變現,我們也不例外。我們為APP增加了會員功能,不過這個功能比較隱晦,它被放到了個人模塊里。在做組件拆分時,很自然的我們把會員功能放到了個人模塊里。過了一段時間,產品要大力拓展會員功能。會員功能在個人組件中的比重也越來越大,以至於我們不得不把會員功能從個人組件中拆分出來。假如在組件化拆分時,我們就了解到會員功能會被拓展,那麼我們在最初就會把會員功能拆分成一個組件。
- 具體實施。 通常,組件化拆分是一個漫長的過程,中間往往會穿插着新功能的迭代。因此,在拆分過程中一定要保證項目的完整和正確,以便可以進行功能迭代。因此拆分步驟要遵循這個原則。首先我們應該先拆分出一個框架。這個框架是這樣的,它有四層:app殼、業務層、業務相關基礎層、業務無關基礎層。這四層每層都是一個moudle,因此我們項目首先會被拆分成四個module。這四個module的拆分應該也是漸進式的,具體做法如下:
- 先從原項目拆分出app殼,這是很簡單的。
- 從原項目中拆分業務相關基礎層和業務無關基礎層應該是按功能模塊為單位的。例如,把網絡作為一個整體移動到業務無關基礎層、把支付移動到業務相關基礎層等。這裡分包的時候要按模塊劃分,方便我們拆分拆分組件。
- 每移動完一個功能,都要commit代碼。
這樣我們整個組件化的架子就搭起來了,剩下的就是將各層拆分到不同的組件。業務相關基礎層和業務無關基礎層的拆分就很簡單了,只需要按照不同的包結構拆分即可。我們的工作主要是將業務層拆分成不同的業務組件,在這個過程中,我們會下沉一些公用的類到業務相關基礎組件中。
- 最後,配置倉庫。
原文鏈接:https://juejin.im/post/5d37a0e8e51d4510a37bacc5
結尾
如果你覺得文章寫得不錯就給個讚唄?如果你覺得那裡值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。