[关闭]
@zyl06 2018-09-21T09:45:08.000000Z 字数 16609 阅读 1219

严选 Android 路由框架优化

Android 路由


0 背景

早前严选 Android 工程,使用原生 Intent 方式做页面跳转,为规范参数传递,做了编码规范,使用静态方法的方式唤起 Activity

  1. public static void start(Context context, ComposedOrderModel model, String skuList) {
  2. Intent intent = new Intent(context, OrderCommoditiesActivity.class);
  3. ...
  4. context.startActivity(intent);
  5. }
  6. public static void start(Context context, ComposedOrderModel model, int skuId, int count) {
  7. Intent intent = new Intent(context, OrderCommoditiesActivity.class);
  8. ...
  9. context.startActivity(intent);
  10. }

OrderCommoditiesActivity

  1. public static void startForResult(Activity context, int requestCode, int selectedCouponId, int skuId, int count, String skuListStr) {
  2. Intent intent = new Intent(context, CouponListActivity.class);
  3. ...
  4. context.startActivityForResult(intent, requestCode);
  5. }

CouponListActivity

不过采用原生的方式,在应用 H5 唤起 APP 和 推送唤起 APP 的场景下会显得力不从心,随着公开的跳转协议越来越多,代码中 switch-case 也会越来越多,最后难以维护。

  1. public class RouterUtil {
  2. public static Intent getRouteIntent(Context context, Uri uri) {
  3. if (uri == null || !TextUtils.equals(uri.getScheme(), "yanxuan")) {
  4. return null;
  5. }
  6. String host = uri.getHost();
  7. if (host == null) {
  8. return null;
  9. }
  10. Class<?> clazz = null;
  11. String param = null;
  12. switch (host) {
  13. case ConstantsRT.GOOD_DETAIL_ROUTER_PATH:
  14. clazz = GoodsDetailActivity.class;
  15. ...
  16. break;
  17. case ConstantsRT.ORDER_DETAIL_ROUTER_PATH:
  18. clazz = OrderDetailActivity.class;
  19. ...
  20. break;
  21. ...
  22. ... 省略 28 case! ☹️
  23. ...
  24. default:
  25. break;
  26. }
  27. Intent intent = null;
  28. if (clazz != null) {
  29. intent = new Intent();
  30. intent.setClass(context, clazz);
  31. }
  32. return intent;
  33. }
  34. }

根据输入 scheme,返回跳转 Activity 的 intent

  1. view.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. if (!TextUtils.isEmpty(schemeUrl)) {
  5. Intent intent = RouterUtil.getRouteIntent(Uri.parse(schemeUrl));
  6. if (intent != null) {
  7. view.getContext().startActivity(intent);
  8. }
  9. }
  10. }
  11. });

RouterUtil.getRouteIntent 使用样例

1 ht-router 接入

参考 DeepLink从认识到实践,接入杭研 ht-router,由此通过注解的方式统一了 H5 唤醒、推送唤醒、正常启动 APP 的逻辑,上面点击跳转的逻辑得到了简化:

  1. view.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. HTRouterManager.startActivity(view.getContext(), schemeUrl, null, false);
  5. }
  6. });

RouterUtil 中冗长的 switch-case 代码也得到得到了极大的改善,统一跳转可通过 scheme 参数直接触发跳转,近 30switch-case 减少至 7

  1. HTRouterManager.init();
  2. ...
  3. // 设置跳转前的拦截,返回 true 拦截不再跳转,返回 false 继续跳转
  4. HTRouterManager.setHtRouterHandler(new HTRouterHandler() {
  5. @Override
  6. public boolean handleRoute(Context context, HTRouterHandlerParams routerParams) {
  7. final Uri uri = !TextUtils.isEmpty(routerParams.url) ? Uri.parse(routerParams.url) : null;
  8. if (uri == null) {
  9. return true;
  10. }
  11. String host = uri.getHost();
  12. if (TextUtils.isEmpty(host)) {
  13. return true;
  14. }
  15. switch (host) {
  16. case ConstantsRT.CATEGORY_ROUTER_PATH: //"category"
  17. ...
  18. break;
  19. ...
  20. ...省略 5
  21. ...
  22. case ConstantsRT.MINE_ROUTER_PATH:
  23. ...
  24. break;
  25. default:
  26. break;
  27. }
  28. return false;
  29. }
  30. });

至于为什么还有 7 个,大体分 2 类

  1. 历史原因

    严选工程中 CategoryL2Activityyanxuan://categoryyanxuan://categoryl2 2 个 scheme,而同一个参数 categoryid 在不同的 scheme 下有不同的含义,为此在拦截器中添加新的字段,CategoryL2Activity 中仅需处理 2 个新加的字段,不必知道自身的 scheme

  2. 跳转 Activity 的不同 fragment

    严选首页 MainPageActivity 拥有 5 个 tab fragment,不同的 tab 会有不同的 scheme,拦截器中直接根据不同的 scheme,添加参数来指定不同的 tab,首页仅需处理 tab 参数显示不同的 fragment

    Alt pic

ht-router 的其他优点、用法、api 见文章 DeepLink从认识到实践,这里不再叙述

2 ht-router 的痛点

ht-router 对工程框架的作用是巨大的,然而随着多期业务迭代和工程复杂度的提升,发现的几个痛点如下:

2.1 apt 生成代码量过大,业务开发较难维护

ht-router 通过 apt 生成的类有 6 个,其中 HTRouterManager 有 600 行代码,去除 init 方法中初始化 router 信息的 100 行左右代码,剩余还有 500 行左右

Alt pic

apt 生成的类目录

Alt pic

HTRouterManager.java

参考 apt 的用法,若要生成一个简单的类,对应的 apt 代码会复杂的多。当目标代码量比较多的情况下,apt 的生成代码就会比较难以维护,根据业务场景添加接口,或者修改字段都会相比更加困难。另外 apt 的调试也比较辛苦,需要编译后再查看目标代码是否是有错误。

这里给 ht-router 的开发同学献上膝盖,为业务团队贡献了很多!

  1. /**
  2. * apt 测试代码
  3. */
  4. public class TestClass {
  5. public static final String STATIC_FIELD = "ht_url_params_map";
  6. public void foo() {
  7. System.out.println("hello world");
  8. }
  9. }

目标代码

  1. TypeSpec.Builder testbuilder = classBuilder("TestClass")
  2. .addModifiers(PUBLIC);
  3. testbuilder.addJavadoc("apt 测试代码\n");
  4. FieldSpec testFieldSpec = FieldSpec
  5. .builder(String.class, "STATIC_FIELD",
  6. PUBLIC, STATIC, FINAL)
  7. .initializer("\"ht_url_params_map\"").build();
  8. testbuilder.addField(testFieldSpec);
  9. MethodSpec.Builder testMethod = MethodSpec.methodBuilder("foo")
  10. .addModifiers(Modifier.PUBLIC)
  11. .returns(void.class);
  12. testMethod.addStatement("System.out.println(\"hello world\")");
  13. testbuilder.addMethod(testMethod.build());
  14. TypeSpec generatedClass = testbuilder.build();
  15. JavaFile javaFile = builder(packageName, generatedClass).build();
  16. try {
  17. javaFile.writeTo(filer);
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }

生成目标代码的 apt 代码

2.2 apt 生成代码量过大,可能出现业务等代码编译错误被掩盖

合并分支后偶现,由于业务代码其他的编译不通过,导致 apt 代码未生成,大量提示报错 HTRouterManager 找不到,但无法定位到真正的业务代码错误逻辑。

Alt pic

由于 HTRouterManager 在业务代码中广泛被使用,暂未有很好的办法解决这个报错,临时的处理办法是从同事处拷贝 apt 文件夹,临时绕过错误报错,修改业务层代码错误后 rebuild

第一次碰到比较懵逼,花了不少时间处理定位和解决问题,(⊙﹏⊙)b

2.3 拦截功能不满足登录需求

针对未登录状态,跳转需要登录状态的 Activity 的场景,我们期望是先唤起登录页,登录成功后,关闭登录页重定向至目标 Activity;若用户退出登录页,则回到上一个页面。针对已登录状态,则直接唤起目标页面。对于这个需求,ht-router 并不满足,虽然提供了 HTRouterHandler,但仅能判断根据返回值判断是否继续跳转,无法在登录回调中决定是否继续跳转。

  1. public static void startActivity(Activity activity, String url, Intent sourceIntent, boolean isFinish, int entryAnim, int exitAnim) {
  2. Intent intent = null;
  3. HTRouterHandlerParams routerParams = new HTRouterHandlerParams(url, sourceIntent);
  4. if (sHtRouterHandler != null && sHtRouterHandler.handleRoute(activity, routerParams)) {
  5. return;
  6. }
  7. ...
  8. }

2.4 需要拦截处理特殊 scheme 的逻辑还在全局

前面 RouterUtil 中的 switch-case30 个大幅降至 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 还是遵循 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

  1. -keep public class * extends android.app.Instrumentation {
  2. <init>();
  3. }
  4. -keep public class * extends android.app.Application {
  5. <init>();
  6. void attachBaseContext(android.content.Context);
  7. }
  8. -keep public class * extends android.app.Activity {
  9. <init>();
  10. }
  11. -keep public class * extends android.app.Service {
  12. <init>();
  13. }
  14. -keep public class * extends android.content.ContentProvider {
  15. <init>();
  16. }
  17. -keep public class * extends android.content.BroadcastReceiver {
  18. <init>();
  19. }
  20. -keep public class * extends android.app.backup.BackupAgent {
  21. <init>();
  22. }
  23. # We need to keep all annotation classes because proguard does not trace annotation attribute
  24. # it just filter the annotation attributes according to annotation classes it already kept.
  25. -keep public class * extends java.lang.annotation.Annotation {
  26. *;
  27. }

解决方法

  1. gradle 1.5.0 之前

    执行 dex 命令时添加 --main-dex-list--minimal-main-dex 参数。而这里 maindexlist.txt 中的内容需要开发生成,参考 main-dex 分析工具

    1. afterEvaluate {
    2. tasks.matching {
    3. it.name.startsWith("dex")
    4. }.each { dx ->
    5. if (dx.additionalParameters == null) {
    6. dx.additionalParameters = []
    7. }
    8. // optional
    9. dx.additionalParameters += "--main-dex-list=$projectDir/maindexlist.txt".toString()
    10. dx.additionalParameters += "--minimal-main-dex"
    11. }
    12. }

    参考文章 MultiDex中出现的main dex capacity exceeded解决之道

  2. gradle 1.5.0 ~ 2.2.0

    现严选使用 gradle plugin 2.1.2,并不支持上面的方法,可使用如下方法。

    1. //处理main dex 的方法测试
    2. afterEvaluate {
    3. def mainDexListActivity = ['SplashActivity', 'MainPageActivity']
    4. project.tasks.each { task ->
    5. if (task.name.startsWith('collect')
    6. && task.name.endsWith('MultiDexComponents')
    7. && task.name.contains("Debug")) {
    8. println "main-dex-filter: found task $task.name"
    9. task.filter { name, attrs ->
    10. String componentName = attrs.get('android:name')
    11. if ('activity'.equals(name)) {
    12. def result = mainDexListActivity.find {
    13. componentName.endsWith("${it}")
    14. }
    15. return result != null
    16. } else {
    17. return true
    18. }
    19. }
    20. }
    21. }
    22. }

    这里过滤掉除 SplashActivity,MainPageActivity 之外的其他 activity,但 main-dex 中未满 65535 之前,其他 activity 或类也可能在 main-dex 中,并不能将 main-dex 优化为最小。

    可参考 DexKnifePlugin 优化 main-dex 为最小。(自己并未实际用过)
    参考文章 Android-Easy-MultiDex

  3. gradle 2.3.0

    gradle 中通过 multiDexKeepProguardmultiDexKeepFile 设置必须放置 main-dex 的类。

    其次设置 additionalParameters 优化 main-dex 为最小

    1. dexOptions {
    2. additionalParameters '--multi-dex', '--minimal-main-dex', '--main-dex-list=' + file('multidex-config.txt').absolutePath'
    3. }

严选 gradle 版本为 2.1.2,然而按照上述的解决方法发现并没有效果,查看 Application 初始化代码,可以发现 HTRouterManager.init 中引用了全部的 Activity

  1. public static void init() {
  2. ...
  3. entries.put("yanxuan://newgoods", new HTRouterEntry(NewGoodsListActivity.class, "yanxuan://newgoods", 0, 0, false));
  4. entries.put("yanxuan://popular", new HTRouterEntry(TopGoodsRcmdActivity.class, "yanxuan://popular", 0, 0, false));
  5. ...
  6. }

3 router 框架优化

3.1 apt 生成代码量过大问题优化

思考框架本身,其实可以发现仅有 router 映射表是需要根据注解编译生成的,其他的全部代码都是固定代码,完全可以 sdk 中直接编码提供。反过来思考为何当初 sdk 开发需要编写繁重的 apt 生成代码,去生成这些固定的逻辑,可以发现 htrouterdispatch-process 工程是一个纯 java 工程,部分纯 java 类的提供在 htrouterdispatch。由于无法引用 Android 类,同时期望业务层接口能完美隐藏内部实现,为此和 Android 相关的类,索性全部由 apt 生成。

  1. apply plugin: 'java' // 使用 apply plugin: 'com.android.library' 编译报错
  2. sourceCompatibility = JavaVersion.VERSION_1_7
  3. targetCompatibility = JavaVersion.VERSION_1_7
  4. dependencies {
  5. compile project (':htrouterdispatch')
  6. compile 'com.google.auto.service:auto-service:1.0-rc2'
  7. compile 'com.squareup:javapoet:1.0.0'
  8. }

为了解决这里的问题,我们可以稍微降低对实现封装的隐藏程度,修改初始化接口,需要业务层将 router 映射表显式的传入。修改后就能发现仅有 HTRouterTable 里面的映射表接口需要 apt 生成,而其余的代码均可通过直接编码。

  1. HTRouterManager.init();
  2. HTRouterManager.init(HTRouterTable.pageRouters(),
  3. HTRouterTable.methodRouters(),
  4. HTRouterTable.interceptors());

HTRouterTable.methodRouters() 和 HTRouterTable.interceptors() 先忽略,后续解释

Alt pic

新建了一个 Android Library htrouter,引用工程 htrouterdispatch,app 工程修改引用 htrouter

经过优化,router 跳转的逻辑代码可通过直接编码方式实现,普通 Android 开发也能轻松修改其中的逻辑,同时 apt 生成的类从 6 个直接减少至 1 个 HTRouterTable。若出现业务层代码编译错误导致 apt 生成失败,最终导致编译器提示 HTRouterTable not found,可仅需注释掉初始化代码即可。

  1. /**
  2. * 用于用户启动Activity或者通过URL获得可以跳转的目标
  3. */
  4. public final class HTRouterTable {
  5. public static final String HT_URL_PARAMS_KEY = "ht_url_params_map";
  6. private static final List<HTRouterEntry> PAGE_ROUTERS = new LinkedList<HTRouterEntry>();
  7. private static final List<HTInterceptorEntry> INTERCEPTORS = new LinkedList<HTInterceptorEntry>();
  8. private static final List<HTMethodRouterEntry> METHOD_ROUTERS = new LinkedList<HTMethodRouterEntry>();
  9. public static List<HTRouterEntry> pageRouters() {
  10. if (PAGE_ROUTERS.isEmpty()) {
  11. PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.category.activity.CategoryPushActivity", "yanxuan://homepage_categoryl2", 0, 0, false));
  12. ...
  13. }
  14. return PAGE_ROUTERS;
  15. }
  16. public static List<HTInterceptorEntry> interceptors() {
  17. if (INTERCEPTORS.isEmpty()) {
  18. PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.recommend.activity.TagActivity", "yanxuan://tag", 0, 0, false));
  19. ...
  20. }
  21. return INTERCEPTORS;
  22. }
  23. public static List<HTMethodRouterEntry> methodRouters() {
  24. if (METHOD_ROUTERS.isEmpty()) {
  25. {
  26. List<Class> paramTypes = new ArrayList<Class>();
  27. paramTypes.add(Context.class);
  28. paramTypes.add(String.class);
  29. paramTypes.add(int.class);
  30. METHOD_ROUTERS.add(new HTMethodRouterEntry("http://www.you.163.com/jumpA", "com.netease.hearttouch.example.JumpUtil", "jumpA", paramTypes));
  31. }
  32. ...
  33. }
  34. return METHOD_ROUTERS;
  35. }
  36. }

3.2 拦截器优化

3.2.1 优化前临时方案

针对登录拦截需求,当时的临时解决方案如下:

  1. 路由注解添加 needLogin 字段
  2. 并修改 apt 生成代码,使 HTRouterEntry 记录 needLogin 信息
  3. 提供 RouterUtil.startActivity 将目标页面的跳转构建成一个 runnable 传入,在登录成功回调中执行 runnable
  1. @HTRouter(url = {PreemptionActivateActivity.ROUTER_URL}, needLogin = true)
  2. public class PreemptionActivateActivity extends Activity {
  3. ...
  4. }
  1. public static boolean startActivity(final Context context, final String schemeUrl,
  2. final Intent sourceIntent, final boolean isFinish) {
  3. return doStartActivity(context, schemeUrl, new Runnable() {
  4. @Override
  5. public void run() {
  6. HTRouterManager.startActivity(context, schemeUrl, sourceIntent, isFinish);
  7. }
  8. });
  9. }
  10. private static boolean doStartActivity(final Context context, final String schemeUrl,
  11. final Runnable runnable) {
  12. if (HTRouterManager.isUrlRegistered(schemeUrl)) {
  13. HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(schemeUrl);
  14. if (entry == null) {
  15. return false;
  16. }
  17. if (entry.isNeedLogin() && !UserInfo.isLogin()) {
  18. LoginActivity.setOnLoginResultListener(new OnLoginResultListener() {
  19. @Override
  20. public void onLoginSuccess() {
  21. runnable.run();
  22. }
  23. @Override
  24. public void onLoginFail() {
  25. // do nothing
  26. }
  27. });
  28. LoginActivity.start(context);
  29. }
  30. return true;
  31. }
  32. return false;
  33. }

可以发现这种处理方式并不通用,同时需要业务层代码全部修改调用方式,未修改的接口还是可能出现以未登录态进入需要登录的页面(这种情况也确实在后面发生过,后来我们要求前端跳转之前,先通过 jsbridge 唤起登录页面(⊙﹏⊙)b)。我们需要一种通用规范的方式处理拦截逻辑,同时能适用各种场景,也能规避业务层的错误。

3.2.2 拦截器优化和设计

为避免业务层绕过拦截器直接调用到 HTRouterManager,将 HTRouterManager.startActivity 等接口修改为 package 引用范围,此外新定义 HTRouterCall 作为对外接口类。

  1. public class HTRouterCall implements IRouterCall {
  2. ...
  3. }
  1. public interface IRouterCall {
  2. // 继续路由跳转
  3. void proceed();
  4. // 继续路由跳转
  5. void cancel();
  6. // 获取路由参数
  7. HTRouterParams getParams();
  8. }

定义拦截器 interface 如下:

  1. public interface IRouterInterceptor {
  2. void intercept(IRouterCall call);
  3. }

总结拦截的需求场景,归纳拦截场景为 3 种:

  1. 全局拦截 → 全局拦截器

    全局拦截器,通过静态接口设置添加

    1. public static void addGlobalInterceptors(IRouterInterceptor... interceptors) {
    2. Collections.addAll(sGlobalInterceptors, interceptors);
    3. }

    登录拦截需求可以理解是一个全局的需求,全部的 Activity 跳转都需要判断是否需要唤起登录页面。

    1. public class LoginRouterInterceptor implements IRouterInterceptor {
    2. @Override
    3. public void intercept(final IRouterCall call) {
    4. HTDroidRouterParams params = (HTDroidRouterParams) call.getParams();
    5. HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(params.url);
    6. if (entry == null) {
    7. call.cancel();
    8. return;
    9. }
    10. if (entry.isNeedLogin() && !UserInfo.isLogin()) {
    11. LoginActivity.setOnLoginResultListener(new OnLoginResultListener() {
    12. @Override
    13. public void onLoginSuccess() {
    14. call.proceed();
    15. }
    16. @Override
    17. public void onLoginFail() {
    18. call.cancel();
    19. }
    20. });
    21. LoginActivity.start(params.getContext());
    22. } else {
    23. call.proceed();
    24. }
    25. }
    26. }

    Alt pic

    登录拦截效果

  2. 业务页面固定拦截 → 注解拦截器

    上面剩余的 7 个 switch-case 拦截,可以理解为特定业务页面唤起都必须进入的一个拦截处理,分别定义 7 个拦截器类,同样通过注解的方式标记。

    以 yanxuan://category 为例子

    1. @HTRouter(url = {"yanxuan://category", "yanxuan://categoryl2"})
    2. public class CategoryL2Activity extends Activity {
    3. ...
    4. }

    对应的注解拦截器

    1. @HTRouter(url = {"yanxuan://category"})
    2. public class CategoryL2Intercept implements IRouterInterceptor {
    3. @Override
    4. public void intercept(IRouterCall call) {
    5. HTRouterParams routerParams = call.getParams();
    6. Uri uri = Uri.parse(routerParams.url);
    7. // routerParams.url 添加额外参数
    8. Uri.Builder builder = uri.buildUpon();
    9. ...
    10. routerParams.url = builder.build().toString();
    11. call.proceed();
    12. }
    13. }

    apt 生成拦截器初始化代码

    1. public static List<HTInterceptorEntry> interceptors() {
    2. if (INTERCEPTORS.isEmpty()) {
    3. ...
    4. INTERCEPTORS.add(new HTInterceptorEntry("yanxuan://category", new CategoryL2Intercept()));
    5. ...
    6. }
    7. return INTERCEPTORS;
    8. }

    HTRouterTable

  3. 业务页面动态拦截

    比如 onClick 方法内执行路由跳转时,需要弹窗提示用户是否继续跳转,其他场景跳转并不需要这个弹窗,这种场景的拦截器我们认为是动态拦截

    1. HTRouterCall.newBuilder(data.schemeUrl)
    2. .context(mContext)
    3. .interceptors(new IRouterInterceptor() {
    4. @Override
    5. public void intercept(final IRouterCall call) {
    6. Log.i("TEST", call.toString());
    7. AlertDialog dialog = new AlertDialog.Builder(mContext)
    8. .setTitle("alert")
    9. .setMessage("是否继续")
    10. .setPositiveButton("继续", new DialogInterface.OnClickListener() {
    11. @Override
    12. public void onClick(DialogInterface dialog, int which) {
    13. call.proceed();
    14. }
    15. })
    16. .setNegativeButton("取消", new DialogInterface.OnClickListener() {
    17. @Override
    18. public void onClick(DialogInterface dialog, int which) {
    19. call.cancel();
    20. }
    21. }).create();
    22. dialog.show();
    23. }
    24. })
    25. .build()
    26. .start();

    Alt pic

优先级:动态拦截器 > 注解拦截器 > 全局拦截器

3.3 sdk 页面 router 支持

我们接入了七鱼、HTImagePick 等 sdk,这些 sdk 也有自己的页面,而这部分页面并不能通过前面的路由方式打开,其原因如下:

  1. 我们不能修改他们的代码
  2. apt 处理的注解仅能针对引入 apt 的 app 工程
  3. 对应的页面唤起需要通过 sdk 提供的特殊接口唤起

    1. public static void openYsf(Context context, String url, String title, String custom) {
    2. ConsultSource source = new ConsultSource(url, title, custom);
    3. Unicorn.openServiceActivity(context, // 上下文
    4. title, // 聊天窗口的标题
    5. source // 咨询的发起来源,包括发起咨询的url,title,描述信息等
    6. );
    7. }

    七鱼客服页面唤起

    1. public void openImagePick(Context context, ArrayList<PhotoInfo> photoInfos, boolean multiSelectMode, int maxPhotoNum, String title) {
    2. HTPickParamConfig paramConfig = new HTPickParamConfig(HTImageFrom.FROM_LOCAL, null,
    3. photoInfos, multiSelectMode, maxPhotoNum, title);
    4. HTImagePicker.INSTANCE.start(context, paramConfig, this);
    5. }

基于此,只需要提供对方法的 router 调用,就能支持 sdk 中的页面路由跳转。具体用法示例如下

  1. 通过 HTMethodRouter 注解标记跳转方法(非静态方法需实现 getInstance 单例)

    1. public class JumpUtil {
    2. private static final String TAG = "JumpUtil";
    3. private static JumpUtil sInstance = null;
    4. public static JumpUtil getInstance() {
    5. if (sInstance == null) {
    6. synchronized (JumpUtil.class) {
    7. if (sInstance == null) {
    8. sInstance = new JumpUtil();
    9. }
    10. }
    11. }
    12. return sInstance;
    13. }
    14. private JumpUtil() {
    15. }
    16. @HTMethodRouter(url = {"http://www.you.163.com/jumpA"}, needLogin = true)
    17. public void jumpA(Context context, String str, int i) {
    18. String msg = "jumpA called: str=" + str + "; i=" + i;
    19. Log.i(TAG, msg);
    20. if (context != null) {
    21. Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
    22. }
    23. }
    24. @HTMethodRouter(url = {"http://www.you.163.com/jumpB"})
    25. public static void jumpB(Context context, String str, int i) {
    26. String msg = "jumpB called: str=" + str + "; i=" + i;
    27. Log.i(TAG, msg);
    28. if (context != null) {
    29. Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
    30. }
    31. }
    32. @HTMethodRouter(url = {"http://www.you.163.com/jumpC"})
    33. public void jumpC() {
    34. Log.i(TAG, "jumpC called");
    35. }
    36. }
  2. 方法路由触发逻辑

    除了设置动画、是否关闭当前页面等参数,这里方法路由的调用方式和页面路由完全一致,同样支持 needLogin 字段,同样支持全局拦截器、注解拦截器、动态拦截器

    1. // JUMPA 按钮点击
    2. public void onMethodRouter0(View v) {
    3. HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpA?a=lilei&b=10");
    4. }
    5. // JUMPB 按钮点击
    6. public void onMethodRouter1(View v) {
    7. HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpB?a=hanmeimei&b=10");
    8. }
    9. // JUMPC 按钮点击
    10. public void onMethodRouter2(View v) {
    11. HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpC");
    12. }
  3. 结果示例

    Alt pic

3.4 main dex 优化处理

这里的处理逻辑较为简单,仅需修改类引用为类名字符串,后续跳转时通过反射获取类

  1. public static List<HTRouterEntry> routers() {
  2. if (ROUTERS.isEmpty()) {
  3. ...
  4. ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.subject.SubjectActivity", "yanxuan://subject", 0, 0, false));
  5. ...
  6. }
  7. return ROUTERS;
  8. }

4 总结

通过优化拦截器,解决登录拦截问题,优化子模块和全局代码划分;通过提供方法路由,解决 sdk 页面的路由跳转问题;通过区分路由表生成代码和其他跳转逻辑,优化 apt 代码生成逻辑的复杂性和和维护性;通过修改路由表对类的直接引用,解决 main-dex 问题。

除此之外,路由框架并未对 module 子工程的 Activity 做路由集成,严选当前也没做更进一步的业务组件化。后续有需求进一步补充文章。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注