|
贝壳找房iOS启动优化实践
贝壳找房iOS启动优化实践
中平、东顺、凯凯
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年02月24日 00:29
1 前言启动速度是App的核心性能指标之一,不仅影响用户体验,还影响拉新、留存、转化等业务。提升启动速度,保障用户体验,促进业务更好地发展,是非常有价值的。本文介绍贝壳找房的iOS启动优化工作,包含三个方面:度量线上启动速度、线下启动性能分析和启动速度治理实践。2 度量线上启动速度2.1 概述度量线上启动速度就是获取线上启动时间。目前,获取线上启动时间的方案有两种:官方提供和自研采集。为了更好地度量、监控线上启动速度,我们采用自研的分阶段采集方案。2.2 官方方案iOS13推出MetricKit框架,用于收集电量和性能数据。它将启动时间的起点定义为:进程创建时刻,终点定义为:第一个CA::transaction::commit() ,启动图完全消失后的第一帧。Xcode 11后,启动时间数据可以通过Window->Organizer->Launch Time查看。MetricKit将启动过程拆分成System Interface、Runtime Init、UIKit Init、Application Init 和 First Frame Render、Extended等六个阶段。System InterfaceApp初始化的准备工作,主要有:加载主二进制 、启动dyld、加载动态库等,然后进行libSystem初始化,即一些系统底层组件的初始化。Runtime Init初始化Objective-C的Runtime,执行+load和static initializer初始化函数等。UIKit Init实例化UIApplication和UIApplicationDelegate,开始事件处理,与系统交互。Application Init主要是处理生命周期回调,包括UIApplicationDelegate App 生命周期回调,如:application:didFinishLaunchingWithOptions: 和 application:willFinishLaunchingWithOptions:。Initial Frame Render首帧渲染,主要是创建、布局和绘制视图等工作,并把准备好的第一帧提交给Render Server进程去渲染。Extended首帧之后的工作,如异步获取数据、视图更新、用户交互等。2.3 自研分阶段采集为了更好地度量和监控启动速度,我们将启动过程拆分成T1、T2 和 T3三个阶段,分别采集对应阶段的耗时。T1阶段:fork函数创建进程到main函数执行,对应MetricKit的System Interface 和 Runtime Init,我们通过系统函数sysctl获取进程创建的时间戳。T2阶段:main函数开始到 AppDelegate类中 application:didFinishLaunchingWithOptions:执行结束,对应MetricKit的UIKit Init和 Application Init。T3阶段:application:didFinishLaunchingWithOptions执行结束到首页viewDidAppear,首页viewDidAppear时机稍稍晚于MetricKit定义的Initial Frame Render,但实现成本低,且更接近用户的视觉感受。采用分阶段采集方案能基本满足度量和监控线上启动速度的要求。在方案实现上,尽可能减少对业务、性能的影响。如结合Method Swizzling实现无侵入获取关键节点。采用线上AB对业务和性能的影响进行控制。3 线下启动性能分析3.1 概述优化启动速度之前,最重要工作的是利用工具发现启动性能问题。目前,应用比较广的工具是官方提供的Instrument。Instrument中的Time Profiler可以发现App运行过程中堆栈耗时情况,System Trace能记录App运行过程中线程和内存的调度情况。两者结合能帮助分析iOS启动性能问题。Xcode 11后,Instrument新增了App Launch工具,它可以看成Time Profiler和System Trace的能力聚合。在性能分析过程中,Instrument 工具遇到工具启动速度较慢、函数堆栈无时序,排查问题成本较高等问题。而我们期望有一个可以内置在App中的性能分析工具:方便易用、能检测出绝大部分的耗时问题、有时序性、可扩展。3.2 自研性能分析工具基于方便易用、后续扩展等诉求,我们设计并实现了iOS性能分析工具BKTimeProfiler,主要功能:覆盖启动T1、T2和T3 全阶段的耗时问题检测。统计T1阶段的+load函数、static Initializers的数量和耗时。统计T2和T3阶段的函数执行顺序、时间、执行次数及其对应的函数堆栈;并显示超过执行时间阈值、耗时百分比阈值的函数集合。统计启动阶段的所有https请求。工具特点:项目无侵入、零成本接入。使用操作简单,80%+的耗时问题可以通过工具检测发现。扩展性强,如可以结合自动化测试,进行线下启动数据信息的收集,完善App启动耗时的监控,提升启动防劣化的能力。3.3 性能分析工具的关键技术3.3.1 动态检测+load方法添加自定义动态库,在动态库的__attribute__(constructor)初始化函数中去 hook 所有 +load 方法来动态获取+load方法的名称和耗时。通过 getsectiondata 函数,读取编译时期写入到Mach-O 文件 DATA 段的__objc_nlclslist和__objc_nlcatlis section(对应 no lazy class 列表和 no lazy category 列表),获取定义了+load方法的类和分类,从而hook这些类和分类中的+load方法,以此获得+load的名称和耗时。3.3.2 获取运行的Objective-C函数及其堆栈耗时使用fishhook对objc_msgSend函数进行hook,借助汇编实现交换的方法,需注意寄存器的保存和恢复。在objc_msgSend方法调用前后,记录调用方法的信息、开始和结束时间,同时记录堆栈深度等。根据自定义设置的最大堆栈深度和最小耗时检测,将符合条件的方法保存下来并还原成方法堆栈样式。3.3.3 依赖注入根据强符号和弱符号来实现依赖注入:利用weak symbol(__attribute__ ((weak))修饰)提供默认实现,如首页控制器名称,函数堆栈深度及耗时阈值等。业务可以使用strong symbol来实现注入,更改对应设置。Method Swizzling可以实现对Objective-C函数的hook,比如hook AppDelegate的didFinishLaunchingWithOptions,UIViewController的viewDidAppear函数。结合Method Swizzling和强、弱符号使得项目中无须引入工具文件,也可以使用其功能。3.4 统一线下测试环境相对稳定的线下测试环境,对启动性能分析非常重要,总结如下:使用Release包。使用真机,选择合适的测试设备:结合线上50分位的启动数据,找到对应的测试机型,此类机型50分位和线上总体50分位的启动数据比较接近,比较好帮助评估优化效果。测试设备的要求:温度正常、电量正常、退出iCloud账号等。确定测试条件:优化T2、T3阶段,杀死进程,静置2-3分钟,重启App即可。完整的启动速度测试关闭手机,静置2-3分钟,重启手机,重新打开App即可。手机重启后,第一次打开App,或者 App更新后,dyld3会构建启动闭包(启动闭包是一个缓存),构建虽然耗时,但是却能够提升后续使用App的启动速度。线下测试中,可以结合BKTimeProfiler实时查看效果,部分函数堆栈耗时效果如下:4 启动速度治理实践4.1 概述贝壳App集合了二手、新房、租赁、装修、IM等业务,启动速度治理需要根据待治理的问题及其收益,结合各个团队情况,确定好治理的优先级。示意如下:其中,T2和T3阶段的性能问题主要有:本地和网络图片加载、同步文件I/O、大数据序列化和反序列化、耗时API等。4.2 治理+load方法T1的initializer阶段主要有+load 和static initializer两类初始化函数,其中,+load是Objective-C的运行时特性,由于执行时机足够早、线程安全、只执行一次,开发中习惯性将很多Method Swizzling和初始化工作放在+load中,这些都增大了T1阶段的耗时。治理+load需要减少现有+load方法和管控新增+load方法。减少现有+load办法:非必要在+load中执行的,懒加载或延迟执行。可采用的方案有:利用section()函数将把数据(如函数指针)写入到可执行文件的__DATA段中,运行时根据需求再从__DATA段取出数据进行相应的操作(调用函数)。管控新增+load方法:确定+load黑白名单制度,将目前治理后的+load纳入白名单。结合CI,对编译产物LinkMap文件,通过grep命令找到+load函数集合。对比编译产物的+load函数集合 和 +load白名单,发现新增的+load方法,IM、邮件提醒;如果确认是必要的,更新到白名单,否则纳入黑名单,推动后续治理。4.3 减少无用代码虽然Xcode开启Dead Code Stripping选项后,可以让C/C++等静态语言会在编译器Link的时候移除未使用的代码。但是因为Objective-C 是建立在运行时的,清理 Objective-C代码需要深度治理,以减少T1阶段耗时。基于减少无用代码的目标,将优化工作拆分成三个阶段:Pod治理、0 PV页面清理、无用类删除,前两个阶段ROI收益较高。4.3.1 Pod治理找到Pod依赖关系图谱,推荐使用开源工具poddotify 。确认各个Pod归属的业务方,推动业务方确认无用的Pod。推动具有相同功能的Pod清理,如网络库、数据解析、Swizzle方案。4.3.2 0 PV 页面清理根据线上埋点统计,获得PV大于0的页面集合,利用LinkMap近似获取代码中各个业务的页面集合。根据 代码中页面集合和 PV大于0的页面集合的差集 ,获得0 PV集合及其所属业务,然后推动各个业务治理。该方案可近似获取0 PV 页面集合,开发人员需要先和PM确定,然后推动移除页面代码。4.3.3 无用类删除利用利用Mach-O文件找出无引用的类( __objc_classlist - (__objc_classrefs+__objc_superrefs)),帮助然后根据LinkMap文件,将这些划分给所属的业务方进一步确认,确认无误后删除;需要注意的是:无引用类不等于无用的类,可能是动态调用等因素导致的误判。删除工作需要先确认再操作。4.4 启动项优化App启动项有:异常监控、埋点统计、网络库初始化、Router初始化、业务项初始化等。优化启动项需要从两个方面入手,第一:基于业务重要程度编排启动项;第二,治理启动项内部耗时。前者需要熟悉业务,后者需要关注性能。启动任务编排:编排原则:根据业务优先级编排,高优及时加载,低优按需、延迟、异步加载。编排示意如下:启动项内部耗时治理,主要是三类问题:同步执行,内部因为I/O操作、大数据序列化和反序列化等导致的耗时。异步执行,当下无耗时,但后续回调会Block主线程。底层API耗时,如获取Wifi信息的C 函数: CNCopySupportedInterfaces();用于获取某个地址的符号信息的C函数:dladdr()4.5 启动性能提升性能问题主要表现在:图片加载耗时、同步文件I/O耗时、大数据序列化和反序列化耗时、API耗时等,根据不同情况分别优化。4.5.1 开屏广告图片加载优化启动阶段,开屏广告图片是服务端下发的,和运营、促活相关。优化时需要兼顾性能和业务。启动时,根据URL判断磁盘中是否存在图片,而不可以将图片加载到内存再来判断图片是否存在,这是因为加载图片到内存会触发CPU的图片解码这样的耗时操作。启动任务完成后,首页出现前,如果异步加载图片并显示,会在中低端机上出现"闪"的效果,不符合业务预期;简单的同步加载又影响体验。采用的方案:同步读取图片文件到内存(耗时比较小),然后利用系统的GPU解码,在性能和业务诉求间取平衡。4.5.2 本地图片加载优化启动阶段,某些机型使用[UIImage imageNamed:]系统API加载耗时不符合预期,如iPhone 8, iOS 13+上加载一张小图2-6ms耗时;基于此设计了两套方案:预加载,默认图片绘制。预加载:iOS 9+之后,[UIImage imageNamed:] 是线程安全的,我们在启动阶段,异步批量加载首页需要的小图集合。默认图片绘制:默认图片是中间有ICON、背景色根据主题确认的,这些图片是使用同步CPU绘制的,过多绘制耗时也比较严重。基于此,我们根据要展示的大小、背景色RGBA属性和ICON名,将绘制好的UIImage缓存,以此来提高绘制UIImage的利用率,同时处理好内存警告。4.5.3 其他性能优化文件I/O、大数据序列化和反序列化等耗时操作尽可能异步处理。网络请求打散,减少冗余的网络请求。接口拆分、延迟优化等提升关键业务数据的请求响应速度,运营Banner首页直出、首页分屏加载,提升性能的同时,保障业务正常进行。优化底层API耗时。如在基础库中用到了dladdrC函数用来判断某些高频动作,这个函数可以获取某个地址的符号信息,但是在测试中发现耗时超过阈值,且调用次数多,累积耗时非常大。4.5.4 二进制重排原理:当进程访问一个虚拟内存页而对应的物理内存不存在时,会触发一次Page In,从而分配物理内存;基于程序局部性原理,可以将启动相关的函数尽可能编排在一起。从而降低Page In数。方案:基于SanitizerCoverage(Clang内置的代码覆盖具)收集启动阶段的函数调用。其核心是:编译器会动在自定义的函数中插__sanitizer_cov_trace_pc_guard函数,基于此收集函数执行,生成order件;并结合Xcode中Build Setting的Linking->Order File指定order。说明:重排比较适合低端机、iOS 13下;iOS 13后,如果是__TEXT段的页,Page In不再解密、验证,而解密比IO耗时还大。iPhone 6s后,物理内存页大小是16KB,而之前设备内存页是4KB,16KB意味着可以加载更多的内容。5 总结启动优化并非追求单方面性能最优,而是尽可能保证全局最优。我们在优化启动速度的同时,也在治理包体积,优化页面加载性能,完善App功能的可用性。这些方向的优化指标之间并非都是正相关的,存在部分负相关。如:App首页的加载性能优化,对提升App启动速度是正向的。但为了提升无网、弱网情况下App页面的可用性,对必要的数据进行了持久化存储,这些数据在启动时候是需要同步加载的,对提升启动速度是负向的。启动优化不是完全独立的,它和其他性能优化工作是相辅相成的。启动优化工作中沉淀的性能分析工具不仅帮助分析启动性能,还支持了页面加载性能分析。包瘦身和启动T1阶段优化有共同目标,即代码瘦身。几个优化方向互相助力,起到了1+1>2的效果。启动优化不是纯技术工作,它和业务密不可分。我们在提升App启动速度的同时,和市场、产品团队合作,建设外链唤起检测工具AppSchemeInspect,完善外链双端唤起效果一致性验收能力。优化广告开屏、首页运营Banner位,首页信息流功能,补齐新用户下载安装到首次打开目标落地页能力,提升拉新用户转化、老客召回的成功率,减少市场投放的成本,促进业务增长。做好性能优化工作,为业务的良好发展奠定基础。随着业务高速发展,App从单一的应用变成多用户角色,多业务聚合的平台级应用。优化App性能,提升用户体验,支撑业务快速迭代,为业务发展保驾护航。
预览时标签不可点
移动端37大前端69移动端 · 目录#移动端上一篇Flutter的Widget之间的通信方式及状态管理下一篇iOS14 动态配置Widget开发与实践关闭更多小程序广告搜索「undefined」网络结果
|
|