WMRouter:美团外卖 Android 开源路由框架,2021 年 Android 高级面试题
基本概念解释
下面开始介绍 WMRouter 的发展背景和过程。为了方便后文的理解,我们先简单了解和
回顾几个基本概念。
路由
根据维基百科的解释,路由(routing)可以理解成在互联的网络通过特定的协议把信息从源地址传输到目的地址的过程。一个典型的例子就是在互联网中,路由器可以根据 IP 协议将数据发送到特定的计算机。
URI
URI(Uniform Resource Identifier,统一资源标识符)是一个用于标识某一互联网资源名称的字符串。URI 的组成如下图所示。
一些常见的 URI 举例如下,包括平时经常用到的网址、IP 地址、FTP 地址、文件、打电话、发邮件的协议等。
[www.meituan.com](
)
[http://127.0.0.1:8080](
)
[ftp://example.org/resource.txt](
)
file:///Users/
tel:863-1234
在 Android 中也提供了android.net.Uri
工具类用于处理 URI,Android 中 URI 常用的几个部分主要是 scheme、host、path 和 query。
Android 中的 Intent 跳转
在 Android 中的 Intent 跳转,分为显式跳转和隐式跳转两种。
显式跳转即指定 ComponentName(类名)的 Intent 跳转,一般通过 Bundle 传参,示例代码如下:
Intent intent = new Intent(context, TestActivity.class);intent.putExtra("param", "value")startActivity(intent);
隐式跳转即不指定 ComponentName 的 Intent 跳转,通过 IntentFilter 找到匹配的组件,IntentFilter 支持 action、category 和 data 的匹配,其中 data 就是 URI。例如下面的代码,会启动系统默认的浏览器打开网页:
Intent intent = new Intent(Intent.ACTION_VIEW);intent.setData(Uri.parse("http://www.meituan.com"))startActivity(intent);
Activity 通过 Manifest 配置 IntentFilter,例如下面的配置可以匹配所有形如demo_scheme://demo_host/***
的 URI。
<activity android:name=".app.UriProxyActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/><category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="demo_scheme" android:host="demo_host"/></intent-filter></activity>
URI 跳转
在美团外卖 C 端早期开发过程中,产品希望通过后台下发 URI 控制客户端跳转指定页面,从而实现灵活的运营配置。外卖 App 采用了 Native+H5 的混合开发模式,Native 页面定义了专用的 URI,而 H5 页面则使用 HTTP/HTTPS 链接在专门的 WebView 容器中加载,两种链接的跳转逻辑不同,实现起来比较繁琐。
Native 页面的 URI 跳转最开始使用的是 Android 原生的 IntentFilter,通过隐式跳转启动,但是这种方式存在灵活性差、功能缺失、Bug 多等问题。例如:
从外部(浏览器、微信等)跳转外卖的 URI 时,系统会直接打开相应的 Activity,而没有经过欢迎页的正常启动流程,一些代码逻辑可能没有执行,例如定位逻辑。
有很多页面在打开前需要确保用户先登录或先定位,每个页面都写一遍判断登录、定位的逻辑非常麻烦,提高了开发维护成本。
运营人员可能会配错 URI,页面跳转失败,有些跳转的地方没有做 try-catch 处理,会产生 Crash;有些地方虽然加了 try-catch,但跳转失败后没有任何响应,用户体验差;跳转失败没有监控,不能及时发现和解决线上业务异常。
为了解决上述问题,我们希望有一个 Android 的 URI 分发组件,可以根据 URI 中不同的 scheme、host、path,进行不同的处理,同时能够在页面跳转过程中进行更灵活的干预。调研发现,现有的一些 Android 路由组件主要都是在解决多工程之间解耦的问题,而 URI 往往只支持通过 path 分发,页面跳转的配置也不够灵活,难以满足实际需要。于是我们决定自行设计实现。
核心设计思路
下图展示了 WMRouter 中 URI 分发机制的核心设计思路。借鉴网络请求的机制,WMRouter 中的每次 URI 跳转视为发起一个 UriRequest;URI 跳转请求被 WMRouter 逐层分发给一系列的 UriHandler 进行处理;每个 UriHandler 处理之前可以被 UriInterceptor 拦截,并插入一些特殊操作。
页面跳转来源
常见的页面跳转来源如下:
来自 App 内部 Native 页面的跳转
来自 App 内 Web 容器的跳转,即 H5 页面发起的跳转
从 App 外通过 URI 唤起 App 的跳转,例如来自浏览器、微信等
从通知中心 Push 唤起 App 的跳转
对于来自 App 内部和 Web 容器的跳转,我们把所有跳转代码统一改成调用 WMRouter 处理,而来自外部和 Push 通知的跳转则全部使用一个独立的中转 Activity 接收,再调用 WMRouter 处理。
UriRequest
UriRequest 中包含 Context、URI 和 Fields,其中 Fields 为 HashMap<String, Object>,可以通过 Key 存放任意数据。简单起见,UriRequest 类同时承担了 Response 的功能,跳转请求的结果,也会被保存到 Fields 中。Fields 可以根据需要自定义,其中一些常见字段举例如下:
Intent 的 Extra 参数,Bundle 类型
用于 startActivityForResult 的 RequestCode,int 类型
用于 overridePendingTransition 方法的页面切换动画资源,int[]类型
本次跳转结果的监听器,OnCompleteListener 类型
每次 URI 跳转请求会有一个 ResultCode(类似 HTTP 请求的 ResponseCode),表示跳转结果,也存放在 Fields 中。常见 Code 如下,用户也可以自定义 Code:
200:跳转成功
301:重定向到其他 URI,会再次跳转
400:请求错误,通常是 Context 或 URI 为空
403:禁止跳转,例如跳转白名单以外的 HTTP 链接、Activity 的 exported 为 false 等
404:找不到目标(Activity 或 UriHandler)
500:发生错误
总结来说,UriRequest 用于实现一次 URI 跳转中所有组件之间的通信功能。
UriHandler
UriHandler 用于处理 URI 跳转请求,可以嵌套从而逐层分发和处理请求。UriHandler 是异步结构,接收到 UriRequest 后处理(例如跳转 Activity 等),如果处理完成,则调用callback.onComplete()
并传入 ResultCode;如果没有处理,则调用callback.onNext()
继续分发。下面的示例代码展示了一个只处理 HTTP 链接的 UriHandler 的实现:
public interface UriCallback {
/**
处理完成,继续后续流程。*/void onNext();
/**
处理完成,终止分发流程。
@param resultCode 结果*/void onComplete(int resultCode);}
public class DemoUriHandler extends UriHandler {public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {Uri uri = request.getUri();// 处理 HTTP 链接 if ("http".equalsIgnoreCase(uri.getScheme())) {try {// 调用系统浏览器 Intent intent = new Intent();intent.setAction(Intent.ACTION_VIEW);intent.setData(uri);request.getContext().startActivity(intent);// 跳转成功 callback.onComplete(UriResult.CODE_SUCCESS);} catch (Exception e) {// 跳转失败 callback.onComplete(UriResult.CODE_ERROR);}} else {// 非 HTTP 链接不处理,继续分发 callback.onNext();}}// ...}
UriInterceptor
UriInterceptor 为拦截器,不做最终的 URI 跳转操作,但可以在最终的跳转前进行各种同步/异步操作,常见操作举例如下:
URI 跳转拦截,禁止特定的 URI 跳转,直接返回 403(例如禁止跳转非 meituan 域名的 HTTP 链接)
URI 参数修改(例如在 HTTP 链接末尾添加 query 参数)
各种中间处理(例如打开登录页登录、获取定位、发网络请求)
……
每个 UriHandler 都可以添加若干 UriInterceptor。在 UriHandler 基类中,handle()方法先调用抽象方法shouldHandle()
判断是否要处理 UriRequest,如果需要处理,则逐个执行 Interceptor,最后再调用handleInternal()
方法进行跳转操作。
public abstract class UriHandler {
// ChainedInterceptor 将多个 UriInterceptor 合并成一个 protected ChainedInterceptor mInterceptor;
public UriHandler addInterceptor(@NonNull UriInterceptor interceptor) {if (interceptor != null) {if (mInterceptor == null) {mInterceptor = new ChainedInterceptor();}mInterceptor.addInterceptor(interceptor);}return this;}
public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) {if (shouldHandle(request)) {if (mInterceptor != null) {mInterceptor.intercept(request, new UriCallback() {@Overridepublic void onNext() {handleInternal(request, callback);}
@Overridepublic void onComplete(int result) {callback.onComplete(result);}});} else {handleInternal(request, callback);}} else {callback.onNext();}}
/**
是否要处理给定的 uri。在 Interceptor 之前调用。*/protected abstract boolean shouldHandle(@NonNull UriRequest request);
/**
处理 uri。在 Interceptor 之后调用。*/protected abstract void handleInternal(@NonNull UriRequest request, @NonNull UriCallback callback);}
URI 的分发与降级
在外卖 C 端 App 中的 URI 分发示意如下图。所有 URI 跳转都会分发到 RootUriHandler,然后根据不同的 scheme 分发到不同的子 Handler。例如 waimai 协议分发到 WmUriHandler,然后进一步根据不同的 path 分发到子 Handler,从而启动相应的 Activity;HTTP/HTTPS 协议分发到 HttpHandler,启动 WebView 容器;对于其他类型 URI(tel、mailto 等),前面的几个 Handler 都无法处理,则会分发到 StartUriHandler,尝试使用 Android 原生的隐式跳转启动系统应用。
每个 UriHandler 都可以根据实际需要实现降级策略,也可以不作处理继续分发给其他 UriHandler。RootUriHandler 中提供了一个全局的分发完成事件监听器,当 UriHandler 处理失败返回异常 ResultCode 或所有子 UriHandler 都没有处理时,执行全局降级策略。
平台化与两端复用
随着外卖 C 端业务的演进,团队成员扩充了数倍,商超生鲜等垂直品类的拆分,以及异地研发团队的建立,客户端的平台化被提上日程。关于外卖平台化更详细的内容,可参考团队之前已经发布的文章 [美团外卖 Android 平台化架构演进实践](
)。
为了满足实际开发需要,在长时间的探索后,逐步形成了如图所示的三层工程结构。
原有的单个工程拆分成多个工程,就不可避免的涉及到多工程之间的耦合问题,主要包括通信问题、复用问题、依赖注入、编译问题,下面详细介绍。
通信问题
当原先的一个工程拆分到各个业务库后,业务库之间的页面需要进行通信,最主要的场景就是页面跳转并通过 Intent 传递参数。
原先的页面跳转使用显式跳转,Activity 之间存在强引用,当 Activity 被拆分到不同的业务库,业务库不能直接互相依赖,因此需要进行解耦。
利用 WMRouter 的 URI 分发机制,刚好可以很容易的解决这个问题。将将所有业务库的 Activity 注册到 WMRouter,各个业务库之间就可以进行页面跳转了。
评论