• 严选 Android 路由框架优化(上篇)


    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 参数直接触发跳转,近 30switch-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 类

    1. 历史原因

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

    2. 跳转 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 测试代码
    ");
    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-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 还是遵循 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 {
        *;
    }
    

    解决方法

    1. 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"
           }
       }
      

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

    2. 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

    3. gradle 2.3.0

      gradle 中通过 multiDexKeepProguardmultiDexKeepFile 设置必须放置 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));
        ...
    }
    

    相关阅读:严选 Android 路由框架优化(下篇)

    本文来自网易云社区,经作者张云龙授权发布。

    原文地址:严选 Android 路由框架优化(上篇)

    更多网易研发、产品、运营经验分享请访问网易云社区

  • 相关阅读:
    动态规划 01背包问题
    日常水题 蓝桥杯基础练习VIP-字符串对比
    本博客导航
    2019 ICPC 南昌 (C E G L)
    [模板]线段树
    [模板]手写双端队列(或普通队列)
    2019 ICPC Asia Yinchuan Regional (G, H)
    与超级源点与超级汇点相关的两题POJ 1062, HDU 4725
    [模板]链式向前星
    [总结]关于反向建图
  • 原文地址:https://www.cnblogs.com/163yun/p/9284059.html
Copyright © 2020-2023  润新知