這是一個APP臃腫的時代!所以為了告別APP臃腫的時代,讓我們進入一個U盤時代,每個業務模塊都是一個具備獨立運行的U盤,插在哪里都可以完美運行,這就是推進業務組件化的初衷也是一個美好的愿景。
目前大部分app的單一項目結構原型。大致如下圖所示:
一眼望去這結構不是挺清晰的么?每個業務都放在單獨的包名下,網絡庫、圖片加載庫等第三方庫與上層業務都完美的剝離了,我們再來看下他們的直接的依賴關系圖:
雖然上面的依賴關系舉例有點太過于極端,但是真實場景中是存在的。各個業務之間代碼互相引用,所以這種結構也是架構整改的根本動機,也是當務之急應該考慮的事情。為了更好的滿足各個業務的迭代而彼此不受影響,更好的解決上面這種讓人頭疼的依賴關系,開始著手app架構整改。
從上面的分析我們可以得出適合業務組件化的幾種情況:
- 業務較多、而且復雜
- 業務之間的依賴需要解耦
- 團隊成員較多,需要各自開發相對獨立的業務
業務組件化方向:
APP業務組件化架構整改的方向就是告別結構臃腫的時代,讓各個業務變得相對獨立。模塊工程和類庫工程之間遵循向下依賴關系,各個模塊之間的遵循平級依賴關系。先看下整改后的各個獨立業務模塊與類庫工程之間的向下依賴關系圖:
整改的方向由一個項目工程拆分成若干個模塊工程,由app殼工程提供統一的入口,每個業務獨立的模塊module共享項目的依賴庫。由殼工程集成需要引入的業務模塊,至于各個獨立的業務模塊之間的調用依賴關系,我們借助一個中間層充當路由功能,這個路由我們放在各個業務模塊共同引用的依賴庫那一層。由路由統一調度他們之間的依賴關系,路由調度解決平級依賴問題示意圖:
通過APP殼工程提供的路由功能,各個模塊之間調用不再采用傳統的顯式調用,而是采用隱式調用的方式。從而使各個模塊之間不再存在依賴關系。
組件化的實現
- 子模塊單獨編譯
- sdk和第三方庫的版本一致性
- 資源重復定義
- 模塊之間頁面跳轉
- 模塊之間數據傳遞
- 模塊初始化處理
APP業務組件化架構整改帶來的好處:
- 加快迭代速度,各個業務模塊組件更加獨立,不再因為業務耦合情況,在發版時候,由于互相等待而遲遲不能發布版本。
- 穩定的公共模塊采用依賴庫方式,提供給各個業務線使用,減少重復開發和維護工作量。
- 迭代頻繁的業務模塊采用組件方式,各業務線研發可以互不干擾、提升協作效率,并控制產品質量。
- 為新業務隨時集成提供了基礎,所有業務可上可下,靈活多變。
- 降低團隊成員熟悉項目的成本,降低項目的維護難度。
- 加快編譯速度,提高開發效率
1、子模塊如何單獨編譯
我們希望在開發模式下,能夠單獨調試自己的模塊,編譯成獨立的apk。而在主程序發布時,成為一個library
嵌入主工程。
首先在子模塊build.gradle
中定義常量,來標示模塊目前是否處于開發模式def isDebug = true
在子模塊的build.gradle
中進行模式配置。debug
模式下編譯成獨立app,release
模式下編譯成library。
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
兩種模式下模塊AndroidManifest.xml
文件是有差別的。作為獨立運行的app,有自己的Application
,要加Launcher
的入口intent
,作為library
不需要。這個問題很好解決,寫兩個不同的AndroidManifest.xml
即可,并在gradle中進行配置。
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
2、sdk和第三方庫的版本一致性
不同module
依賴sdk版本不一致,會因兼容性問題導致編譯問題。
不同module
引用了同一個第三方庫的不同版本,并且這個庫沒有做到向前兼容,就有可能出現方法找不到、參數不對應等問題。
所以有必要統一整個project
的依賴版本。
在最外層build.gradle
中定義的常量能被整個project
的build.gradle
文件引用,統一的版本定義可以放在這里。
ext {
android_compileSdkVersion = 23
android_buildToolsVersion = '23.0.3'
android_minSdkVersion = 15
android_targetSdkVersion = 23
lib_appcompat = 'com.android.support:appcompat-v7:23.2.1'
lib_gson = 'com.google.code.gson:gson:2.6.1'
lib_butterknife = 'com.jakewharton:butterknife:8.4.0'
lib_butterknife_compiler = 'com.jakewharton:butterknife-compiler:8.4.0'
}
3、資源的重復定義
說到資源的重復定義,筆者趟過坑,如果主工程和子模塊中重復定義了同名的資源。
主工程中
<string name="daddy">爸爸</string>
子工程中
<string name="daddy">干爹</string>
雖然編譯不會出錯,但是最后子模塊中用到daddy的地方都會顯示爸爸。
編譯時子模塊的資源會和主工程合并到同一個類中,所以資源重名會有問題。
但是資源也要模塊化呀,總不能在底層找個統一的地方都扔在里面,gradle提供了一個解決方案來避免重復定義的問題。
resourcePrefix "a_"
強制模塊中的資源名稱帶有a_
前綴,否則編譯不過。
聊到這里,我們知道了如何使用gradle獨立編譯子模塊,以及如何處理分模塊導致的一些問題。但是除了主工程統一調度外,模塊與模塊之間也需要互相調起和訪問,所以需要協議去統一,這個協議是模塊間共同定義與使用的,所以寫在底層。
4、模塊之間頁面跳轉
首先想到的就是配置uri
去匹配模塊AndroidManifest.xml
中的intentFilter
來啟動相應Activity,這種方式是解耦的,但有缺點,要跳轉其它模塊,得先去看別的模塊的AndroidManifest.xml
進行入口適配,還得研究具體Activity中的傳參設置,雖然代碼依賴上解耦了,但是實現邏輯上沒有解耦,忍不了。需要在底層創建一個路由協議,讓使用者通過協議方便地調用。
用注解把需要的參數寫在路由協議的接口中。
public interface IRouterUri {
@RouterUri("test://host_liujc")
public Intent getIntentActivityA(@RounterParam("name") String name, @RounterParam("age") int age);
@RouterUri("test://host_b")
public Intent getIntentActivityB();
}
其中@RouterUri
表示跳轉改頁面需要匹配的uri,這個uri最終會拿去和moduleA中的AndroidManifest.xml
中對應activity的intentFilter去匹配。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterUri {
String value() default "";
}
@RounterParam用來表示目標activity需要的參數,最終會在目標activity中進行解析。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RounterParam {
String value() default "";
}
為什么用注解的方式寫接口而不是直接定義跳轉方法呢?
用注解的方式,可以把參數更直觀地展現在最醒目的方法聲明中。而寫成實現的方法,參數會被寫在方法內部,定義起來不方便,而且要帶上少量邏輯,不夠簡潔。參考retrofit
框架,也是用注解方式去實現,簡潔、方便。
為什么接口返回的是Intent,而不是直接進行頁面跳轉呢?
因為我們的項目中,實現這個跳轉可能是activity
,可能是fragment
,也可能startActivityForResult
需要帶入一個自定義的requestCode
。所以為了靈活性,直接返回Intent
。
寫好了接口,還需要將接口中的參數組裝成一個可進行跳轉的Intent
。使用Proxy
生成類動態代理這個接口。
public class RounterBus {
//靜態map存儲代理接口的實例
private static HashMap<Class, Object> sRounterMap = new HashMap<Class, Object>();
/**
* 得到動態代理路由接口的實例
*
* @param c 接口類
* @return
*/
public static IRouterUri getRounter(Class<?> c) {
IRouterUri rounter = (IRouterUri) sRounterMap.get(c);
if (rounter == null) {
rounter = (IRouterUri) Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//從方法注解的獲取uri
RouterUri routerUri = method.getAnnotation(RouterUri.class);
if (routerUri == null || TextUtils.isEmpty(routerUri.value())) {
throw new IllegalArgumentException(
"invoke a rounter method, bug not assign a rounterUri");
}
Uri.Builder uriBuilder = Uri.parse(routerUri.value()).buildUpon();
//從參數值和參數注解,獲取信息,拼入uri的query
Annotation[][] annotations = method.getParameterAnnotations();
if (annotations != null && annotations.length > 0) {
for (int i = 0, n = annotations.length; i < n; i++) {
Annotation[] typeAnnotation = annotations[i];
if (typeAnnotation == null || typeAnnotation.length == 0) {
throw new IllegalArgumentException("method " + method.getName() + ", args at " + i + " lack of annotion RouterUri");
}
boolean findAnnotaion = false;
for (Annotation a : typeAnnotation) {
if (a != null && (a.annotationType() == RounterParam.class)) {
uriBuilder.appendQueryParameter(((RounterParam) a).value(), GsonInstance.getInstance().toJson(args[i]));
findAnnotaion = true;
break;
}
}
if (!findAnnotaion) {
throw new IllegalArgumentException("method " + method.getName() + " args at " + i + ", lack of annotion RouterUri");
}
}
}
return getIntentByRouterUri(uriBuilder.build());
}
});
sRounterMap.put(c, rounter);
}
return rounter;
}
private static Intent getIntentByRouterUri(Uri uriBuilder) {
Context context = AppContext.get();
PackageManager pm = context.getPackageManager();
Uri uri = uriBuilder;
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
//查詢這個intent是否能被接收用來進行跳轉
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
if (activities != null && !activities.isEmpty()) {
return intent;
} else {
if (BuildConfig.IS_DEBUG) {
Toast.makeText(context, "子模塊作為獨立程序啟動時,跳不到其他模塊", Toast.LENGTH_SHORT).show();
} else {
throw new IllegalArgumentException("can't resolve uri with " + uri.toString());
}
}
return null;
}
}
上面代碼包裝了一個路由總線,來獲取并緩存路由接口的實例。
例如:需要調起moduleA中的ActivityA
Intent intent = RounterBus.getRounter(IRouterUri.class).getIntentActivityA("heihei", 23);
if (intent != null) {
MainActivity.this.startActivity(intent);
}
5、模塊之間的數據傳遞
ri uri = getIntent().getData();
if (uri != null) {
// 完整的url信息
String url = uri.toString();
Log.e(TAG, "url: " + uri);
// scheme部分
String scheme = uri.getScheme();
Log.e(TAG, "scheme: " + scheme);
// host部分
String host = uri.getHost();
Log.e(TAG, "host: " + host);
// //port部分
// int port = uri.getPort();
// Log.e(TAG, "host: " + port);
// // 訪問路徑
// String path = uri.getPath();
// Log.e(TAG, "path: " + path);
List<String> pathSegments = uri.getPathSegments();
// Query部分
String query = uri.getQuery();
Log.e(TAG, "query: " + query);
//獲取指定參數值
String name= uri.getQueryParameter("name");
Log.e(TAG, "name: " + goodsId);
6、application初始化
子模塊作為application
時,有一些初始化的工作需要在Application.onCreate
時進行。而作為library時,調不到這個onCreate。所以自己寫一個靜態方法,供主工程的Application調用。
public class ApplicationA extends BaseChildApplication {
@Override public void onCreate() {
super.onCreate();
//給底層library設置context
AppContext.init(getApplicationContext());
}
/**
* 作為library時需要初始化的內容
*/
@Override public void onCreateAsLibrary(Application application) {
super.onCreateAsLibrary(application);
}
}
主工程的Application.onCreate時記得初始化子模塊。
public class MainApplication extends Application {
@Override public void onCreate() {
super.onCreate();
AppContext.init(getApplicationContext());
ApplicationA.onCreateAsLibrary();
ApplicationB.onCreateAsLibrary();
}
}
想調試A模塊,but某些功能需要依賴B這時只需要把B模塊作為library引入A。并且記得在B模塊Application.onCreate時初始化一下A模塊。是不是很輕量級?常用的話在gradle中設置一個開關就更方便了。
def isDebugWithB = true
if (isDebugWithB.toBoolean()) {
compile project(':moduleB')
}
ApplicationB.onCreateAsLibrary();