一. 引言

咱们经过埋点发现部分用户发动耗时能够到达10秒左右,有的乃至能够到达20秒左右,首要会集在中低端机型(iPhone6iPhone7iPhone8系列);试想一个场景:你和女朋友约会马上要迟到了,于是决定打车,翻开出行App,成果发动了十几秒,当时会是什么心态。那么怎样提高App的发动速度呢?接下来我将用我的实践进程来和大家探讨。

二.优化作用

针对发动耗时埋点的计算数据,进行剖析总结,得出是有必要针对发动耗时做必定优化,尤其在低版别机型,低内存状况下,提高这部分运用App用户的体会作用,增加用户的留存率。

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

优化前:

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

优化后:

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

从最新的1.4.6版别发动时长的计算数据看:

  • 小于3s的占比到达了99.82%
  • 3s-4s的占比为0.07%
  • 4s-5s的占比为0.03%
  • 大于5s的占比为0.08%

从优化作用看,相对还算比较不错,因而这儿对发动相关的优化做一下总结,首要从如下几方面来剖析:

  • 项目中哪些办法导致发动耗时长
  • 为什么这些办法会导致发动耗时长
  • 怎样优化这些办法削减发动耗时

而至于怎样查找导致发动耗时长的办法和后续的办理、监控机制、告警机制等,能够检查之前其他项目组发布的这篇文章:货拉拉用户端体会优化–发动优化篇

三. 具体优化进程

优化的细节比较多,全体能够分为三期:

  • 第一期:首要对耗时长的函数做办理以及低版别机型做了特别处理;
  • 第二期:深化事务,对整个发动进程中的事务流程做优化;
  • 第三期:要点处理被迫发动场景下的耗时影响。

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

第一期优化的时分,首要经过计算办法耗时以及 InstrumentApp Launch,Time Profiler等东西,在低版别的机型比方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地址的埋点
  • 上报神策激活事情里边的WKWebViewuserAgent埋点

原因剖析

  • 获取wifi地址之所以耗时在于,该办法需求读取Wifi列表文件触及到IOcopy操作。
+ (NSString *)wifiBssid{
    NSString *bssid = @"";
    NSArray *interFaces = CFBridgingRelease(CNCopySupportedInterfaces());
    NSDictionary *info;
    for (NSString *ifname in interFaces) {
        info = CFBridgingRelease(CNCopyCurrentNetworkInfo(( __bridge CFStringRef)ifname));
        if (info && [info count]) {
            break;
        }
    }
    if ([info.allKeys containsObject:@"BSSID"]) {
        bssid = info[@"BSSID"];
    }
    return bssid;
}
  • 获取WKWebViewuserAgent耗时在于: WKWebView的初次创立,由于WKWebView是一个多进程组件,WKWebView的创立进程如下:

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

WKWebView的创立进程咱们知道,初次创立WKWebView,需求外部单独创立一个WKWebView进程来处理网络请求、内容加载/烘托,之后还需求将WKWebView进程与App里边的WKWebView内存对象进行相关,这儿外部创立WKWebView进程,相对耗时较大。

优化计划

神策 埋点 公共参数里边获取 wifi 地址耗时优化计划:

  • 由于获取Wifi地址,只要在当时手机网络为Wifi状况下才能获取到,假如当时手机是移动网络,获取到的必定为空,所以移动网络状况下是没有必要履行获取Wifi地址操作的,因而获取Wifi地址之前增加一层网络判别,只要在Wifi网络才去获取Wifi地址。
  • 由于神策初始化是在发动的时分,所以这儿获取网络状况能够用Alamofire库的NetworkReachabilityManager去获取当时网络状况,由于NetworkReachabilityManager在初次初始化会去获取一次,因而能够确保运用的时分现已获取到了网络状况。而AFNetworkReachabilityManager由于创立初始化的时分默许网络状况是AFNetworkReachabilityStatusUnknown,而是等到监听回调才去更新网络状况,无法确保初次运用的时分现已获取到网络状况了。

上报神策激活事情里边的 WKWebView userAgent 埋点 耗时优化计划:

  • 由于神策激活事情埋点的上报跟产品承认了后,是能够略微推迟上报的,并且WKWebViewuserAgent,只跟目前手机体系的版别和手机类型有关,手机类型是固定的,因而能够经过体系的版别号进行磁盘缓存。因而全体优化逻辑如下:

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

优化作用

  • 神策埋点公共参数里边获取wifi地址优化后,由于咱们是出行产品,打车的时分大部分是在移动网络下,因而这个wifi造成的发动耗时和卡顿大幅减小。
  • 上报神策激活事情里边的WKWebViewuserAgent埋点优化后,发动耗时在低版别低内存状况下,耗时减小了大约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. 判别当时手机设备类型是否为低版别机型

首要咱们看一下设备类型组成:设备名称 + 大版别 + 小版别。

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

首要获取设备的类型,然后获取设备类型里边的大版别号,然后跟指定的类型版别(默许为11)做比照,低于指定版别,为低端机型。

// 低类型 手机 (iPhoneX及其以下)
    private class func isLowerPhoneDevice(_ lowPhoneVersionLimit: Int) -> Bool {
        let tipStr = "iPhone"
        var isLowMdapAssignDeviceVersion = false         let deviceName = phoneDeviceName()
        if deviceName.contains(tipStr) {
            let array = deviceName.components(separatedBy: ",")
            if array.count == 2 {
                let phoneVersionName = (array.first ?? "") as NSString
                if phoneVersionName.length > tipStr.count {
                    let tipRange = NSRange(location: tipStr.count, length: phoneVersionName.length - tipStr.count)
                    let version = phoneVersionName.substring(with: tipRange)
                    let deviceVersion = Int(version) ?? 0
                    if deviceVersion < lowPhoneVersionLimit {
                        isLowMdapAssignDeviceVersion = true                     }
                }
            }
        }
        return isLowMdapAssignDeviceVersion
    }
    /// 设备的名字
    private class func phoneDeviceName() -> String {
        var systemInfo = utsname()
        uname(&systemInfo)
        let machineMirror = Mirror(reflecting: systemInfo.machine)
        let identifier = machineMirror.children.reduce("") { identifier, element in             guard let value = element.value as? Int8, value != 0 else { return identifier }
            return identifier + String(UnicodeScalar(UInt8(value)))
        }
        return identifier
    }
  1. 使命异步履行

创立异步履行串行行列,增加异步使命,并异步履行

  // 增加 异步履行 使命
    public func addAsynQueueTaskBlock(taskBlock: FJFLaunchTaskBlock?) {
        self.addTaskBlock(taskBlock, .asynQueue)
    }
    // 依据 使命 所属 类型 增加 使命
    public func addTaskBlock(_ taskBlock: FJFLaunchTaskBlock?, _ blongType: FJFLaunchTaskBlongType) {
        let task_item = DispatchWorkItem {
            taskBlock?()
        }
        switch(blongType) {
        case .mainQueue:
            task_item.perform()
        case .asynQueue:
            self.asyn_queue.async(execute: task_item)
        }
    }
  1. 主线程使命推迟履行

依据手机类型版别判别是否将需求延期履行,假如为低版别机型,则将使命增加到推迟履行数组,对使命依据优先级进行排序;若为高版别直接履行使命。

// 依据 手机版 判别 是否 需求延期使命
    public func checkTaskBlockNeedDelay(_ taskBlock: FJFLaunchTaskBlock?,
                                        _ blongType: FJFLaunchTaskBlongType,
                                        _ taskPriorityType: FJFLaunchTaskPriorityType = .defaultPriority) {
        if self.isLowMdapAssignPhoneVersion {
            self.addDelayTaskBlock(taskBlock, blongType, taskPriorityType)
        } else {
            self.addTaskBlock(taskBlock, blongType)
        }
    }
    // 依据 使命 所属 类型 增加 推迟履行 使命
    public func addDelayTaskBlock(_ taskBlock: FJFLaunchTaskBlock?,
                                  _ blongType: FJFLaunchTaskBlongType,
                                  _ taskPriorityType: FJFLaunchTaskPriorityType = .defaultPriority) {
        self.delayLaunchTaskArray.append(FJFLaunchTask.init(taskBlock, blongType, taskPriorityType))
        self.delayLaunchTaskArray = self.delayLaunchTaskArray.sorted(by: { (obj1: FJFLaunchTask, obj2: FJFLaunchTask) -> Bool in             return obj1.taskPriorityType.rawValue > obj2.taskPriorityType.rawValue
        })
    }

履行推迟使命的时分,监听runloop周期,当runloop进入行将进入休眠或许退出时分,去履行推迟使命,使命履行完毕后,关闭runloop监听。

 // 履行 推迟 使命
    public func execDelayTask() {
        self.startRunloopIdleMonitor()
    }
    /// 敞开 主线程 runloop 闲暇 监听
    public func startRunloopIdleMonitor() {
        //获取当时RunLoop
        let runLoop: CFRunLoop = CFRunLoopGetMain()
        //定义一个观察者
        let activities = CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue
        mainObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, true, 0) { [weak self] (_, _) in             guard let self = self else {
                return             }
            if let tmpTask = self.delayLaunchTaskArray.first {
                self.addTaskBlock(tmpTask.taskBlock, tmpTask.taskBlongType)
                self.delayLaunchTaskArray.removeFirst()
            }
            if self.delayLaunchTaskArray.count == 0 {
                self.endRunloopIdleMonitor()
            }
        }
        if let tmpObserver = mainObserver {
            //增加当时RunLoop的观察者
            CFRunLoopAddObserver(runLoop, tmpObserver, .commonModes)
        }
    }

这儿之所以将推迟履行的使命放到runloop闲暇的时分去履行,首要原因是为了避免卡顿,由于推迟履行的使命,一般是耗时使命,假如多个使命在同一个runloop周期履行,会导致当时runloop周期过期繁忙,主线程无法呼应其他操作,因而进行对推迟履行使命进行分发,分发到不同的runloop闲暇时分去履行,能够有用避免卡顿。

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

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

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

由于在第一期优化的一起,也进行了相关函数耗时的上报和计算,针对上报的数据,对不同版别机型和内存状况的办法耗时进行整理,一起也跟其他搭档的卡顿和包体积优化一起整合,进行了第二期的优化。第二期优化首要有几点:

  • 发动流程中无用的类和流程优化
  • 包体积优化:二方库的动态库改为静态库, 无用类和三方库删去等
  • 卡顿办理优化: 对城市列表接口优化,对数据处理优化,对UI布局优化等。
  1. 发动流程中无用的类和流程优化

发动流程这儿,随着迭代进行,有许多类和流程是能够优化的。

原因剖析

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

优化计划

  • 对无用的类和代码,找对应负责人进行承认,承认无用后,删去。
  • 关于特定场景用到的类,声明为可选项,运用的时分增加判别,不设置默许值,不运用懒加载
  • 关于区分登录或未登录状况的流程,进行校对,增加状况判别,削减不必要的调用。

优化作用

由于去掉了无用类的生成和区分的一些状况,减小了不必要的函数调用,全体耗时,缩短了80ms-150ms

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

其他搭档进行包体积优化,将一些二方库改为静态库,一起删去了无用的类和三方库,以及将图片放到Asset.catalog里边办理等操作来减小包体积。

原因剖析

  • 将二方库从动态库改为静态库,比方付出组件,IM组件等,削减了发动链接里边的rebasebind操作,也相应削减了发动耗时
  • 无用类和无用三方库的删去,削减了发动阶段,类的加载,相应也削减了发动耗时
  • 图片资源文件紧缩以及Asset.catalog来办理,加快了图片的加载时刻,也削减发动耗时

优化计划

  • 经过修正对应二方库的podspec文件,将库类型指定为静态库s.static_framework = true
  • 经过WBBlades东西对swift代码进行扫描,找出无用的类和代码,然后再进行二次承认删去无用的类。
  • 经过对三方,二方库依赖关系进行剖析和承认,去掉无用的库
  • 经过imageOptiom对资源的无损紧缩,然后采用Asset.catalog办理图片

优化作用

由于包体积优化是在搭档分支上,合并到迭代集成分支的时分,测验了下,全体耗时缩短了60ms-150ms左右。

  1. 卡顿办理优化

其他搭档进行卡顿办理的时分,顺带将发动流程中的计算到的卡顿也进行办理了,比方拉取城市列表数据处理,对广告接口数据处理,对安全中心等布局的优化等。

原因剖析

发动耗时优化其实算是卡顿办理的一个子集,因而其他搭档在做卡顿办理的时分,触及到发动相关的办理,也相应的能削减发动耗时。

优化计划

  • 城市列表数据是每次发动都会依据版别号去拉取,拉取回来后会进行省市等处理,然后进行缓存,由于城市列表数据量较大,处理都在主线程上,因而在低版别机型会偶然发生卡顿,尤其是第一次装置拉取,没有版别缓存的时分,这儿搭档创立了专门用来处理卡顿的串行行列,异步去履行处理流程和存储流程,处理完结后再到主线程回调。
  • 关于主页广告列表等相同较大数据量的请求处理,搭档也是放到专门用来处理卡顿的串行行列,异步去履行处理流程,处理完结后再到主线程回调。
  • 关于安全中心、轮播图里边存在的布局频繁更新等问题,则是跟对应负责人评论后,削减了调用频次,以及处理了进入后台定时器依赖履行等问题,对发动耗时也有必定下降。

优化作用

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

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

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

第三期:被迫发动及计算反常数据优化

1. 被迫发动的优化

由于咱们项目是一个出行项目,会有对应的定位更新功用,因而会存在由于方位更新导致的被迫发动,并且占比相对较大,从神策计算数据计算数据看,被迫发动占比能够到达30%多。

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

什么是被迫发动

咱们把AppiOS体系触发、App仍然处于后台的发动,称之为App的被迫发动。

iOS7之后,苹果新增了后台使用程序改写功用,该功用允许操作体系在必定的时刻距离内(这个时刻距离依据用户不同的操作习惯而有所不同,或许是几个小时,也或许是几天)拉起使用程序并一起让其保持在后台,以便使用程序能够获取最新的数据并更新相关内容,然后能够确保用户在翻开使用程序的时分能够第一时刻检查到最新的内容。

例如:新闻或许交际媒体类型的使用程序,能够运用这个功用在后台获取到最新的数据内容,在用户翻开使用程序是能够缩短使用程序发动和获取内容展现的等待时刻,最终提高产品的用户体会。

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

但这儿用户并没有翻开使用程序,更没有浏览第一个页面,整个后台使用改写的进程,关于用户而言,完全是透明的,无感知的。

项目哪些功用会触发被迫发动

运用Xcode创立新的使用程序,默许状况下后台改写功用是关闭的,咱们能够在Capabilities标签中敞开Background Modes,然后就能够勾选所需求功用了。

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

目前项目里边勾选了如下三种功用:

  • Location updates:

这种模式下,按照苹果文档的官方说法,假如你在Xcodebackground modes中敞开了Location updates,并且用户授权了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稳定性来说是有必要针对被迫发动进行优化的。

优化计划

  • 针对Background Modes里边的会导致被迫发动的选项,依据项目需求去掉Background fetch选项,削减被迫发动次数
  • 针对App的发动,经过applicationState判别是否为UIApplicationStateBackground,假如是判别为被迫发动,能够在发动耗时计算的时分,将被迫发动的计算数据不上报。

优化作用

对被迫发动的发动耗时计算数据过滤不进行上报后,全体发动耗时3s以内占比,能够到达99.7左右。

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

2. 外链、推送直接跳转其他页面数据计算反常

从计算数据上,咱们也发现,许多外链、推送发动App直接跳转到WebVc或许其他界面的时分,得到的发动计算时长也很长。

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

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

原因剖析

咱们App发动耗时计算是到主页的viewDidAppear完毕,当从外链发动或许点击推送,直接进入其他页面时,这时分未履行到主页的viewDidAppear办法,而是从其他页面返回到主页的时分,才去履行viewDidAppear

优化计划

由于App发动耗时计算是到主页的viewDidAppear完毕,这个标准是一切事务线对齐的,因而在外链或许点击推送冷发动时加了判别,假如当时为冷发动跳转,就将跳转事情先存储,等主页viewDidAppear履行完毕,才进行跳转。

优化作用

由于这种case的量不多,因而此次优化,只是将3s以内占比,提高了0.1%左右。

3. 体系预热(prewarm)导致反常数据优化

咱们从发动计算数据上发现,在iOS15及以上相关体系,经常会呈现发动耗时几十秒的状况,通常是load办法的加载就现已到达几十秒了,并且都是相对性能较好的机型。

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

原因剖析

prewarm 机制:

AppleiOS 15 中引入了 prewarm (预热)机制,体系或许会依据设备的状况,比方当设备内存和磁盘空间足够以及用户发动该App的时刻习惯等,提前帮你预热(prewarm) 你的 App

也便是提前发动不再运转的 App 进程,以削减用户手动发动 App 等待的时刻。prewarm 履行一个 App 的发动序列直到(但不包含)当 main() 调用 UIApplicationMain。也便是prewarm帮助提前完结了pre-main之前的一切操作。

这为体系供给了一个机会来构建和缓存它需求的任何低层结构,以等待一个完整的发动。也便是说,prewarm 机制能够削减发动时刻,咱们乃至能够在 load 办法中做一些资源的预加载。

由于存在这个机制,而咱们App发动耗时计算是从cocoapods排序的第一个库的load时刻开端,然后到主页的viewDidAppear完毕。

因而假如该App被预热了,也便是库的load等办法先履行,这时分计算开端时刻现已符号,然后暂停,暂停这段时刻也被算进去,导致发动耗时尤其是load办法的加载呈现几十秒这种状况,计算数据呈现了误差,之所以只要计算到几十秒的,而没有几分钟或许几个小时,是由于计算库做了过滤,超越1分钟,就直接丢弃这次计算。

优化计划

咱们能够依据如下办法判别此次发动是否为预热prewarm发动:

 func isPrewarmLaunch() -> Bool {
        let systemVersion = UIDevice.current.systemVersion.toFloat() ?? 0.0
        if systemVersion >= 15.0 {
            let environment = ProcessInfo.processInfo.environment
            for key in environment.allKeys() {
                if key.contains(substring: "prewarm") {
                    return true
                }
            }
        }
        return false
    }

因而决定发动组件里边增加判别,判别此次发动是否为预热prewarm发动,假如此次发动为预热prewarm发动,则计算的开端点用main函数的开端履行的时刻作为起点,假如不是预热发动,则仍然运用cocoapods排序的第一个库的load的加载时刻作为起点。

优化作用

由于该优化收拢到发动组件内部去判别,而架构组针对发动组件也在做重构,因而没有及时修正该计算问题。

四. 总结

在这个发动耗时优化的进程中,能够总结出如下四点相关的经验:

  • 最要害的点:找到耗时高的函数。先办理耗时相对最高的那些函数,剖析耗时高的实质原因,然后采取对应的办法进行优化,优化之后进行codeReview,跟搭档一起探讨优化操作或许发生的影响。
  • 测验环境共同:测验时应尽量在最差环境下测验。比方低版别机型,低内存状况下来计算发动耗时,像我会写一个能够不断耗费内存的Demo进程,然后将手机内存耗费到100M以下,乃至更低,然后分别去测验低版别机型的发动耗时状况,在这种状况下得到的计算数据,再去推测本次优化能够到达的预期,相对来说比较准确。
  • 要有降级计划:发动耗时优化,之所以称为优化,就代表这是一项如虎添翼的操作。因而针对每次版别优化,最好有降级计划,避免本次优化带来其他额定问题的时分,能够及时回退到以前计划,确保App全体的稳定性。
  • 防劣化建造: 要想长期保持优异的发动数据,防劣化建造必不可少。首要codereview重视代码和事务逻辑对稳定性发生的影响;接着对二方库、三方库的晋级和引入进行审查和评价;然后每次迭代集成包的时分,测验和研制都会经过移动测验渠道-MTC,对各种机型跑一遍性能测验并输出测验报告,而我本身电脑也装置了appium自动化测验,也会对iPhone6等低版别机型、低内存下,跑下发动时长数据,得到最小值、最大值、均值,并和前次迭代做比照,假如差距比较大,就去剖析此次迭代代码,找到原因并联系相关开发人员修正。最终上线后重视每日发动时长数据,并设置告警机制,当某天计算数据突然劣化,就经过相关埋点和日志数据剖析原因,并优化。