@zyl06
2018-09-21T17:45:08.000000Z
字数 16609
阅读 1348
Android
路由
早前严选 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 使用样例
参考 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从认识到实践,这里不再叙述
ht-router
对工程框架的作用是巨大的,然而随着多期业务迭代和工程复杂度的提升,发现的几个痛点如下:
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 代码
合并分支后偶现,由于业务代码其他的编译不通过,导致 apt 代码未生成,大量提示报错 HTRouterManager 找不到,但无法定位到真正的业务代码错误逻辑。
由于 HTRouterManager 在业务代码中广泛被使用,暂未有很好的办法解决这个报错,临时的处理办法是从同事处拷贝 apt 文件夹,临时绕过错误报错,修改业务层代码错误后 rebuild
第一次碰到比较懵逼,花了不少时间处理定位和解决问题,(⊙﹏⊙)b
针对未登录状态,跳转需要登录状态的 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;
}
...
}
前面 RouterUtil 中的 switch-case
从 30 个大幅降至 7 个(即便是 7 个,感觉代码也不优雅),但这里的特殊处理逻辑属于各个页面的业务逻辑,不应该在 RouterUtil 中。路由的一个很大作用,就是将各个页面解耦,能为后期模块化等需求打下坚实基础,而这里的全局拦截处理逻辑,显然是和模块解耦是背道而驰的。
当然这些特殊的处理逻辑完全可以挪到各个 Activity 中,但是不是有机制能很好的处理这种场景,同时 Activity 还是不需要关心自身当前的 scheme 是什么?
我们发现接入的子工程如图片选择器等也有自己的页面,而 apt 的代码生成功能是对 app 工程生效,不支持其他子工程的路由注解,为此子工程的页面就无法享受路由带来的好处。
最初通过 multidex
方案解决了 65535 问题后,2年后的现在,又爆出了 Too many classes in –main-dex-list
错误。
原因:dex 分包之后,各 dex 还是遵循 65536 的逻辑,而打包流程中 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));
...
}
思考框架本身,其实可以发现仅有 router 映射表是需要根据注解编译生成的,其他的全部代码都是固定代码,完全可以 sdk 中直接编码提供。反过来思考为何当初 sdk 开发需要编写繁重的 apt 生成代码,去生成这些固定的逻辑,可以发现 htrouterdispatch-process
工程是一个纯 java 工程,部分纯 java 类的提供在 htrouterdispatch
。由于无法引用 Android 类,同时期望业务层接口能完美隐藏内部实现,为此和 Android 相关的类,索性全部由 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'
}
为了解决这里的问题,我们可以稍微降低对实现封装的隐藏程度,修改初始化接口,需要业务层将 router 映射表显式的传入。修改后就能发现仅有 HTRouterTable
里面的映射表接口需要 apt 生成,而其余的代码均可通过直接编码。
HTRouterManager.init();
→
HTRouterManager.init(HTRouterTable.pageRouters(),
HTRouterTable.methodRouters(),
HTRouterTable.interceptors());
HTRouterTable.methodRouters() 和 HTRouterTable.interceptors() 先忽略,后续解释
新建了一个 Android Library
htrouter
,引用工程htrouterdispatch
,app 工程修改引用htrouter
经过优化,router 跳转的逻辑代码可通过直接编码方式实现,普通 Android 开发也能轻松修改其中的逻辑,同时 apt 生成的类从 6 个直接减少至 1 个 HTRouterTable
。若出现业务层代码编译错误导致 apt 生成失败,最终导致编译器提示 HTRouterTable not found
,可仅需注释掉初始化代码即可。
/**
* 用于用户启动Activity或者通过URL获得可以跳转的目标
*/
public final class HTRouterTable {
public static final String HT_URL_PARAMS_KEY = "ht_url_params_map";
private static final List<HTRouterEntry> PAGE_ROUTERS = new LinkedList<HTRouterEntry>();
private static final List<HTInterceptorEntry> INTERCEPTORS = new LinkedList<HTInterceptorEntry>();
private static final List<HTMethodRouterEntry> METHOD_ROUTERS = new LinkedList<HTMethodRouterEntry>();
public static List<HTRouterEntry> pageRouters() {
if (PAGE_ROUTERS.isEmpty()) {
PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.category.activity.CategoryPushActivity", "yanxuan://homepage_categoryl2", 0, 0, false));
...
}
return PAGE_ROUTERS;
}
public static List<HTInterceptorEntry> interceptors() {
if (INTERCEPTORS.isEmpty()) {
PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.recommend.activity.TagActivity", "yanxuan://tag", 0, 0, false));
...
}
return INTERCEPTORS;
}
public static List<HTMethodRouterEntry> methodRouters() {
if (METHOD_ROUTERS.isEmpty()) {
{
List<Class> paramTypes = new ArrayList<Class>();
paramTypes.add(Context.class);
paramTypes.add(String.class);
paramTypes.add(int.class);
METHOD_ROUTERS.add(new HTMethodRouterEntry("http://www.you.163.com/jumpA", "com.netease.hearttouch.example.JumpUtil", "jumpA", paramTypes));
}
...
}
return METHOD_ROUTERS;
}
}
针对登录拦截需求,当时的临时解决方案如下:
needLogin
字段HTRouterEntry
记录 needLogin
信息RouterUtil.startActivity
将目标页面的跳转构建成一个 runnable 传入,在登录成功回调中执行 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.setOnLoginResultListener(new OnLoginResultListener() {
@Override
public void onLoginSuccess() {
runnable.run();
}
@Override
public void onLoginFail() {
// do nothing
}
});
LoginActivity.start(context);
}
return true;
}
return false;
}
可以发现这种处理方式并不通用,同时需要业务层代码全部修改调用方式,未修改的接口还是可能出现以未登录态进入需要登录的页面(这种情况也确实在后面发生过,后来我们要求前端跳转之前,先通过 jsbridge 唤起登录页面(⊙﹏⊙)b)。我们需要一种通用规范的方式处理拦截逻辑,同时能适用各种场景,也能规避业务层的错误。
为避免业务层绕过拦截器直接调用到 HTRouterManager
,将 HTRouterManager.startActivity
等接口修改为 package
引用范围,此外新定义 HTRouterCall
作为对外接口类。
public class HTRouterCall implements IRouterCall {
...
}
public interface IRouterCall {
// 继续路由跳转
void proceed();
// 继续路由跳转
void cancel();
// 获取路由参数
HTRouterParams getParams();
}
定义拦截器 interface 如下:
public interface IRouterInterceptor {
void intercept(IRouterCall call);
}
总结拦截的需求场景,归纳拦截场景为 3 种:
全局拦截 → 全局拦截器
全局拦截器,通过静态接口设置添加
public static void addGlobalInterceptors(IRouterInterceptor... interceptors) {
Collections.addAll(sGlobalInterceptors, interceptors);
}
登录拦截需求可以理解是一个全局的需求,全部的 Activity 跳转都需要判断是否需要唤起登录页面。
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();
}
}
}
登录拦截效果
业务页面固定拦截 → 注解拦截器
上面剩余的 7 个 switch-case
拦截,可以理解为特定业务页面唤起都必须进入的一个拦截处理,分别定义 7 个拦截器类,同样通过注解的方式标记。
以 yanxuan://category 为例子
@HTRouter(url = {"yanxuan://category", "yanxuan://categoryl2"})
public class CategoryL2Activity extends Activity {
...
}
对应的注解拦截器
@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 添加额外参数
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
业务页面动态拦截
比如 onClick 方法内执行路由跳转时,需要弹窗提示用户是否继续跳转,其他场景跳转并不需要这个弹窗,这种场景的拦截器我们认为是动态拦截
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("是否继续")
.setPositiveButton("继续", 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();
优先级:动态拦截器 > 注解拦截器 > 全局拦截器
我们接入了七鱼、HTImagePick
等 sdk,这些 sdk 也有自己的页面,而这部分页面并不能通过前面的路由方式打开,其原因如下:
对应的页面唤起需要通过 sdk 提供的特殊接口唤起
public static void openYsf(Context context, String url, String title, String custom) {
ConsultSource source = new ConsultSource(url, title, custom);
Unicorn.openServiceActivity(context, // 上下文
title, // 聊天窗口的标题
source // 咨询的发起来源,包括发起咨询的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 调用,就能支持 sdk 中的页面路由跳转。具体用法示例如下
通过 HTMethodRouter
注解标记跳转方法(非静态方法需实现 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");
}
}
方法路由触发逻辑
除了设置动画、是否关闭当前页面等参数,这里方法路由的调用方式和页面路由完全一致,同样支持 needLogin 字段,同样支持全局拦截器、注解拦截器、动态拦截器
// 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");
}
结果示例
这里的处理逻辑较为简单,仅需修改类引用为类名字符串,后续跳转时通过反射获取类
public static List<HTRouterEntry> routers() {
if (ROUTERS.isEmpty()) {
...
ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.subject.SubjectActivity", "yanxuan://subject", 0, 0, false));
...
}
return ROUTERS;
}
通过优化拦截器,解决登录拦截问题,优化子模块和全局代码划分;通过提供方法路由,解决 sdk 页面的路由跳转问题;通过区分路由表生成代码和其他跳转逻辑,优化 apt 代码生成逻辑的复杂性和和维护性;通过修改路由表对类的直接引用,解决 main-dex
问题。
除此之外,路由框架并未对 module 子工程的 Activity 做路由集成,严选当前也没做更进一步的业务组件化。后续有需求进一步补充文章。