本文首發於《程序員》雜誌五月刊,此版本有部分糾錯與調整java
萬維網發明人 Tim Berners-Lee 談到設計原理時說過:「簡單性和模塊化是軟件工程的基石;分佈式和容錯性是互聯網的生命。」 因而可知模塊化之於軟件工程領域的重要性。android
從 2016 年開始,模塊化在 Android 社區愈來愈多的被說起。隨着移動平臺的不斷髮展,移動平臺上的軟件慢慢走向複雜化,體積也變得臃腫龐大;爲了下降大型軟件複雜性和耦合度,同時也爲了適應模塊重用、多團隊並行開發測試等等需求,模塊化在 Android 平臺上變得勢在必行。阿里 Android 團隊在年初開源了他們的容器化框架 Atlas 就很大程度說明了當前 Android 平臺開發大型商業項目所面臨的問題。git
那麼什麼是模塊化呢?《 Java 應用架構設計:模塊化模式與 OSGi 》一書中對它的定義是:模塊化是一種處理複雜系統分解爲更好的可管理模塊的方式。程序員
上面這種描述太過生澀難懂,不夠直觀。下面這種類比的方式則可能加容易理解。github
咱們能夠把軟件看作是一輛汽車,開發一款軟件的過程就是生產一輛汽車的過程。一輛汽車由車架、發動機、變數箱、車輪等一系列模塊組成;一樣,一款大型商業軟件也是由各個不一樣的模塊組成的。安全
汽車的這些模塊是由不一樣的工廠生產的,一輛 BMW 的發動機多是由位於德國的工廠生產的,它的自動變數箱多是 Jatco(世界三大變速箱廠商之一)位於日本的工廠生產的,車輪多是中國的工廠生產的,最後交給華晨寶馬的工廠統一組裝成一輛完整的汽車。這就相似於咱們在軟件工程領域裏說的多團隊並行開發,最後將各個團隊開發的模塊統一打包成咱們可以使用的 App 。架構
一款發動機、一款變數箱都不可能只應用於一個車型,好比同一款 Jatco 的 6AT 自動變速箱既可能被安裝在 BMW 的車型上,也可能被安裝在 Mazda 的車型上。這就如同軟件開發領域裏的模塊重用。app
到了冬天,特別是在北方咱們可能須要開着車走雪路,爲了安全起見每每咱們會將汽車的公路胎升級爲雪地胎;輪胎能夠很輕易的更換,這就是咱們在軟件開發領域談到的低耦合。一個模塊的升級替換不會影響到其它模塊,也不會受其它模塊的限制;同時這也相似於咱們在軟件開發領域提到的可插拔。框架
上面的類比很清晰的說明的模塊化帶來的好處:分佈式
在《安居客 Android 項目架構演進》這篇文章中,我介紹了安居客 Android 端的模塊化設計方案,這裏我仍是拿它來舉例。但首先要對本文中的組件和模塊作個區別定義
組件:指的是單一的功能組件,如地圖組件(MapSDK)、支付組件(AnjukePay)、路由組件(Router)等等;
模塊:指的是獨立的業務模塊,如新房模塊(NewHouseModule)、二手房模塊(SecondHouseModule)、即時通信模塊(InstantMessagingModule)等等;模塊相對於組件來講粒度更大。
具體設計方案以下圖:
整個項目分爲三層,從下至上分別是:
咱們在談模塊化的時候,其實就是將業務模塊層的各個功能業務拆分層獨立的業務模塊。因此咱們進行模塊化的第一步就是業務模塊劃分,可是模塊劃分並無一個業界通用的標準,所以劃分的粒度須要根據項目狀況進行合理把控,這就須要對業務和項目有較爲透徹的理解。拿安居客來舉例,咱們會將項目劃分爲新房模塊、二手房模塊、IM 模塊等等。
每一個業務模塊在 Android Studio 中的都是一個 Module ,所以在命名方面咱們要求每一個業務模塊都以 Module 爲後綴。以下圖所示:
對於模塊化項目,每一個單獨的 Business Module 均可以單獨編譯成 APK。在開發階段須要單獨打包編譯,項目發佈的時候又須要它做爲項目的一個 Module 來總體編譯打包。簡單的說就是開發時是 Application,發佈時是 Library。所以須要在 Business Module 的 build.gradle 中加入以下代碼:
if(isBuildModule.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}複製代碼
isBuildModule 在項目根目錄的 gradle.properties 中定義:
isBuildModule=false複製代碼
一樣 Manifest.xml 也須要有兩套:
sourceSets {
main {
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}複製代碼
如圖:
debug 模式下的 AndroidManifest.xml :
<application ... >
<activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>複製代碼
realease 模式下的 AndroidManifest.xml :
<application ... >
<activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
<data android:host="com.baronzhang.android.newhouse" android:scheme="router" />
</intent-filter>
</activity>
</application>複製代碼
同時針對模塊化咱們也定義了一些本身的遊戲規則:
對業務進行模塊化拆分後,爲了使各業務模塊間解耦,所以各個 Bussiness Module 都是獨立的模塊,它們之間是沒有依賴關係。那麼各個模塊間的跳轉通信如何實現呢?
好比業務上要求從新房的列表頁跳轉到二手房的列表頁,那麼因爲是 NewHouseModule 和 SecondHouseModule 之間並不相互依賴,咱們經過想以下這種顯式跳轉的方式來實現 Activity 跳轉顯然是不可能的實現的。
Intent intent = new Intent(NewHouseListActivity.this, SecondHouseListActivity.class);
startActivity(intent);複製代碼
有的同窗可能會想到用隱式跳轉,經過 Intent 匹配規則來實現:
Intent intent = new Intent(Intent.ACTION_VIEW, "<scheme>://<host>:<port>/<path>");
startActivity(intent);複製代碼
可是這種代碼寫起來比較繁瑣,且容易出錯,出錯也不太容易定位問題。所以一個簡單易用、解放開發的路由框架是必須的了。
我本身實現的路由框架分爲路由(Router)和參數注入器(Injector)兩部分:
Router 提供 Activity 跳轉傳參的功能;Injector 提供參數注入功能,經過編譯時生成代碼的方式在 Activity 獲取獲取傳遞過來的參數,簡化開發。
路由(Router)部分經過 Java 註解結合動態代理來實現,這一點和 Retrofit 的實現原理是同樣的。
首先須要定義咱們本身的註解(篇幅有限,這裏只列出少部分源碼)。
用於定義跳轉 URI 的註解 FullUri:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FullUri {
String value();
}複製代碼
用於定義跳轉傳參的 UriParam( UriParam 註解的參數用於拼接到 URI 後面):
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UriParam {
String value();
}複製代碼
用於定義跳轉傳參的 IntentExtrasParam( IntentExtrasParam 註解的參數最終經過 Intent 來傳遞):
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntentExtrasParam {
String value();
}複製代碼
而後實現 Router ,內部經過動態代理的方式來實現 Activity 跳轉:
public final class Router {
...
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
FullUri fullUri = method.getAnnotation(FullUri.class);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(fullUri.value());
//獲取註解參數
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
HashMap<String, Object> serializedParams = new HashMap<>();
//拼接跳轉 URI
int position = 0;
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotations = parameterAnnotations[i];
if (annotations == null || annotations.length == 0)
break;
Annotation annotation = annotations[0];
if (annotation instanceof UriParam) {
//拼接 URI 後的參數
...
} else if (annotation instanceof IntentExtrasParam) {
//Intent 傳參處理
...
}
}
//執行Activity跳轉操做
performJump(urlBuilder.toString(), serializedParams);
return null;
}
});
}
...
}複製代碼
上面是 Router 實現的部分代碼,在使用 Router 來跳轉的時候,首先須要定義一個 Interface(相似於 Retrofit 的使用方式):
public interface RouterService {
@FullUri("router://com.baronzhang.android.router.FourthActivity")
void startUserActivity(@UriParam("cityName") String cityName, @IntentExtrasParam("user") User user);
}複製代碼
接下來咱們就能夠經過以下方式實現 Activity 的跳轉傳參了:
RouterService routerService = new Router(this).create(RouterService.class);
User user = new User("張三", 17, 165, 88);
routerService.startUserActivity("上海", user);複製代碼
經過 Router 跳轉到目標 Activity 後,咱們須要在目標 Activity 中獲取經過 Intent 傳過來的參數:
getIntent().getIntExtra("intParam", 0);
getIntent().getData().getQueryParameter("preActivity");複製代碼
爲了簡化這部分工做,路由框架 Router 中提供了 Injector 模塊在編譯時生成上述代碼。參數注入器(Injector)部分經過 Java 編譯時註解來實現,實現思路和 ButterKnife 這類編譯時註解框架相似。
首先定義咱們的參數註解 InjectUriParam :
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectUriParam {
String value() default "";
}複製代碼
而後實現一個註解處理器 InjectProcessor ,在編譯階段生成獲取參數的代碼:
@AutoService(Processor.class)
public class InjectProcessor extends AbstractProcessor {
...
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//解析註解
Map<TypeElement, TargetClass> targetClassMap = findAndParseTargets(roundEnvironment);
//解析完成後,生成的代碼的結構已經有了,它們存在InjectingClass中
for (Map.Entry<TypeElement, TargetClass> entry : targetClassMap.entrySet()) {
...
}
return false;
}
...
}複製代碼
使用方式相似於 ButterKnife ,在 Activity 中咱們使用 Inject 來註解一個全局變量:
@Inject User user;複製代碼
而後 onCreate 方法中須要調用 inject(Activity activity) 方法實現注入:
RouterInjector.inject(this);複製代碼
這樣咱們就能夠獲取到前面經過 Router 跳轉的傳參了。
因爲篇幅限制,加上爲了便於理解,這裏只貼出了極少部分 Router 框架的源碼。但願進一步瞭解 Router 實現原理的能夠到 GiuHub 去翻閱源碼,Router 的實現還比較簡陋,後面會進一步完善功能和文檔,以後也會有單獨的文章詳細介紹。源碼地址:github.com/BaronZ88/Ro…
對於多個 Bussines Module 中資源名衝突的問題,能夠經過在 build.gradle 定義前綴的方式解決:
defaultConfig {
...
resourcePrefix "new_house_"
...
}複製代碼
而對於 Module 中有些資源不想被外部訪問的,咱們能夠建立 res/values/public.xml,添加到 public.xml 中的 resource 則可被外部訪問,未添加的則視爲私有:
<resources>
<public name="new_house_settings" type="string"/>
</resources>複製代碼
模塊化的過程當中咱們經常會遇到重複依賴的問題,若是是經過 aar 依賴, gradle 會自動幫咱們找出新版本,而拋棄老版本的重複依賴。若是是以 project 的方式依賴,則在打包的時候會出現重複類。對於這種狀況咱們能夠在 build.gradle 中將 compile 改成 provided,只在最終的項目中 compile 對應的 library ;
其實從前面的安居客模塊化設計圖上能看出來,咱們的設計方案能必定程度上規避重複依賴的問題。好比咱們全部的第三方庫的依賴都會放到 OpenSoureLibraries 中,其餘須要用到相關類庫的項目,只須要依賴 OpenSoureLibraries 就行了。
對於大型的商業項目,在重構過程當中可能會遇到業務耦合嚴重,難以拆分的問題。咱們須要先理清業務,再動手拆分業務模塊。好比能夠先在原先的項目中根據業務分包,在必定程度上將各業務解耦後拆分到不一樣的 package 中。好比以前新房和二手房因爲同屬於 app module,所以他們以前是經過隱式的 intent 跳轉的,如今能夠先將他們改成經過 Router 來實現跳轉。又好比新房和二手房中公用的模塊能夠先下放到 Business Component Layer 或者 Basic Component Layer 中。在這一系列工做完成後再將各個業務拆分紅多個 module 。
模塊化重構須要漸進式的展開,不可一觸而就,不要想着將整個項目推翻重寫。線上成熟穩定的業務代碼,是通過了時間和大量用戶考驗的;所有推翻重寫每每費時費力,實際的效果一般也很不理想,各類問題層出不窮得不償失。對於這種項目的模塊化重構,咱們須要一點點的改進重構,能夠分散到每次的業務迭代中去,逐步淘汰掉陳舊的代碼。
各業務模塊間確定會有公用的部分,按照我前面的設計圖,公用的部分咱們會根據業務相關性下放到業務組件層(Business Component Layer)或者基礎組件層(Common Component Layer)。對於過小的公有模塊不足以構成單獨組件或者模塊的,咱們先放到相似於 CommonBusiness 的組件中,在後期不斷的重構迭代中視狀況進行進一步的拆分。過程當中完美主義能夠有,切記不可過分。
以上就是我在模塊化探索實踐方面的一些經驗,不住之處還望你們指出。
若是你喜歡個人文章,就關注下個人 知乎專欄 或者在 GitHub 上添個 Star 吧!
- 知乎專欄:zhuanlan.zhihu.com/baron
- GitHub:github.com/BaronZ88
- 我的博客:baronzhang.com