App启动优化 - 实践一

内容概要:
1. 启动速度
2. 如何测量启动时间
3. 影响启动时间的原因
4. 启动优化的方案

我们的应用在运行前应该减少操作,推迟一些启动行为,从而在启动前一点点时间进行初始化。下面让我们来看本章内容概要。

一、启动速度

在不同平台上,应用的启动时间有所不同。苹果在开发者大会中提出400毫秒是一个不错的启动时间。原因在于,当你看着应用在运行时,手机上的启动动画
能够给用户带来一种在主屏幕和应用之间切换时的持续感。这些动画占用时间,并且会给你一个机会隐藏启动时间。
显然根据情况会有所不同,app扩展程序也是应用启动的一部分,它们启动的时间不同。手机,电视和手表是不同的设备,但400毫秒是一个很好的启动目标。此外,启动时间不要超过20秒,如果超过20秒,OS会终止应用,以为它进入了死循环。
最后在所支持的最慢的设备上进行测试也很重要,在Apple平台支持的所有设备上,这些时间都是常量值。如果你在iPhone 6s上测试的结果达到400毫秒,很可能在iPhone 5上达不到。在前面的理论部分我们知道启动时需要做什么,要解析图像、映射图像、重设基址图像、绑定图像、启动图像初始化器、调用主函数,还有一些操作,包括运行框架初始化器以及加载NIB,最终在应用委托里收到回调。最后两个操作也计算在我们前面说的400毫秒的时间里。
启动应用时,分冷启动和热启动。

  • 热启动是指启动时应用已经在内存里,或者因为启动过,之前退出了,但还在内核的磁盘缓存里,或者因为你刚把它复制过去。
  • 冷启动是指启动时应用不在磁盘缓存里。

测量冷启动时间通常更为重要。冷启动时间更为中重要的原因是,当用户重启手机后启动应用,或很长时间后启动应用,这时非常需要一个快速启动。为了测量冷启动时间,必须在每次测量之间重启设备。如果你正尝试优化热启动时间,那冷启动时间应该也会随之加快。你可以通过加速开发周期加快热启动,但是请时不时地测试一下冷启动。

二、如何测量启动时间

在主函数启动之前该如何测量时间?dyld里有内置的测量系统,可以通过设置环境变量DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS访问。安装操作系统时候就可用了,但它打印了很多内部调试信息,并没有什么用。它缺少了某些你可能想知道的信息。现在我们就来改进,在新的OS里进步显著。
它可以为你提供更为相关的信息,这些信息应该会提供可操作的方法,来加快启动时间。当加载每一个dylib,调试程序必须暂停启动才能解析应用的符号和加载断点,这通过USB线将非常耗时。但是dyld清楚这一点,它把调试时间从注册时间里减出去,所以不必为此担心。但是你会注意到它,因为dyld会显示比你在钟表中所观察到的数字精细得多。这是预期的和能够接受的,如果你看到了那个数字,一切都是正确的。这里只是提示下。
在Xcode里设置环境变量,如下图所示:

image_00

运行后,控制台的输出信息如下:

Total pre-main time: 10.6 seconds (100.0%)
         dylib loading time:  240.09 milliseconds (2.2%)
        rebase/binding time:  351.29 milliseconds (3.3%)
            ObjC setup time:  11.83 milliseconds (0.1%)
           initializer time:  10 seconds (94.3%)
       slowest intializers :
              MyAwesomeApp :  10.0 seconds (94.2%)

下面的时间条代表上面不同部分所占时间,而白色的虚线代表400毫秒:

image_01

上面的基本步骤就是前面理论部分讲的启动顺序。

三、优化方案

(一)dylib加载优化

关于dylib加载,还有从中看到的速度缓慢,需特别了解的是嵌入式dyld会非常昂贵。我们知道一个应用大概包含100到400个Dylib,但是操作系统的dylib很快,这是因为构建操作系统时,我们预计算了大量dylib的数据。但是我们在开发操作系统时,无法做到每个应用里的每个dylib。我们无法预计算你要嵌入应用的dylib,所以加载时必须要经过一个耗时的过程。其解决方案是少用dylib,而这将非常困难。这并不是说完全不能使用,有很多方法可以合并已有的dylib。
可以使用静态存档,把dylib用这种方法链接到应用。还可以使用延迟加载,也就是使用dlopen()函数。但是dlopen()函数会带来细微的性能和正确性的问题,实际上会导致之后做更多的工作量,而这些工作量被延迟执行了。所以这是一个可行的选项,但是必须要仔细思考清楚,尽量减少这种延迟加载的操作。

优化方案:

  1. 使用更少的dylib;
  2. 合并现有的dylib;
  3. 使用静态存档;
  4. 懒加载;
image_02
(二)重设基址和绑定优化

重设基址和绑定需要350毫秒时间,根据前面的理论部分我们知道,重设基址由于I/O会更慢一些,而绑定在计算上会昂贵,但它已经完成I/O。所以I/O是为了它们,它们混合在一起,时间也混合在一起。我们深入研究一下,就会发现时间消耗在修复DATA段里的指针。所以我们必须减少指针的修复。用其他工具可以看到在DATA,分区,dyld信息中修复的指针。还能显示正在哪些段和分区操作,你会很清楚地了解到在修复什么。比如,若看到一个在ObjC分区的ObjC类符号,很可能你有很多ObjC类。所以你能做的一件事就是减少ObjC类对象和ivars的数量。有很多编码样式都鼓励只有一个或两个函数的小类,这些特殊的模式可能会导致速度逐渐变慢。当你越加越多时,更要格外小心。现在有100或者1000个类不成问题,但有些大型应用有上万个类。在这种情况下,将会消耗更多的启动时间,因为内核要把它们读入页面。
还可以做一件事情,可以尝试减少使用C++虚拟函数。虚拟函数将会创建我们称之为V表格的东西,这和ObjC元数据相同,因为它们在DATA段创建了必须修复的结构。它们比ObjC元数据小,但它们对于某些应用程序来说仍然很重要。
还可以使用Swift的结构体。因为Swift通常使用更少这种带有指针修复的数据。并且Swift更为内联,可以更好的使用code-gen减少消耗。所以转为Swift语言也是一个好方法。
还有一点,需要小心机器生成的代码。曾经有过这样的例子,你可能用DSL或一些自定义语言描述某个结构,然后有一个程序从中生成其他代码。如果这些程序中有很多指针,它们将变得非常昂贵,因为生成代码时会生成非常非常大的结构。也有生成兆量级数据的情况。但好处是比较容易进行控制,因为你只需要改变代码生成器,使其使用非指针的内容,比如偏移基址,结构。

优化方案:

  • 减少__DATA指针;
  • 减少ObjC元数据 - 类,选择器和类别;
  • 减少C++虚拟函数;
  • 使用Swift结构;
  • 检查机器生成的代码 - 使用偏移量而非指针,标记为只读;
image_03
(三)ObjC Setup优化

关于设置ObjC,前面理论部分讲过它做的工作,它要处理类的注册,要处理非脆弱ivar,要处理分类的注册,还要让选择器唯一。这里我们不用处理太多,因为这些问题通过之前对重设基址,数据,和绑定的修复时都已经解决,之前所做的减少和在这里做的完全相同。

image_04

(四)初始化器的优化

初始化器有两种类型,显示初始化器,比如+load,前面理论部分建议用+initiailize取代它,这将导致ObjC运行时在类被实例化而不是文件被加载时初始化代码。
或者在C/C++里,有一个可以放在函数上的属性,可以让函数像初始化器一样生成代码,因此这是显示初始化器,但不建议这么做。建议选择调用site initializers取代上面的方式。调用site initializers是指调用像dispatch_once()函数,或者在跨平台代码里的pthread_once(),或者在C++代码中的std::once()。所有这些函数基本上都有相同的功能,这些函数的代码只会在第一次点击时运行一次。dispatch_once()在系统里很优秀,第一次执行以后,几乎等同于无操作,直接跳过。所以强烈建议不要使用显示初始化器。
另一种初始化器是隐式初始化器。隐式初始化器大部分来自带有非默认初始化器,非默认构造函数的C++全局变量。可以选择调用site initializers取代它,在很多地方可以把全局变量替换成想要初始化的无全局结构或指针的对象。还有一种选择是没有非默认初始化器。所以在C++中,初始化器称为POD,一个普通的旧数据。
如果对象只是普通的旧数据,静态链接器,或者静态链接器将会为DATA分区预计算所有的数据,只把数据放在那里,不一定要运行,不一定要修复。最后一点,很难找到它,因为它们是隐性的,但是编译器会收到警告 --- -Wglobal-constructors,如果这么做,只要产生其中一个,就会有警告。所以把它添加到编译器使用的标志里是个好方法。还有一个选择就是使用Swift重新编写。理由就是Swift有全局变量,并且会被初始化,它们确保在使用前被初始化,但是其方法不是用初始化器,在后台使用一次dispatch_once()。所以转为Swift将会做到这一点。
最后在初始化器里请不要调用dlopen()函数,它将会带来巨大的性能问题。原因有很多,dyld在运行时,是在应用启动之前,我们可以做一些诸如关闭锁的操作,因为是单线程。当dlopen()出现在那种情况下,初始化器的运行发生了改变,可能会有多线程,必须要打开锁,将会带来巨大的性能下降,还会带来细微的死锁和未定义的行为。还有,不要在初始化器上开启线程,也是出于同样的理由。

九、参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 背景 一个项目做的时间长了,启动流程往往容易杂乱,库也用的越来越多,APP的启动时间也会慢慢变长。本次将针对iOS...
    酱油瓶2阅读 3,416评论 0 12
  • 这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。...
    MTDeveloper阅读 736评论 0 1
  • App 运行理论 理论速成Mach-O 术语Mach-O 是针对不同运行时可执行文件的文件类型。文件类型:Exec...
    未明一二阅读 514评论 1 3
  • 本文分为理论【1-4】和实践【5-6】两部分: main()函数之前发生了什么 Mach-O格式 虚拟内存基础知识...
    WSJay阅读 853评论 0 1
  • 这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。...
    茗涙阅读 1,844评论 0 3