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 测试代码\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-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 路由框架优化(上篇)

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

严选 Android 路由框架优化(上篇)的更多相关文章

  1. 严选 Android 路由框架优化(下篇)

    3 router 框架优化 3.1 apt 生成代码量过大问题优化 思考框架本身,其实可以发现仅有 router 映射表是需要根据注解编译生成的,其他的全部代码都是固定代码,完全可以 sdk 中直接编 ...

  2. Android 路由框架ARouter最佳实践

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/76165252 本文出自[赵彦军的博客] 一:什么是路由? 说简单点就是映射页面跳转 ...

  3. Android路由框架-ARouter详解

    文章大纲 一.页面路由基本介绍1.什么是页面路由2.为什么要使用页面路由二.页面路由框架ARouter介绍1.常用功能介绍2.常见应用场景三.源码下载四.参考文章   一.页面路由基本介绍 1.什么是 ...

  4. WMRouter:美团外卖Android开源路由框架

    WMRouter是一款Android路由框架,基于组件化的设计思路,功能灵活,使用也比较简单. WMRouter最初用于解决美团外卖C端App在业务演进过程中的实际问题,之后逐步推广到了美团其他App ...

  5. Android Multimedia框架总结(十三)CodeC部分之OpenMAX框架初识及接口与适配层实现

    转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598 前言:上篇中介绍O ...

  6. 2017年Android百大框架排行榜

    框架:提供一定能力的小段程序 >随意转载,标注作者"金诚"即可 >本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. >本文已经开源到Gith ...

  7. android 优秀框架整理

    程序员界有个神奇的网站,那就是github,这个网站集合了一大批优秀的开源框架,极大地节省了开发者开发的时间,在这里我进行了一下整理,这样可以使我们在使用到时快速的查找到,希望对大家有所帮助! 1. ...

  8. 2017年Android百大框架排行榜(转)

    一.榜单介绍 排行榜包括四大类: 单一框架:仅提供路由.网络层.UI层.通信层或其他单一功能的框架 混合开发框架:提供开发hybrid app.h5与webview结合能力.web app能力的框架 ...

  9. Android学习之——优化篇(2)

    一.高级优化     上篇主要从0基础优化的方式,本篇主要将从程序执行性能的角度出发,分析各种经常使用方案的不足.并给出对象池技术.基础数据类型替换法.屏蔽函数计算三种能够节省资源开销和处理器时间的优 ...

随机推荐

  1. oracle里的统计信息

    1 oracle里的统计信息 Oracle的统计信息是这样的一组数据,存储在数据字典,从多个维度描述了oracle数据库对象的详细信息,有6种类型 表的统计信息:记录数.表块的数量.平均行长度等 索引 ...

  2. Java中静态变量、静态代码块、非静态代码块以及静态方法的加载顺序

    在研究单例设计模式的时候,用到了静态变量和静态方法的内容,出于兴趣,这里简单了解一下这四个模块在类初始化的时候的加载顺序. 经过研究发现,它们的加载顺序为: 1.非静态代码块 2.静态变量或者静态代码 ...

  3. 第一章 为什么使用NoSQL

    1.1 关系型数据库的价值 1.1.1 获取持久化数据 1.1.2 并发 通过”事务“ 来控制,出错有“回滚”机制. 1.1.3 集成                共享数据库集成,多个应用程序将数据 ...

  4. VMware虚拟机上网络连接模式bridged(桥接模式)

    VMware虚拟机上网络连接模式bridged(桥接模式)的实质就是虚拟机本身利用主机的网卡对外直接作为一个真实的物理主机存在. 也就是理论上此时的虚拟机和主机没什么关系,只是和主机公用了一块网卡,其 ...

  5. ceph 对接openstack liberty

    Ceph 准备工作 官方文档:http://docs.ceph.com/docs/master/rbd/rbd-openstack/ 官方中文文档:http://docs.ceph.org.cn/rb ...

  6. 第二章 Java内存区域与内存溢出异常(待续)

    ·········

  7. 关于XSS漏洞的简介以及分类

    不得不说注入的时代已经过去了,最近xss貌似比较热门.我就去恶补了一下,我表示我只是菜鸟,对xss不了解.所以从最基本的学起. 什么xss漏洞? 一.XSS攻击简介 作为一种HTML注入攻击,XSS攻 ...

  8. hadoop本地调试方法

    Mapreduce 是Hadoop上一个进行分布式数据运算和统计的框架,但是每次运行程序的时候都需要将程序打包并上传的集群环境中运行,这就会让程序的调试变得十分不方便.所以在这里写下这篇博客和大家交流 ...

  9. Easyui Datagrid 如何实现后台交互显示用户数据列表

    转自:https://blog.csdn.net/Tomsheng321/article/details/50722571?utm_source=blogxgwz9 新手初学的时候可能有个疑问:如何在 ...

  10. ubuntu12 安装redis和phpRedisAdmin详细流程

    一.Ubuntu安装redis(redis默认端口6379) 方式一.直接下载源码,编译(redis可以编译源码之后直接运行,不需要安装) 1.1执行命令,从官网下载源码编译: $ wget http ...