货拉拉技术

货拉拉出行iOS用户端启动优化实践

一. 引言

我们通过埋点发现部分用户启动耗时可以达到 10秒左右,有的甚至可以达到 20秒左右,主要集中在中低端机型( iPhone6、 iPhone7、 iPhone8系列);试想一个场景:你和女朋友约会马上要迟到了,于是决定打车,打开出行 A``pp,结果启动了十几秒,当时会是什么心态。那么如何提升 A``pp的启动速度呢?接下来我将用我的实践过程来和大家探讨。

二.优化效果

针对启动耗时埋点的统计数据,进行分析总结,得出是有必要针对启动耗时做一定优化,尤其在低版本机型,低内存情况下,提升这部分使用 App用户的体验效果,增加用户的留存率。

这是我们做了几期优化后的成果

优化前:

Image

优化后:

Image

从最新的 1.``4``.``0版本启动时长的统计数据看:

  • 小于 3s的占比达到了 99.7``7``%

  • 3s-4s的占比为 0.1``3``%

  • 4s-5s的占比为 0.0``4``%

  • 大于 5s的占比为 0.``06``%

从优化效果看,相对还算比较不错,因此这里对启动相关的优化做一下总结,主要从如下几方面来分析:

  • 项目中哪些方法导致启动耗时长

  • 为什么这些方法会导致启动耗时长

  • 怎么优化这些方法减少启动耗时

而至于怎么查找导致启动耗时长的方法和后续的治理、监控机制、告警机制等,可以查看之前其他项目组发布的这篇文章:货拉拉用户端体验优化--启动优化篇

三. 详细优化过程

优化的细节比较多,总体可以分为三期:

  • 第一期:主要对耗时长的函数做治理以及低版本机型做了特殊处理;

  • 第二期:深入业务,对整个启动过程中的业务流程做优化;

  • 第三期:重点解决被动启动场景下的耗时影响。

第一期:耗时长的函数与低版本机型做处理

第一期优化的时候,首先通过计算方法耗时以及 Instrument的 AppLaunch, TimeProfiler等工具,在低版本的机型比如 iPhone6、iPhone7,低内存( App启动是系统内存小于 100M的情况下),找到所有函数耗时大于 100MS以上的函数,然后将这些函数进行整理。

发现低版本低内存情况:如下函数存在着严重的耗时:

  • 启动的 launchView,耗时大约在 150ms-250ms之间

  • 神策的初始化函数,耗时大约在 300ms-650ms之间

  • 地图SDK初始化函数,耗时大约在 250ms-500ms之间

  • toastView初始化,耗时大约在 100ms-260ms之间

  • 首页高德地图初始化, 耗时大约在 600ms-1200ms之间

  • 首页选址栏背景框动效图加载,耗时大约在 300ms-600ms之间

同时针对这些在低版本机型低内存情况下耗时长的函数,统计这些函数在高版本机型比如说 iPhone11,iPhone12等机型上,低内存情况下的耗时进行对比。

发现在高版本低内存情况下,以上的函数耗时情况如下:

  • 启动的 launchView,耗时大约在 50ms-100ms之间

  • 神策的初始化函数,耗时大约在 100ms-200ms之间

  • 地图SDK初始化函数,耗时大约在 50ms-150ms之间

  • toastView初始化,耗时大约在 30ms-80ms之间

  • 首页高德地图初始化, 耗时大约在 200ms-350ms之间

  • 首页选址栏背景框动效图加载,耗时大约在 80ms-200ms之间

从以上函数在高低版本机型耗时可以看出,耗时函数之间的差别还是比较大,因此对这些函数进行进一步分析,同时应该针对手机的高低版本采取不同的处理方案。

  1. ### launchView 优化

项目中的 launchView每次启动的时候,都会生成,但是只有在首次安装,弹出隐私弹框的时候才会用到。而 launchView加载的图片是放在项目文件夹里面,没有用 Assets.catalog来进行管理,同时图片文件较大,可以进行无损压缩。

原因分析

之所以将 launchView加载的图片,进行无损压缩,是为了减小图片的大小,这样图片数据二进制相对也减少,读取的时间也能减少。虽然放到 Assets.catalog管理的时候, Assets.catalog会再次对图片进行内部压缩处理,但通过对比 ipa包解析出来的图片文件大小,可以看出无损压缩前后, ipa的对应的 launchView的图片大小,还是有减少。

而之所以选择 Assets.catalog来管理:

一方面是 Assets.catalog管理的图片,由于 Assets.catalog在编译后,生成 .car文件, .car文件里面存储图片相关各种属性以及图片的二进制数据,在图片进行加载的时候,可以通过 mmap加载.car文件,解析 .car获取到图片相关基础属性,然后通过 BOM快速找到图片二进制,并进行加载,加载时间相对放在文件夹的图片加载时间,有至少一个量级的减少,同时 Assets.catalog对图片资源的采用苹果内部压缩算法,在解压缩和解码等方面效率更高,有利于加快图片的显示。

另一方面 Assets.catalog会识别具有相似属性的图像,例如透明度、颜色空间、色域等,并且能够把它们组织到一个较大的图集中,这样就无需存储额外相同的元数据了,同样在提交 AppStore后,会进行 Slicing对 @2x,@3x的图片进行分割到不同设备上,从而减小包体积大小,

优化方案

因为 launchView只在首次安装弹出隐私弹框的时候才用到,因此对 launchView的生成添加判断,只在首次安装才生成 launchView,同时将 launchView加载的图片,先进行无损压缩,然后放到 Assets.catalog进行管理。

优化效果

在低版本机型低内存情况,经优化启动的launchView,耗时大约在 80ms-180ms之间,耗时缩短了 60ms左右。

  1. ### 神策的初始化函数

神策函数初始化的主要耗时在于两方面:

  • 设置神策埋点公共参数里面获取 wifi地址的埋点

  • 上报神策激活事件里面的 WKWebView的 userAgent埋点

原因分析

  • 获取 wifi地址之所以耗时在于,该方法需要读取 Wifi列表文件涉及到 IO和 copy操作。

  1. +(NSString*)wifiBssid{

  2. NSString*bssid =@"";

  3. NSArray*interFaces =CFBridgingRelease(CNCopySupportedInterfaces());

  4. NSDictionary*info;

  5. for(NSString*ifname in interFaces){

  6. info =CFBridgingRelease(CNCopyCurrentNetworkInfo(( __bridge CFStringRef)ifname));

  7. if(info &&[info count]){

  8. break;

  9. }

  10. }

  11. if([info.allKeys containsObject:@"BSSID"]){

  12. bssid = info[@"BSSID"];

  13. }

  14. return bssid;

  15. }

  • 获取WKWebView的 userAgent耗时在于: WKWebView的首次创建,因为 WKWebView是一个多进程组件, WKWebView的创建过程如下:

Image

从 WKWebView的创建过程我们知道,首次创建 WKWebView,需要外部单独创建一个 WKWebView进程来处理网络请求、内容加载/渲染,之后还需要将 WKWebView进程与 App里面的 WKWebView内存对象进行关联,这里外部创建 WKWebView进程,相对耗时较大。

优化方案

神策 埋点 公共参数里面获取 wifi 地址耗时优化方案:

  • 因为获取 Wifi地址,只有在当前手机网络为 Wifi情况下才能获取到,如果当前手机是移动网络,获取到的必然为空,所以移动网络情况下是没有必要执行获取 Wifi地址操作的,因此获取 Wifi地址之前添加一层网络判断,只有在 Wifi网络才去获取 Wifi地址。

  • 因为神策初始化是在启动的时候,所以这里获取网络状态可以用 Alamofire库的 NetworkReachabilityManager去获取当前网络状态,因为 NetworkReachabilityManager在首次初始化会去获取一次,因此可以保证使用的时候已经获取到了网络状态。而 AFNetworkReachabilityManager因为创建初始化的时候默认网络状态是 AFNetworkReachabilityStatusUnknown,而是等到监听回调才去更新网络状态,无法保证首次使用的时候已经获取到网络状态了。

上报神策激活事件里面的 WKWebView 的 userAgent 埋点 耗时优化方案:

  • 因为神策激活事件埋点的上报跟产品确定了后,是可以稍微延迟上报的,而且 WKWebView的 userAgent,只跟目前手机系统的版本和手机型号有关,手机型号是固定的,因此可以通过系统的版本号进行磁盘缓存。因此整体优化逻辑如下:

Image

优化效果

  • 神策埋点公共参数里面获取 wifi地址优化后,因为我们是出行产品,打车的时候大部分是在移动网络下,因此这个 wifi造成的启动耗时和卡顿大幅减小。

  • 上报神策激活事件里面的 WKWebView的 userAgent埋点优化后,启动耗时在低版本低内存情况下,耗时减小了大约 200ms上下。

  1. ### 地图SDK初始化

地图 SDK是我们地图部封装的一个二方库,里面除了必要的地图初始化外,还有关于开启位置上报和配置 IQKeyboardManager两个操作。

原因分析

  • 这里开启位置上报因为涉及到地图侧 AB缓存读取等操作,导致在低版本手机上耗时相对较长

  • IQKeyboardManager的配置,因为 IQKeyboardManager单例的调用,触发 IQKeyboardManager的初始化,这里也会有 base64图片资源转换为 UIImage等操作,在低版本手机上耗时也相对较长。

  • 跟地图侧研发确认后,这两者都是可以延迟到首页的 viewDidAppear即启动完成后再去执行。

优化方案

因为这两者都可以延迟到启动统计完成之后再去执行,而且只在低版本机型上耗时才比较大,因此添加机型判断,如果是低版本机型就做延迟执行操作,如果是高版本机型则保留执行顺序。

优化效果

在低版本机型上做了延迟加载,地图 SDK初始化操作的整体耗时,可以缩短差不多 80-200ms,左右。

  1. ### toastView初始化

这里的 toastView是一个二方库使用到的 toastView,主要是支付, IM这些统一的组件二方库,而非业务侧自定义的 toastView。

原因分析

toastView是个单例,初始化的时候,会触发里面 toastView图标的加载,涉及到 IO操作在低版本低内存机型上耗时较大。

优化方案

因为该 toastView是二方库才用到,而像支付,IM等二方库,都是在启动完成之后,点击进入不同页面才有可能用到 toastView,因此是可以延迟到启动统计完成之后再去执行,而且只在低版本机型上耗时才比较大,因此添加机型判断,如果是低版本机型就做延迟执行操作,如果是高版本机型则保留执行顺序。

优化效果

在低版本机型上做了延迟加载,地图SDK初始化操作的整体耗时,可以缩短差不多 100-260ms。

  1. ### 首页高德地图初始化

a.背景

首页高德地图创建,在低版本机型上调用 MAMapView创建方法耗时就可以达到 800ms左右,而在高版本机型上耗时就只有 200ms左右

b. 原因分析

因为看不到高德源码,因此将这个问题反馈给高德地图,但对方一直没有进行优化。

c.优化方案

因为在低版本机型上耗时与高版本机型差异较大,而首页地图展示在低版本机型比如 iPhone6,iPhone7等上面延迟放到首页 viewDidAppear之后,对比起来效果也没差太多,因此跟领导商量后决定将首页地图在低版本机型上延迟到启动统计完成之后再去执行,如果是高版本机型则保留之前执行顺序。

d.优化效果

在低版本机型上对首页高德地图初始化做了延迟加载,使得整体的启动耗时,可以缩短差不多 600ms左右。

  1. ### 首页选址栏背景框动效图加载

首页地址选址栏的背景动效,地图侧那边用的是 140张选址背景图循环播放来做的动效,这里涉及大量图片的 IO操作,总体耗时比较大。

原因分析

这里本来想用 lottie动画来做选址背景动效,但由于 lottie动画 iOS平台有部分属性不支持,导致动画效果不理想,后来采用了多张图片滚动播放做动效的效果。但这里涉及大量图片 IO且都在主线程上,耗时比较大,因此将该问题反馈给地图侧。

优化方案

后面经过地图侧内部商量后,找 UI减少了图片数量,然后先显示一张背景占位图,异步去加载相关图片,加载完成之后,在回到主线程显示背景动效。

优化效果

在低版本机型上统计通过该优化,使得整体耗时,差不多可以缩短 300ms左右。

  1. ### 其他优化

a.异步串行队列执行的任务

以上就是第一期最主要的优化,同样第一期也将几个操作放入到异步串行队列去执行:

  • 初始化 HDID

  • 初始化 ASA广告

  • 初始化安全 SDK

这三者本来耗时就不高,之所以只放这几个操作,是因为之前将一些三方库初始化,放到异步串行队列执行,会出现一些异常情况,比如将微信分享 SDK的初始化在启动时异步执行,会偶现个别手机微信分享会卡顿,放回主线程初始化就没问题,也在微信开放平台提了工单,但都没有得到可靠回复。因此出于稳定性考虑,只放了这三个初始化到异步串行队列去执行。

而第一期里面有一个核心内容就是启动管理器,上文提到的启动任务延迟执行和启动任务异步执行,这些都是放到启动管理器里面去管理。

b.启动管理器

启动器主要做了如下三方面事情:

  1. ##### 判断当前手机设备型号是否为低版本机型

首先我们看一下设备型号组成:设备名称 + 大版本 + 小版本。

Image

首先获取设备的型号,然后获取设备型号里面的大版本号,然后跟指定的型号版本(默认为 11)做对比,低于指定版本,为低端机型。

  1. // 低型号 手机 (iPhoneX及其以下)

  2. privateclass func isLowerPhoneDevice(_ lowPhoneVersionLimit:Int)->Bool{

  3. let tipStr ="iPhone"

  4. var isLowMdapAssignDeviceVersion =falselet deviceName = phoneDeviceName()

  5. if deviceName.contains(tipStr){

  6. let array = deviceName.components(separatedBy:",")

  7. if array.count ==2{

  8. let phoneVersionName =(array.first ??"")asNSString

  9. if phoneVersionName.length > tipStr.count {

  10. let tipRange =NSRange(location: tipStr.count, length: phoneVersionName.length - tipStr.count)

  11. let version = phoneVersionName.substring(with: tipRange)

  12. let deviceVersion =Int(version)??0

  13. if deviceVersion < lowPhoneVersionLimit {

  14. isLowMdapAssignDeviceVersion =true}

  15. }

  16. }

  17. }

  18. return isLowMdapAssignDeviceVersion

  19. }

  20. /// 设备的名字

  21. privateclass func phoneDeviceName()->String{

  22. var systemInfo = utsname()

  23. uname(&systemInfo)

  24. let machineMirror =Mirror(reflecting: systemInfo.machine)

  25. let identifier = machineMirror.children.reduce(""){ identifier, element in guard let value = element.value as?Int8, value !=0else{return identifier }

  26. return identifier +String(UnicodeScalar(UInt8(value)))

  27. }

  28. return identifier

  29. }

  1. ##### 任务异步执行

创建异步执行串行队列,添加异步任务,并异步执行

  1. // 添加 异步执行 任务

  2. public func addAsynQueueTaskBlock(taskBlock:FJFLaunchTaskBlock?){

  3. self.addTaskBlock(taskBlock,.asynQueue)

  4. }

  5. // 依据 任务 所属 类型 添加 任务

  6. public func addTaskBlock(_ taskBlock:FJFLaunchTaskBlock?, _ blongType:FJFLaunchTaskBlongType){

  7. let task_item =DispatchWorkItem{

  8. taskBlock?()

  9. }

  10. switch(blongType){

  11. case.mainQueue:

  12. task_item.perform()

  13. case.asynQueue:

  14. self.asyn_queue.async(execute: task_item)

  15. }

  16. }

  1. ##### 主线程任务延迟执行

依据手机型号版本判断是否将需要延期执行,如果为低版本机型,则将任务添加到延迟执行数组,对任务依据优先级进行排序;若为高版本直接执行任务。

  1. // 依据 手机版 判断 是否 需要延期任务

  2. public func checkTaskBlockNeedDelay(_ taskBlock:FJFLaunchTaskBlock?,

  3. _ blongType:FJFLaunchTaskBlongType,

  4. _ taskPriorityType:FJFLaunchTaskPriorityType=.defaultPriority){

  5. ifself.isLowMdapAssignPhoneVersion {

  6. self.addDelayTaskBlock(taskBlock, blongType, taskPriorityType)

  7. }else{

  8. self.addTaskBlock(taskBlock, blongType)

  9. }

  10. }

  11. // 依据 任务 所属 类型 添加 延迟执行 任务

  12. public func addDelayTaskBlock(_ taskBlock:FJFLaunchTaskBlock?,

  13. _ blongType:FJFLaunchTaskBlongType,

  14. _ taskPriorityType:FJFLaunchTaskPriorityType=.defaultPriority){

  15. self.delayLaunchTaskArray.append(FJFLaunchTask.init(taskBlock, blongType, taskPriorityType))

  16. self.delayLaunchTaskArray =self.delayLaunchTaskArray.sorted(by:{(obj1:FJFLaunchTask, obj2:FJFLaunchTask)->Boolinreturn obj1.taskPriorityType.rawValue > obj2.taskPriorityType.rawValue

  17. })

  18. }

执行延迟任务的时候,监听 runloop周期,当 runloop进入即将进入休眠或者退出时候,去执行延迟任务,任务执行完毕后,关闭 runloop监听。

  1. // 执行 延迟 任务

  2. public func execDelayTask(){

  3. self.startRunloopIdleMonitor()

  4. }

  5. /// 开启 主线程 runloop 空闲 监听

  6. public func startRunloopIdleMonitor(){

  7. //获取当前RunLoop

  8. let runLoop:CFRunLoop=CFRunLoopGetMain()

  9. //定义一个观察者

  10. let activities =CFRunLoopActivity.beforeWaiting.rawValue |CFRunLoopActivity.exit.rawValue

  11. mainObserver =CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities,true,0){[weak self](_, _)in guard letself=selfelse{

  12. return}

  13. iflet tmpTask =self.delayLaunchTaskArray.first {

  14. self.addTaskBlock(tmpTask.taskBlock, tmpTask.taskBlongType)

  15. self.delayLaunchTaskArray.removeFirst()

  16. }

  17. ifself.delayLaunchTaskArray.count ==0{

  18. self.endRunloopIdleMonitor()

  19. }

  20. }

  21. iflet tmpObserver = mainObserver {

  22. //添加当前RunLoop的观察者

  23. CFRunLoopAddObserver(runLoop, tmpObserver,.commonModes)

  24. }

  25. }

这里之所以将延迟执行的任务放到 runloop空闲的时候去执行,主要原因是为了避免卡顿,因为延迟执行的任务,一般是耗时任务,如果多个任务在同一个 runloop周期执行,会导致当前 runloop周期过期繁忙,主线程无法响应其他操作,因此进行对延迟执行任务进行分发,分发到不同的 runloop空闲时候去执行,可以有效防止卡顿。

经过第一个版本的优化后,启动耗时占比 3s以内的占比差不多在 98上线波动:

Image

第二期:对业务流程做优化

因为在第一期优化的同时,也进行了相关函数耗时的上报和统计,针对上报的数据,对不同版本机型和内存情况的方法耗时进行整理,同时也跟其他同事的卡顿和包体积优化一起整合,进行了第二期的优化。第二期优化主要有几点:

  • 启动流程中无用的类和流程优化

  • 包体积优化:二方库的动态库改为静态库, 无用类和三方库删除等

  • 卡顿治理优化: 对城市列表接口优化,对数据处理优化,对 UI布局优化等。

  1. ### 启动流程中无用的类和流程优化

启动流程这里,随着迭代进行,有很多类和流程是可以优化的。

原因分析

比如首页 UI的改造,有些类已经没有用到,而有些类是可选项,特定场景才用到,但是为了使用方便,设置了默认值,因此默认值生成的时候也有一定耗时;而有些流程只在登录或者未登录情况下才使用,但未做状态区分,比如一键登录预取号逻辑,只需要在未登录情况下调用。因此针对这些流程进行整理优化。

优化方案

  • 对无用的类和代码,找对应负责人进行确认,确认无用后,删除。

  • 对于特定场景用到的类,声明为可选项,使用的时候添加判断,不设置默认值,不使用懒加载

  • 对于区分登录或未登录状态的流程,进行校对,添加状态判断,减少不必要的调用。

优化效果

因为去掉了无用类的生成和区分的一些状态,减小了不必要的函数调用,整体耗时,缩短了 80ms-150ms。

  1. ### 包体积优化:二方库的动态库改为静态库, 无用类和三方库删除等

其他同事进行包体积优化,将一些二方库改为静态库,同时删除了无用的类和三方库,以及将图片放到 Asset.catalog里面管理等操作来减小包体积。

原因分析

  • 将二方库从动态库改为静态库,比如支付组件, IM组件等,减少了启动链接里面的 rebase和 bind操作,也相应减少了启动耗时

  • 无用类和无用三方库的删除,减少了启动阶段,类的加载,相应也减少了启动耗时

  • 图片资源文件压缩以及 Asset.catalog来管理,加快了图片的加载时间,也减少启动耗时

优化方案

  • 通过修改对应二方库的 podspec文件,将库类型指定为静态库 s.static_framework=true

  • 通过 WBBlades工具对 swift代码进行扫描,找出无用的类和代码,然后再进行二次确认删除无用的类。

  • 通过对三方,二方库依赖关系进行分析和确认,去掉无用的库

  • 通过 imageOptiom对资源的无损压缩,然后采用 Asset.catalog管理图片

优化效果

因为包体积优化是在同事分支上,合并到迭代集成分支的时候,测试了下,整体耗时缩短了 60ms-150ms左右。

  1. ### 卡顿治理优化

其他同事进行卡顿治理的时候,顺带将启动流程中的统计到的卡顿也进行治理了,比如拉取城市列表数据处理,对广告接口数据处理,对安全中心等布局的优化等。

原因分析

启动耗时优化其实算是卡顿治理的一个子集,因此其他同事在做卡顿治理的时候,涉及到启动相关的治理,也相应的能减少启动耗时。

优化方案

  • 城市列表数据是每次启动都会依据版本号去拉取,拉取回来后会进行省市等处理,然后进行缓存,因为城市列表数据量较大,处理都在主线程上,因此在低版本机型会偶尔产生卡顿,尤其是第一次安装拉取,没有版本缓存的时候,这里同事创建了专门用来处理卡顿的串行队列,异步去执行处理流程和存储流程,处理完成后再到主线程回调。

  • 对于首页广告列表等同样较大数据量的请求处理,同事也是放到专门用来处理卡顿的串行队列,异步去执行处理流程,处理完成后再到主线程回调。

  • 对于安全中心、轮播图里面存在的布局频繁更新等问题,则是跟对应负责人讨论后,减少了调用频次,以及处理了进入后台定时器依赖执行等问题,对启动耗时也有一定降低。

优化效果

这边关于启动流程的卡顿的优化,也从整体上减小了启动耗时。

经过第二个版本的优化后,启动耗时占比 3s以内的占比差不多在 98.6上线波动:

Image

第三期:被动启动优化

第三期最主要的优化是一个被动启动的优化。

因为我们项目是一个出行项目,会有对应的定位更新功能,因此会存在由于位置更新导致的被动启动,而且占比相对较大,从神策统计数据统计数据看,被动启动占比可以达到 30%多。

Image

什么是被动启动

我们把 App由 iOS系统触发、 App仍然处于后台的启动,称之为 App的被动启动。

在 iOS7之后,苹果新增了后台应用程序刷新功能,该功能允许操作系统在一定的时间间隔内(这个时间间隔根据用户不同的操作习惯而有所不同,可能是几个小时,也可能是几天)拉起应用程序并同时让其保持在后台,以便应用程序可以获取最新的数据并更新相关内容,从而可以确保用户在打开应用程序的时候可以第一时间查看到最新的内容。

例如:新闻或者社交媒体类型的应用程序,可以使用这个功能在后台获取到最新的数据内容,在用户打开应用程序是可以缩短应用程序启动和获取内容展示的等待时间,最终提升产品的用户体验。

应用程序的被动启动,应用程序的第一个页面( UIViewController)也会被加载,也会触发启动耗时的统计。

但这里用户并没有打开应用程序,更没有浏览第一个页面,整个后台应用刷新的过程,对于用户而言,完全是透明的,无感知的。

项目哪些功能会触发被动启动

使用 Xcode创建新的应用程序,默认情况下后台刷新功能是关闭的,我们可以在 Capabilities标签中开启 BackgroundModes,然后就可以勾选所需要功能了。

Image

目前项目里面勾选了如下三种功能:

  • Location updates:

这种模式下,按照苹果文档的官方说法,如果你在 Xcode的 background modes中开启了 Locationupdates,并且用户授权了 Always访问位置权限,而且项目代码里面也注册了 region monitoring或者 significant change service``, 那么你把App 杀掉后,系统仍然可以唤醒你的App,但此次唤醒大概就给你10s的时间处理地址位置数据,如果你执行处理地理位置是个长时间任务你需要向系统申请额外的时间进行处理: beginBackgroundTaskWithName:expirationHandler:

Location and Maps Programming Guide

  • Background fetch

开启该选项,需要设置一个时间间隔,从而让 iOS在一定间隔时间内在后台启动该应用,执行指定数据的获取工作,而此过程最多只能执行30s。

虽然开启该选项,默认情况下 iOS是不进行后台获取的, minimumBackgroundFetchInterval的默认值 UIApplicationBackgroundFetchIntervalNever,你可以将值设置为 UIApplicationBackgroundFetchIntervalMinimum,要求系统尽可能频繁地去调用。

  • Remote notifications

开启该选项是支持静默推送,它有别于一般的推送,应用收到此类推送后,不会有任何的界面提示,而当应用退出或者挂起时收到此类推送, iOS 也会启动或者唤醒对应的应用。例如一个阅读应用,用户订阅的博客更新了,那么可以先发一个静默推送,应用收到此种推送后,可以先把用户订阅的博客内容都下载好,再通知用户,这样用户一打开应用就可以马上开始阅读。收到静默推送,会回调对应的回调方法,而此回调方法最多只能执行 30 秒钟。

被动启动对启动耗时影响

App的被动启动的时候 App此时仍然是处于后台的,不像正常启动 App已经回到前台。

App处于后台的时候, App的任务优先级相对处于前台的进程是低的,各种资源比如 CPU时间片,内存等的分配等优先级也相对低,经测试当 App被动启动的时,如果前台有其他 App在运行,比如高德地图这种内存占比大的进程,这时候被动启动的 App,在执行比如图片加载、 WKWebView等耗时任务的时间消耗相对比正常启动时候来得大。

这种情况来说启动统计的耗时也相对较大,而且也容易产生卡顿,因此从 App稳定性来说是有必要针对被动启动进行优化的。

优化方案

  • 针对 BackgroundModes里面的会导致被动启动的选项,根据项目需要去掉 Backgroundfetch选项,减少被动启动次数

  • 针对 App的启动,通过 applicationState判断是否为 UIApplicationStateBackground,如果是判断为被动启动,可以在启动耗时统计的时候,将被动启动的统计数据不上报。

优化效果

对被动启动的启动耗时统计数据过滤不进行上报后,整体启动耗时 3s以内占比,可以达到 99.7左右。

四. 总结

在这个启动耗时优化的过程中,可以总结出如下四点相关的经验:

  • 最关键的点:找到耗时高的函数。先治理耗时相对最高的那些函数,分析耗时高的本质原因,然后采取对应的方法进行优化,优化之后进行 codeReview,跟同事一起探讨优化操作可能产生的影响。

  • 测试环境一致:测试时应尽量在最差环境下测试。比如低版本机型,低内存情况下来统计启动耗时,像我会写一个可以不断消耗内存的 Demo进程,然后将手机内存消耗到 100M以下,甚至更低,然后分别去测试低版本机型的启动耗时情况,在这种情况下得到的统计数据,再去推测本次优化可以达到的预期,相对来说比较准确。

  • 要有降级方案:启动耗时优化,之所以称为优化,就代表这是一项锦上添花的操作。因此针对每次版本优化,最好有降级方案,防止本次优化带来其他额外问题的时候,可以及时回退到以前方案,保证 App整体的稳定性。

  • 防劣化建设: 要想长期保持优秀的启动数据,防劣化建设必不可少。首先 codereview关注代码和业务逻辑对稳定性产生的影响;接着对二方库、三方库的升级和引入进行审查和评估;然后每次迭代集成包的时候,测试和研发都会通过移动测试平台-MTC,对各种机型跑一遍性能测试并输出测试报告,而我自身电脑也安装了 appium自动化测试,也会对 iPhone6等低版本机型、低内存下,跑下启动时长数据,得到最小值、最大值、均值,并和上次迭代做对比,如果差距比较大,就去分析此次迭代代码,找到原因并联系相关开发人员修复。最后上线后关注每日启动时长数据,并设置告警机制,当某天统计数据突然劣化,就通过相关埋点和日志数据分析原因,并优化。