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

WMRouter是一款Android路由框架,基于组件化的设计思路,功能灵活,使用也比较简单。html

WMRouter最初用于解决美团外卖C端App在业务演进过程当中的实际问题,以后逐步推广到了美团其余App,所以咱们决定将其开源,但愿更多技术同行一块儿开发,应用到更普遍的场景里去。Github项目地址与使用文档详见 github.com/meituan/WMR…java

本文先简单介绍WMRouter的功能和适用场景,而后详细介绍WMRouter的发展背景和过程。android

功能简介

WMRouter主要提供URI分发、ServiceLoader两大功能。git

URI分发功能可用于多工程之间的页面跳转、动态下发URI连接的跳转等场景,特色以下:github

  1. 支持多scheme、host、path
  2. 支持URI正则匹配
  3. 页面配置支持Java代码动态注册,或注解配置自动注册
  4. 支持配置全局和局部拦截器,可在跳转前执行同步/异步操做,例如定位、登陆等
  5. 支持单次跳转特殊操做:Intent设置Extra/Flags、设置跳转动画、自定义StartActivity操做等
  6. 支持页面Exported控制,特定页面不容许外部跳转
  7. 支持配置全局和局部降级策略
  8. 支持配置单次和全局跳转监听
  9. 彻底组件化设计,核心组件都可扩展、按需组合,实现灵活强大的功能

基于SPI (Service Provider Interfaces) 的设计思想,WMRouter提供了ServiceLoader模块,相似Java中的java.util.ServiceLoader,但功能更加完善。经过ServiceLoader能够在一个App的多个模块之间经过接口调用代码,实现模块解耦,便于实现组件化、模块间通讯,以及和依赖注入相似的功能等。其特色以下:浏览器

  1. 使用注解自动配置
  2. 支持获取接口的全部实现,或根据Key获取特定实现
  3. 支持获取Class或获取实例
  4. 支持无参构造、Context构造,或自定义Factory、Provider构造
  5. 支持单例管理
  6. 支持方法调用

其余特性:性能优化

  1. 优化的Gradle插件,对编译耗时影响很小
  2. 编译期和运行时配置检查,避免配置冲突和错误
  3. 编译期自动添加Proguard混淆规则,免去手动配置的繁琐
  4. 完善的调试功能,帮助及时发现问题

适用场景

WMRouter适用但不限于如下场景:微信

  1. Native+H5混合开发模式,须要进行页面之间的互相跳转,或进行灵活的运营跳转连接下发。能够利用WMRouter统一页面跳转逻辑,根据不一样的协议(HTTP、HTTPS、用于Native页面的自定义协议)跳转对应页面,且在跳转过程当中可使用UriInterceptor对跳转连接进行修改,例如跳转H5页面时在URL中加参数。网络

  2. 统一管理来自App外部的URI跳转。来自App外部的URI跳转,若是使用Android原生的Manifest配置,会直接启动匹配的Activity,而不少时候但愿先正常启动App打开首页,完成常规初始化流程(例如登陆、定位等)后再跳转目标页面。此时可使用统一的Activity接收全部外部URI跳转,到首页时再用WMRouter启动目标页面。架构

  3. 页面跳转有复杂判断逻辑的场景。例如多个页面都须要先登陆、先定位后才容许打开,若是使用常规方案,这些页面都须要处理相同的业务逻辑;而利用WMRouter,只须要开发好UriInterceptor并配置到各个页面便可。

  4. 多工程、组件化、平台化开发。多工程开发要求各个工程之间能互相通讯,也可能遇到和外卖App相似的代码复用、依赖注入、编译等问题,这些问题均可以利用WMRouter的URI分发和ServiceLoader模块解决。

  5. 对业务埋点需求较强的场景。页面跳转做为最多见的业务逻辑之一,经常须要埋点。给每一个页面配置好URI,使用WMRouter统一进行页面跳转,并在全局的OnCompleteListener中埋点便可。

  6. 对App可用性要求较高的场景。一方面,能够对页面跳转失败进行埋点监控上报,及时发现线上问题;另外一方面,页面跳转时能够执行判断逻辑,发现异常(例如服务端异常、客户端崩溃等)则自动打开降级后的页面,保证关键功能的正常工做,或给用户友好的提示。

  7. 页面A/B测试、动态配置等场景。在WMRouter提供的接口基础上进行少许开发配置,就能够实现:根据下发的A/B测试策略跳转不一样的页面实现;根据不一样的须要动态下发一组路由表,相同的URI跳转到不一样的一组页面(实现方面能够自定义UriInterceptor,对匹配的URI返回301的UriResult使跳转重定向)。

基本概念解释

下面开始介绍WMRouter的发展背景和过程。为了方便后文的理解,咱们先简单了解和回顾几个基本概念。

路由

根据维基百科的解释,路由(routing)能够理解成在互联的网络经过特定的协议把信息从源地址传输到目的地址的过程。一个典型的例子就是在互联网中,路由器能够根据IP协议将数据发送到特定的计算机。

URI

URI(Uniform Resource Identifier,统一资源标识符)是一个用于标识某一互联网资源名称的字符串。URI的组成以下图所示。

一些常见的URI举例以下,包括平时常常用到的网址、IP地址、FTP地址、文件、打电话、发邮件的协议等。

在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多等问题。例如:

  1. 从外部(浏览器、微信等)跳转外卖的URI时,系统会直接打开相应的Activity,而没有通过欢迎页的正常启动流程,一些代码逻辑可能没有执行,例如定位逻辑。

  2. 有不少页面在打开前须要确保用户先登陆或先定位,每一个页面都写一遍判断登陆、定位的逻辑很是麻烦,提升了开发维护成本。

  3. 运营人员可能会配错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拦截,并插入一些特殊操做。

页面跳转来源

常见的页面跳转来源以下:

  1. 来自App内部Native页面的跳转
  2. 来自App内Web容器的跳转,即H5页面发起的跳转
  3. 从App外经过URI唤起App的跳转,例如来自浏览器、微信等
  4. 从通知中心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() {
                    @Override
                    public void onNext() {
                        handleInternal(request, callback);
                    }

                    @Override
                    public 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,各个业务库之间就能够进行页面跳转了。

此时WMRouter已经承载了两项功能:

  1. 后台下发的运营URI跳转 (waimai://*)
  2. 内部页面跳转,用于代替原有的显式跳转,实现工程解耦 (wm_router://page/*)

因为后台下发的URI是和产品、运营、H五、iOS等各端统一制定的协议,支持的页面、格式、参数等都不能随意改动,而内部页面跳转使用的URI,则须要根据实际开发须要进行配置,两套URI协议不能兼容,所以使用了不一样的scheme。

复用问题与ServiceLoader模块

业务库之间常常须要复用代码。一些通用代码逻辑能够下沉到平台层从而复用,例如业务无关的通用View组件;而有些代码不适合下沉到平台层,例如业务库A要使用业务库B中的某个界面模块,而这个界面模块和业务库B的耦合很紧密。具体到外卖实际业务场景中,商家页在商家休息时会展现推荐商家列表,其样式和首页相同(如图),而两个页面不在一个工程中,商家页但愿能直接从首页业务库中获取商家列表的实现。

为了解决上述问题,咱们调研了解到Java中SPI (Service Provider Interfaces) 的设计思想与java.util.ServiceLoader工具类,还学习到美团平台为了解决相似问题而开发的ServiceLoader组件。

考虑到实际需求差别,咱们从新开发了本身的ServiceLoader实现。相比Java中的实现,WMRouter的实现借鉴了美团平台的设计思路,不只支持经过接口获取全部实现类,还支持经过接口和惟一的Key获取特定的实现类。另外WMRouter的实现还支持直接加载实现类的Class、用Factory和Provider建立对象、单例管理、方法调用等功能。在Gradle插件的实现思路上,借鉴了美团平台的ServiceLoader并作了性能优化,给平台提出了改进建议。

业务库之间代码复用的需求示意如图,业务库A须要复用业务库B中的ServiceImpl但又不能直接引用,所以经过WMRouter加载:

  1. 抽取接口(或父类,后面再也不重复说明)下沉到平台层,实现类ServiceImpl实现该接口,并声明一个Key(字符串类型)。
  2. 调用方经过接口和Key,由ServiceLoader加载实现类,经过接口访问实现类。

URI跳转和ServiceLoader看起来彷佛没有关联,但通讯和复用需求的本质均可以理解成路由,页面经过URI分发跳转时的协议是Activity+URI,在这里ServiceLoader的协议是Interface+Key。

依赖注入

为了兼容外卖独立App和美团App外卖频道的两端差别,平台层的一些接口要在两个主工程分别实现,并注入到底层。常规Java代码注入的方式写起来很繁琐,而使用WMRouter的ServiceLoader功能能够更简单的实现和依赖注入相似的效果。

对于WMRouter来讲,全部依赖它的工程(包括主工程、业务库、平台库)都是同样的,任何一个库想要调用其余库中的代码,均可以经过WMRouter路由转发。前面的通讯和复用问题,是同级的业务库之间经过WMRouter调用,而依赖注入则是底层库经过WMRouter调用上层库,其本质和实现都是相同的。

ServiceLoader模块在加载实现类时提供了单例管理功能,可用于管理一些全局的Service/Manager,例如用户帐户管理类UserManager

编译问题

因为历史缘由,主工程做为一个没有业务逻辑的壳工程,对业务库却有较多依赖,特别是对业务库的初始化配置繁琐,和各业务库耦合紧密。另外一方面,WMRouter跳转的页面、加载的实现类,须要在Application初始化时注册到WMRouter中,也会增长主工程和业务库的耦合。开发过程当中各业务库频繁更新,常常出现没法编译的问题,严重影响开发。

为了解决这个问题,一方面WMRouter增长了注解支持,在Activity类、ServiceLoader实现类上配置注解,就能够在编译期间自动生成代码注册到WMRouter中。

// 没有注解时,须要在Application初始化时代码注册各个页面,耦合严重
register("/home", HomeActivity.class);
register("/order", OrderListActivity.class);
register("/shop", ShopActivity.class)
register("/account", MyAccountActivity.class);
register("/address", MyAddressActivity.class);
// ...
复制代码
// 增长注解后,只须要在各个Activity上经过注解配置便可
@RouterUri(path = "/shop")
public class ShopActivity extends BaseActivity {

}
复制代码

另外一方面,ServiceLoader还支持指定接口加载全部实现类,主工程能够经过统一接口,加载各个业务库中全部实现类并进行初始化,最终实现和业务库的完全隔离。

开发过程当中,各个业务库能够像插件同样按需集成到主工程,能大幅减小不能编译的问题,同时因为编译时能够跳过不须要的业务库,编译速度也能获得提升。

WMRouter的推广

在WMRouter解决了外卖C端App的各类问题后,发现公司内甚至公司外的其余App也遇到了类似的问题和需求,因而决定对WMRouter进行推广和开源。

因为WMRouter是一个开放式组件化框架,UriRequest能够存听任意数据,UriHandler、UriInterceptor能够彻底自定义,不一样的UriHandler能够任意组合,具备很大的灵活性。但过于灵活容易致使易用性的降低,即便对于最常规最简单的应用,也须要复杂的配置才能完成功能。

为了在灵活性与易用性之间平衡,一方面,WMRouter对包结构进行了合理的划分,核心接口和实现类提供基础通用能力,尽量保留最大的灵活性;另外一方面,封装了一系列通用实现类,并组合成一套默认实现,从而知足绝大多数常规使用场景,尽量下降其余App的接入成本,方便推广。

总结

目前业界已有的一些Android路由框架,不能知足外卖C端App在开发过程当中的实际须要,所以咱们开发了WMRouter路由框架。借鉴网络请求的思想,设计了基于UriRequest、UriHandler、UriInterceptor的URI分发机制,在保证功能灵活强大的同时,又尽量的下降了使用难度;另外一方面,借鉴SPI的设计思想、Java和美团平台的ServiceLoader实现,开发了本身的ServiceLoader模块,解决外卖平台化过程当中的四个问题(通讯问题、复用问题、依赖注入、编译问题)。在通过了近一年的不断迭代完善后,WMRouter已经成为美团多个App中的核心基础组件之一。

参考资料

  1. Routing - Wikipedia
  2. 统一资源标志符 - 维基百科
  3. RFC 3966 - The tel URI for Telephone Numbers
  4. RFC 6068 - The 'mailto' URI Scheme
  5. Intent 和 Intent 过滤器
  6. Introduction to the Service Provider Interfaces
  7. 美团外卖Android平台化架构演进实践

做者简介

子健,美团高级工程师,2015年加入美团,前后负责外卖客户端首页、商家容器、评价等业务模块的开发维护,以及平台化、性能优化等技术工做。

渊博,美团高级工程师,2016年加入美团,目前做为外卖商家端Android App主力开发,主要负责商家端和蜜蜂端业务技术需求开发。

云驰,美团高级工程师,2016年加入美团,目前负责外卖客户端搜索、IM等业务库,及外卖多端统一工做。

招聘

美团外卖诚招Android、iOS、FE高级/资深工程师和技术专家,Base北京、上海、成都,欢迎有兴趣的同窗投递简历到wukai05@meituan.com。

相关文章
相关标签/搜索