iOS启动时间优化

背景

之前有收到用户反馈 App 的启动时间较长,在和市面上大部分 App 启动时间相比后,确实发现 App 启动较慢,于是开始分析项目中导致启动时间变长的原因,并对启动时间进行优化。

现状分析

一般而言,启动时间指用户从点击 App 那一刻开始到看到 App 第一个页面之间消耗的时间。

苹果将启动时间分为两部分:pre-main 的时间和 main() 之后的时间(当然还有我们人为加上去的闪屏显示时间)。

  • pre-main时间:即调用 main() 函数之前的加载时间,在这段时间里系统会进行加载动态库、注册 Objc 类等系统操作。

  • main() 之后的时间:即从调用 main( ) 函数到看到第一个页面之间的时间(从 main 函数开始到第一个页面的 - viewDidAppear 被调用)。

统计结果

以下为各个机型启动时间的统计结果,由于冷启动的启动时间受系统影响波动较大,启动时间均测试5次以上取平均值,统计时间均不包含闪屏广告页的时间(单位为秒):

iPhone 8 Plus iPhone 6s Plus iPhone SE
pre-main 时间 0.879 0.869 0.958
main之后的时间 1.622 1.762 1.885
总时间 2.501 2.631 2.843

以下为main之后时间的统计结果:

Launch时间:main 开始到 didFinishLaunchingWithOptions 结束的时间

首页渲染时间:didFinishLaunchingWithOptions 结束到 RootViewController 的 - viewDidAppear 被调用的时间

iPhone 8 Plus iPhone 6s Plus iPhone SE
Launch时间 0.701 0.740 0.849
首页渲染时间 0.921 1.022 1.036
总时间 1.622 1.762 1.885

经过统计 pre-main 的时间基本稳定在 0.8s-0.9s 左右,Launch 时间稳定在 0.8s 左右,首页渲染时间稳定在1s左右,均存在优化空间

优化方案

pre-main 阶段优化

以下为 iPhone 6s Plus 正常启动消耗的pre-main时间(苹果提供了内建的测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1):

Total pre-main time: 866.86 milliseconds (100.0%)
         dylib loading time: 328.28 milliseconds (37.8%)
        rebase/binding time:  49.19 milliseconds (5.6%)
            ObjC setup time:  62.85 milliseconds (7.2%)
           initializer time: 426.38 milliseconds (49.1%)
           slowest intializers :
             libSystem.B.dylib :   7.52 milliseconds (0.8%)
    libMainThreadChecker.dylib :  37.19 milliseconds (4.2%)
          libglInterpose.dylib :  61.17 milliseconds (7.0%)
         libMTLInterpose.dylib :  22.23 milliseconds (2.5%)
                       MyMoney : 392.50 milliseconds (45.2%)

pre-main时间主要由 4 部分组成:

  1. dylib loading:

    这一阶段 dyld 会分析应用依赖的 dylib ,所以,依赖的 dylib 越少越好。在这一步,我们能做的优化就是检查是否存在不需要的 dylib ,移除不必要的 dylib 。

    在项目优化实践中,我们移除了一个没有必要的动态库,并将几个动态库合成为一个动态库,减少动态库数量。

  2. rebase/binding:

    这一阶段系统主要注册 Objc 类。所以,指针数量越少越好。这一步能做的优化有:

    • 清理项目中无用的类

    • 删减没有被调用到或者已经废弃的方法

    • 删减一些无用的静态变量

    可通过 AppCode 等工具扫描项目中未使用的代码。

  3. Objc srtup:

    这一阶段没有什么特别能优化的地方,如果 rebase/binding 阶段优化的好这步耗时也会很少。

  4. initializer:

    这一阶段,dyld 开始运行程序的初始化函数,调用每个 Objc 类和分类的 +load 方法,调用 C/C++ 中的构造器函数。initializer阶段执行完后,dyld 开始调用 main() 函数。在这一步,检查 +load 方法,尽量把事情推迟到 +initiailize 方法里执行。

    在这里我们修改了部分原本代码中直接在 +load 函数初始化逻辑改为在 +initialize 中加载,也就是到使用时才加载。

main()函数之后的优化

didFinishLaunchingWithOptions 优化

思路:目前 App 的 didFinishLaunchingWithOptions 方法里执行了几十项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。

通过打点计时器统计各个业务的耗时时间(iPhone 6s Plus):

各个业务耗时时间

整理 didFinishLaunchingWithOptions ,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions 里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。

首页渲染优化
  1. 减少启动期间创建的 UIViewController 数量

通过打符号断点-[UIViewController viewDidLoad]发现 App 启动过程中创建了 12 个 UIViewController(包括闪屏),即在启动过程中创建了 12 个视图控制器,导致首页渲染时间较长。

  1. 延迟首页耗时操作

App 首页有个侧滑页面及侧滑手势,并且该页面是用 xib 构建的,将该 ViewController 改为代码构建,同时延迟该页面的创建时机,等首页显示后再创建该页面及侧滑手势,这个改动节省了 300-400ms。

  1. 去除启动时没必要及不合理的操作

项目中使用了自定义的侧滑返回,在每次 push 的时候都会截图,启动的时候自定义导航栏会截取两张多余首页的图片,并且截图用的 API (renderInContext) 性能较差,耗时 800ms 左右,去掉启动截图的操作。

闪屏请求回调里写plist文件的操作放在主线程,导致启动时占用主线程,将文件读写移到子线程操作。

闪屏优化

闪屏在启动的时候也占据了很长的时间,合理的闪屏显示逻辑同样能大大的减少用户的等待时间。

闪屏显示通常都是有一个倒计时,倒计时结束后显示首页。通常倒计时都是使用 NSTimer ,且每秒倒计时结束都需要修改页面上的文字,这些操作必须在主线程做,并且 NSTimer 依赖于主线程的 runloop 状态,主线程阻塞会导致定时器不准。所以为了保证闪屏倒计时的正常显示,首页是在闪屏显示结束后才去创建的。

主线程工作状态如下:

这种情况下用户等待的时间是从“创建闪屏”到“创建、显示首页”结束的时间。

由于闪屏显示时间较长(通常都有几秒),用户在等待闪屏结束的这段时间如果用来创建首页内容肯定是够的,于是就考虑能不能先显示闪屏,在闪屏倒计时的同时去创建、显示首页。

在将首页创建、显示逻辑提前后发现并没有那么简单,就像前面提到的闪屏倒计时使用 NSTimer,NSTimer 又依赖于 runloop,且每秒倒计时结束都必须修改文字,这些操作必须依赖于主线程。只是把首页创建逻辑提前就导致闪屏倒计时不准确,主要表现在第一秒格外的长,因为闪屏显示后主线程就去做创建首页的工作(其实还包括之前被移到首页显示后才启动的服务,也会在这时候一起被主线程处理),必须等首页内容创建完成后 NSTimer 的倒计时才会触发,也必须等首页内容创建完成后才能修改文字。

这时候的主线程工作状态如下:

这时用户等待的时间并没有明显缩短,而且还出现了倒计时不准的 bug 。

出现上面这种情况主要是由两个原因导致的:

  • 使用的定时器依赖于主线程。

  • 修改文字操作依赖于主线程。

第一个问题比较容易解决,不使用 NSTimer,改用 GCD 定时器放在其他线程即可。可是第二个问题是更新 UI ,苹果爸爸明确表示更新 UI 必须放在主线程,一时好像就无解了,但是真的无解吗?

回过头再看一下需求,其实我们要做的就是每秒修改用户看到的文字,并且不依赖于主线程,不依赖于主线程第一时间就想到了动画,iOS 动画是在另一个进程(BackBoard 进程)实现的,主线程阻塞是不会影响 iOS 动画的,利用这一特性就能完美的解决这个问题。我们将要每秒要显示的文字提前生成图片,然后使用生成的图片数组做动画,这样在闪屏显示期间主线程就可以去做其他事情,保证闪屏显示时间的同时减少了用户的等待时间。

优化后的工作状态:

总结

对于启动时间优化其实就是遵循一个原则:尽早让用户看到首页内容。根据这一原则将一些非必须的操作尽量往后移,通常是移到首页显示后执行,同时对于无法往后移的操作,尽可能不占用主线程,主线程尽量只做 UI 操作,将其他操作移到子线程(或者像上述动画一样移到其他进程)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,597评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,053评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,583评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,888评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,772评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,536评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,922评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,554评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,838评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,861评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,677评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,483评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,928评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,104评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,403评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,945评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,156评论 2 339