iOS 性能调优之启动时间优化

目的

从点击 App 图标到加载 App 闪屏之间会有个动画,我们希望 App 启动速度比这个动画更快。需要注意的是启动时间一旦超过 20s,系统会认为发生了死循环并杀掉 App 进程。当然启动时间最好以 App 所支持的最低配置设备为准。

启动时间的划分

启动时间可以拆分为两个部分

  • 第一部分是点击App后到main函数执行之前的时间
    这一部分主要是在处理系统dylib(动态链接库)和自身App可执行文件的加载.
  • 第二部分是main函数执行之后到AppDelegate中didFinishLaunchingWithOptions方法执行结束的时间
    这一部分主要是构建第一个界面,并完成渲染展示.
    我们把第一部分时间叫做t1第二部分时间叫做t2

t1

  • t1在iOS中被称作pre-main time
  • Xcode提供了显示t1各阶段时间的方法
    1. Product -> Scheme -> Edit Scheme
  1. Run -> Arguments -> Enviroment Variables -> 添加字段{DYLD_PRINT_STATISTICS:Yes}
  1. 重新运行项目,会在控制台看到项目的per-main time

1. dylib loading time

这一阶段主要在load dylibs image(这里的image表示镜像)
在每一个动态库加载过程中dyld需要做下面几步

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

通常,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载.针对这一步骤的优化有:

  • 减少非系统库的依赖
  • check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
  • 合并非系统库
  • 使用静态资源,比如把代码加入主程序

2. rebase/binding time

由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址. rebase修复的是指向当前镜像内部的资源指针, 而bind指向的是镜像外部的资源指针.
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO.bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算.
通过下面方法查看相关的资源指针

  • 打开Products下.app文件所在的文件夹
  • cd 到此目录下
  • 在命令行中输入以下命令
xcrun dyldinfo -rebase -bind -lazy_bind xxx.app/xxx
  • 这时会输出所有的资源指针

该阶段的优化关键在于减少__DATA指针的数量
可优化的操作有以下几点

  • 减少class(类),selector(选择子)以及category(分类)这类元数据的数量(下面是使用AppCode分析未使用的代码,可以看出有大量优化空间)
  • 减少C++虚函数数量
  • 使用swift stuct(其实本质上就是为了减少符号的数量)

3. Objc setup time

这一步主要做了以下操作

  • 注册Objc类 (class registration)
  • 把category的定义插入方法列表 (category registration)
  • 保证每一个selector唯一 (selctor uniquing)

由于前面两步的处理,这里已经没有什么可以优化的了

4. initializer time

以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有以下几点:

  • 用+ initialize方法代替+load方法
  • 使用 dispatch_one() pthread_once() std::once() 代替 C/C++ __ atribute__((constructor))(__ attribute__((constructor))用法解析
  • 减少静态构造函数
  • 不要在初始化方法中调用 dlopen(),对性能有影响。因为 dyld 在 App 开始前运行,由于此时是单线程运行所以系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

针对per-main time优化的总结

  • 减少非系统的framework依赖,如果framework 在当前 App 支持的所有 iOS 系统版本中都存在则设为 required,否则设置为 optional,optional 会有额外检查合并非系统库
  • 删除无用的 class/selector/category
  • 删除无用的方法调用、静态变量等减少 C++ 虚函数(减少创建虚函数表时间)
  • 使用 Swift 的 struct (从而减少符号数量)
  • 为不需要写的属性添加 readonly减少 +load() 方法,尽量使用 +initialize() 代替使用
  • dispatch_one() pthread_once() std::once() 代替 C/C++ __ attribute((constructor))__
  • 减少静态构造函数
  • 初始化方法中不要使用 dlopen()

t2

t2中所耗时间基本都是我们自己代码所造成的,所以优化的关键是找出每一个方法耗费的时间。
然后针对耗时的操作进行优化。
我们这里提供4种查看代码耗时的方法。

1. 使用Instruments

使用Instruments App Launch来抓取启动过程中各方法的耗时情况,具体可以参照下面的文章:
使用Instruments - App Launch查看启动问题
缺点:Instruments的功能很强大但是不够灵活。

2. 使用打点计时

此方法的原理为在你需要检测的方法前后做记录,计算前后两次打点间的时间间隔计算出方法耗时。
此方法可以参考这篇文章[贝聊科技]一次立竿见影的启动时间优化
缺点:此方法只能在方法调用的最外层获取到方法耗时,如果方法中调用了多个方法,要精确到具体哪个方法就需要多次设置打点的位置,使用也不是很方便。

3. 定时获取主线程中的调用栈

我们可以设置一个定时器,定时拉取主线程中调用栈信息,通过分析记录调用栈中各方法的入栈出栈时间,可以大致分析出各方法的耗时,这样就解决了方法嵌套时,无法定位耗时方法的问题。
调用栈分析器
调用栈分析器的使用方法可以看这篇文章iOS - 优化App冷启动速度
缺点:此方法存在两个问题,第一是拉去调用栈也会消耗时间,造成时间记录增加。第二就是定时器循环过程中会造成精度问题,例如调用时间小于定时器时间的方法不会被记录,调用时间恰好之比循环时间多了一点点就要增加整个循环时间间隔。

4. hook objc_msgSend 方法

使用fishhook钩住objc_msgSend,并在objc_msgSend前后插入我们的监听方法,为了不影响objc_msgSend的调用速度,插入的监听方法都需要使用汇编实现。
这里hook objc_msgSend的核心代码使用的是戴铭老师的。



使用时通过SMCallTrace类调用就可以了就可以了。
这里我们实现一个demo来验证一下
首先在didFinishLaunchingWithOptions方法中开始记录,并模拟一些耗时操作。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [SMCallTrace start];//开始记录
    [self test1];//耗时操作
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    ViewController *vc = [ViewController new];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}

在第一个显示出来的界面中模拟一个有层级的耗时方法,在viewDidAppear方法中结束记录,并打印个方法耗时。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test2];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [SMCallTrace stop];
    [SMCallTrace save];
}


- (void)test2 {
    sleep(0.79);
    [self test3];
}

- (void)test3 {
    sleep(1.77);
}

打印结果

 0| 2001.08|-[AppDelegate test1]
 path[AppDelegate test1]
 0|   1.67|-[UIWindow initWithFrame:]
 path[UIWindow initWithFrame:]
 0|   4.05|-[UIWindow setRootViewController:]
 path[UIWindow setRootViewController:]
 0| 1008.65|-[UIWindow makeKeyAndVisible]
 path[UIWindow makeKeyAndVisible]
 1| 1001.20|  -[ViewController test2]
 path[UIWindow makeKeyAndVisible] - [ViewController test2]
 2| 1001.15|    -[ViewController test3]
 path[UIWindow makeKeyAndVisible] - [ViewController test2] - [ViewController test3]

从打印结果可以看出耗时方法有test1, makeKeyAndVisible, test2, test3.
其中makeKeyAndVisible耗时是因为test2和test3在vc的viewDidLoad方法中调用的。
这样我们分析出具体的耗时方法就可以针对的调整调用时机,或者放到子线程调用了。

关注公众号回复476o7获取demo


参考文章:
https://www.jianshu.com/p/ce36d9efb6b0
https://www.jianshu.com/p/f26c4f16692a
https://www.jianshu.com/p/027a8abca64f
http://www.jianshu.com/p/d280feedbca0
http://www.cocoachina.com/ios/20161102/17931.html

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