启动优化,我们有点“牛”之实践篇

1,211 阅读14分钟

1. 启动优化方案

在上一盘文章《启动优化,我们有点“牛”之理论篇》中,我们介绍了启动优化的背景与挑战,在优化方案中阐述了一般性能优化过程中需要遵守的几点原则,最后对业界的启动优化方案做了简单的综述。

在这篇文章中,我们会对启动优化实战方案做比较详细的阐述。正所谓“民之失德,乾糇以愆;他山之石,可以攻玉”,我们在最优化时候也借鉴了一些业界方案,并且在此基础上做了小小的创新。在具体到启动优化实战过程中,虽然做的事情看似复杂繁琐,很难用一套体系化的、通用的方案来解决。但其实抽象来看,整个启动优化所做的事情是可以概括为如下几点:

  • main函数前优化

  • main函数后优化

  • 业务流程优化

  • 交互优化

  • 启动器开发

  • 启动动态弹缩

  • 启动时间防腐化

下面针对上述几点分别进行阐述。

main函数前优化

main函数前优化,涉及很多操作系统底层原理性的知识。虽然看似比较复杂,但就启动而言所做的优化并不十分复杂。在《Optimizing App Startup Time》《Optimizing App Launch》已经详细介绍了main函数前所经历的过程以及每个过成的优化手段,这里就不再赘述。

依照以往经验,大部分App启动耗时的卡口大多不在这里。不过在App启动优化前,最好还是检测一下,如果发现main函数前耗时比较严重,可以根据WWDC的建议,做有针对性的优化。我们在优化时候,检测出几个load函数耗时比较大,做了针对性优化。

main函数后优化

和main函数之前相比,main函数后并没有那么多复杂的底层原理性知识,也没有特别高深的技术含量,但却是最繁琐、ROI最高的优化。这里需要我们根据App业务的需求、交互特性、基础库使用情况等做有针对性的优化。需要开发人员沉下心来,仔细梳理自己App的启动流程,和产品同学讨论业务流程,结合多种性能评测工具来做启动优化。

这里我们不会过多介绍仅针对我们App有效的优化手段, 而是将App在启动优化可能会遇到的共性问题列出来,供大家参考。同样main函数后优化方案可以抽象为如下几类:

1. 提前启动任务

一些诸如网络IO、磁盘IO等耗时且可以在子线程异步执行的操作,可以根据业务特点,适当提前执行。这样在使用的时候可以快速使用,避免阻塞启动流程。这里列举一些常用的可以提前的任务:

  • 首页业务数据。首页数据缓存或者首页网络请求,可以在启动阶段提前执行以加快首页渲染速度。

  • 图片资源预载。加载首页、闪屏、多tab等需要的图片,也可以提前预载。

  • 地理定位。一些首页强依赖定位的App,可以提前定位的时机。

  • 其他可以提前的任务。。。

2. 延后启动任务

一些在启动阶段不必须的任务,可以放到启动之后在运行。大部分App在经过长年累月的迭代,如果没有良好的架构规范约束的话,大量诸如三方SDK初始化等启动阶段不必要的任务都会堆积到启动阶段。按以往经验,此部分是启动优化ROI最高的部分。这里列举一些常用的可以延后的任务:

  • 启动无关的三方SDK初始化

  • 非启动页面多tab懒加载

  • 启动无关的磁盘IO、网络IO管控。这里由于我们磁盘IO和网络IO使用的统一的中间件,因此我们在中间件层面做了统一管控,配置白名单,非白名单内的磁盘IO和网络IO在启动阶段会先suspend,等启动完成后再择机执行。这样既达到了启动优化目的,又对上层中间件使用同学无感,降低了开发同学开发的复杂度。

  • 其他启动无关的启动任务。。。

3. 并行启动任务

目前iPhone都是多核处理器,在启动阶段可以将一些可以并行的、耗时的任务,从主线程拆出来放到子线程运行,同时可以根据任务的重要程度设置子队列的优先级。大家可以根据自己App启动的特点,通过instruments或者火焰图等工具,定位主线程比较耗时的任务或者阻塞主线程的任务,分拆到子线程中去,从而优化启动性能。

4. 优化启动任务

一些无法延后的任务,是启动阶段必须的任务。这部分任务则可以进行性能优化,使其占用的资源最小化,提高启动性能。大体上有以下几种方式:

  • 空间换时间。这是最常用、十分有效的优化手段之一。比如经常使用的缓存就属于这一类。我们在启动优化过程中,将上次网络拉取的首页数据作为缓存来渲染启动数据,然后网络拉取完成再二次渲染。由于我们业务特点,两次渲染首页数据相差不大,因此用户体验也比较好。另外还会把首页每个区块的size缓存到模型数据中,对于网络新拉取的数据,我们同样会根据渲染模板缓存一份全局的size,从而避免计算size导致的耗时操作,加快了启动进程。

  • 更快的数据结构和算法。大家在开发过程中遇到数据量比较大的场景,就需要关注处理数据算法的时间复杂度了,如果超过了O(n2)就需要着手优化算法的性能了。

  • 技术栈选择。这里提到技术栈选择,主要针对近些年在客户端上流行起来的一些非官方native技术栈。比如H5、RN、Weex和Flutter等。这些技术解决了诸如跨端一致性、开发效率、动态化等问题。但据以往经验,如果对启动性能体验要求比较高的场景,还是不推荐在首页使用这些技术。可以在非首页场景则可以根据业务特点、团队技术栈特点权衡使用。

  • 其他优化手段。。。

这里需要强调一点,正如上一篇文章优化总体原则“要定位应用性能瓶颈”里面提到的,“不要主观猜测,让性能评测数据说话”,大家在优化过程中“要使用恰当的性能评测工具和评测方法”,“要抓重点,才能有的放矢,重点解决核心瓶颈问题”,这样才能事半功倍。

业务流程优化

业务流程优化需要开发同学梳理现有的业务逻辑和实现方式,找到其中可以优化的流程进行优化。如果优化会修改用户交互,还需要和产品同学沟通优化是否合理。

这一部分由于每个业务都有自己的特点,很难输出通用的方案。这里可以举个我们做启动优化时候的case供大家参考。现在很多应用启动都会有自定义的开机屏,用展示广告或者平台活动,1688客户端也不例外。我们在梳理启动流程的时候,发现开机屏的初始化和首页多tab的初始化顺序是并行的,而从用户交互流程和业务流程来看,如果有开机屏数据的话,先加载开机屏,等开机屏加载完成后再加载首页才是比较合理的流程。经过优化后,我们闪屏出现的速度比之前提高了近50%,即提升了用户启动体验又不会影响业务流程。

交互优化

我们讲性能优化时候,往往会更多关注所优化对象的数据指标,比如启动时间从2s优化到1s,页面加载时间从1.5s优化到1s,滑动帧率从40fps优化到55fps。但如果再深一层挖掘优化背后的目的就会发现,性能数据指标并不是优化的目的,真正的目的是改善用户体验。通过改善用户体验带来更高的商业价值。因此我们在一些场景下,作为技术同学,其实可以将视野放的更广一些,从业务流程和交互视角出发,有时候会有带来更优秀的用户体验。

比如我们在页面加载过程中常用的loading图或加载进度图,就可以减少用户等待页面加载过程中的焦虑,从而减少页面的跳失率。近些年很多应用还对简单的loading图做了交互优化,使用和页面结构大体相似的骨架图(甚至可以从交互前面的页面带来一些已有的数据,诸如图片、标题等),从而给用户带来更优秀的体验。

交互优化需要技术同学以用户体验为出发点,发挥创造力,结合技术手段和非技术手段带来最优的用户体验!

启动器

上面讲了启动任务的提前、延后、并行等方案,抽象来看就是启动任务的管理。如果每个应用开发同学在添加启动任务的时候,都需要手动去管理代码调用顺序、优先级、所在队列等,会极大影响开发效率。而且由于开发同学水平和对启动流程了解程度的参差不齐,会导致随着业务版本迭代,越来越多不恰当的启动任务堆积在启动过程中,随之而来的负面影响就是启动越来越慢,用户的体验也越来越差,甚至可能会影响到启动的跳失率。

为了统一管理启动任务,我们开发了自己的启动器AMLauncher。之所以开发我们自己的启动器,是因为我们之前使用BeeHive来管理启动任务和模块服务解耦。经过长时间版本迭代,已经积累了近百个启动任务。然尔BeeHive本身启动任务的编排无法满足我们的诉求,分布在各个模块的启动任务又耦合了BeeHive的代码和配置协议,三方的启动器无法很好的适配BeeHive。因此我们选择在兼容BeeHive的配置协议和代码基础上,开发AMLauncher,满足我们对启动任务管理的诉求。

AMLauncher的整体架构图如下:

下面就几个特性做简单介绍

  • 场景化能力。AMLauncher可以自定义扩展启动场景,不同的启动任务可以根据其特点配置不同的场景。比如一些启动过程中必须的启动任务就可以配置为launch(启动场景),一些重要但是启动非必须的任务可以配置home(启动后场景),一些不重要任务可配置为idle(runloop 闲时场景),如果需要其他自定义的场景(比如push场景),只需要规定场景ID,然后在场景触发时刻触发即可。这样开发同学就可以轻松的实现启动任务的提前、延后等功能。

  • 并发能力。AMLauncher里面有主队列、串行队列和并行队列。可以根据需要配置队列类型

  • 动态编排能力。AMLauncher启动任务可以通过云端下发,从而具备动态编排的能力

  • 任务监控。每个加入到配置中的启动任务,AMLauncher都会自动监控其运行时间和运行成功状态,然后上传到云端。为后续性能优化和线上大盘数据提供支持。

  • **弹缩能力。**由于上述几点能力的支持,我们可以方便的根据用户特征、机型特征做动态的启动任务编排,从而达到启动总体最优的效果。下面针对启动弹缩能力展开讨论。

启动动态弹缩

《体验优化,我们有点不一样》中,我们论述了做体验不一样的方案。主要特点有:

  • 容器化架构下的性能优化

  • 性能动态弹缩方案

  • 性能数据和业务数据关联,挖掘性能优化价值

  • 性能优化对B、潜B等不同类型的人群下钻

在启动优化的方案上,我们也使用了性能动态弹缩方案,同时简单地挖掘性能优化价值。整个弹缩方案如下:

整个方案的大体流程是根据用户不同的特征,结合远端雷荷波平台下发的配置,输入到客户端上的决策引擎(算法模型或固定规则),然后决策不同的性能弹缩策略。最后引入AB实验来验证优化效果,以及性能优化对业务指标的影响。

在启动弹缩上,我们对上述方案做了最小闭环的验证。通过远端雷荷波平台下发的配置,然后根据用户机型性能特点、用户启动类型,做启动任务的差异化处理。比如在一些性能十分差的机型上,为了减少用户启动可交互时间,会将诸如weex windvane等sdk初始化从home场景后延迟到idle场景。在针对换端启动场景,会将落地页所需要的启动任务提前,以提升换端场景的启动速度。

启动时间防腐化

正如上一篇文章所讲,性能优化是一场持久战。不仅需要我们能够有短期的攻坚能力,还需要我们能够长久保持住战果。我们在启动时间防腐化上的方案如下:

  • 开发阶段防腐。在main函数前,对load函数做了时间监控,防止在load函数里做过于耗时的操作。在main函数后,由于统一使用启动器来管理启动任务,我们将启动任务配置的提交权限做了卡口,每次新增&修改任务都需要做严格的code review,防止因为开发同学随意添加启动任务,影响启动性能。

  • 测试阶段防腐。我们和测试同学一起,建立每个版本的性能自动化测试保障。希望能够在测试阶段就发现性能问题,防止版本迭代造成性能腐化。

  • 灰度&线上阶段防腐。线上阶段我们会对启动性能做监控和预警,在触发性能预警阈值时及时排查,解决在开发和测试阶段没有发现的性能问题。

2.优化效果

经过一段时间启动专项攻坚,1688主客客户端启动时间有了明显的进步。目前全版本的启动时间月同比下降了42.9%,在集团众多App的横向比较中也名列前茅。随着启动时间的下降冷启动跳失率也下降了54%。这样印证了我们之前提到的性能优化时间可以带来一定的业务价值。

除了大盘时间,我们针对C类用户、潜B用户和B类用户的启动时间做了下钻,结合高、中、低性能机型维度的下钻,从而为启动弹缩提供了数据特征维度。

3.总结与展望

本文简单阐述了我们启动优化的方案,对“启动器”和“动态弹缩”做了相对详细的介绍。由于时间有限,启动动态弹缩方案中使用了固定规则的决策方案,在优化验证上也没有使用严格的AB验证。后续我们在“页面加载时间优化”专项,希望能够在动态弹缩方案上使用一些简单的算法模型来提供弹缩的效果,在优化验证上引入严格的AB实验来验证性能数据和业务数据的效果。