严选 Android 路由框架优化(下篇)
3 router 框架优化
3.1 apt 生成代码量过大问题优化
思考框架本身,其实可以发现仅有 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;
}
}
3.2 拦截器优化
3.2.1 优化前临时方案
针对登录拦截需求,当时的临时解决方案如下:
- 路由注解添加
needLogin
字段 - 并修改 apt 生成代码,使
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)。我们需要一种通用规范的方式处理拦截逻辑,同时能适用各种场景,也能规避业务层的错误。
3.2.2 拦截器优化和设计
为避免业务层绕过拦截器直接调用到 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();
优先级:动态拦截器 > 注解拦截器 > 全局拦截器
3.3 sdk 页面 router 支持
我们接入了七鱼、HTImagePick
等 sdk,这些 sdk 也有自己的页面,而这部分页面并不能通过前面的路由方式打开,其原因如下:
- 我们不能修改他们的代码
- apt 处理的注解仅能针对引入 apt 的 app 工程
对应的页面唤起需要通过 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");
}
结果示例
3.4 main dex 优化处理
这里的处理逻辑较为简单,仅需修改类引用为类名字符串,后续跳转时通过反射获取类
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;
}
4 总结
通过优化拦截器,解决登录拦截问题,优化子模块和全局代码划分;通过提供方法路由,解决 sdk 页面的路由跳转问题;通过区分路由表生成代码和其他跳转逻辑,优化 apt 代码生成逻辑的复杂性和和维护性;通过修改路由表对类的直接引用,解决 main-dex
问题。
除此之外,路由框架并未对 module 子工程的 Activity 做路由集成,严选当前也没做更进一步的业务组件化。后续有需求进一步补充文章。
本文来自网易云社区,经作者张云龙授权发布。
更多网易研发、产品、运营经验分享请访问网易云社区。
严选 Android 路由框架优化(下篇)的更多相关文章
- 严选 Android 路由框架优化(上篇)
0 背景 早前严选 Android 工程,使用原生 Intent 方式做页面跳转,为规范参数传递,做了编码规范,使用静态方法的方式唤起 Activity public static void star ...
- Android 路由框架ARouter最佳实践
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/76165252 本文出自[赵彦军的博客] 一:什么是路由? 说简单点就是映射页面跳转 ...
- Android路由框架-ARouter详解
文章大纲 一.页面路由基本介绍1.什么是页面路由2.为什么要使用页面路由二.页面路由框架ARouter介绍1.常用功能介绍2.常见应用场景三.源码下载四.参考文章 一.页面路由基本介绍 1.什么是 ...
- WMRouter:美团外卖Android开源路由框架
WMRouter是一款Android路由框架,基于组件化的设计思路,功能灵活,使用也比较简单. WMRouter最初用于解决美团外卖C端App在业务演进过程中的实际问题,之后逐步推广到了美团其他App ...
- Vue 2.x + Webpack 3.x + Nodejs 多页面项目框架(下篇——多页面VueSSR+热更新Server)
Vue 2.x + Webpack 3.x + Nodejs 多页面项目框架(下篇--多页面VueSSR+热更新Server) @(HTML/JS) 这是Vue多页面框架系列文章的第二篇,上一篇(纯前 ...
- 2017年Android百大框架排行榜
框架:提供一定能力的小段程序 >随意转载,标注作者"金诚"即可 >本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. >本文已经开源到Gith ...
- android 优秀框架整理
程序员界有个神奇的网站,那就是github,这个网站集合了一大批优秀的开源框架,极大地节省了开发者开发的时间,在这里我进行了一下整理,这样可以使我们在使用到时快速的查找到,希望对大家有所帮助! 1. ...
- 2017年Android百大框架排行榜(转)
一.榜单介绍 排行榜包括四大类: 单一框架:仅提供路由.网络层.UI层.通信层或其他单一功能的框架 混合开发框架:提供开发hybrid app.h5与webview结合能力.web app能力的框架 ...
- Android应用性能优化(转)
人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这 ...
随机推荐
- Maria数据库
项目上要进行数据库选型,业务上来讲,数据是非常结构化的数据,使用传统关系数据库更适合:另外项目采用微服务框架,每个服务的数据库应该尽可能轻量级, 最后考虑Maria数据库. MariaDB简介: Ma ...
- MFC学习(七) 单文档程序
1 MFC单文档程序的主要类 (1)文档类(Document) 即应用程序处理的数据对象,文档一般从 MFC 中 CDocument 中派生.CDocument 类用于相应数据文件的读取以及存储 Cv ...
- Angular2快速入门-3.多个组件(分离新闻列表页和详细页)
上篇(Angular2快速入门-2.创建一个新闻列表)已经完成新闻列表的展示,并且点击新闻列表的时候,下面可以展示出新闻的详细信息,这节我们把新闻详细和新闻列表页面分离出来 新闻详细单独一个compo ...
- python's twenty_fourth day for me 内置方法
str repr: 1,当需要使用__str__的场景时找不到__str__就找__repr__ 2,当需要使用__repr__的场景时找不到__repr__的时候就找父类的repr. 3,双下re ...
- js发送windows提示信息
js发送windows提示信息 效果图 代码 Notification.requestPermission(function() { if(Notification.permission === 'g ...
- Java面向对象-构造方法,this关键字
Java面向对象-构造方法,this关键字 构造方法 概念:构造方法是一个特殊的方法,这个特殊方法用于创建实例时执行初始化操作: 上代码: package com.java1234.chap03.se ...
- Halcon学习之一:查询图像参数
版权声明:本文为博主原创文章,未经博主允许不得转载. 1.get_grayval ( Image : : Row, Column : Grayval ) 计算Image图像中坐标为(Row,Colum ...
- EXT.net 窗体传值
ext.net 窗体传值 EXT.net 窗体传值 子窗体代码 protected void btnClose_Click(object sender,EventArges e) { PageCont ...
- 关于CountDownLatch控制线程的执行顺序
在上一篇文章中说过使用thread.join()方法.newSingleThreadExecutor单线程池来控制线程执行顺序.在文章的末尾我提出了一种构想,可否使用经典的生产者和消费者模型来控制执行 ...
- 使用Nuget发布自己的类库包
NuGet是一个为大家所熟知的Visual Studio扩展,通过这个扩展,开发人员可以非常方便地在Visual Studio中安装或更新项目中所需要的第三方组件,同时也可以通过NuGet来安装一些V ...