干货 | 携程国际站点Trip.com的无线异步启动框架
作者简介
赵辉,专注Android平台和Java技术栈,目前主要负责Trip.com App的性能、网络、存储等基础框架,热爱阅读源码。
受携程全球化战略的影响,IBU(国际业务部)迎来了高速发展时期,Trip.com app作为国际业务的载体,接入的业务线与日俱增,随之而来的一系列问题也日趋明显。
如何管理启动流程和优化启动时间便是其中之一,经过若干版本的迭代优化,Trip.com app的启动时间有了明显改观,更重要的是,我们完成了对整个app的启动流程监控,使得在多个版本迭代过程中启动时间始终维持在较低水平。
本文并不是优化启动时间的“最佳实践”文章,不会去具体分析如何优化Android/iOS的启动时间,而是对Trip.com这样的平台型app在启动流程优化方面的一些思考及实践的经验。
如果想了解启动时间优化最佳实践,可以参考Android Developer上的App startup time和iOS wwdc上的Optimizing App Startup Time。
介绍我们的启动流程方案之前,我们先看下一般app的启动流程。
抽象地来说,Android和iOS的启动流程大致分为三个阶段:
1、启动入口
2、进入首页
3、进入业务线页面
如图:
我们这里把用户进入二级页面也算了进来,注意这里所有的流程都是同步按序执行,流程非常简单,但是不可避免地会比较耗时且难以管理维护。
一个新的业务模块、框架层模块或者sdk(下文简称模块)接入平台,如果需要初始化,就需要在启动入口或者app首页加入该模块的初始化代码,而所有业务团队的初始化代码耦合在一起最明显的问题就是:无法进行有效地监控,所以启动时间随着版本迭代越来越长,而且无法区分时间变长的原因是哪些代码导致的,甚至可能由于部分模块的问题导致启动过程发生Crash,所有这些问题都是不可忍受的。
所以,我们需要从框架的角度重新思考:像Trip.com这样一个承载很多垂直业务的平台型App该如何优化启动流程?
对模块来说,它的初始化代码理论上只需要关心以下几点:
这里我们假设有个业务模块叫“酒店模块”,一个框架层的模块叫“Storage模块”,有个第三方sdk叫“ImageLoader模块”:
1、代码执行在哪些模块初始化之后,比如“酒店模块”执行初始化代码之前需要保证“Storage模块”初始化完成;
2、进入模块的任何页面或者使用模块功能之前,其模块的初始化代码必须已经执行完成,比如“ImageLoader模块”在使用之前必须保证已经执行完它的init方法;
在一般启动流程中,以上几点显然很容易支持:
1、“酒店模块”初始化代码写在“Storage模块”初始化之后即可;
2、“ImageLoader”使用之前启动流程一定已经走完;
但是在这些基础能力之外,我们同时希望:
1、“酒店模块”的初始化代码可以写在酒店项目代码中;
2、如果“酒店模块”和“ImageLoader模块”如果没有依赖关系的话,可以让它们同时进行初始化;
3、“酒店模块”的初始化不会影响用户进入首页,更不会影响用户进入机票模块;
4、可以很方便地查看app启动的流程、每个模块初始化消耗的时间和模块间的依赖关系;
5、“酒店模块”如果发生了crash,只会影响用户使用酒店的功能;
这时候我们需要有这样一个启动框架来支持这些能力,这些能力抽象如下:
“酒店模块”的初始化代码可以写在酒店项目代码中
对有很多垂直业务的app来说,很容易想到的一个框架思想就是"组件化",显然,组件化的思想也适用于启动流程。
模块的初始化代码应该放在各自的项目模块中,由启动框架统一调度执行,反过来,启动框架也有助于整体项目的组件化拆分。
事实上,Trip.com app的组件化很大程度上也依赖了启动框架:每个模块代码物理隔离,在启动框架中进行各自的初始化,这些初始化代码包含了组件化架构必要的路由框架和跨模块调用框架。
之前的项目架构:
组件化之后的项目架构:
可以看出来,使用了启动框架之后,原来入口和下层模块之间的依赖就解开了,意味着业务模块可以单独编译或者剥离项目,实现了真正的组件化。
另外对于平台研发团队来说,由于代码都在各个团队的项目中,所以我们可以把启动过程中发现的问题分发给各个负责的团队解决,这也是“组件化”方式开发的好处之一。
如果“酒店模块”和“ImageLoader模块”如果没有依赖关系的话,可以让它们同时进行初始化。
框架应该可以让启动流程中的任务尽可能地并发以保证最大化利用cpu,缩短启动时间。当然,影响启动时间的因素很多,比如启动任务的属性是io密集还是cpu密集、任务执行线程的优先级、是否有足够的cpu时间片分配给启动任务同时不会影响ui线程、任务间的依赖关系、并发执行的线程数设置多少,所有这些因素或许根本没有办法去精确度量,这也是启动框架无线端部分最重要且最复杂的部分。
并发执行模型如下图,可以看出来,相比于按序执行的普通启动流程,用户看到首页及进入业务模块的时间点都提前了很多。
“酒店模块”的初始化不会影响用户进入首页,更不会影响用户进入机票模块
用户进入首页的时候不需要等待“酒店模块”初始化完成,而进入“酒店模块”的时候也不需要“机票模块”初始化完成,而只需要保证“酒店模块”及部分依赖的基础模块初始化完成即可。这一点听上去相当Cool,但是换用户的角度来说,这难道不是基本诉求吗?
可以很方便地查看app启动的流程、每个模块初始化消耗的时间和模块间的依赖关系。
如果可以让开发测试,甚至产品经理可以很方便地看到我们的启动流程中都有哪些任务、每个任务执行了多久、任务之间的时序状况如何,那对了解app的启动会有非常大的帮助,而且,有了这些可视化数据,我们就可以比较版本迭代过程中的变化,从而发现问题,有目的地进行持续优化。
“酒店模块”如果发生了Crash,只会影响用户使用酒店的功能
这一点和第三点概念类似,酒店模块一旦出现异常,一方面,我们希望暴露问题,让相关开发人员及时排查,另一方面,我们也不希望用户直接Crash,而是可以顺利使用机票、火车等功能。
Trip.com无线平台研发团队经过多个版本的思考和实践,实现了一整套解决方案:Rocket,业务线只需要使用Rocket提供的简单的api就可以完成接入。
简单来说,Rocket做了三件事:
1、无线两端(Android、iOS)的启动框架
2、启动自动化实验
3、Debug及Release阶段监控
下图描述了Rocket整体方案的流程:
下面就无线端启动框架和自动化实验简单介绍其实现。
无线端需要整理并划分出启动流程中有逻辑关联的代码块,这些代码块可以认为是一个个的启动任务。
由于启动任务的代码需要分散在各个项目模块中(组件化),所以启动框架需要有分发的能力:即整合各模块中的启动任务。实现方式的话,以Android为例,使用编译时注解或者配置文件的方式都是可以的。
接下来的问题是如何并发?采用哪种线程池?
虽然启动任务执行快速,但是经过大量测试发现,采用固定线程数的线程池会比不定数量线程池效果好,那么到底需要多少线程?
上文提到,整体启动时间的影响因素非常复杂,很难量化,所以Trip.com采用自动化实验的方式来确定到底需要多少线程,这一点会在自动化实验设计一节展开。
而并发执行带来了两个问题:
1、任务依赖如何解决?
假设有5个启动任务,依赖关系如图:
可以很清晰地看到,任务之间的依赖问题其实就是有向无环图的排序,只要每个任务声明自己依赖哪些其他任务,框架就可以拿到任务执行的顺序,至于并发状态下保证执行顺序,Android和iOS可以有不同的实现,以Android为例,目前使用了CAS类型的锁来实现依赖任务间的时序。
2、如果应用必须等待几个必要任务完成才可以进入首页,如何处理?
针对这个问题,我们还是可以通过CAS类型的锁来保证几个必要任务执行结束(锁释放)之后才允许执行首页流程的代码,同样的思路可以解决等待任务的所有场景,比如:锁住酒店页面的代码,直到酒店模块所依赖的模块全部初始化结束再释放锁。
上文提到,影响启动时间的因素很多,且很多因素都难以度量,比如启动线程池的线程数量、不同类型任务间的依赖关系、每个任务执行线程的优先级、应用是否首次启动等等。
在不能够以理论来决定这些因素的时候,我们可以换个思路,用实际运行结果来决定:对几台用户主流设备进行若干次启动,并记录下启动的各项数据上报。利用实验,我们可以对这些影响因素进行手动调优,比如手动调整线程数,进行30次测试,最后决定线程数量。
另外,通过这样的方式,app在上线之前可以先大致预判出上线之后的启动时间,实践证明,这样的方式可以有效测量并缩短启动时间。
实验的脚本考虑到兼容两端,所以我们使用Appium,它基于Android的UiAutomator和iOS的UIAutomation,无需修改项目代码,所以理论上只需要Android和iOS两端收集的实验数据契约一致即可。在上报实验数据的同时,我们会同时生成实验号进行上报,这样就可以在我们Nemo(前端可视化框架)上筛选出本次实验的各种看板。
下图展示了我们前端的启动时序图:
因为我们保证了实验上报的数据和生产环境的用户数据一致,所以这些前端可视化看板在实验阶段和生产阶段是一样的。
借助Rocket的一整套方案,Trip.com app启动时间减少超过40%,在不需要大量维护精力的情况下,启动时间连续若干版本维持在了较低水平。
当然目前也存在一些不足,比如无线端的任务调度的实现方式还有优化空间、可视化的前端看板也需要提供排查、分发启动问题的能力等等。
启动时间和启动流程的优化对app至关重要,希望本文可以给读者带来一些思考。
【推荐阅读】
11月23日上海,
携程首届技术峰会,
公众号粉丝购票平台输入优惠码“F180”
可180元购票(原价280元)
戳“阅读原文”直达~