iOS App 启动性能优化

本文来自于腾讯Bugly公众号(weixinBugly),未经做者赞成,请勿转载,原文地址:https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA程序员

做者:samsonxu算法

导语

本文介绍了如何优化iOS App的启动性能。设计模式

本文分为四个部分:缓存

  • 第一部分科普了一些和App启动性能相关的前置知识
  • 第二部分主要讲如何定制启动性能的优化目标
  • 第三部分经过在WiFi管家这个具体项目的优化过程,分享一些有用的经验
  • 第四部分是关键点的总结。

【第一部分】一些小科普

由于篇幅的限制,没有办法很详尽的说明一些原理性的东西,只是方便你们了解哪些事情可能跟启动性能有关。同时,内容相对也比较入门,大神们请跳过这一部分。安全

1. App启动过程

  • 解析Info.plist
    • 加载相关信息,例如如闪屏
    • 沙箱创建、权限检查
  • Mach-O加载
  • 若是是胖二进制文件,寻找合适当前CPU类别的部分
  • 加载全部依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 执行声明为__attribute__((constructor))的C函数
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  • 程序执行
  • 调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching

2. 如何测量启动过程耗时

冷启动比热启动重要

当用户按下home键的时候,iOS的App并不会立刻被kill掉,还会继续存活若干时间。理想状况下,用户点击App的图标再次回来的时候,App几乎不须要作什么,就能够还原到退出前的状态,继续为用户服务。这种持续存活的状况下启动App,咱们称为热启动,相对而言冷启动就是App被kill掉之后一切从头开始启动的过程。咱们这里只讨论App冷启动的状况。性能优化

main()函数以前

在不越狱的状况下,以往很难精确的测量在main()函数以前的启动耗时,于是咱们也每每容易忽略掉这部分数据。小型App确实不须要太过关注这部分。但若是是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),这部分耗时将会变得突出。所幸,苹果已经在Xcode中加入这部分的支持。微信

苹果提供的方法
  • 在Xcode的菜单中选择ProjectSchemeEdit Scheme...,而后找到 RunEnvironment Variables+,添加name为DYLD_PRINT_STATISTICSvalue1的环境变量。
    网络

  • 在Xcode运行App时,会在console中获得一个报告。例如,我在WiFi管家中加入以上设置以后,会获得这样一个报告:
Total pre-main time:  94.33 milliseconds (100.0%)
           dylib loading time:  61.87 milliseconds (65.5%)
          rebase/binding time:   3.09 milliseconds (3.2%)
              ObjC setup time:  10.78 milliseconds (11.4%)
             initializer time:  18.50 milliseconds (19.6%)
             slowest intializers :
               libSystem.B.dylib :   3.59 milliseconds (3.8%)
     libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)
                      GTFreeWifi :   7.09 milliseconds (7.5%)
如何解读
  1. main()函数以前总共使用了94.33ms
  2. 在94.33ms中,加载动态库用了61.87ms,指针重定位使用了3.09ms,ObjC类初始化使用了10.78ms,各类初始化使用了18.50ms。
  3. 在初始化耗费的18.50ms中,用时最多的三个初始化是libSystem.B.dylib、libBacktraceRecording.dylib以及GTFreeWifi。
main()函数以后

main()函数开始至applicationWillFinishLaunching结束,咱们统一称为main()函数以后的部分。架构

3. 影响启动性能的因素

App启动过程当中每个步骤都会影响启动性能,可是有些部分所消耗的时间少之又少,另外有些部分根本没法避免,考虑到投入产出比,咱们只列出咱们能够优化的部分:并发

main()函数以前耗时的影响因素
  • 动态库加载越多,启动越慢。
  • ObjC类越多,启动越慢
  • C的constructor函数越多,启动越慢
  • C++静态对象越多,启动越慢
  • ObjC的+load越多,启动越慢

实验证实,在ObjC类的数目同样多的状况下,须要加载的动态库越多,App启动就越慢。一样的,在动态库同样多的状况下,ObjC的类越多,App的启动也越慢。须要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难查察以为出,但1000个类和10000个类的分别就开始明显起来。

一样的,尽可能不要写__attribute__((constructor))的C函数,也尽可能不要用到C++的静态对象;至于ObjC的+load方法,彷佛你们已经习惯不用它了。任何状况下,能用dispatch_once()来完成的,就尽可能不要用到以上的方法。

main()函数以后耗时的影响因素
  • 执行main()函数的耗时
  • 执行applicationWillFinishLaunching的耗时
  • rootViewController及其childViewController的加载、view及其subviews的加载
applicationWillFinishLaunching的耗时

若是有这样这样的代码:

//AppDelegate.m
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.rootViewController = [[[MQQTabBarController alloc] init] autorelease];

    self.window = [[[UIWindow alloc] init] autorelease];
    [self.window makeKeyAndVisible];
    self.window.rootViewController = self.rootViewController;

    UITabBarController *tabBarViewController = [[[UITabBarController alloc] init] autorelease];


    NSLog(@"%s", __PRETTY_FUNCTION__);
    return YES;
}

...

//MQQTabBarController.m
@implementation MQQTabBarController

- (void)viewDidLoad {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    UIViewController *tab1 = [[[MQQTab1ViewController alloc] init] autorelease];
    tab1.tabBarItem.title = @"red";
    [self addChildViewController:tab1];

    UIViewController *tab2 = [[[MQQTab2ViewController alloc] init] autorelease];
    tab2.tabBarItem.title = @"blue";
    [self addChildViewController:tab2];

    UIViewController *tab3 = [[[MQQTab3ViewController alloc] init] autorelease];
    tab3.tabBarItem.title = @"green";
    [self addChildViewController:tab3];
}

...
@end

那么-[MQQTabBarController viewDidLoad]-[AppDelegate application:didFinishLaunchingWithOptions:]-[MQQTab1ViewController viewDidLoad]-[MQQTab2ViewController viewDidLoad]-[MQQTab2ViewController viewDidLoad] 完成的前后顺序是怎样的呢?

答案是:

  1. -[MQQTabBarController viewDidLoad]
  2. -[MQQTab1ViewController viewDidLoad]
  3. -[AppDelegate application:didFinishLaunchingWithOptions:]
  4. -[MQQTab2ViewController viewDidLoad] (点击了第二个tab以后加载)
  5. -[MQQTab3ViewController viewDidLoad] (点击了第三个tab以后加载)

通常而言,大部分状况下咱们都会把界面的初始化过程放在viewDidLoad,可是这个过程会影响消耗启动的时间。特别是在相似TabBarController这种会嵌套childViewController的ViewController的状况,它也会把部分children也初始化,所以各类viewDidLoad会递归的进行。

最简单的解决的方法,是把viewController延后加载,但实际上这属于一种掩耳盗铃,确实,applicationWillFinishLaunching的耗时是降下来了,但用户体验上并无感受变快。

更好一点的解决方法有点相似facebook,主视图会第一时间加载,但里面的数据和界面都会延后加载,这样用户就会阶段性的得到视觉上的变化,从而在视觉体验上感受App启动得很快。

【第二部分】优化的目标

因为每一个App的状况有所不一样,须要加载的数据量也有所不一样,事实上咱们没法使用一种统一的标准来衡量不一样的App。苹果。

  • 应该在400ms内完成main()函数以前的加载
  • 总体过程耗时不能超过20秒,不然系统会kill掉进程,App启动失败

400ms内完成main()函数前的加载的建议值是怎样定出来的呢?其实我也没有太深究过这个问题,可是,当用户点击了一个App的图标时,iOS作动画到闪屏图出现的时长正好是这个数字,我想也许跟这个有关。

针对不一样规模的App,咱们的目标应该有所取舍。例如,对于像手机QQ这种集整个SNG的代码大成撸出来的App,对动态库的使用在所不免,但对于WiFi管家,因为在用户链接WiFi的时候须要很是快速的响应,因此快速启动就很是重要。

那么,如何定制优化的目标呢?首先,要肯定启动性能的界限,例如,在各类App性能的指标中,哪一此属于启动性能的范畴,哪一些则于App的流畅度性能?我认为应该首先把启动过程分为四个部分:

  1. main()函数以前
  2. main()函数以后至applicationWillFinishLaunching完成
  3. App完成全部本地数据的加载并将相应的信息展现给用户
  4. App完成全部联网数据的加载并将相应的信息展现给用户

1+2一块儿决定了咱们须要用户等待多久才能出现一个主视图,同时也是技术上能够精确测量的时长,1+2+3决定了用户视觉上的等待出现有用信息所须要的时长,1+2+3+4决定了咱们须要多少时间才能让咱们须要展现给用户的全部信息所有出现。

淘宝的iOS客户端无疑是各部分都作得很是优秀的典型。它所承载的业务彻底不比微信和手机QQ少,但几乎瞬间完成了启动,并利用缓存机制使得用户立刻看到“貌似完整”的界面,而后当即又刷新了刚刚联网更新回来的信息。也就是说,不管是技术上仍是视觉上,它都很是的“快”。

【第三部分】WiFi管家启动优化实践

先show一下成果:

1. 移除不须要用到的动态库

由于WiFi管家是个小项目,用到的动态库很少,自动化处理的优点不大,我这里也就简单的把依赖的动态移除出项目,再根据编译错误一个一个加回来。若是有靠谱的方法,欢迎你们补充一下。

2. 移除不须要用到的类

项目作久了总有一些吊诡的类像幽灵同样驱之不去,因为【不要相信产品经理】的思想做怪,需求变动后,有些类可能用不上了,但却由于担忧需求再变回来就没有移除掉,后来就完全忘记要移除了。

为了解决这个历史问题,在这个过程当中我试过多种方法来扫描没有用到的类,其中有一种是编译后对ObjC类的指针引用进行反向扫描,惋惜实际上收获不是很明显,并且还要写不少例外代码来处理一些特殊状况。后来发现一个叫作fui(Find Unused Imports)的开源项目能很好的分析出再也不使用的类,准确率很是高,惟一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。

使用方法是在Terminal中cd到项目所在的目录,而后执行fui find,而后等上那么几分钟(是的你没有看错,真的须要好几分钟甚至须要更长的时间),就能够获得一个列表了。因为这个工具还不是100%靠谱,可根据这个列表,在Xcode中手动检查并删除再也不用到的类。

实际上,平常对代码工程的维护很是重要,若是制定好一套半废弃代码的维护方法,小问题就不会积累成大问题。有时候对于一些暂时再也不使用的代码,我也很纠结于要不要svn rm,由于从代码历史中找删除掉的文件仍是不太方便。不知道你们有没有相关的经验能够分享,也请不吝赐教。

3. 合并功能相似的类和扩展(Category)

因为Category的实现原理,和ObjC的动态绑定有很强的关系,因此实际上类的扩展是比较占用启动时间的。尽可能合并一些扩展,会对启动有必定的优化做用。不过我的认为也不能由于它占用启动时间而去逃避使用扩展,毕竟程序员的时间比CPU的时间值钱,这里只是强调要合并一些在工程、架构上没有太大意义的扩展。

4. 压缩资源图片

压缩图片为何能加快启动速度呢?由于启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操做量就小了,启动固然就会快了。

事实上,Xcode在编译App的时候,已经自动把须要打包到App里的资源图片压缩过一遍了。然而Xcode的压缩会相对比较保守。另外一方面,咱们正常的设计师因为须要符合其正常的审美须要生成的正常的PNG图片,所以图片大小是比较大的,然而若是以程序员的直男审美而采用过激的压缩会直接激怒设计师。

解决各类矛盾的方法就是要找出一种至关靠谱的压缩方法,并且最好是基本无损的,并且压缩率还要特别高,至少要比Xcode自动压缩的效果要更好才有意义。通过各类试验,最后发现惟一可靠的压缩算法是TinyPNG,其它各类方法,要么没效果,要么产生色差或模糊。可是很是惋惜的是TinyPNG并非彻底免费的,并且须要经过网络请求来压缩图片(应该是为了保护其牛逼的压缩算法)。

为了解决这个问题,我写了一个类来执行这个请求,请参见阅读原文里的SSTinyPNGRequest和SSPNGCompressor。由于这个项目只有我一我的在用因此代码写得有点随意,有问题能够私聊也能够在评论里问,有改进的方法也很是欢迎指正。另外说明一下,使用这个类须要你自行到 这里 申请APIKey,每个用户每个月有500张图片压缩是免费的,而每一个邮箱能够注册一个用户,你懂的。

5. 优化applicationWillFinishLaunching

随着项目作的时间长了,applicationWillFinishLaunching里要处理的代码会越积越多,WiFi管家的iOS版本有一段时间没有控制好,里面的逻辑乱得有点丢人。由于可能涉及到一些项目的安全性问题,这里不能分享全部的优化细节及发现的思路。仅列出在applicationWillFinishLaunching中主要须要处理的业务及相关问题的改进方案。

这里大部分都是一些苦逼活,但有一点特别值得分享的是,有一些优化,是没法在数据上体现的,可是视觉上却能给用户较大的提高。例如在【各类业务请求配置更新】的部分,通过分析优化后,启动过程并发的http请求数量从66条压缩到了23条,如此一来为启动成功后新闻资讯及其图片的加载留出了更多的带宽,从而保证了在第一时间完成新闻资讯的加载。实际测试代表,光作KPI的事情是不够的,人仍是须要有点理想,通过优化,在视觉体验上进步很是明显。

另外,过程当中请教过SNG的大牛们,据说他们由于须要在applicationWillFinishLaunching里处理的业务更多,因此还作了管理器管理这些任务,不过由于WiFi管家是个小项目,有点杀鸡用牛刀的感受,所以没有深刻研究。

6. 优化rootViewController加载

考虑到我做为一只高级程序猴,工资很高,为了给公司节约成本,在优化以前,固然须要先测试一下哪些ViewController的加载耗时比较大,而后再深刻到具体业务中看哪些部分存在较大的优化空间。同时,先作优化效果明显的部分也有利于加强本身的信心。

在开始讲述问题以前,咱们先来看一下WiFi管家的UI层次结构:

一个看似简单的界面因为承载了不少业务需求,代码量其实已经很是惊人。这里我不具体讲述这些惊人的业务量了,抽象而言可WiFi管家的UI架构整体而言基于TabBarController的框架,三个tab分别是“链接”、“发现”及“个人”。App启动的时候,根据加载原理,会加载TabBarController、第一个Tab(“链接”)的ViewController及其全部childViewController。

UI构架请看以下示意图,其中蓝色的部分须要在App启动的时候当即加载:

对全部启动相关的模块打锚点计算耗时后,发现tabBarController和connectingViewController分别占用了applicationWillFinishLaunching耗时的31%和24%。加载耗费了大量时间,这跟它所须要承载的逻辑任务彷佛并不对称。因而检查相关代码进行深刻分析,发现了几个问题比较严重:

  1. 有些程序员可能架构意识不是太强,直接在tabBarController的启动过程当中插入了各类奇怪的业务,例如检查WiFi链接状态变化、配置拉取,而这些业务显然应该在另外的某些地方统一处理,而不该该在一个ViewController上。

  2. 因为一些历史缘由,链接页的视图控制器connectingViewController包含了三个childViewController:WiFiViewController、3GViewController、errorViewController,分别在WiFi状态、3G状态和出错状态下展现界面(三选一,其中一个展现的时候其它两个视图会隐藏)。

  3. 大部分view都是直接加载完的。有些界面的加载很是复杂,好比再进入App时会展现一个检查WiFi可用性和安全性的动画,因为须要叠加较多图片,这部分视图的加载耗时较多。

因为随着几回改版以后,链接页的UI架构已经变得很不合理,历史包袱仍是比较重的,并且耦合比较严重,几乎没法改动,所以决定重构。至于tabBarController,检查代码后决定简单的把不相关的业务作一些迁移,优化childViewController的加载过程,不做重构。

改进后的结构大体以下图,其中蓝色部分须要在App启动的时候当即加载:

因为本篇主要讲启动性能优化,重构涉及的软件工程和设计模式方面的东西就不详细论述了,对启动优化的过程,主要是使用了更合理的分层结构,使得启动得以在更短的时间内完成。

至此,WiFi管家的启动性能基本优化完毕。

7. 挖掘最后一点性能优化

因为WiFi管家是一个具备WiFi链接能力的App,所以有可能在后台过程当中完成冷启动过程(其实是在用户进入系统的WiFi设置时,iOS会启动WiFi管家,以便请求WiFi密码)。在这种状况下,整个rootViewController都是不须要加载的。

【第四部分】总结

  • 利用DYLD_PRINT_STATISTICS分析main()函数以前的耗时
  • 从新梳理架构,减小动态库、ObjC类的数目,减小Category的数目
  • 按期扫描再也不使用的动态库、类、函数,例如每两个迭代一次
  • 用dispatch_once()代替全部的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load
  • 在设计师可接受的范围内压缩图片的大小,会有意外收获
  • 利用锚点分析applicationWillFinishLaunching的耗时
  • 将不须要立刻在applicationWillFinishLaunching执行的代码延后执行
  • rootViewController的加载,适当将某一级的childViewController或subviews延后加载
  • 若是你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController
  • 不该放过的一些小细节
  • 异步操做并不影响指标,但有可能影响交互体验,例如大量网络请求致使数据拥堵
  • 有时候一些交互上的优化比技术手段效果更明显,视觉上的快决不是冰冷的数据能够解释的,好好和大家的设计师谈谈动画

更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索