0 背景
早前嚴選 Android 工程,使用原生 Intent 方式做頁面跳轉,為規范參數傳遞,做了編碼規范,使用靜態方法的方式喚起 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 喚起 APP 和 推送喚起 APP 的場景下會顯得力不從心,隨着公開的跳轉協議越來越多,代碼中 switch-case
也會越來越多,最后難以維護。
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;
case ConstantsRT.ORDER_DETAIL_ROUTER_PATH:
clazz = OrderDetailActivity.class;
...
break;
...
... 省略 28 個 case! ☹️
...
default:
break;
}
Intent intent = null;
if (clazz != null) {
intent = new Intent();
intent.setClass(context, clazz);
}
return intent;
}
}
根據輸入 scheme,返回跳轉 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 使用樣例
1 ht-router 接入
參考 DeepLink從認識到實踐,接入杭研 ht-router,由此通過注解的方式統一了 H5 喚醒、推送喚醒、正常啟動 APP 的邏輯,上面點擊跳轉的邏輯得到了簡化:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HTRouterManager.startActivity(view.getContext(), schemeUrl, null, false);
}
});
RouterUtil
中冗長的 switch-case
代碼也得到得到了極大的改善,統一跳轉可通過 scheme 參數直接觸發跳轉,近 30 個 switch-case
減少至 7 個
HTRouterManager.init();
...
// 設置跳轉前的攔截,返回 true 攔截不再跳轉,返回 false 繼續跳轉
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 類
-
歷史原因
嚴選工程中
CategoryL2Activity
有yanxuan://category
和yanxuan://categoryl2
2 個 scheme,而同一個參數categoryid
在不同的 scheme 下有不同的含義,為此在攔截器中添加新的字段,CategoryL2Activity
中僅需處理 2 個新加的字段,不必知道自身的 scheme -
跳轉 Activity 的不同 fragment
嚴選首頁 MainPageActivity 擁有 5 個 tab fragment,不同的 tab 會有不同的 scheme,攔截器中直接根據不同的 scheme,添加參數來指定不同的 tab,首頁僅需處理 tab 參數顯示不同的 fragment
-
ht-router
的其他優點、用法、api 見文章 DeepLink從認識到實踐,這里不再敘述
2 ht-router 的痛點
ht-router
對工程框架的作用是巨大的,然而隨着多期業務迭代和工程復雜度的提升,發現的幾個痛點如下:
2.1 apt 生成代碼量過大,業務開發較難維護
ht-router
通過 apt 生成的類有 6 個,其中 HTRouterManager
有 600 行代碼,去除 init 方法中初始化 router 信息的 100 行左右代碼,剩余還有 500 行左右
apt 生成的類目錄
HTRouterManager.java
參考 apt 的用法,若要生成一個簡單的類,對應的 apt 代碼會復雜的多。當目標代碼量比較多的情況下,apt 的生成代碼就會比較難以維護,根據業務場景添加接口,或者修改字段都會相比更加困難。另外 apt 的調試也比較辛苦,需要編譯后再查看目標代碼是否是有錯誤。
這里給 ht-router 的開發同學獻上膝蓋,為業務團隊貢獻了很多!
/**
* apt 測試代碼
*/
public class TestClass {
public static final String STATIC_FIELD = "ht_url_params_map";
public void foo() {
System.out.println("hello world");
}
}
目標代碼
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();
}
生成目標代碼的 apt 代碼
2.2 apt 生成代碼量過大,可能出現業務等代碼編譯錯誤被掩蓋
合並分支后偶現,由於業務代碼其他的編譯不通過,導致 apt 代碼未生成,大量提示報錯 HTRouterManager 找不到,但無法定位到真正的業務代碼錯誤邏輯。
由於 HTRouterManager 在業務代碼中廣泛被使用,暫未有很好的辦法解決這個報錯,臨時的處理辦法是從同事處拷貝 apt 文件夾,臨時繞過錯誤報錯,修改業務層代碼錯誤后 rebuild
第一次碰到比較懵逼,花了不少時間處理定位和解決問題,(⊙﹏⊙)b
2.3 攔截功能不滿足登錄需求
針對未登錄狀態,跳轉需要登錄狀態的 Activity 的場景,我們期望是先喚起登錄頁,登錄成功后,關閉登錄頁重定向至目標 Activity;若用戶退出登錄頁,則回到上一個頁面。針對已登錄狀態,則直接喚起目標頁面。對於這個需求,ht-router
並不滿足,雖然提供了 HTRouterHandler
,但僅能判斷根據返回值判斷是否繼續跳轉,無法在登錄回調中決定是否繼續跳轉。
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 個,感覺代碼也不優雅),但這里的特殊處理邏輯屬於各個頁面的業務邏輯,不應該在 RouterUtil 中。路由的一個很大作用,就是將各個頁面解耦,能為后期模塊化等需求打下堅實基礎,而這里的全局攔截處理邏輯,顯然是和模塊解耦是背道而馳的。
當然這些特殊的處理邏輯完全可以挪到各個 Activity 中,但是不是有機制能很好的處理這種場景,同時 Activity 還是不需要關心自身當前的 scheme 是什么?
2.5 sdk 頁面,無法添加路由注解
我們發現接入的子工程如圖片選擇器等也有自己的頁面,而 apt 的代碼生成功能是對 app 工程生效,不支持其他子工程的路由注解,為此子工程的頁面就無法享受路由帶來的好處。
2.6 router 初始化為類引用,阻礙 main dex 優化
最初通過 multidex
方案解決了 65535 問題后,2年后的現在,又爆出了 Too many classes in –main-dex-list
錯誤。
原因:dex 分包之后,各 dex 還是遵循 65535 的限制,而打包流程中 dx --dex --main-dex-list=<maindexlist.txt>
中的 maindexlist.txt
決定了哪些類需要放置進 main-dex
。默認 main-dex
包含 manifest 中注冊的四大組件,Application、Annonation、multi-dex 相關的類。由於 app 中 四大組件 (特別是 Activity) 比較多和 Application 中的初始化代碼,最終還是可能導致 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 之前
執行
dex
命令時添加--main-dex-list
和--minimal-main-dex
參數。而這里maindexlist.txt
中的內容需要開發生成,參考 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
現嚴選使用 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 優化為最小。
可參考 DexKnifePlugin 優化 main-dex 為最小。(自己並未實際用過) 參考文章 Android-Easy-MultiDex
-
gradle 2.3.0
gradle 中通過 multiDexKeepProguard 或 multiDexKeepFile 設置必須放置
main-dex
的類。其次設置
additionalParameters
優化main-dex
為最小dexOptions { additionalParameters '--multi-dex', '--minimal-main-dex', '--main-dex-list=' + file('multidex-config.txt').absolutePath' }
嚴選 gradle 版本為 2.1.2
,然而按照上述的解決方法發現並沒有效果,查看 Application 初始化代碼,可以發現 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));
...
}
本文來自網易雲社區,經作者張雲龍授權發布。
更多網易研發、產品、運營經驗分享請訪問網易雲社區。