iOS 启动优化总结

前言

App启动作为用户体验的第一关,直接决定着用户对App的第一印象,启动越慢用户流失的概率就越高,良好的启动速度是用户体验不可缺少的一环。
1、启动定义
启动有两种定义:
广义:点击图标到首页数据加载完毕
狭义:点击图标到 Launch Image 完全消失第一帧
不同产品的业务形态不一样,对于视频App,点击图标到首页数据加载完毕;对像多屏互动这样首页是静态的 App 来说,Launch Image 消失就是首页数据加载完成。由于标准很难对齐,所以我们一般使用狭义的启动定义:即启动终点为启动图完全消失的第一帧(若存在开屏广告则减去广告展示时间)。
启动的种类
据场景的不同,启动可以分为三种:首次启动、冷启动,热启动、升级后启动。
首次启动:安装应用后的首次启动。此时没有之前的状态,也没有本地缓存。这意味着将会出现以下两种情况中的一种:没有需要加载的内容(因此加载时间会缩短),或者需要从服务器上下载初始数据(可能需要加载时间)。
冷启动:系统里没有任何进程的缓存信息,典型示例是 重启手机直接启动 App
热启动:iOS 13之前 这是指当应用处于后台,但并未被挂起或关闭时,用户切换至应用而触发的启动;iOS 13之后 dyld3对第三方应用的放开,如果把 App 进程杀了再立刻重新启动,这次启动就是热启动,因为进程缓存还在;
升级后启动:通常而言,升级后的启动与冷启动没有差别。但是,不同的启动叫法表明了本地存储发生变化的时刻是不同的,这些变化包括模式、内容、之前版本挂起的同步操作,以及内部的 API/ 默认依赖。
2、启动流程
据考虑的角度的不同,分有两种不同的启动流程:
2.1、应用启动任务流程
加载应用的默认项(NSUserDefaults、捆绑的配置等)
• 检查私有 / 测试版本
• 初始化应用标识符,包括但不限于对匿名用户使用的供应商标识符(Identififier for
Vendor,IDFV)、广告标识符(Identififier for Advertiser,IDFA)等
• 初始化崩溃报告系统
• 建立分析方法
• 使用操作或 GCD 建立网络
• 建立 UI 基础设施(导航、主题、初始 UI)
• 显示登录提示或从服务器加载最新内容及其他更新
• 建立内存缓存(如图片缓存)
上述列举的内容只是应用在首次启动时可能执行的任务。其中一些还会在后续启动中执行。问题是,任务数量的快速增加必然会导致应用的启动速度变慢。

2.2、应用启动原理流程

  1. 点击图标,创建进程
  2. mmap 主二进制,找到 dyld 的路径 (mmap 的全称是 memory map,是一种内存映射技术,可以把文件映射到虚拟内存的地址空间里,这样就可以像直接操作内存那样来读写文件。)
  3. mmap dyld,把入口地址设为_dyld_start (dyld 是启动的辅助程序,是 in-process 的,即启动的时候会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交给 dyld;iOS 13 开始 Apple 对三方 App 启用了 dyld3,dyld3 的最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。)
  4. 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
  5. 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
  6. 对每个二进制做 bind 和 rebase(Rebase:修复内部指针。这是因为 Mach-O 在 mmap 到虚拟内存的时候,起始地址会有一个随机的偏移量 slide,需要把内部的指针指向加上这个 slide。Bind:修复外部指针。这个比较好理解,因为像 printf 等外部函数,只有运行时才知道它的地址是什么,bind 就是把指针指向这个地址),主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据有定义,过程:MMU 找到空闲的物理内存页面 ;触发磁盘 IO,把数据读入物理内存;如果是 TEXT 段的页,要进行解密;对解密后的页,进行签名验证)
  7. 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
  8. +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
  9. 初始化 UIApplication,启动 Main Runloop
  10. 执行 will/didFinishLaunch,这里主要是业务代码耗时
  11. Layout,viewDidLoad 和Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间
  12. Display,drawRect 会调用
  13. Prepare,图片解码发生在这一步
  14. Commit,首帧渲染数据打包发给 RenderServer,启动结束


    640.png

    注:Apple 在 MetricsKit 里对启动终点定义是第一个CA::Transaction::commit();
    关于Commit ,iOS 的渲染是在一个单独的进程 RenderServer 做的,App 会把 Render Tree 编码打包给 RenderServer,RenderServer 再调用渲染框架(Metal/OpenGL ES)来生成 bitmap,放到帧缓冲区里,硬件根据时钟信号读取帧缓冲区内容,完成屏幕刷新。CATransaction 就是把一组 UI 上的修改,合并成一个事务,通过 commit 提交。
    渲染可以分为四个步骤
    Layout(布局),源头是 Root Layer 调用[CALayer layoutSubLayers],这时候 UIViewController 的 viewDidLoad 和 LayoutSubViews 会调用,autolayout 也是在这一步生效
    Display(绘制),源头是 Root Layer 调用[CALayer display],如果 View 实现了 drawRect 方法,会在这个阶段调用
    Prepare(准备),这个过程中会完成图片的解码
    Commit(提交),打包 Render Tree 通过 XPC 的方式发给 Render Server
    3、优化实践
    3.1、启动任务拆分优化
    (1) 确定在展示 UI 前必须执行的任务。
    如果应用是第一次启动,那么没有必要加载任何用户偏好,如主题、刷新间隔、缓存大小等。此时是没有任何自定义值的。初始缓存肆意增长也是没问题的,因为它的增长不会超过最终的限制值。崩溃报告系统应第一个被初始化。
    (2) 按顺序执行任务。
    排序是非常重要的,因为任务之间可能具有相互依赖性,同时,排序还可以节省用户的宝贵时间。例如,如果先触发了访问令牌的验证操作,那么其他任务可能会并行执行,因为验证过程需要进行网络连接。但是这样就会导致一种情况:如果其他任务先完成,而验证还未完成,应用就必须等待验证完成才能继续执行。
    (3) 将任务拆分为两类:一类是必须在主线程中执行的任务,另一类是可以在其他线程中执行的任务 ,然后分别执行。还可以进一步将在非主线程中执行的任务分为可以并发执行的和不能并发执行的。
    (4) 其他任务可以在加载 UI 后执行或异步执行。
    延迟其他子系统(如记录仪和分析方法)的初始化。在应用的后续阶段将一些操作(例如,写日志消息或跟踪事件)放入队列中,直到子系统完全完成初始化。
    3.2、基于启动原理优化
    t(App 总启动时间) = t1(main 调用之前的加载时间) + t2(main 调用之后的加载时间),分main函数之前和main函数之后的相关方法优化


    11.jpeg
3.2.1、监控方法

主要是通过埋点监控实时的启动时间,点位主要是:

iOS13(含)以上的系统采用 runloop 中注册一个 kCFRunLoopBeforeTimers 的回调获取到的 App 首屏渲染完成的时机更准确。

iOS13 以下的系统采用 CFRunLoopPerformBlock 方法注入 block 获取到的 App 首屏渲染完成的时机更准确。

|

//注册block

CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];

CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){

NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];

NSLog(@``"runloop block launch end:%f"``,stamp);

});

//注册kCFRunLoopBeforeTimers回调

CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];

CFRunLoopActivity activities = kCFRunLoopAllActivities;

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, ``0``, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

if (activity == kCFRunLoopBeforeTimers) {

NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];

NSLog(@``"runloop beforetimers launch end:%f"``,stamp);

CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);

}

});

CFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);

|

为什么这么监控呢,首屏渲染完成时间我们希望和 MetricKit 对齐,即获取到 CA::Transaction::commit()方法被调用的时间;而通过 Runloop 源码分析和线下调试,我们发现 CA::Transaction::commit(),CFRunLoopPerformBlock,kCFRunLoopBeforeTimers 这三个时机的顺序从早到晚依次是:CFRunLoopPerformBlock -> CA::Transaction::commit -> kCFRunLoopBeforeTimers 。

3.2.2、检测工具

1、目前使用的检测工具为美图输出的MTHawkeye工具

MTHawkeye

def hawkeye

pod 'MTHawkeye/DefaultPluginsWithoutLog', :configurations => 'Debug'

pod 'FLEX', :configurations => ['Debug']

pod 'fishhook', :configurations => ['Debug']

pod 'MTAppenderFile', :configurations => ['Debug']

end

2、获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:

Total pre-main time: 1.2 seconds (100.0%)

     dylib loading time: 147.51 milliseconds (12.0%)

    rebase/binding time: 112.82 milliseconds (9.2%)

        ObjC setup time:  45.94 milliseconds (3.7%)

       initializer time: 919.07 milliseconds (75.0%)

       slowest intializers :

         libSystem.B.dylib :   6.79 milliseconds (0.5%)

libMainThreadChecker.dylib :  34.62 milliseconds (2.8%)

      libglInterpose.dylib : 353.67 milliseconds (28.8%)

                     TCLTV : 944.10 milliseconds (77.0%)

3、 System Trace: 这个模板提供了系统行为的全面信息。它显示线程的调度、系统线程的转化和内存使用情况。这个模板可以使用在OS X或iOS中。在分析启动时间时,可以结合Time Profiler 一起来使用。

3.2.3、优化方法

1)在t1阶段加快App启动:

  • 尽量使用静态库,减少动态库的使用,动态链接比较耗时,如果要用动态库,尽量将多个dylib动态库合并成一个
  • 尽量避免对系统库使用optional linking,如果App用到的系统库在你所有支持的系统版本上都有,就设置为required,因为optional会有些额外的检查
  • 减少Objective-C Class、Selector、Category的数量,可以合并或者删减一些OC类(怎样删减无用代码:ViewConteroller 渗透率,hook 对应的声明周期方法即可统计;Class 渗透率,遍历运行时的所有类,通过 Objective C Runtime 的标志位判断类是否被访问;行级渗透率,需要用编译期插桩,对包大小和执行速度均有损。)
  • 删减一些无用的静态变量,删减没有被调用到或者已经废弃的方法
  • 将不必须在+load中做的事情尽量挪到+initialize中,+initialize是在第一次初始化这个类之前被调用,+load在加载类的时候就被调用;或者做load方法迁移
  • 尽量不要用C++虚函数,创建虚函数表有开销
  • 不要使用__attribute__((constructor))将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用dispatch_once(),pthread_once()或 std::once()
  • 在初始化方法中不调用dlopen(),dlopen()有性能和死锁的可能性
  • 在初始化方法中不创建线程

2)在t2阶段加快App启动:

  • 尽量不要使用xib/storyboard,而是用纯代码作为首页UI,如果要用xib/storyboard,不要在xib/storyboard中存放太多的视图
  • 使用简单的广告页作为过渡,将首页的计算操作及网络请求放在广告页展示时异步进行。
  • application:didFinishLaunchingWithOptions:里的任务尽量延迟加载或懒加载
  • 不要在NSUserDefaults中存放太多的数据,NSUserDefaults是一个plist文件,plist文件会被反序列化一次
  • 避免在启动时打印过多的log,少用NSLog,因为每一次NSLog的调用都会创建一个新的NSCalendar实例
  • 为了防止使用GCD创建过多的线程,解决方法是创建串行队列,或者使用带有最大并发数限制的NSOperationQueue
  • 不要在主线程执行磁盘、网络、Lock或者dispatch_sync、发送消息给其他线程等操作

参考

1、iOS 启动优化汇总

2、美团外卖iOS App冷启动治理

3、基于二进制文件重排的解决方案 APP启动速度提升超15%

4、《高性能iOS应用开发》

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

推荐阅读更多精彩内容