干货 | Trip.com APP 启动优化实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"引言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"启动是用户对App的第一印象,对于用户体验尤为重要,所以我们花了很多时间在启动时间的优化上。本文将分享Trip.com App的启动优化实践,从分析App启动的过程开始,在了解启动流程的基础上制定大的优化原则和小的具体方案,希望能对大家有所帮助。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、App启动的流程分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想做启动优化,首先要了解清楚启动的各个流程,然后才能对各个环节去做针对性措施。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"借用WWDC对启动阶段的定义图:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ce\/cea0e917c4380ae7dc613e8cb63f7b63.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 System Interface"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加载App可执行文件"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Load dylibs"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加载动态链接器"},{"type":"codeinline","content":[{"type":"text","text":"dyld"}]},{"type":"text","text":" ,"},{"type":"codeinline","content":[{"type":"text","text":"dyld"}]},{"type":"text","text":"会递归加载App依赖的动态库,然后执行符号绑定"},{"type":"codeinline","content":[{"type":"text","text":"Rebase"}]},{"type":"text","text":", "},{"type":"codeinline","content":[{"type":"text","text":"Bind"}]},{"type":"text","text":"。一般应用会加载 100 到 400 个 dylib 文件,幸运是大部分是系统库,且系统会在操作系统启动时计算和缓存系统动态库。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Apple为了解决安全问题,引入"},{"type":"codeinline","content":[{"type":"text","text":"ASLR"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Code Sign"}]},{"type":"text","text":",如果不作符号修正,程序将没法正常运行,所以会有Rebase和Bind过程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f0\/f0572ce87eb87cb3dda7b1ca67af8e80.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rebase"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在镜像内部调整指针的指向,其实就是将内部指针都加上偏移量(Slide=实际新地址-旧地址)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Bind"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修正指外部的指针,比如上图中malloc,这个符号不存在于我们App的Mach-O中,需要从外部的镜像中获取,这时候就需要Bind操作把这个关联起来。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"libSystem init"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用系统的一些初始化方法,这部分一般时间比较固定,可以不用太关注。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 Runtime Init"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Objc和Swift的初始化"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过"},{"type":"codeinline","content":[{"type":"text","text":"_dyld_objc_notify_register"}]},{"type":"text","text":"注册回调,在image加载完时初始化语言相关。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加载category"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面语言初始化完之后,会加载所有category,处理category的所有方法,协议和属性等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用所有+load"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也是通过向 dyld 注册回调,在image加载完时,通过"},{"type":"codeinline","content":[{"type":"text","text":"load_images"}]},{"type":"text","text":" 触发,处理该image相关的所有+load方法,按照继承层级依次调用:父类+load→子类+load→category +load,注意category的+load不会覆盖原类。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"调用C++的构造函数属性函数 "},{"type":"codeinline","content":[{"type":"text","text":"attribute((constructor))"}]},{"type":"text","text":" "}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.3 UIKit Init"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实例化 UIApplication 和 UIApplicationDelegate"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"开始事件处理和系统集成"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.4 Application Init"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这部分是我们熟悉的 UIApplicationDelegate 的几个生命周期调用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"application:willFinishLaunchingWithOptions:"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"application:didFinishLaunchingWithOptions:"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"applicationDidBecomeActive:"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"scene:willConnectToSession:options:"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sceneWillEnterForeground:"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sceneDidBecomeActive:"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.5 Initial Frame Render"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里是App渲染第一帧,主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。这里面布局计算,图片解码,图层树的递归commit到Render Server等都是可能影响耗时的点,所以要特别注意。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.6 Extended"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里按照苹果的定义,是异步获取数据展示界面的逻辑。比如我们首页要从网络请求数据然后展示最新数据在页面上。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、针对启动的各个流程我们能做什么"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 总体原则"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不管哪个流程,我们都想尽量遵循下面两个原则:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"删"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"删的原则是指,对App启动和运行不是必须的任务,或者跟首页渲染第一帧无关的任务,都从启动流程中删除。对于删除的任务,可以进行懒加载的形式,需要时再调用;也可以换到其他的时机去触发,比如首页渲染完之后。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"压"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"压的原则是指,对App启动和运行必须的任务,或者直接影响首页渲染第一帧的任务,都尽可能压缩其运行时间。至于做法,可以是优化方法内的实现,使其运行更快;也可以将方法执行的线程切换到子线程,以并发的形式降低其对整个启动过程的影响。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 具体方案"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.1 减少动态库"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"动态库的加载在启动阶段是必须的,所以我们要尽量减少非必要的动态库。对此我们做了以下几点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)梳理所有动态库,将用不到的或者可以简单替代的动态库删除"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以通过"},{"type":"codeinline","content":[{"type":"text","text":"otool -L xxx.app\/xxx"}]},{"type":"text","text":" 或者打开打包后的产物,从xxx.app\/Frameworks路径中找到所有动态库,逐个筛选,将其中可以废弃和替代的动态库删除。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)通过推进社区(第三方SDK)将现有动态库转成静态库"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为依赖了第三方SDK,我们是不包含源码的,所以这部分需要推进社区提供静态库的版本,或者通过cocoapods等工具打包SDK的静态库版本。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)将我们自己的SDK编译成静态库"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于我们自己的SDK,因为有源码,所以直接修改"},{"type":"codeinline","content":[{"type":"text","text":"MACH_O_TYPE"}]},{"type":"text","text":" 为"},{"type":"codeinline","content":[{"type":"text","text":"Static Library"}]},{"type":"text","text":" 重新打包即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)App最低支持系统版本升级到12.2"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为iOS在12.2版本及以上才内置了Swift的支持,所以在此之前Swift的动态库都是随着 App下发的,也在xxx.app\/Frameworks 里。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然,这个决策是会直接应用到用户和订单的,所以是要有数据支持的,我们是根据用户占比到达某个阈值才支持12.2的。如果允许,甚至可以升级到iOS 13,因为iOS13以上dlyd3做了很多加载和缓存的优化。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.2 删除无用代码"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果符号越多,很显然Rebase和Bind的处理时间就会越长,Objc的初始化也受影响,所以我们需要尽可能减少代码:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)通过逆向二进制或者生成linkmap,解析所有方法(TEXT.text)和引用到的方法(__DATA _objcselrefs),找出无用方法删除"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)解析所有类(DATA.objcclasslist)和引用到的类(DATA.objcclassrefs),找出无用的类删除"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)使用第三方工具或者clang扫描重复代码,精简去重"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)使用"},{"type":"codeinline","content":[{"type":"text","text":"LLVM_LTO"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"GCC_OPTIMIZATION_LEVEL"}]},{"type":"text","text":"等其他编译选项优化二进制大小"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.3 合并category"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"合并category,可以减少category加载时的耗时。不过这部分收益不大,并且也会影响编程习惯,所以我们并没有投入很多时间,不再赘述。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.4 删除+load"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前会有很多代码为了省事,加到了+load中,这部分很显然占用启动时间,所以尽量要把这其中的代码转移,可以放到initialize中懒加载,或者放到启动任务中并发执行,尽量减少这部分的影响。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Xcode调试时,可以通过正则添加所有+load方法的断点"},{"type":"codeinline","content":[{"type":"text","text":"br s -r \"\\+\\[.+ load\\]$\""}]},{"type":"text","text":" ,然后使用"},{"type":"codeinline","content":[{"type":"text","text":"br list"}]},{"type":"text","text":"打印出所有+load列表,这样方便我们定位所有+load。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.5 UIApplication子类优化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了减少UIKit Init的时间,可以对UIApplication的子类初始化工作优化。我们这部分不存在,所以没有做什么工作。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.6 启动任务并发"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想象一下,如果"},{"type":"codeinline","content":[{"type":"text","text":"application:didFinishLaunchingWithOptions:"}]},{"type":"text","text":"里面执行的所有启动任务不作任何处理,那么代码框架将会很乱,你的优化也只能单点单点去做。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我们将"},{"type":"codeinline","content":[{"type":"text","text":"application:didFinishLaunchingWithOptions:"}]},{"type":"text","text":"阶段所有方法任务化,一个任务做一种类型的事。任务拆分好之后,就可以根据任务之间的相关性,选择哪些任务是可以并发执行,哪些任务是必须有依赖关系前后执行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5c\/5cd64985aa766e62ec4baf9a031bc038.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ef\/efefd0878a9060f7d46f99b6ac098584.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然,任务的拆分颗粒度也很重要,拆分太粗的话,很难达到最优的组合,可能一个任务里的方法之间仍然有并行的空间。拆分太细的话,也有可能导致同一时间并发数太多,造成额外的线程切换开销。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.7 I\/O处理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尤其要注意启动阶段的I\/O,一般出现于读取磁盘中的文件,比如配置文件等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Instrument→App Launch 去查看启动过程就会发现,如果主线程执行出现很多灰色的块,那就是I\/O,找到这些I\/O产生的方法,尽量在子线程并发执行,避免阻塞主线程。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.8 首页数据的预加载和懒加载"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首页上有很多数据要加载,比如图片、上次缓存在本地的数据等等,这些数据的加载如果在写代码时不作特殊处理,那会在主线程执行,不知不觉就会有很多耗时。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)预加载"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对首页渲染必须的数据,比如一个icon,或者一个翻译的数据,我们通过在启动任务(之前提到的拆分的并发任务)中新增加一个预加载启动任务,专门负责在"},{"type":"codeinline","content":[{"type":"text","text":"application:didFinishLaunchingWithOptions:"}]},{"type":"text","text":" 的过程中并发执行数据的获取。因为获取数据大多比较耗时,所以放在子线程充分利用启动阶段的空闲。同时这类任务大多数是I\/O操作,并不会占用太多CPU资源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更进一步,其实可以对首页用到的资源在运行时作个标记,记录到磁盘,下次启动的时候读取这个记录,对用到的资源进行提前预加载,这样避免hard code很多资源名在代码中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)懒加载"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首页的数据往往很多,但并不是一开始要全部用到。可以对数据作区分,和第一屏展示无关的,使用懒加载,真正用到的时候再去加载。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.9 二进制重排"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)"},{"type":"codeinline","content":[{"type":"text","text":"page fault"}]},{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于虚拟内存的机制,应用启动时不会把所有数据加载到内存,而是以页为单位逐步从磁盘中加载,内存中的虚拟地址和磁盘中的物理地址有个映射关系。当程序执行时,如果发现要访问的东西不在内存里,就会触发一次"},{"type":"codeinline","content":[{"type":"text","text":"page fault"}]},{"type":"text","text":" ,去磁盘中加载新的一页。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"启动阶段有很多方法要调用,而这些方法在Mach-O中的位置又是在编译时确认的。如果有10个方法刚好在不同页,可能就要产生10次"},{"type":"codeinline","content":[{"type":"text","text":"page fault"}]},{"type":"text","text":" 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二进制重排要做的就是将启动阶段要用到的方法,在编译时提前确定,通过.order文件告诉编译器,这样这些方法会排布在Mach-O的最前面,之前的10次"},{"type":"codeinline","content":[{"type":"text","text":"page fault"}]},{"type":"text","text":" 很可能就变成一两次"},{"type":"codeinline","content":[{"type":"text","text":"page fault"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过在Other C Flags中添加"},{"type":"codeinline","content":[{"type":"text","text":"-fsanitize-coverage=func,trace-pc-guard"}]},{"type":"text","text":" 再通过"},{"type":"codeinline","content":[{"type":"text","text":"__sanitizer_cov_trace_pc_guard"}]},{"type":"text","text":"记录启动阶段所有方法的调用,再将这些写入到.order文件中,在Xcode的"},{"type":"codeinline","content":[{"type":"text","text":"ORDER_FILE"}]},{"type":"text","text":" 设置中配置即可生效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过测试,我们的二进制重排大概优化100-200ms。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.10 其他通用手段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对启动任务和首页渲染阶段,通用的手段是通过instrument,profile出耗时长的任务,对任务针对性地做方法优化。如果有的方法是第三方库的,那就需要推进社区去更新。我们在做的过程中给Firebase和Google的一些SDK提了很多issue,对方开发人员配合很积极,对我们帮助很大。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、成果如何"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过长期的优化,以上手段全部用完之后,我们的启动时间从原来的2秒,优化到1秒以内。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"总结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在优化启动时间的过程中,我们的收获不仅是对启动时间的优化,也对系统的启动机制有了更深的了解,同时优化了我们自己的代码,使其变得更加更加健壮和高性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者简介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Shanks,携程移动开发专家,关注移动端基础技术。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文转载自:携程技术中心(ID:ctriptech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/smWjs2X8HWvcvKW_DSXYJA","title":"xxx","type":null},"content":[{"type":"text","text":"干货 | Trip.com APP 启动优化实践"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章