|
一、背景
冷启动时长是App性能的重要指标。作为用户体验的第一道“门”,它直接决定了用户对App的第一印象。美团外卖iOS客户端于2013年11月启动,至今经历了数十个版本的迭代开发。产品形态不断完善,业务功能日趋复杂。与此同时,外卖App也从原来的独立商业App演变为平台App。整合闪购、跑腿等新业务。因此,App冷启动时需要完成越来越复杂的工作,这给App的冷启动性能带来了挑战。对此,我们团队根据业务形态的变化和外卖APP的特点,对冷启动进行了持续、有针对性的优化工作,旨在呈现更流畅的用户体验。
2. 冷启动定义
一般来说,大家将iOS冷启动的流程定义为:从用户点击App图标开始,到完成方法执行。这个过程主要分为两个阶段:
但当执行完成后,用户还没有看到App的主界面,无法开始使用App。例如,在一个外卖应用中,应用还需要做一些初始化工作,然后经过定位、首页请求、首页渲染等过程后,用户才能真正看到数据内容并开始使用它。我们认为此时冷启动已经完成。我们将这个过程定义为T3。
综上所述,外卖APP的冷启动流程定义为:从用户点击App图标到用户可以看到App主界面内容的过程,即T1+T2+T3。 App冷启动过程中,这三个阶段都有很多可以优化的地方。
3. 问题状态、性能和库存问题
经过数十个版本的迭代开发,美团外卖iOS客户端在冷启动过程中积累了多个性能问题。解决这些性能瓶颈是冷启动优化工作的首要目标。这些问题主要包括:
注:启动项的定义是App启动过程中需要完成的某项任务。我们称之为启动项。例如某个SDK的初始化、某个功能的预加载等。
性能增量问题
一般来说,App在早期阶段,冷启动不会出现明显的性能问题。冷启动性能问题并不是某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也逐渐延长。最后,当我们注意到它并想要优化它时,问题已经变得棘手了。外卖应用性能问题的增加主要来自于启动项的增加。随着版本的迭代,启动过程中启动项任务简单粗暴地堆积起来。如果每个版本冷启动时间增加0.1s,那么几个版本之后,冷启动时间将会明显增加。
四、治理思路
管理冷启动性能问题有三个主要目标:
解决库存问题:优化当前性能瓶颈点,优化启动流程,缩短冷启动时间。
管控增量问题:规范冷启动流程,通过代码范式和文档指导后续冷启动流程代码的维护,控制时间增量。
完善监控:完善冷启动性能指标监控,收集更详细的数据,及时发现性能问题。
5、规范启动流程
截至2017年底,美团外卖用户规模已达2.5亿,美团外卖App也完成了从支持单一业务的App到支持多种业务的平台App的演变(多终端的推广)复用美团外卖iOS,支持和思考),公司的一些新兴业务也被整合到了外卖App中。下图是外卖APP的架构图。外卖架构主要分为三层。最底层是基本组件层。中间层是外卖平台层。平台层向下管理基础组件,向上为业务组件提供统一的适配接口。上层是基础组件层,包括外卖业务的子业务组件(外卖App和美团App中的外卖渠道可以复用子业务组件)以及相连的其他非外卖业务。
App的平台化为业务各方提供了高效、标准化的统一平台,但同时平台化和业务快速迭代也带来了冷启动的问题:
现有启动项堆积严重,导致启动速度变慢。
新的启动项缺乏添加范例,混乱,修改有风险,并且难以阅读和维护。
面对这个问题,我们首先对当前启动过程中的所有启动项进行了梳理,然后为App平台设计了一种新的启动项管理方式:分阶段启动和启动项自注册。
分阶段推出
早期因为业务比较简单,所以所有的启动项都不区分,简单的堆砌成方法。但随着业务的增长,越来越多的启动项代码堆积在一起,导致性能不佳,代码臃肿混乱。
通过对SDK的梳理和分析,我们发现启动项也需要根据完成的任务进行分类。有些启动项是启动后需要立即执行的操作,例如崩溃监控、统计报告等,否则会导致信息收集不足;有些启动项需要在较早的时间节点完成,比如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项是可以延迟的,比如一些自定义配置、调用一些业务服务等。支付SDK、地图SDK等。我们做的分阶段启动就是先把启动流程合理划分为几个启动阶段,然后分配每个启动项根据其执行的优先级分配到相应的启动阶段,优先级较高的进入启动阶段。较早的阶段,优先级较低的阶段被放置在较晚的阶段。
以下是我们对美团外卖App启动阶段的重新定义,对所有启动项进行了梳理和重新分类,映射到合理的启动阶段。一方面,这样可以推迟不需要提前执行的启动项的执行,缩短启动时间;另一方面可以对启动项进行分类,方便后续的阅读和维护。然后将这些规则实现为启动项的维护文档,指导后续启动项的添加和维护。
通过上述工作,我们整理出了十几个可以推迟的启动项,约占所有启动项的30%,有效优化了启动项占用的冷启动时间。
启动项自注册
确定了启动项的分阶段启动计划后,我们面临的问题是如何执行这些启动项。比较容易想到的解决方案是在启动时创建一个启动管理器,然后读取所有启动项,然后在时间节点到来时触发启动项的执行。这种方法有两个问题:
所有启动项必须预先写入文件中(.m 文件中,或组织在 .plist 文件中)。这种集中式的编写方式会导致代码臃肿,难以阅读和维护。
启动项代码无法复用:启动项无法收敛到子业务库,需要在外卖APP和美团APP中重复实现,与外卖APP平台化方向不符。
我们希望的是启动项维护方式是可插拔的,启动项和业务模块之间没有耦合,一个实现两端可以复用。下图是我们采用的启动项管理方式,我们称之为启动项自注册:在子业务模块内部定义启动项,封装成方法,自声明启动阶段(例如:启动项A,在独立App中可以声明在stage中执行,而在美团App中则声明在stage中执行)。这样,两端启动项得到复用,不相关的启动项相互隔离,增加/删除启动项更加方便。
那么如何声明启动项的启动阶段呢?如何在合适的时机触发启动项的执行?从代码上来说,一个启动项最终会对应一个函数的执行,所以只要运行时能够获取到函数指针,就可以触发启动项。美团平台开发的组件启动管理基础架构 Kylin 正是这样做的:Kylin 的核心思想是在编译时将数据(如函数指针)写入到可执行文件的段中,然后从运行时响应的段。操作(调用函数)。
为什么要使用借用段?原因是能够覆盖所有启动阶段,例如 main() 之前的阶段。
Kylin实现原理简述:Clang提供了很多编译器函数,可以完成不同的功能。其中之一是 () 函数。 ()函数提供了读写二进制段的能力。它可以将一些编译时可以确定的常量写入数据段。在具体实现上,主要分为两个部分:编译时和运行时。编译时,编译器会将带有((()))标记的数据写入指定的数据段,例如向数据段写入{key(key代表不同的启动阶段),*}对。运行时,在适当的时间节点,根据key读出函数指针,完成函数调用。
可以将上面的方法封装成宏来简化代码。以调用宏(“Key”,“Value”)为例,最终会扩展为:
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;">__attribute__((used, section(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"__DATA"</span> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">","</span> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"__kylin__"</span>))) <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">static</span> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">const</span> KLN_DATA __kylin__0 = (KLN_DATA){(KLN_DATA_HEADER){<span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"Key"</span>, KLN_STRING, KLN_IS_ARRAY}, <span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"Value"</span>};<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
使用该示例,编译器将启动函数注册到启动阶段 A:
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;">KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 启动项代码A</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;">KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 启动项代码B</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
在启动过程中,所有注册到该时间节点的启动项都会在启动阶段被触发。这样,几乎不需要额外的辅助代码,我们就以非常简洁的方式完成了启动项的自注册。
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;">- (<span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">BOOL</span>)application:(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">UIApplication</span> *)application didFinishLaunchingWithOptions:(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">NSDictionary</span> *)launchOptions {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 其他逻辑</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A]; <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 在此触发所有注册到STAGE_KEY_A时间节点的启动项</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 其他逻辑</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">return</span> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(174, 135, 250);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">YES</span>;<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
在完成现有启动项的梳理和优化后,我们还输出了后续启动项的添加和维护规范,规范了后续启动项的分类原则、优先级和启动阶段。目的是控制性能问题的增量,保证优化结果。
6. 优化main()之前
在调用main()函数之前,基本上所有的工作都是由操作系统完成的。开发者可以干预的地方并不多,所以这时候想要优化的话,首先要了解操作系统在 main() 之前做了什么。操作系统在main()之前所做的就是将可执行文件(Mach-O格式)加载到内存空间中,然后加载动态链接库dyld,然后进行一系列的动态链接操作和初始化操作(加载、绑定,以及初始化方法)。网上这方面的资料很多,但重复率很高。这是 WWDC 主题:App Time。
加载过程——从exec()到main()
真正的加载过程从exec()函数开始,这是一个系统调用。操作系统首先为进程分配一块内存空间,然后执行以下操作:
将App对应的可执行文件加载到内存中。
将 Dyld 加载到内存中。
Dyld 执行动态链接。
下面我们简单分析一下Dyld在每个阶段都做了什么:
最后dyld会调用main()函数,main()会调用(),main()过程就完成了。
了解了main()之前的加载过程后,我们可以分析一下影响T1时间的一些因素:
加载的动态库越多,启动速度就越慢。
ObjC 类,它们的方法越多,启动就越慢。
ObjC 的负载越多,启动就越慢。
C的函数越多,启动就越慢。
C++ 的静态对象越多,启动速度就越慢。
针对以上几点,我们做了以下优化工作:
代码瘦身
随着业务迭代,不断添加新的代码,无用的代码和资源文件也被丢弃。然而,在项目中,无用的代码和文件常常被遗弃在角落里,没有及时清理。这些无用的部分一方面增加了App的包大小,另一方面也减慢了App的冷启动速度。因此,及时清理这些无用的代码和资源是非常有必要的。
通过了解Mach-O文件,可以知道::包含了代码中的所有方法,并且包含了所有使用到的方法的引用。通过计算两组之间的差异,您可以获得所有未使用的方法。代码。核心方法如下,详细请参考:
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;">def referenced_selectors(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">path</span>):<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> re_sel = re.compile(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"__TEXT:__objc_methname:(.+)"</span>) //获取所有方法<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> refs = set()<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">lines</span> = <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">os</span>.<span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">popen</span>(<span class="" style="max-width: 100%;line-height: inherit;color: rgb(238, 220, 112);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">"/usr/bin/otool -v -s __DATA __objc_selrefs %s"</span> % <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">path</span>).readlines() # ios & mac //真正被使用的方法<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">for</span> line <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">in</span> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">lines</span>:<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> results = re_sel.findall(line)<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">if</span> results:<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> refs.add(results[<span class="" style="max-width: 100%;line-height: inherit;color: rgb(174, 135, 250);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">0</span>])<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(248, 35, 117);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">return</span> refs<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
通过这个方法,我们检查了十几个无用的类和250+个无用的方法。
+负载优化
目前iOS App中或多或少都写了一些+load方法,用于在App启动时执行一些操作。 +load方法是分阶段执行的,但是+load方法过多会减慢启动速度。对于大中型应用程序来说,更是如此。通过对App中+load方法的分析,我们发现,虽然很多代码需要在App启动时较早的时候进行初始化,但并不需要像+load那样处于非常靠前的位置,可以延迟进行直到App冷启动。某个时间节点,比如一些路由操作。其实+load也可以当作启动项,所以在替换+load方法的具体实现上,我们仍然使用上面的Kylin方法。
使用示例:
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 用WMAPP_BUSINESS_INIT_AFTER_HOMELOADING声明替换+load声明即可,不需其他改动</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />WMAPP_BUSINESS_INIT_AFTER_HOMELOADING() { <br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> <span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 原+load方法中的代码</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /></span></code></pre></p>
<p><pre style="margin-top: 0px;margin-bottom: 0px;padding: 0px;max-width: 100%;font-size: inherit;color: inherit;line-height: inherit;box-sizing: border-box !important;overflow-wrap: break-word !important;"> <code class="" style="margin-right: 2px;margin-left: 2px;padding: 0.5em;max-width: 100%;line-height: 18px;font-size: 14px;letter-spacing: 0px;font-family: Consolas, Inconsolata, Courier, monospace;border-radius: 0px;color: rgb(169, 183, 198);background: rgb(40, 43, 46);box-sizing: border-box !important;overflow-wrap: normal !important;display: block !important;overflow: auto !important;"><span style="max-width: 100%;font-size: 15px;box-sizing: border-box !important;overflow-wrap: break-word !important;"><span class="" style="max-width: 100%;line-height: inherit;color: rgb(128, 128, 128);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">// 在某个合适的时机触发注册到该阶段的所有方法,如冷启动结束后</span><br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />[<span class="" style="max-width: 100%;line-height: inherit;color: rgb(91, 218, 237);box-sizing: border-box !important;overflow-wrap: inherit !important;word-break: inherit !important;">[KLNKylin sharedInstance</span>] executeArrayForKey:@kWMAPP_BUSINESS_INITIALIZATION_AFTER_HOMELOADING_KEY] <br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}</span></code></pre></p>
7、优化耗时操作
main()之后的主要工作是各种启动项的执行(上面已经介绍过),例如主界面的构建等。资源的加载,例如图片I/O、图片解码、文档等。这些操作中可能隐藏着一些耗时的操作,仅仅通过阅读是很难发现的。如何发现这些耗时点?找到合适的工具,你会事半功倍。
时间
Time是Xcode自带的时间性能分析工具。它按照固定的时间间隔跟踪每个线程的堆栈信息。通过统计比较时间间隔之间的堆栈状态,可以计算出某个方法执行了多长时间,并得到一个大概值。 。关于如何使用Time,网上有很多使用教程,这里不再赘述,附上使用文档:with Swift:。
火焰图
除了Time之外,火焰图也是分析CPU时间消耗的有力工具。与Time相比,火焰图更加清晰。火焰图分析的产物是调用堆栈的耗时图片。它被称为火焰图,因为整个图看起来像一个舞动的火焰。火焰的尖端是调用堆栈的顶部,底部是堆栈的底部。垂直方向代表调用栈的深度,水平方向代表消耗的时间。网格的宽度越大,它就越有可能成为瓶颈。分析火焰图主要是看比较宽的火焰,特别注意那些类似“平顶山”的火焰。以下是美团平台开发的性能分析工具的分析效果图:
通过火焰图的分析,我们发现了冷启动过程中的诸多问题,并成功优化了0.3S+的时间。优化内容总结如下:
8.优化串行操作
冷启动过程中,很多操作是串联执行的,几个任务是串联执行的,时间肯定很长。若能改串为并,冷启动时间可大大缩短。
如何使用闪屏页面
现在很多应用程序启动时并不直接进入主页。相反,他们会向用户显示一个持续很短时间的闪屏页面。如果使用得当,这个闪屏页面可以帮助我们节省一些启动时间。因为当一个App相对复杂的时候,启动时第一次构建App的UI是一个耗时的过程。假设这个时间是0.2秒。如果我们先构建首页UI,然后添加这个闪屏页面,那么在冷启动时,App实际上会卡住0.2秒,但是如果我们先使用闪屏页面作为App,那么构建过程就会是非常快。因为只有一个简单的闪屏页面,而且这个会展示给用户很短的一段时间,那么我们就可以利用这段时间来构建首页UI,一石二鸟。 |
|