0 背景
早前嚴(yán)選 Android 工程,業(yè)務(wù)模塊和功能模塊不多,工程較為簡單,全部的業(yè)務(wù)代碼均在主 app 工程,全部的業(yè)務(wù) Activity 均在 module/ 目錄下,相關(guān)的網(wǎng)絡(luò)請求封裝在 http 目錄下,使用 volley 封裝支持 http 請求和 wzp 請求;業(yè)務(wù)請求協(xié)議實現(xiàn),均放在 app/httptask/ 下,業(yè)務(wù)層使用請求不區(qū)分是 wzp 還是 http(s);全部的工具方法,如 DeviceUtil、BitmapHelper 均在 commno/util/ 里面;全局事件 EventBus event model 放在 eventbus/。
common/
util/
DeviceUtil
...
module/
PayCompleteActivity
...
http/
httptask/
LoginWzpTask
...
eventbus/
...
其中頁面之間的跳轉(zhuǎn),使用原聲 Intent 方式。為規(guī)范參數(shù)傳遞,做了編碼規(guī)范,使用靜態(tài)方法的方式喚起 Activity
public static void start(Context context, ComposedOrderModel model, String skuList) {
Intent intent = new Intent(context, OrderCommoditiesActivity.class);
...
context.startActivity(intent);
}
public static void start(Context context, ComposedOrderModel model, int skuId, int count) {
Intent intent = new Intent(context, OrderCommoditiesActivity.class);
...
context.startActivity(intent);
}
OrderCommoditiesActivity
public static void startForResult(Activity context, int requestCode, int selectedCouponId, int skuId, int count, String skuListStr) {
Intent intent = new Intent(context, CouponListActivity.class);
...
context.startActivityForResult(intent, requestCode);
}
CouponListActivity
針對推送和 H5 scheme 喚起和跳轉(zhuǎn) Activity,我們編寫了統(tǒng)一 scheme 跳轉(zhuǎn)派發(fā)邏輯:
public class RouterUtil {
public static Intent getRouteIntent(Context context, Uri uri) {
if (uri == null || !TextUtils.equals(uri.getScheme(), "yanxuan")) {
return null;
}
String host = uri.getHost();
if (host == null) {
return null;
}
Class<?> clazz = null;
String param = null;
switch (host) {
case ConstantsRT.GOOD_DETAIL_ROUTER_PATH:
clazz = GoodsDetailActivity.class;
...
break;
...
}
Intent intent = null;
if (clazz != null) {
intent = new Intent();
intent.setClass(context, clazz);
}
return intent;
}
}
根據(jù)輸入 scheme,返回跳轉(zhuǎn) Activity 的 intent
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!TextUtils.isEmpty(schemeUrl)) {
Intent intent = RouterUtil.getRouteIntent(Uri.parse(schemeUrl));
if (intent != null) {
view.getContext().startActivity(intent);
}
}
}
});
RouterUtil.getRouteIntent 使用樣例
因為是單一 app 工程(排除和業(yè)務(wù)無關(guān)的第三方組件),所以工程內(nèi)全部的 Activity類、接口、EventBus model 都是可見的,且由于工程量較小,Activity 頁面數(shù)量尚不多,早期使用上述做法并不會碰到問題。但很快隨著版本迭代,業(yè)務(wù)量的增長,很快爆發(fā)出來的就是 scheme 跳轉(zhuǎn)派發(fā)邏輯的維護(hù)問題
public class RouterUtil {
public static Intent getRouteIntent(Context context, Uri uri) {
...
switch (host) {
case ConstantsRT.GOOD_DETAIL_ROUTER_PATH:
clazz = GoodsDetailActivity.class;
...
break;
case ConstantsRT.ORDER_DETAIL_ROUTER_PATH:
clazz = OrderDetailActivity.class;
...
break;
...
... 省略 28 個 case! ??
...
default:
break;
}
...
}
}
當(dāng)嚴(yán)選 2.x.x 版本的時候,我們的 switch-case 就達(dá)到 30 個,代碼明顯不好維護(hù)了。同時,我們的 scheme 協(xié)議完全是按照業(yè)務(wù)需求來增加,不支持 scheme 跳轉(zhuǎn)的大量 Activity 很容易和 iOS 不統(tǒng)一,如頁面實現(xiàn)和參數(shù)使用方面,導(dǎo)致后期開放成 scheme 協(xié)議的時候,需要大量的溝通和業(yè)務(wù)代碼修改。
- 頁面實現(xiàn):Android Activity 還是 Fragment,iOS ViewController 還是 UIView
- 參數(shù)使用:平臺相關(guān)的參數(shù),以及參數(shù)的定義形式
當(dāng)嚴(yán)選 3.x.x 版本的時候,工程中就已經(jīng)出現(xiàn)跨工程接口復(fù)用的問題(如跨工程需要支持埋點、本地異常日志記錄模塊等);當(dāng)嚴(yán)選 4.x.x 版本的時候,需要處理處理跨模塊 wzp 請求復(fù)用、跨工程 EventBus 通信問題。上述的簡單設(shè)計已經(jīng)完全不滿足場景,本文就介紹嚴(yán)選在多版本迭代過程中,如何逐步處理和優(yōu)化頁面組件化、基礎(chǔ)功能組件化。
1 頁面組件化 ht-router 接入
參考 DeepLink從認(rèn)識到實踐,接入杭研 ht-router,由此通過注解的方式統(tǒng)一了 H5 喚醒、推送喚醒、正常啟動 APP 的邏輯,上面點擊跳轉(zhuǎn)的邏輯得到了簡化:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HTRouterManager.startActivity(view.getContext(), schemeUrl, null, false);
}
});
RouterUtil 中冗長的 switch-case 代碼也得到得到了極大的改善,統(tǒng)一跳轉(zhuǎn)可通過 scheme 參數(shù)直接觸發(fā)跳轉(zhuǎn),近 30 個 switch-case 減少至 7 個
HTRouterManager.init();
...
// 設(shè)置跳轉(zhuǎn)前的攔截,返回 true 攔截不再跳轉(zhuǎn),返回 false 繼續(xù)跳轉(zhuǎn)
HTRouterManager.setHtRouterHandler(new HTRouterHandler() {
@Override
public boolean handleRoute(Context context, HTRouterHandlerParams routerParams) {
final Uri uri = !TextUtils.isEmpty(routerParams.url) ? Uri.parse(routerParams.url) : null;
if (uri == null) {
return true;
}
String host = uri.getHost();
if (TextUtils.isEmpty(host)) {
return true;
}
switch (host) {
case ConstantsRT.CATEGORY_ROUTER_PATH: //"category"
...
break;
...
...省略 5 個
...
case ConstantsRT.MINE_ROUTER_PATH:
...
break;
default:
break;
}
return false;
}
});
至于為什么還有 7 個,大體分 2 類
-
歷史原因
嚴(yán)選工程中
CategoryL2Activity有yanxuan://category和yanxuan://categoryl22 個 scheme,而同一個參數(shù)categoryid在不同的 scheme 下有不同的含義,為此在攔截器中添加新的字段,CategoryL2Activity中僅需處理 2 個新加的字段,不必知道自身的 scheme -
跳轉(zhuǎn) Activity 的不同 fragment
嚴(yán)選首頁 MainPageActivity 擁有 5 個 tab fragment,不同的 tab 會有不同的 scheme,攔截器中直接根據(jù)不同的 scheme,添加參數(shù)來指定不同的 tab,首頁僅需處理 tab 參數(shù)顯示不同的 fragment
ht-router的其他優(yōu)點、用法、api 見文章 DeepLink從認(rèn)識到實踐,這里不再敘述
2 ht-router 的痛點
ht-router 對工程框架的作用是巨大的,然而隨著多期業(yè)務(wù)迭代和工程復(fù)雜度的提升,逐漸發(fā)現(xiàn)路由框架的多個痛點:
下述痛點,在其他第三方框架上很多并不存在。當(dāng)時集成的時候,router 框架剛興起,ARouter、天貓統(tǒng)跳、ActivityRouter 等并沒有像現(xiàn)在的功能強大;另外如 ARouter,通過 path 定義 group 和跳轉(zhuǎn)目標(biāo),而嚴(yán)選工程以 host 標(biāo)識跳轉(zhuǎn)目標(biāo),也有些差異
2.1 apt 生成代碼量過大,業(yè)務(wù)開發(fā)較難維護(hù)
ht-router 通過 apt 生成的類有 6 個,其中 HTRouterManager 有 600 行代碼,去除 init 方法中初始化 router 映射表的 100 行左右代碼,剩余還有 500 行左右
apt 生成的類目錄
HTRouterManager.java
參考 apt 的用法,若要生成一個簡單的類,對應(yīng)的 apt 代碼會復(fù)雜的多。當(dāng)目標(biāo)代碼量比較多的情況下,apt 的生成代碼就會比較難以維護(hù),根據(jù)業(yè)務(wù)場景添加接口,或者修改字段都會相比更加困難。另外 apt 的調(diào)試也比較辛苦,需要編譯后再查看目標(biāo)代碼是否是有錯誤。
這里給 ht-router 的開發(fā)同學(xué)獻(xiàn)上膝蓋,為業(yè)務(wù)團(tuán)隊貢獻(xiàn)了很多!
/**
* apt 測試代碼
*/
public class TestClass {
public static final String STATIC_FIELD = "ht_url_params_map";
public void foo() {
System.out.println("hello world");
}
}
目標(biāo)代碼
TypeSpec.Builder testbuilder = classBuilder("TestClass")
.addModifiers(PUBLIC);
testbuilder.addJavadoc("apt 測試代碼\n");
FieldSpec testFieldSpec = FieldSpec
.builder(String.class, "STATIC_FIELD",
PUBLIC, STATIC, FINAL)
.initializer("\"ht_url_params_map\"").build();
testbuilder.addField(testFieldSpec);
MethodSpec.Builder testMethod = MethodSpec.methodBuilder("foo")
.addModifiers(Modifier.PUBLIC)
.returns(void.class);
testMethod.addStatement("System.out.println(\"hello world\")");
testbuilder.addMethod(testMethod.build());
TypeSpec generatedClass = testbuilder.build();
JavaFile javaFile = builder(packageName, generatedClass).build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
生成目標(biāo)代碼的 apt 代碼
2.2 路由表和業(yè)務(wù)直接關(guān)聯(lián)
由于整個路由表在 HTRouterManager 中,偶現(xiàn)(常見合并分支后)由于業(yè)務(wù)代碼編譯不通過,導(dǎo)致 apt 代碼未生成,大量提示報錯 HTRouterManager 找不到,但無法定位到真正的業(yè)務(wù)代碼錯誤邏輯。
由于 HTRouterManager 在業(yè)務(wù)代碼中廣泛被使用,暫未有很好的辦法解決這個報錯,臨時的處理辦法是從同事處拷貝 apt 文件夾,臨時繞過錯誤報錯,修改業(yè)務(wù)層代碼錯誤后 rebuild
第一次碰到比較懵逼,花了不少時間處理定位和解決問題,(⊙﹏⊙)b
2.3 攔截功能不滿足登錄需求
針對未登錄狀態(tài),跳轉(zhuǎn)需要登錄狀態(tài)的 Activity 的場景,我們期望是先喚起登錄頁,登錄成功后,關(guān)閉登錄頁重定向至目標(biāo) Activity;若用戶退出登錄頁,則回到上一個頁面。針對已登錄狀態(tài),則直接喚起目標(biāo)頁面。對于這個需求,ht-router 并不滿足,雖然提供了 HTRouterHandler,但僅能判斷根據(jù)返回值判斷是否繼續(xù)跳轉(zhuǎn),無法在登錄回調(diào)中決定是否繼續(xù)跳轉(zhuǎn)。
public static void startActivity(Activity activity, String url, Intent sourceIntent, boolean isFinish, int entryAnim, int exitAnim) {
Intent intent = null;
HTRouterHandlerParams routerParams = new HTRouterHandlerParams(url, sourceIntent);
if (sHtRouterHandler != null && sHtRouterHandler.handleRoute(activity, routerParams)) {
return;
}
...
}
2.4 需要攔截處理特殊 scheme 的邏輯還在全局
前面 RouterUtil 中的 switch-case 從 30 個大幅降至 7 個(即便是 7 個,感覺代碼也不優(yōu)雅),但這里的特殊處理邏輯屬于各個頁面的業(yè)務(wù)邏輯,不應(yīng)該在 RouterUtil 中。路由的一個很大作用,就是將各個頁面解耦,能為后期模塊化等需求打下堅實基礎(chǔ),而這里的全局?jǐn)r截處理邏輯,顯然是和模塊解耦是背道而馳的。
當(dāng)然這些特殊的處理邏輯完全可以挪到各個 Activity 中,但是不是有機制能很好的處理這種場景,同時 Activity 是否需要關(guān)心自身當(dāng)前的 scheme 是什么?
2.5 sdk 頁面,無法添加路由注解
我們發(fā)現(xiàn)接入的子工程如圖片選擇器等也有自己的頁面,而 apt 的代碼生成功能是對 app 工程生效,不支持其他子工程的路由注解,為此子工程的頁面就無法享受路由帶來的好處。
2.6 router 初始化為類引用,阻礙 main dex 優(yōu)化
最初通過 multidex 方案解決了 65535 問題后,2年后的現(xiàn)在,又爆出了 Too many classes in –main-dex-list 錯誤。
原因:dex 分包之后,各 dex 還是遵循 65535 的限制,而打包流程中 dx --dex --main-dex-list=<maindexlist.txt> 中的 maindexlist.txt 決定了哪些類需要放置進(jìn) main-dex。默認(rèn) main-dex 包含 manifest 中注冊的四大組件,Application、Annonation、multi-dex 相關(guān)的類。由于 app 中 四大組件 (特別是 Activity) 比較多和 Application 中的初始化代碼,最終還是可能導(dǎo)致 main-dex 爆表。
查看 ${android-sdks}/build-tools/${build-tool-version}/mainDexClasses.rules
-keep public class * extends android.app.Instrumentation {
<init>();
}
-keep public class * extends android.app.Application {
<init>();
void attachBaseContext(android.content.Context);
}
-keep public class * extends android.app.Activity {
<init>();
}
-keep public class * extends android.app.Service {
<init>();
}
-keep public class * extends android.content.ContentProvider {
<init>();
}
-keep public class * extends android.content.BroadcastReceiver {
<init>();
}
-keep public class * extends android.app.backup.BackupAgent {
<init>();
}
# We need to keep all annotation classes because proguard does not trace annotation attribute
# it just filter the annotation attributes according to annotation classes it already kept.
-keep public class * extends java.lang.annotation.Annotation {
*;
}
解決方法
-
gradle 1.5.0 之前
執(zhí)行
dex命令時添加--main-dex-list和--minimal-main-dex參數(shù)。而這里maindexlist.txt中的內(nèi)容需要開發(fā)生成,參考 main-dex 分析工具afterEvaluate { tasks.matching { it.name.startsWith("dex") }.each { dx -> if (dx.additionalParameters == null) { dx.additionalParameters = [] } // optional dx.additionalParameters += "--main-dex-list=$projectDir/maindexlist.txt".toString() dx.additionalParameters += "--minimal-main-dex" } } -
gradle 1.5.0 ~ 2.2.0
現(xiàn)嚴(yán)選使用 gradle plugin 2.1.2,并不支持上面的方法,可使用如下方法。
//處理main dex 的方法測試 afterEvaluate { def mainDexListActivity = ['SplashActivity', 'MainPageActivity'] project.tasks.each { task -> if (task.name.startsWith('collect') && task.name.endsWith('MultiDexComponents') && task.name.contains("Debug")) { println "main-dex-filter: found task $task.name" task.filter { name, attrs -> String componentName = attrs.get('android:name') if ('activity'.equals(name)) { def result = mainDexListActivity.find { componentName.endsWith("${it}") } return result != null } else { return true } } } } }這里過濾掉除 SplashActivity,MainPageActivity 之外的其他 activity,但 main-dex 中未滿 65535 之前,其他 activity 或類也可能在 main-dex 中,并不能將 main-dex 優(yōu)化為最小。
可參考 DexKnifePlugin 優(yōu)化 main-dex 為最小。(自己并未實際用過)
參考文章 Android-Easy-MultiDex -
gradle 2.3.0
gradle 中通過 multiDexKeepProguard 或 multiDexKeepFile 設(shè)置必須放置
main-dex的類。其次設(shè)置
additionalParameters優(yōu)化main-dex為最小dexOptions { additionalParameters '--multi-dex', '--minimal-main-dex', '--main-dex-list=' + file('multidex-config.txt').absolutePath' }
嚴(yán)選 gradle 版本為 2.1.2,然而按照上述的解決方法發(fā)現(xiàn)并沒有效果,查看 Application 初始化代碼,可以發(fā)現(xiàn) HTRouterManager.init 中引用了全部的 Activity 類
public static void init() {
...
entries.put("yanxuan://newgoods", new HTRouterEntry(NewGoodsListActivity.class, "yanxuan://newgoods", 0, 0, false));
entries.put("yanxuan://popular", new HTRouterEntry(TopGoodsRcmdActivity.class, "yanxuan://popular", 0, 0, false));
...
}
2.7 業(yè)務(wù)擴張導(dǎo)致路由表過大,內(nèi)存和性能受損
嚴(yán)選 4.1.7 版本,頁面跳轉(zhuǎn)路由表已經(jīng)注冊了 125 個 scheme 協(xié)議,其中對外公開使用的 39 個scheme,為此我們 app 工程的頁面路由表巨大,而這個路由表在 app 啟動的時候就必須初始化裝載到內(nèi)存,整個 app 運行過程中,必須一直持有這部分內(nèi)存。而一次進(jìn)程生命周期中,只有少部分 scheme 協(xié)議會被用到。
除了內(nèi)存消耗,另一個比較嚴(yán)重和明顯的是性能損失。原因是 ht-router 進(jìn)行路由表匹配的時候,支持正則匹配。根據(jù) scheme 在路由表中查找路由數(shù)據(jù)時,需要遍歷查找。而現(xiàn)在路由表已經(jīng)變得巨大,將會導(dǎo)致每次路由查找非常的耗時。
http://m.you.163.com/product/{id}.html
支持匹配以下這種形式的 scheme:
http://m.you.163.com/product/1.html
http://m.you.163.com/product/101.html
...
2.8 路由跳轉(zhuǎn)不支持自定義降級
ht-router 跳轉(zhuǎn)路由表中沒有的 url 時,支持自動將 scheme 替換成 http,然后降級成 H5 頁面去加載。而推送業(yè)務(wù)場景中,因為 app 版本兼容性原因需要的降級方案復(fù)雜的多:
- 新版本支持商品詳情頁,老版本不支持詳情頁,當(dāng)老版本 app 打開推送內(nèi)容的時候,我們期望打開 H5 頁面展示商品詳情
- 新版本支持推送跳轉(zhuǎn)紅包雨界面,當(dāng)老版本 app 打開推送內(nèi)容的時候,我們期望打開 H5 頁面引導(dǎo)用戶下載更新 APP
- 新版本支持購物車獨立頁面,老版本僅有首頁購物車 tab 頁,我們期望相同的推送,老版本能打開首頁購物車 tab 頁
以上業(yè)務(wù)場景,要求我們路由跳轉(zhuǎn)支持自定義的降級方案
2.9 路由表不支持跨工程
由于跨工程后,假設(shè) 2 個業(yè)務(wù)工程都集成了 ht-router,使用 apt 實現(xiàn)的路由表生成邏輯會被執(zhí)行 2 次,由于生成的 class 類是相同包名和類名,為此后期編譯會產(chǎn)生類沖突。此外,多個路由表如何整合使用,也是我們要考慮的地方。
3 router 框架優(yōu)化
3.1 apt 生成代碼量過大問題優(yōu)化
思考框架本身,其實可以發(fā)現(xiàn)僅有 router 映射表是需要根據(jù)注解編譯生成的,其他的全部代碼都是固定代碼,完全可以 sdk 中直接編碼提供。反過來思考為何當(dāng)初 sdk 開發(fā)需要編寫繁重的 apt 生成代碼,去生成這些固定的邏輯,可以發(fā)現(xiàn) htrouterdispatch-process 工程是一個純 java 工程,部分純 java 類的提供在 htrouterdispatch。由于無法引用 Android 類,同時期望業(yè)務(wù)層接口能完美隱藏內(nèi)部實現(xiàn),為此和 Android 相關(guān)的類,索性全部由 apt 生成。
apply plugin: 'java' // 使用 apply plugin: 'com.android.library' 編譯報錯
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
dependencies {
compile project (':htrouterdispatch')
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.0.0'
}
為了解決這里的問題,可以修改 HTRouterManager 的初始化接口,使用 router 映射表顯式的傳入,其中幾個參數(shù)均有 HTRouterTable 提供。修改后就能發(fā)現(xiàn)僅有 HTRouterTable 里面的映射表接口需要 apt 生成,而其余的代碼均可通過直接編碼。
public class HTRouterManager {
public static void init() {
...
}
}
→
public class HTRouterManager {
public static void init(Map<String, IRouterGroup> pageGroups,
List<HTMethodRouterEntry> methodEntries,
List<HTInterceptorEntry> annoInterceptors) {
...
}
}
public final class HTRouterTable {
...
private final HashMap<String, IRouterGroup> mRouterGroups = new HashMap<String, IRouterGroup>();
private final List<HTInterceptorEntry> mInterceptors = new LinkedList<HTInterceptorEntry>();
private final List<HTMethodRouterEntry> mMethodRouters = new LinkedList<HTMethodRouterEntry>();
private Map<String, IRouterGroup> pageRouterGroup() {
if (mRouterGroups.isEmpty()) {
mRouterGroups.put("m", new HTRouterGroup$$m());
...
}
return mRouterGroups;
}
private List<HTInterceptorEntry> interceptors() {
if (mInterceptors.isEmpty()) {
mInterceptors.add(new HTInterceptorEntry("http://www.you.163.com/activity/detail/{id}.shtml", new ProductDetailInterceptor()));
...
}
return mInterceptors;
}
private List<HTMethodRouterEntry> methodRouters() {
if (mMethodRouters.isEmpty()) {
{
List<Class> paramTypes = new ArrayList<Class>();
paramTypes.add(Context.class);
paramTypes.add(String.class);
paramTypes.add(int.class);
mMethodRouters.add(new HTMethodRouterEntry("http://www.you.163.com/jumpA", "com.netease.hearttouch.example.JumpUtil", "jumpA", paramTypes));
}
...
}
return mMethodRouters;
}
}
HTRouterTable.pageRouterGroup()、HTRouterTable.methodRouters() 和 HTRouterTable.interceptors() 先忽略,后續(xù)解釋
新建了一個 Android Library
htrouter,引用工程htrouterdispatch,app 工程修改引用htrouter
經(jīng)過優(yōu)化,router 跳轉(zhuǎn)的邏輯代碼可通過直接編碼方式實現(xiàn),普通 Android 開發(fā)也能輕松修改其中的邏輯,同時 apt 生成的類從 6 個直接減少至 1 個 HTRouterTable 和幾個 HTRouterGroup。而自動生成的路由表和業(yè)務(wù)邏輯已經(jīng)沒有直接聯(lián)系,就不會出現(xiàn)因為路由表未生成導(dǎo)致的大量編譯出錯問題。
3.2 路由表過大問題優(yōu)化
參考 ARouter 的路由表分組和延遲加載概念,我們也引入相關(guān)機制。ARouter 使用 path 的第一個 segment 作為 group,為此 group 概念由業(yè)務(wù)層定義。而嚴(yán)選工程中,前期協(xié)議并沒有考慮到 group,為此 url 中并沒有業(yè)務(wù)字段指定 group,且全部 Activity 綁定是根據(jù) host 字段,基本不定義 path 等。我這里采用 host 的第一個字母作為 group,生成如下 RouterGroup 映射表。
public final class HTRouterGroup$$m implements IRouterGroup {
private final List<HTRouterEntry> mPageRouters = new LinkedList<HTRouterEntry>();
public List<HTRouterEntry> pageRouters() {
if (mPageRouters.isEmpty()) {
mPageRouters.add(new HTRouterEntry("com.netease.hearttouch.example.MainActivity", "yanxuan://member", 2131034128, 2131034126, false));
...
}
return mPageRouters;
}
}
而 HTRouterTable 持有全部的 RouterGroup 對象,支持路由跳轉(zhuǎn)時按需加載路由分組的內(nèi)容,降低內(nèi)存消耗。同時進(jìn)行路由表匹配查找時,就可以僅查找對應(yīng)路由分組的內(nèi)容,查找量是原來的 1/26(26 個字母),極大的降低路由查找性能開銷
public final class HTRouterTable {
public static Map<String, IRouterGroup> pageRouterGroup() {
if (ROUTER_GROUPS.isEmpty()) {
ROUTER_GROUPS.put("m", new HTRouterGroup$$m());
...
}
return ROUTER_GROUPS;
}
}
3.3 攔截器優(yōu)化
3.3.1 優(yōu)化前臨時方案
針對登錄攔截需求,當(dāng)時的臨時解決方案如下:
- 路由注解添加
needLogin字段 - 并修改 apt 生成代碼,使
HTRouterEntry記錄needLogin信息 - 提供
RouterUtil.startActivity將目標(biāo)頁面的跳轉(zhuǎn)構(gòu)建成一個 runnable 傳入,在登錄成功回調(diào)中執(zhí)行 runnable
@HTRouter(url = {PreemptionActivateActivity.ROUTER_URL}, needLogin = true)
public class PreemptionActivateActivity extends Activity {
...
}
public static boolean startActivity(final Context context, final String schemeUrl,
final Intent sourceIntent, final boolean isFinish) {
return doStartActivity(context, schemeUrl, new Runnable() {
@Override
public void run() {
HTRouterManager.startActivity(context, schemeUrl, sourceIntent, isFinish);
}
});
}
private static boolean doStartActivity(final Context context, final String schemeUrl,
final Runnable runnable) {
if (HTRouterManager.isUrlRegistered(schemeUrl)) {
HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(schemeUrl);
if (entry == null) {
return false;
}
if (entry.isNeedLogin() && !UserInfo.isLogin()) {
LoginActivity.addOnLoginResultListener(new OnLoginResultListener() {
@Override
public void onLoginSuccess() {
runnable.run();
}
@Override
public void onLoginFail() {
// do nothing
}
});
LoginActivity.start(context);
}
return true;
}
return false;
}
可以發(fā)現(xiàn)這種處理方式并不通用,同時需要業(yè)務(wù)層代碼全部修改調(diào)用方式,未修改的接口還是可能出現(xiàn)以未登錄態(tài)進(jìn)入需要登錄的頁面(這種情況也確實在后面發(fā)生過,后來我們要求前端跳轉(zhuǎn)之前,先通過 jsbridge 喚起登錄頁面(⊙﹏⊙)b)。我們需要一種通用規(guī)范的方式處理攔截邏輯,同時能適用各種場景,也能規(guī)避業(yè)務(wù)層的錯誤。
3.3.2 攔截器優(yōu)化和設(shè)計
為避免業(yè)務(wù)層繞過攔截器直接調(diào)用到 HTRouterManager,將 HTRouterManager.startActivity 等接口修改為 package 引用范圍,此外新定義 HTRouterCall 作為對外接口類。
public class HTRouterCall implements IRouterCall {
...
}
public interface IRouterCall {
// 繼續(xù)路由跳轉(zhuǎn)
void proceed();
// 繼續(xù)路由跳轉(zhuǎn)
void cancel();
// 獲取路由參數(shù)
HTRouterParams getParams();
}
定義攔截器 interface 如下:
public interface IRouterInterceptor {
void intercept(IRouterCall call);
}
總結(jié)攔截的需求場景,歸納攔截場景為 3 種:
-
全局?jǐn)r截 → 全局?jǐn)r截器
全局?jǐn)r截器,通過靜態(tài)接口設(shè)置添加
public static void addGlobalInterceptors(IRouterInterceptor... interceptors) { Collections.addAll(sGlobalInterceptors, interceptors); }登錄攔截需求可以理解是一個全局的需求,全部的 Activity 跳轉(zhuǎn)都需要判斷是否需要喚起登錄頁面。
public class LoginRouterInterceptor implements IRouterInterceptor { @Override public void intercept(final IRouterCall call) { HTDroidRouterParams params = (HTDroidRouterParams) call.getParams(); HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(params.url); if (entry == null) { call.cancel(); return; } if (entry.isNeedLogin() && !UserInfo.isLogin()) { LoginActivity.setOnLoginResultListener(new OnLoginResultListener() { @Override public void onLoginSuccess() { call.proceed(); } @Override public void onLoginFail() { call.cancel(); } }); LoginActivity.start(params.getContext()); } else { call.proceed(); } } }Alt pic登錄攔截效果
-
業(yè)務(wù)頁面固定攔截 → 注解攔截器
上面剩余的 7 個
switch-case攔截,可以理解為特定業(yè)務(wù)頁面喚起都必須進(jìn)入的一個攔截處理,分別定義 7 個攔截器類,同樣通過注解的方式標(biāo)記。以 yanxuan://category 為例子
@HTRouter(url = {"yanxuan://category", "yanxuan://categoryl2"}) public class CategoryL2Activity extends Activity { ... }對應(yīng)的注解攔截器
@HTRouter(url = {"yanxuan://category"}) public class CategoryL2Intercept implements IRouterInterceptor { @Override public void intercept(IRouterCall call) { HTRouterParams routerParams = call.getParams(); Uri uri = Uri.parse(routerParams.url); // routerParams.url 添加額外參數(shù) Uri.Builder builder = uri.buildUpon(); ... routerParams.url = builder.build().toString(); call.proceed(); } }apt 生成攔截器初始化代碼
public static List<HTInterceptorEntry> interceptors() { if (INTERCEPTORS.isEmpty()) { ... INTERCEPTORS.add(new HTInterceptorEntry("yanxuan://category", new CategoryL2Intercept())); ... } return INTERCEPTORS; }HTRouterTable
-
業(yè)務(wù)頁面動態(tài)攔截
比如 onClick 方法內(nèi)執(zhí)行路由跳轉(zhuǎn)時,需要彈窗提示用戶是否繼續(xù)跳轉(zhuǎn),其他場景跳轉(zhuǎn)并不需要這個彈窗,這種場景的攔截器我們認(rèn)為是動態(tài)攔截
HTRouterCall.newBuilder(data.schemeUrl) .context(mContext) .interceptors(new IRouterInterceptor() { @Override public void intercept(final IRouterCall call) { Log.i("TEST", call.toString()); AlertDialog dialog = new AlertDialog.Builder(mContext) .setTitle("alert") .setMessage("是否繼續(xù)") .setPositiveButton("繼續(xù)", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { call.proceed(); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { call.cancel(); } }).create(); dialog.show(); } }) .build() .start();Alt pic
優(yōu)先級:動態(tài)攔截器 > 注解攔截器 > 全局?jǐn)r截器
3.4 sdk 頁面 router 支持
我們接入了七魚、HTImagePick 等 sdk,這些 sdk 也有自己的頁面,而這部分頁面并不能通過前面的路由方式打開,其原因如下:
我們不能修改他們的代碼
apt 處理的注解僅能針對引入 apt 的 app 工程
-
對應(yīng)的頁面喚起需要通過 sdk 提供的特殊接口喚起
public static void openYsf(Context context, String url, String title, String custom) { ConsultSource source = new ConsultSource(url, title, custom); Unicorn.openServiceActivity(context, // 上下文 title, // 聊天窗口的標(biāo)題 source // 咨詢的發(fā)起來源,包括發(fā)起咨詢的url,title,描述信息等 ); }七魚客服頁面喚起
public void openImagePick(Context context, ArrayList<PhotoInfo> photoInfos, boolean multiSelectMode, int maxPhotoNum, String title) { HTPickParamConfig paramConfig = new HTPickParamConfig(HTImageFrom.FROM_LOCAL, null, photoInfos, multiSelectMode, maxPhotoNum, title); HTImagePicker.INSTANCE.start(context, paramConfig, this); }
基于此,只需要提供對方法的 router 調(diào)用,就能支持 sdk 中的頁面路由跳轉(zhuǎn)。具體用法示例如下
-
通過
HTMethodRouter注解標(biāo)記跳轉(zhuǎn)方法(非靜態(tài)方法需實現(xiàn)getInstance單例)public class JumpUtil { private static final String TAG = "JumpUtil"; private static JumpUtil sInstance = null; public static JumpUtil getInstance() { if (sInstance == null) { synchronized (JumpUtil.class) { if (sInstance == null) { sInstance = new JumpUtil(); } } } return sInstance; } private JumpUtil() { } @HTMethodRouter(url = {"http://www.you.163.com/jumpA"}, needLogin = true) public void jumpA(Context context, String str, int i) { String msg = "jumpA called: str=" + str + "; i=" + i; Log.i(TAG, msg); if (context != null) { Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } } @HTMethodRouter(url = {"http://www.you.163.com/jumpB"}) public static void jumpB(Context context, String str, int i) { String msg = "jumpB called: str=" + str + "; i=" + i; Log.i(TAG, msg); if (context != null) { Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } } @HTMethodRouter(url = {"http://www.you.163.com/jumpC"}) public void jumpC() { Log.i(TAG, "jumpC called"); } } -
方法路由表生成
public final class HTRouterTable { private final List<HTMethodRouterEntry> mMethodRouters = new LinkedList<HTMethodRouterEntry>(); ... private List<HTMethodRouterEntry> methodRouters() { if (mMethodRouters.isEmpty()) { { List<Class> paramTypes = new ArrayList<Class>(); paramTypes.add(Context.class); paramTypes.add(String.class); paramTypes.add(int.class); mMethodRouters.add(new HTMethodRouterEntry("http://www.you.163.com/jumpA", "com.netease.hearttouch.example.JumpUtil", "jumpA", paramTypes)); } { List<Class> paramTypes = new ArrayList<Class>(); paramTypes.add(Context.class); paramTypes.add(String.class); paramTypes.add(int.class); mMethodRouters.add(new HTMethodRouterEntry("http://www.you.163.com/jumpB", "com.netease.hearttouch.example.JumpUtil", "jumpB", paramTypes)); } { List<Class> paramTypes = new ArrayList<Class>(); mMethodRouters.add(new HTMethodRouterEntry("http://www.you.163.com/jumpC", "com.netease.hearttouch.example.JumpUtil", "jumpC", paramTypes)); } } return mMethodRouters; } } -
方法路由觸發(fā)邏輯
除了設(shè)置動畫、是否關(guān)閉當(dāng)前頁面等參數(shù),這里方法路由的調(diào)用方式和頁面路由完全一致,同樣支持
needLogin字段,同樣支持全局?jǐn)r截器、注解攔截器、動態(tài)攔截器// JUMPA 按鈕點擊 public void onMethodRouter0(View v) { HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpA?a=lilei&b=10"); } // JUMPB 按鈕點擊 public void onMethodRouter1(View v) { HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpB?a=hanmeimei&b=10"); } // JUMPC 按鈕點擊 public void onMethodRouter2(View v) { HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpC"); } -
結(jié)果示例
Alt pic
3.5 main dex 優(yōu)化處理
這里的處理邏輯較為簡單,僅需修改類引用為類名字符串,后續(xù)跳轉(zhuǎn)時通過反射獲取類
public final class HTRouterGroup$$m implements IRouterGroup {
private final List<HTRouterEntry> mPageRouters = new LinkedList<HTRouterEntry>();
public List<HTRouterEntry> pageRouters() {
if (mPageRouters.isEmpty()) {
mPageRouters.add(new HTRouterEntry("com.netease.hearttouch.example.MainActivity", "yanxuan://m.you.163.com", 2131034128, 2131034126, false));
...
}
return mPageRouters;
}
}
3.6 自定義降級優(yōu)化
為推送等提供降級跳轉(zhuǎn)方案,添加了 downgradeUrls 參數(shù)設(shè)置,若當(dāng)前 urlA 并不識別,則會依次對 urlB,urlC 做判斷并嘗試跳轉(zhuǎn)
HTRouterCall.newBuilder(urlA)
.context(ProductDetailActivity.this)
.downgradeUrls(urlB, urlC)
.sourceIntent(sourceIntent)
.requestCode(1001)
.forResult(true)
.build()
.start();
3.7 跨工程路由表整合
ht-router 不支持多工程,是因為 apt 生成的代碼包名都是固定的,而多工程就會多次執(zhí)行 apt 代碼生成邏輯,最終會生成多個相同包名和類名的類,最后產(chǎn)生類沖突。類沖突的解決辦法較為簡單,只需要將包名改成外部傳入就可以了
apt {
arguments {
routerPkg 'com.netease.demo.router'
}
}
app 工程指定
routerPkg參數(shù)為 'com.netease.demo.router'
app 工程生成的路由表為 com.netease.demo.router.HTRouterTable。
多個業(yè)務(wù)工程生成了多個 HTRouterTable 后,需要業(yè)務(wù)開發(fā)調(diào)用多次 HTRouterManager.init 將各個路由表注冊進(jìn)去。為了隱藏多個路由表的實現(xiàn)細(xì)節(jié),同時避免業(yè)務(wù)開發(fā)調(diào)用初始化方法可能產(chǎn)生的錯誤,這里通過 AspectJ 進(jìn)行自動收集并注冊路由表。業(yè)務(wù)層僅需要在 Application 中執(zhí)行 HTRouterCall.init() 即可。
- 漏調(diào)初始化方法
- HTRouterTable 引用錯誤,因為生成的全部路由表都是
HTRouterTable
@Aspect
public final class HTRouterTable {
...
@After("execution(void com.netease.hearttouch.router.HTRouterCall.init())")
public void init(JoinPoint joinPoint) {
HTRouterManager.init(pageRouterGroup(), methodRouters(), interceptors());
}
...
}
4 接口組件化
除了嚴(yán)選最初集成的 HTHTTP 網(wǎng)絡(luò)庫、HTRecycleView、HTEventbus 等基礎(chǔ)模塊之外,可以保持獨立,后面開發(fā)的很多業(yè)務(wù)模塊,甚至基礎(chǔ)模塊都需要使用到原 app 工程里面的功能或者類型,于是我們碰到了 微信 Android 同樣的問題,也同樣開始了初步的接口下沉和類型下沉,具體下沉的實例和背景原因如下:
-
基礎(chǔ)功能類下沉
前面講述過,我們在 app 工程的
common/util/下積累了一部分 util 類,如 FileUtil、BitmapHelper、CookieUtil、LogUtil 等,這里部分是業(yè)務(wù)無關(guān),部分是業(yè)務(wù)相關(guān)。如 FileUtil、BitmapHelper 和業(yè)務(wù)邏輯無關(guān),而CookieUtil 和 LogUtil 和業(yè)務(wù)相關(guān)。此外,基礎(chǔ)功能代碼和其他代碼也存在少量耦合,在下沉的過程中,容易導(dǎo)致更多的代碼下沉。- CookieUtil 提供了業(yè)務(wù)相關(guān)的 Set-Cookie 解析保存和提取組裝到網(wǎng)絡(luò)請求、WebView,WebView Cookie 需要添加相關(guān)的 domain、httpOnly 設(shè)置。
- LogUtil 提供了不同情況的日志表示邏輯。測試包將 error 日志通過對話框的形式展現(xiàn)給測試,同時記錄錯誤日志到本地文件,普通日志正常 adb 展示;線上包將 error 日志上報服務(wù)器,普通日志關(guān)閉。這里對話框復(fù)用了
SingleAlertBuilder,寫本地文件復(fù)用了存儲模塊
-
數(shù)據(jù)類型下沉
工程里面全局事件總線通過 event 發(fā)送,在構(gòu)建多工程模塊的情況下,需要涉及跨工程通信的時候,就需要將相關(guān) event 下沉到底層。
以上基礎(chǔ)功能下沉和數(shù)據(jù)類型下沉過程中,我們構(gòu)建了 yxcommonbase、yxlogger、yxstorage 等基礎(chǔ)工程,app 工程和其他工程依賴這幾個基礎(chǔ)工程,這里不僅有業(yè)務(wù)工程、還有部分基礎(chǔ)工程,如網(wǎng)絡(luò)診斷工程。由于下沉的頻率較高,基本上每個版本都要下沉一點點,導(dǎo)致其他基礎(chǔ)工程無法以 aar 包的形式集成 app,如 app 工程引用基礎(chǔ)模塊 A,A 引用 yxcommonbase,雖然 A 代碼完全沒變,由于 yxcommonbase 發(fā)生了一點變化,我們還是需要重新發(fā)布 A aar。由于這種多級引用關(guān)系的存在,而 yxcommonbase 又是最底層的模塊,最終導(dǎo)致的是全部的基礎(chǔ)工程都無法以 aar 包的形式集成 app。最嚴(yán)重的還是長期的下沉最終會導(dǎo)致模塊邊界破壞、基礎(chǔ)工程中心化。
4.1 方案選擇
為阻止下沉情況,我們需要重新整理通信方式、功能調(diào)用方式,考慮有以下選擇:
4.1.1 方法路由
首先能想到的就是前面提到的方法路由,使用方法路由能很好的統(tǒng)一 sdk 的頁面路由、并支持服務(wù)端命令推送,在這方面使用方法路由是非常合適的。而方法路由一個最大的問題就是,每個方法都需要定義協(xié)議,而工程間的通信、功能復(fù)用情況會非常多,很容易導(dǎo)致協(xié)議過多,維護(hù)成本過大,為此在工程間通信方面并不是好的選擇。此外對于特殊參數(shù),如 Context、Bitmap 等,方法路由就顯得比較無力。
4.1.2 ServiceLoader
排除通過方法路由進(jìn)行通信,更安全和開發(fā)更適應(yīng)的方式是接口通信。使用接口通信的方式,Java 原生提供的方式有 ServiceLoader
Jdk6 提供的一種 SPI(Service Provider Interfaces)機制,流程如下:
在 META-INF/services 下放置配置文件,Caculator 在接口工程 A,CalulatorImpl 在實現(xiàn)工程 B
文件名:com.example.Caculator
文件內(nèi)容:com.example.CalulatorImpl
接口使用工程 C 定義 Factory 類,通過 get() 來得到需要的具體實例,工程 C 并不需要知道 Caculator 的實現(xiàn)類。
public class CalculatorFactory {
public CalculatorFactory() {
ServiceLoader<Caculator> loader = ServiceLoader.load(Caculator.class);
mIterator = loader.iterator();
}
private Iterator<Caculator> mIterator;
Caculator get() {
return mIterator.next();
}
boolean hasNextDisplay() {
return mIterator.hasNext();
}
}
查看源碼可以發(fā)現(xiàn),ServiceLoader 通過 c.newInstance() 接口創(chuàng)建實現(xiàn)對象,為此有以下缺點:
- 不支持單例和使用參數(shù)來創(chuàng)建對象
- 只能使用
LazyIterator獲取實現(xiàn)對象,當(dāng)有多個實現(xiàn)類時,使用工程需要通過遍歷的方式查找目標(biāo)對象,將不需要的實現(xiàn)類創(chuàng)建對象,浪費性能 - 需要定義接口工程,和實現(xiàn)工程中的配置文件,實現(xiàn)過程編碼較多
public S next() {
...
c = Class.forName(cn, false, loader);
...
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
...
}
throw new Error(); // This cannot happen
}
代碼片段來源 Android-25 ServiceLoader.java
4.1.3 ARouter Provider 路由
ARouter 提供了 Provider 路由,使用樣例如下:
@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;
@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}
@Override
public void init(Context context) {
mContext = context;
Log.e("testService", HelloService.class.getName() + " has init.");
}
}
((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");
HelloServiceImpl 使用樣例
使用頁面路由相同的方式,通過注解 path 來定義 Provider 路由。HelloServiceImpl 必須實現(xiàn) IProvider 接口,實現(xiàn) void init(Context context) 方法。如果我們將 HelloService 接口定義在接口工程,HelloServiceImpl 定義在接口實現(xiàn)工程 A(或 app 主工程),那工程 B 就能使用 Provider 路由來調(diào)用到工程 A 中的方法。
但也有幾點不便之處:
-
navigation()調(diào)用值需要做強制類型轉(zhuǎn)換,業(yè)務(wù)開發(fā)需要查詢協(xié)議文檔,確定path和 接口HelloService之間的聯(lián)系public Object navigation() { return navigation(null); } -
提供的接口實現(xiàn)類要有無參構(gòu)造函數(shù),和
init(Context context)方法,不支持其他的構(gòu)造函數(shù),實用類為單例public class LogisticsCenter { ... public synchronized static void completion(Postcard postcard) { ... Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination(); // 重點 1:單例 IProvider instance = Warehouse.providers.get(providerMeta); if (null == instance) { // There's no instance of this provider IProvider provider; try { // 重點 2:無參構(gòu)造函數(shù) provider = providerMeta.getConstructor().newInstance(); // 重點 3:init(Context context) 初始化方法 provider.init(mContext); Warehouse.providers.put(providerMeta, provider); instance = provider; } catch (Exception e) { throw new HandlerException("Init provider failed! " + e.getMessage()); } } ... } ... } 接口類
HelloService相關(guān)的參數(shù)類型,或者返回值類型,需下沉到接口工程
4.1.4 WMRouter RouterService 功能
美團(tuán)參考 ServiceLoader 的原理,提供了自己的 RouterService 功能,使用如下:
@RouterService(interfaces = ILocationService.class, key = DemoConstant.SINGLETON, singleton = true)
public class FakeLocationService implements ILocationService {
private final Handler mHandler = new Handler();
@Override
public boolean hasLocation() {
return false;
}
@Override
public void startLocation(final LocationListener listener) {
...
}
}
ILocationService locationService = Router.getService(ILocationService.class, DemoConstant.SINGLETON);
接口實例獲取
相比 ARouter,有以下優(yōu)點:
-
接口服務(wù)實例獲取不需要強制類型轉(zhuǎn)換,業(yè)務(wù)開發(fā)不容易寫錯
public static <I, T extends I> T getService(Class<I> clazz, String key) { return ServiceLoader.load(clazz).get(key); } -
支持單例管理,無參構(gòu)造、Context 構(gòu)造和自定義 Factory
針對有多個構(gòu)造函數(shù)的服務(wù)類,需要定義多個自定義 Factory 類
public interface IFactory { @NonNull <T> T create(@NonNull Class<T> clazz) throws Exception; } 支持相同接口的多個實現(xiàn)類,業(yè)務(wù)工程通過
key選擇
此外,同 ServiceLoader,ARouter 方法,上述方案非常優(yōu)秀,但尚有有相同不便之處:
- 接口參數(shù)類型和返回類型要下沉到接口工程,而參數(shù)類型和返回類型可能和原工程有耦合,觸發(fā)更多的代碼需要下沉到接口工程
- 接口實現(xiàn)類,不支持靜態(tài)方法
- 需要修改原有代碼邏輯
- 添加基類或接口
- 修改方法為接口方法
4.2 AutoApi 組件方案
總結(jié)以上方法,開發(fā) AutoApi 組件,使用注解自動配置,自動生成接口類和實現(xiàn)類,支持跨工程共享接口。
4.2.1 支持多構(gòu)造函數(shù)創(chuàng)建接口實例
@AutoApiClassAnno
public class AddUtil {
private int mData1 = 0;
private int mData2 = 0;
@AutoApiConstructAnno
public AddUtil(int data1, int data2) {
mData1 = data1;
mData2 = data2;
}
@AutoApiConstructAnno
public AddUtil(int data) {
mData1 = data;
mData2 = data;
}
@AutoApiMethodAnno
public int calu() {
return mData1 + mData2;
}
}
服務(wù)類通過注解標(biāo)記,提供對外構(gòu)造函數(shù)和普通方法
自動生成接口類和工廠類如下:
/**
* com.netease.demo.autoapi.AddUtil's api Interface
*/
public interface AddUtilApi extends ApiBase {
int calu();
}
/**
* com.netease.demo.autoapi.AddUtil api Class's factory Interface
*/
public interface AddUtilApiFactory {
AddUtilApi newInstance(int data1, int data2);
AddUtilApi newInstance(int data);
}
接口使用樣例:
AddUtilApiFactory factory = AutoApi.getApiFactory("AddUtilApi");
AddUtilApi api = factory.newInstance(11, 12);
int result0 = api.calu(); // 23
4.2.2 支持靜態(tài)方法
@AutoApiClassAnno
public class AddUtil {
...
@AutoApiMethodAnno
public int calu() {
return mData1 + mData2;
}
@AutoApiMethodAnno
public static int add(int a, int b) {
return a + b;
}
}
服務(wù)類
/**
* com.netease.demo.autoapi.AddUtil's api Interface
*/
public interface AddUtilApi extends ApiBase {
int calu();
int add(int a, int b);
}
自動生成接口類
AddUtilApiFactory factory = AutoApi.getApiFactory("AddUtilApi");
AddUtilApi api = factory.newInstance(11, 12);
int result1 = api.calu(); // 23
int result2 = api.add(11, 12); // = 23
接口使用
4.2.3 支持靜態(tài)方法構(gòu)造接口實例(支持單例)
@AutoApiClassAnno(name = "AppSingleton")
public class Singleton {
private static Singleton sInstance = null;
@AutoApiClassBuildMethodAnno()
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
private Singleton() {
}
@AutoApiMethodAnno()
public String foo1(String str1, String str2) {
Log.i("Singleton", "foo1 called");
return str1 + "_" + str2;
}
}
服務(wù)類
/**
* com.netease.demo.autoapi.Singleton api Class's factory Interface
*/
public interface AppSingletonApiFactory {
AppSingleton getInstance();
}
自動生成的接口工廠類
AppSingletonApiFactory apiFactory = AutoApi.getApiFactory("AppSingleton");
AppSingleton singleton = apiFactory.getInstance();
String result = singleton.foo1("var1", "var2");
apiFactory.getInstance()通過Singleton.getInstance()獲取實例
4.2.4 避免服務(wù)類相關(guān)類型下沉至接口工程
嚴(yán)選 app 工程引入了 HTHttp 組件,其中 HTHttp 工程是基于 Volley 封裝的一個網(wǎng)絡(luò)庫。此外,嚴(yán)選工程基于 HTHttp 封裝了 WZP 請求(網(wǎng)易郵件私有協(xié)議)。WZP 的封裝、配置、初始化等代碼和嚴(yán)選 app 主工程耦合較大,而我們的業(yè)務(wù)子工程便需要 WZP 請求功能。若采用現(xiàn)有開源方案,我們基本需要將 WZP 模塊下沉至底層,而這不是我們期望看到的。這里我提供的 AutoApi 組件通過自動生成參數(shù)接口類型,來避免類型下沉的問題。
@AutoApiClassAnno(includeSuperApi = true)
public class SharedWzpCommonRequestTask extends BaseWzpCommonRequestTask {
private String mUrl;
private String mApi;
private Class mModelClass;
@AutoApiClassBuildMethodAnno()
public static SharedWzpCommonRequestTask newInstance(String url, Class modelClass,
Map<String, String> queryParams,
Map<String, String> headerMap,
Map<String, Object> bodyMap) {
Builder builder = new Builder();
return builder.setApi(url)
.setModelClass(modelClass)
.setQueryParams(queryParams)
.setHeaders(headerMap)
.setBodyMap(bodyMap)
.build();
}
private SharedWzpCommonRequestTask(SharedWzpDataModel data) {
mUrl = data.mUrl;
mApi = data.mApi;
mModelClass = data.mModelClass;
}
...
}
public class BaseWzpCommonRequestTask {
...
@AutoApiMethodAnno()
public Request query(@AutoApiCallbackAnno HttpListener listener) {
...
}
@AutoApiMethodAnno()
public Request queryArray(@AutoApiCallbackAnno HttpListener listener) {
...
}
}
@AutoApiClassAnno(allPublicNormalApi = true, includeSuperApi = true)
public interface HttpListener extends BaseHttpListener {
void onCancel();
}
public interface BaseHttpListener {
void onHttpSuccess(String httpName, Object result);
void onHttpError(String httpName, int errorCode, String errorMsg);
}
@AutoApiClassAnno(allPublicNormalApi = true)
public class Request {
public void cancel() {
...
}
}
WZP HttpTask 服務(wù)類及相關(guān)類
public interface SharedWzpCommonRequestTaskApi extends ApiBase {
RequestApi query(HttpListenerApi listener);
RequestApi queryArray(HttpListenerApi listener);
}
public interface HttpListenerApi extends ApiBase {
void onCancel();
void onHttpSuccess(String httpName, Object result);
void onHttpError(String httpName, int errorCode, String errorMsg);
}
public interface RequestApi extends ApiBase {
void cancel();
}
編譯自動生成接口類
public interface SharedWzpCommonRequestTaskApiFactory {
SharedWzpCommonRequestTaskApi newInstance(String url, Class modelClass, Map<String, String> queryParams, Map<String, String> headerMap, Map<String, Object> bodyMap);
}
編譯自動生成工廠接口類
SharedWzpCommonRequestTaskApiFactory apiFactory = AutoApi.getApiFactory("SharedWzpCommonRequestTaskApi");
SharedWzpCommonRequestTaskApi api = apiFactory.newInstance("/xhr/test/a", null, null, null, null);
RequestApi request = api.query(new HttpListenerApi() {
@Override
public void onCancel() { }
@Override
public void onHttpSuccess(String httpName, Object result) { }
@Override
public void onHttpError(String httpName, int errorCode, String errorMsg) { }
@Override
public Object getApiServiceTarget() {
return null;
}
});
// 取消請求
request.cancel();
業(yè)務(wù)子工程使用接口服務(wù)
以上避免了接口參數(shù)類型 HttpListener 和返回類型 Request 下沉至接口工程,業(yè)務(wù)子工程對于服務(wù)類相關(guān)的代碼類型并無感知
4.2.5 實際對象獲取
在跨工程中,我們也能使用 EventBus 進(jìn)行通信,EventBus 通過 event 的 class 類型來區(qū)分事件類型,為了支持子工程能給 app 工程發(fā)送事件,但又不想 app 工程中的 event 類下沉到底層,也不想讓子工程感知到 event 實際類型信息(如event classname),我們可以通過接口類的 getApiServiceTarget 方法獲取實際對象
@AutoApiClassAnno()
public class EventA {
...
}
app 工程 Event 類型
EventAApiFactory factory = AutoApi.getApiFactory("EventAApi");
EventAApi api = factory.newInstance();
EventBus.getDefault().post(api.getApiServiceTarget());
業(yè)務(wù)子工程發(fā)送 EventBus 事件
4.2.6 支持接口泛型
@AutoApiClassAnno
public class JsonUtil {
...
@AutoApiMethodAnno
public static <T> T toJsonObj(String jsonStr, Class<T> clazz) {
try {
if (!TextUtils.isEmpty(jsonStr)) {
return JSONObject.parseObject(jsonStr, clazz, Feature.IgnoreNotMatch);
}
} catch (Throwable e) {
LogUtil.yxLogE(e);
}
return null;
}
}
app 工程服務(wù)類
public interface JsonUtilApi extends ApiBase {
...
<T> T toJsonObj(String jsonStr, Class<T> clazz);
}
自動生成的接口類
5 總結(jié)
嚴(yán)選比起其他大廠,Android 組件化方面做得還比較初步,但也根據(jù)我們的業(yè)務(wù)場景做了適合我們自己的方案
在路由方案方面,我們做了如下優(yōu)化:
- 通過區(qū)分路由表生成代碼和其他跳轉(zhuǎn)邏輯,優(yōu)化 apt 代碼生成邏輯的復(fù)雜性和和維護(hù)性;
- 通過優(yōu)化攔截器,解決登錄攔截問題,優(yōu)化子模塊和全局代碼劃分;
- 通過提供方法路由,解決 sdk 頁面的路由跳轉(zhuǎn)問題;
- 通過修改路由表對類的直接引用,解決
main-dex問題; - 路由表根據(jù) host 自動分組,使用過程中懶加載路由表,優(yōu)化路由表內(nèi)存占用較大問題和路由查找性能開銷問題
- 路由跳轉(zhuǎn)支持自定義降級
- 支持跨工程的多路由表,使用 AspectJ 自動收集和初始路由表
在接口組件化方面,我們通過提供方法路由,支持方法推送,并開發(fā)了 AutoApi 組件方案
- 僅根據(jù)注解自動生成接口類和實現(xiàn)類,支持跨工程共享接口;
- 支持構(gòu)造函數(shù)和靜態(tài)方法創(chuàng)建接口實例;
- 支持單例;
- 支持對外提供服務(wù)類的普通方法和靜態(tài)方法;
- 通過
includeSuperApi支持不修改服務(wù)類基類,提供基類的服務(wù)方法; - 避免服務(wù)類接口相關(guān)數(shù)據(jù)類型下沉至接口工程;
- 支持 EventBus;
- 支持接口參數(shù)泛型
htrouter 源碼地址:https://github.com/bitterbee/htrouter
auto-api 源碼地址:https://github.com/bitterbee/auto-api