演讲嘉宾介绍:
刘志龙(花名正纬),阿里云资深开发工程师,主要从事Android端应用开发,负责阿里云APP的Android端架构设计、中间件开发;阿里云APP服务于阿里云官网用户,用户可以便捷的在移动端管控云上资源,了解云栖社区资讯等。
本次分享将主要围绕以下几个方面:
一、为什么需要路由框架
二、ARouter的技术方案
三、使用ARouter的最佳实践
四、未来开发计划
一、为什么需要路由框架
原生的路由方案存在的问题
首先谈一谈原生的路由方案存在的问题以及为什么需要路由框架。我们所使用的原生路由方案一般是通过显式intent和隐式intent两种方式实现的,而在显式intent的情况下,因为会存在直接的类依赖的问题,导致耦合非常严重;而在隐式intent情况下,则会出现规则集中式管理,导致协作变得非常困难。而且一般而言配置规则都是在Manifest中的,这就导致了扩展性较差。除此之外,使用原生的路由方案会出现跳转过程无法控制的问题,因为一旦使用了StartActivity()就无法插手其中任何环节了,只能交给系统管理,这就导致了在跳转失败的情况下无法降级,而是会直接抛出运营级的异常。
为什么要用路由组件
前面提到的主要是开发与协作中的问题,而使用一款路由框架时还会涉及到其他的两个大方面:一方面是组件化,而另一方面就是Native和H5的问题。刚才所提到的主要是开发和协作中作为开发者所需要面对的问题,而一旦一款APP达到一定体量的时候,业务就会膨胀得比较严重,而开发团队的规模也会越来越大,这时候一般都会提出组件化的概念。组件化就是将APP按照一定的功能和业务拆分成多个小组件,不同的组件由不同的开发小组来负责,这样就可以解决大型APP开发过程中的开发与协作的问题,将这些问题分散到小的APP中。目前而言组件化已经有非常多比较成熟的方案了,而自定义路由框架也可以非常好地解决整个APP完成组件化之后模块之间没有耦合的问题,因为没有耦合时使用原生的路由方案肯定是不可以的。
路由框架的特点
为了解决以上的问题就需要实现一个自定义的路由框架,而路由框架一般都具有以下的三种特点:
- 分发:把一个URL或者请求按照一定的规则分配给一个服务或者页面来处理,这个流程就是分发,分发是路由框架最基本的功能,当然也可以理解成为简单的跳转。
- 管理:将组件和页面按照一定的规则管理起来,在分发的时候提供搜索、加载、修改等操作,这部分就是管理,也是路由框架的基础,上层功能都是建立在管理之上。
- 控制:就像路由器一样,路由的过程中,会有限速、屏蔽等一些控制操作,路由框架也需要在路由的过程中,对路由操作做一些定制性的扩展,比方刚才提到的AOP,后期的功能更新,也是围绕这个部分来做的。
ARouter的7个优势
ARouter大致有以下7个优势:
- 优势一:直接解析URL路由,解析参数并赋值到对应目标字段的页面中。
- 优势二:支持多模块项目,因为现在很少有APP是单模块的项目,一般都是多模块单工程的,由不同的团队负责不同的模块开发,这时候支持多模块项目开发就显得尤为重要。
- 优势三:支持InstantRun,目前很多路由框架并不支持InstantRun,而InstantRun是Google在AndroidStudio2.0阿尔法版本中提供的新功能,其类似于代码的日更新,其只不过面向的是开发过程,这样做可以在开发的过程中减少开发和编译的次数,可以简单地将代码修改即时地同步到APK中,从而可以大规模降低开发复杂度。
- 优势四:允许自定义拦截器,ARouter是支持拦截器的,而拦截器其实就是AOP的实现,可以自定义多个拦截器解决一些面向行为编程上出现的问题。
- 优势五:ARouter可以提供IoC容器,IoC其实就是控制反转,这一部分做过服务端开发的朋友可能比较了解,因为服务端开发经常用到的Spring框架能够提供的一个非常重要的能力就是控制反转。
- 优势六:映射关系自动注册,在页面不是很多的小型APP上面,自动注册并不会体现出太大优势,但是对于大型APP而言,可能页面数量已经达到的几十个或者数百个,在这样的情况下,自动注册就显得非常重要了,因为不可能将每一个页面都通过代码的方式进行注册。
- 优势七:灵活的降级策略,ARouter可以提供很多种降级策略供用户自行选择,而原生的路由方案存在无法灵活降级的问题,StartActivity()一旦失败将会抛出运营级异常。
二、ARouter的技术方案
接下来进入分享的第二部分:ARouter的技术方案。其实如果大家看过ARouter的源码就会知道ARouter提供了两个SDK,分别是面向两个不同的阶段。本身API这个SDK是面向运行期的,而Compiler这个SDK则是作用于编译期的,从工程上ARouter就是划分成了这两个SDK。
下图是按照功能组件的方式来对于整个框架进行划分的,其实ARouter在设计上使用了三种思想:Bootstrapping、Extensibility以及Simple & Enough。首先,ARouter的组件是自举的,这个概念借鉴了编程中的自举;除此之外ARouter组件还具有良好的扩展性,因为像Route这样的东西是整个APK的基础组件,不可能经常变更,也不可能经常升级,所以应该具有良好的扩展性,而不需要通过经常升级来解决问题;而ARouter最重要的宗旨就是简单并且够用,ARouter不会有非常复杂的使用方式和调用方式,但是功能却是非常全面的。
接下来分享一下ARouter的具体解决方案,也就是ARouter是如何解决上述问题的。
页面注册:注解&注解处理器
首先,对于页面自动注册的问题,ARouter是可以自动注册映射关系的,因为大型APP的页面往往很多,会存在几十甚至上百个页面,所以手动注册映射关系会非常麻烦,需要写很多重复冗余的代码,并且需要调用很多接口,而为了避免这样的麻烦,ARouter实现了页面的自动注册。而为了解决隐式intent的问题和将所有配置都存储在Manifest中这样集中式的问题,首先想到的就是分布式管理,可以将所有的配置都放在目标页面,这样就实现了“All In One”,就是一个页面中所有的配置都要聚合在该页面中,这样就解决上面的问题。不同的页面由不同的配置负责,这样修改也变得非常容易,而不需要将配置散落在整个APP四处。
页面注册的整个流程如下图所示:首先通过注解处理器扫出被标注的类文件;然后按照不同种类的源文件进行分类,这是因为ARouter是一个框架,其能够提供的功能非常多,所以不仅仅提供了跳转功能,它也能够实现模块之间的解耦,除此之外ARouter还能够提供很多的功能,像刚才提到的拦截器可以实现自动注册,其实ARouter中的所有组件都是自动注册的;在按照不同种类的源文件进行分类完成之后,就能够按照固定的命名格式生成映射文件,这部分完成之后就意味着编译期的部分已经结束了;而最后一步的初始化其实是发生在运行期的,在运行期只需要通过固定的包名来加载映射文件就可以了,因为生成是由开发者自己完成的,所以会了解其中的规则,就可以在使用的时候利用相应的规则反向地提取出来。这就是页面自动注册的整个流程。
加载:分组管理,按需加载
下图表现的就是刚才提到的按需加载。ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。因为每个模块中可能有N个分组,每个分组中可能有N个页面,如果一次性地将所有的页面全部加载进来,那么整个复杂度可能不只是O(N^2),但是每个模块都只加载其根节点,从算法的角度考虑可能就是复杂度为O(N)的方案,也就是有多少个模块就只需要加载多少个结点。下图中的三个圈中体现的就是ARouter初始化时加载的状况。那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载。其实在整个APP运行的周期中,并不是所有的页面都需要被访问到,可能只有20%的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。
拦截器
分享完分组管理和按需加载之后,接下来分享一下关于拦截器的内容。原生的路由方案中存在的问题就是其无法在页面跳转的过程中插入一些自定义逻辑,而拦截器就是ARouter中提出的针对AOP思想的实现。直接讲拦截器可能不容易让大家理解,那么就用这样形象的比喻来解释一下,拦截器就是像是一个汉堡,汉堡中夹心的无论是生菜、牛肉还是芝士都像拦截器一样,当在做汉堡时就相当于在做APK,打包了哪些模块就相当于在汉堡中放了哪些层,在吃的时候就会把这一层都咬掉,但是汉堡的每一层都有可能是芝士、牛肉或者铁片,当遇到某一层是铁片的时候就无法咬下去了,也就是被拦截住了。同样的拦截器就是需要当条件符合的时候才能让跳转流程继续执行,同样像汉堡一样,如果使用了太多的拦截器最终会导致汉堡变成了“巨无霸”,所有的拦截器会在任意两次跳转之间生效,声明了大量的拦截器会影响整个跳转流程的性能,拦截器的更详细内容会在第三部分的最佳实践中继续为大家介绍。
InstantRun兼容
接下来分享一下ARouter如何实现对于InstantRun的兼容。市面上的框架一般对于这一部分的兼容都是缺失的,对于InstantRun的兼容从技术上看并不是非常难以实现的,在实现时只需仔细阅读InstantRun的源码就可以了。在实现对于InstantRun的兼容时是存在如下图所示的四种情况的,当AndroidSDK版本大于21的时候,会存在SplitAPK的特性支持的,会允许将一个APK切分成多个小APK,当然其实这并不是APK的切分,而实际上是Dex的切分,也就每个依赖都会打包成小的Dex放在APP+包名的目录下的,这与传统情况下是不同的。
依赖注入的实现
接下来分享依赖注入的实现,这一部分是路由框架在进行大规模组件之间解耦时比较重要的一点。其实依赖注入就是对于控制反转思想的实现,这部分服务端使用的比较多,客户端可能使用不是非常多。ARouter对于依赖注入的实现主要分成如下图所示的两个部分。
下图所示的代码就是在编译期生成的注入辅助类,这部分实际上就是模仿了用户的写法,通过一定的工具和规则生成这样的代码,免去用户手写重复和冗余的代码,在用户的角度来看也是自动注入,这一部分就是依赖注入的具体实现,大家也可以参考GitHub上的源码来研究具体实现。
三、最佳实践
接下来就进入到了本次分享的重点:ARouter的最佳实践,在这部分将分享如何在项目中运用ARouter,如何让ARouter帮助我们加快开发的速度。页面跳转
从外部导航到内部页面
接下来要分享的也是路由框架的一个非常重要的功能:从外部导航到内部页面。可以看到下图中的两个截图分别是使用了自定义的Scheme,另一张图则是使用了原声的HTTPS的Scheme。
接下来具体分享这部分是如何实现的,首先需要在APP的Manifest声明一个activity,但是是这个activity不需要页面,只需要注册一个intent-filter就可以了。这个intent-filter就是用于监听刚刚生成的Scheme的,而且Scheme可以换成任何想要的,比如HTTP或者HTTPS,也可以使用自定义Scheme。为什么说这里是一个最佳实践呢,其实通常情况下使用隐式intent的时候,每一个从外面跳转进来的页面都需要注册上intent-filter,每个页面都需要设置export=true,也就是需要让每一个页面都可以导出,在外部可以访问到。但是这样做会带来非常严重的安全风险,就像是一个房子有十个门还是只有一个门,看门的成本是不同的。而现在使用的这种场景只需要对外暴露出一个activity,然后在这个activity中注册一个intent-filter,这样之后所有的外部路由请求都会经过这唯一的门,然后在这个activity中获取到URL并将其交给ARouter,剩下的就由路由框架做分发了。
处理登录逻辑 : 拦截器的运用
以上分享的就是如何从外部的URL跳转到内部的页面并解析参数,接下来分享如何处理登录逻辑。登录逻辑是每个APP都会有的功能,有的APP是只要用户进入就需要登录的,也有的APP是对于一些页面需要登录,另外一些页面也不需要登录,而对于后面的这种APP而言,在每个页面中都需要判断是否用户登录了则是非常不合适的做法,这也是最开始考虑到系统原生的路由方案不支持在系统中插入自定义跳转逻辑的比较坑的状况。所以假如使用ARouter,就能够使用ARouter所提供的拦截器的机制解决登录问题。使用ARouter解决登录逻辑只需要实现登录拦截器就可以了,不需要在每一个页面都判断是不是需要登录,而只需要在登录拦截器中进行判断。登录拦截器会作用在所有的跳转之间,假设从来源页面跳转到下面的A、B、C和D这四个目标页面,可以看到图中绿色的是不需要登录页面的,可以直接跳转进入,也就是如绿色的箭头展示的一样是可以直接放行的;而对于C页面而言,则属于需要登录的页面,这时就会被拦截器拦截并直接导航到登录页,在用户完成登录或者取消登录后,通过回调或者广播等形式回到拦截器,然后根据从拦截器中得到的结果判断可以直接往下跳转还是终止本次跳转流程,每一个拦截器中都有一个回调,这个回调可以终止本次路由过程也允许直接放行。这就是典型的面向切面编程,当然登录拦截器只是诸多拦截器之一,可以声明N个拦截器可以实现登录的判断以及用户权限的判断等,这些就交给开发者*发挥了。谈到这部分还会存在一个问题就是如何才能在一个地方判断出所有的页面哪些需要登录,哪些不需要登录,如果这时候保存两个非常大的列表,一个用于保存需要登录的页面,另一个保存不需要登录的页面,将会是非常不合适的了。
标识目标页面信息 : 配置extra参数
所以接下来分享一下如何配置页面的参数,刚刚提到了“All In One”,这是什么意思呢?其实就是希望所有页面中的配置都能够浓缩到这一个页面中,也就是高内聚低耦合的思想,不希望页面的配置逃出页面,配置到像Manifest的其他地方。像在拦截器中配置哪些地方需要登录哪些不需要登录的话就违背了刚才提出的这个原则,ARouter框架的设计思想就是希望所有的属性标注在自己的页面中。可以看一下页面中标注的页面注解,如下图所示可注解中在IDE的提示中有extras这样的参数,大家看到这个数字应该非常熟悉,这个数字就是int的最小值,而为什么extras这个参数是int呢?其实是因为int本身在Java中是由4个字节实现的,每个字节是8位,所以一共是32个标志位,去除掉符号位还剩下31个,也就是说转化成为二进制之后,一个int中可以配置31个1或者0,而每一个0或者1都可以表示一项配置,这时候只需要从这31个位置中随便挑选出一个表示是否需要登录就可以了,只要将标志位置为1,就可以在刚才声明的拦截器中获取到这个标志位,通过位运算的方式判断目标页面是否需要登录,这样是简单并且高效的,因为位运算的速度要远远高于字符串比对以及其他的方式的,而且一个int值就可以提供31个开关。目前而言没有一个目标页面需要配置30多个属性,所以使用int是足够的,而开发者只需要实现一个简单的位运算的工具类就可以提取出二进制int中的每一位,并对其中每一个值进行判断。
模块间通信解耦 :控制反转
除此之外,另一个比较重要的问题就是如何实现模块间的通信解耦。实现组件化的时候希望对于不同的组件进行分别打包,而且模块之间应该不存在任何依赖,可以看出下图中左边的图中的四个组件完全是耦合依赖的,这样就导致四个组件之间根本无法解耦,所以打包的时候也必须一起打包,否则就会出现No Class Found的问题,所以现在的实现是如下图右边所示的,通过IoC容器,也就是控制反转容器将耦合解开。为什么这样能将耦合解开呢?其实是因为这样就可以让各个组件之间不产生直接依赖,而是通过IoC控制反转容器拿到对方的实例,这样我们在写代码的时候就不会存在直接依赖的问题。而ARouter本身也是一个IoC容器,它在实现这部分功能的时候用到的一个元素就是Service,如果大家做过服务端开发的话就会对于Service很熟悉了,Service就是将一部分功能和组件封装起来成为接口,以接口的形式对外提供能力,所以在这部分就可以将每个功能作为一个Service,而Service的实现就是具体的业务功能,这部分也需要通过IoC容器进行获取。这样整个的流程将通过用户的直接依赖转化成通过控制反转容器依赖的这种形式。下图就具体地解释了刚才提到的两种情况也就是ByName和ByType的依赖查找的方式,而上一张图中则是依赖注入。依赖查找是应用在不希望在类初始化的时候就把一些功能注入进来的场景以及在某些页面上才会触发这样的功能的情况下,那么只需要在使用到的时候去获取这个服务就可以了。而这种情况就是通过用户的主动依赖查找来获取服务,其实就是图中所示的ByName和ByType的依赖查找的方式。
解决运行期动态修改路由的问题
然后需要分享的就是如何解决运行期动态修改路由的问题。如下图所示,这种情况下只需要实现一个服务就可以了,从下图也可以看出为什么说ARouter的组件都是自举的,因为服务的查找还是需要依赖于底层路由的查找的,所以服务功能的实现是由路由层作为基础的,并且服务是用来解决动态修改路由的问题的,所以只需要实现一个服务。其实这个PathReplaceService就是ARouter提供的一个服务,是在ARouter的Frossard层提供的服务,其实就是一个接口,只需要将其实现并标注上就可以,因为有自动注册的机制,所以在APP启动的时候就会注册到ARouter框架上,这样之后框架在跳转的时候就会跳转到这个服务,而如果没有实现,框架就无法调用,自然也就不会有这部分功能。这样就实现了ARouter框架的非常好的可扩展性,后期ARouter框架不需要更改其底层基础,只需要声明更多的服务,由用户主动实现,并在最后运行期的时候通过自动注册的方式将这些服务加载到框架中。解决降级问题
接下来分享的就是关于解决降级的问题。其实这部分的方法和刚才的方法是异曲同工的,只需要实现另一个服务就好了,ARouter在发展中会越来越多地为大家提供各种服务让用户自己进行具体的实现,当然如果不实现也不会有这部分功能,如果APP实现了降级服务,那么随便标识一个注解就可以了,当然这个注解是由用户决定的,可以选择自己喜欢的规则,可以将这些服务都放在不同的分组下或者都放在同一个分组下。而现在相当于放在了SDK这个分组下面,对于这一部分只需要实现onLost()方法就可以了,ARouter如果发现在目标跳转的情况下失败了,就会回调这个onLost()方法。onLost()方法的第二个参数postCard翻译过来就是明信片,这里面就包含了本次跳转中所有的内容,通过拿到这些内容就可以实现自己的降级方案。下图中所列举的例子是通过跳转到第三方的H5的错误页面来解决的,因为APP不能够重复发布,但是H5是可以重复发布的,所以可以通过H5的方式解决降级问题,把去向的目标页面作为目标的参数传递到H5中。
四、未来的开发计划
最后想分享的就是ARouter的未来开发计划。未来ARouter会支持插件化并且支持生成映射关系文档,因为插件化是现在很多大型APP中会使用的技术方案,很多的Dex和功能是动态地下发到APP中的,而在这种情况下,是无法找到所有的Dex文件的,也就是对于没有加载过的Dex而言,里面的映射关系是跳转不过去的,所以一旦Dex文件位置发生变动,常规的方案是无法找到Dex的,也不能实现映射文件初始化,这一部分会在后面的版本中进行支持。因为像手淘和360等很多插件化的方案之后也许会开源,这样可能越来越多的APP会支持插件化,如果ARouter作为一个技术组件如果不能支持插件化的话,就会造成麻烦。ARouter是从去年的年底时开始开源的,到现在大概经过了两三个月,目前已经有一千多个Star,已经有一部分开发者在关注了,而我们也有一个沟通与交流的群,大家如果感兴趣的话可以直接到GitHub上找到ARouter的源码来分析具体的实现,如果大家有更好的思路和方案也可以贡献代码,和我们一起更好地完善ARouter。当然一个技术选型肯定是简单又好用的,并且应该是长期进行维护保证足够稳定的,ARouter也具有这样的特点,欢迎大家选用并贡献代码。