iOS客户端启动优化

App启动分成两部分

  • pre-main 阶段的定义为 APP 开始启动到系统调用 main 函数这一段时间
  • main 阶段则代表从main函数入口到主 UI 框架的 viewDidAppear

Pre-main

dyld:dynamic loader,它的作用是加载一个进程所需要的image,dyld是开源的

Pre-main的流程.png

Load Dylibs

dyld加载App的可执行文件,App内嵌的库,系统动态库。每个库,就是Mach-o文件,其头部都包含依赖库信息,dyld会递归加载这些库。

  • 系统库会有缓存,dyld共享库缓存,加载很快
  • 内嵌的库加载较慢

加载过程

  1. 分析所依赖的动态库
  2. 找到动态库的mach-o文件
  3. 打开文件
  4. 验证文件
  5. 在系统核心注册文件签名
  6. 对动态库的每一个segment调用mmap()

优化

  • 减少非系统库的依赖
  • 使用静态库而不是动态库
  • 合并多个非系统动态库为一个动态库

Rebase && Binding

ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

  • Rebase 修正内部(指向当前mach-o文件)的指针指向。是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中
  • Bind 修正外部指针指向。外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的


    Rebase && Binding.png

优化

  • 减少Objc类数量, 减少selector数量,把未使用的类和函数都可以删掉。跟单职责有点冲突?
  • 减少C++虚函数数量
  • 转而使用swift stuct(其实本质上就是为了减少符号的数量,使用swift语言来开发?)

OBJC Runtime Setup

  • 读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
  • 注册 Objc 类,ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中;
  • 读取 protocol 以及 category 的信息,把category的定义插入方法列表 (category registration),
  • 确保 selector 的唯一性

Initializers

以上三步属于静态调整,都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和栈中写入内容。 工作主要有:

  • Objc的+load()函数
  • C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
  • 非基本类型的C++静态全局变量的创建。(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度

优化

  • 使用 +initialize 来替代 +load
  • 不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。
    • 比如使用 dispatch_once(),pthread_once()std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。
    • 也尽量不要用到C++的静态对象。

Pre-main 总结

  1. pre-main阶段耗时的影响因素:

    • 动态库加载越多,启动越慢。
    • ObjC类越多,函数越多,启动越慢。
    • 可执行文件越大启动越慢。
    • C的constructor函数越多,启动越慢。
    • C++静态对象越多,启动越慢。
    • ObjC的+load越多,启动越慢。
  2. 整体上pre-main阶段的优化有:

    • 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
    • 检查下 framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
    • 合并或者删减一些OC类和函数;关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高);有一个叫做FUI的开源项目能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。
    • 删减一些无用的静态变量,
    • 删减没有被调用到或者已经废弃的方法
    • 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
    • 类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;
    • 用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
    • 在设计师可接受的范围内压缩图片的大小,会有意外收获。压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。

Main

总体原则无非就是减少启动的时候的步骤,以及每一步骤的时间消耗。
main阶段的优化大致有如下几个点:

  • 减少启动初始化的流程,能懒加载的就懒加载,能放后台初始化的就放后台,能够延时初始化的就延时,不要卡主线程的启动时间,已经下线的业务直接删掉;
  • 优化代码逻辑,去除一些非必要的逻辑和代码,减少每个流程所消耗的时间;
  • 启动阶段使用多线程来进行初始化,把CPU的性能尽量发挥出来;
  • 使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤;
  • 主UI框架tabBarController的viewDidLoad函数里,去掉一些不必要的函数调用

其中遇到几个坑:

  • 并不是什么任务都适合放子线程,有些任务在主线程大概10ms,放到子线程需要几百ms,因为某些任务内部可能会用到UIKit的api,又或者某些操作是需要提交到主线程去执行的,关键是要搞清楚这个任务里边究竟做了啥,有些SDK并不见得适合放到子线程去初始化,需要具体情况具体去测试和分析。

实际优化效果

由于只是去掉了几个静态库,而且本来pre-main阶段的耗时就不长,基本在200ms-500ms左右,所以pre-main阶段优化前后效果并不明显,有时候还没有前后测试的误差大。。。
main的阶段的优化效果还是很明显的:

  • iPhone5C iOS10.3.3系统优化前main阶段的时间消耗为4秒左右,优化后基本在1.8秒内;
  • iPhone7 iOS10.3.3系统优化前main阶段的时间消耗为1.1秒左右,优化后基本在600ms内;
  • iPhoneX iOS11.3.1系统优化前main阶段的时间消耗基本在1.5秒以上,优化后在1秒内;

可以看到,同样arm64架构的机器,main阶段是iPhone7比iPhoneX更快,说明就操作系统来说,iOS11.3要比iOS10.3慢不少;

参考文章

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

推荐阅读更多精彩内容