以前公司的 UI 设计师和咱们提过好几回启动时间的事情,当时在开发业务,因此没有时间去作这件事。最近发完版本,终于有时间搞一搞启动时间了。git
通常而言,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。咱们进行优化的时候,咱们将启动时间分为 pre-main
时间和 main
函数到第一个界面渲染完成时间这两个部分。github
为何这么划分呢?你们都知道 APP 的入口是 main
函数,在 main
以前,咱们本身的代码是不会执行的。而进入到 main
函数之后,咱们的代码都是从性能优化
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
开始执行的,因此很明显,优化这两部分的思路是不同的。网络
为了方便起见,咱们将 pre-main
时间成为 t1
时间,而将main
函数到第一个界面渲染完成这段时间称为 t2
时间。架构
咱们先来看第一部分,也就是从 main
函数到第一个界面渲染完成这段时间。在开始以前,咱们先来磨练一个咱们本身的工具。app
生活中,咱们计量一段时间通常是用计时器。这里咱们要想知道哪些操做,或者说哪些代码是耗时的,咱们也须要一个打点计时器。用过 profile
的朋友都知道这个工具很强大,可使用它来分析出哪些代码是耗时的。可是它不够灵活,咱们来看一下咱们的这个计时器应该怎么设计。框架
如上图所示,在时间轴上,咱们从 start 开始打点计时,而后咱们在第一个小红旗那里打了一个点,记录这段代码的耗时,而后又在第二个小红旗那里打了一个点,记录这中间代码的耗时。而后在结束的地方打一个点,而后把全部打点的结果展现出来。同时,咱们为每段计时加上标注,用来区分这段时间是执行了什么操做花费的时间。这样一来,咱们就能快速精准的知道到底是谁拖慢了启动。ide
下面这张截图是贝聊老师端没有开始优化的耗时,由于涉及到公司具体的业务,因此我将部分信息加了遮挡。借助于咱们的工具,咱们能够定位任何一行代码的耗时。函数
咱们看 t2
耗时那里,总共花费了 6.361
秒,这是从 didFinishLaunchingWithOptions
到第一个界面渲染出来花费的时间。从这个结果来看,咱们的启动时间的优化已经到了刻不容缓的地步了。工具
再仔细分析一下上面的结果, t2
时间也分为了两个部分,didFinishLaunchingWithOptions
花了 4.010
秒,第一个页面渲染耗时花了 2.531
秒。好,看样子大魔头住在 didFinishLaunchingWithOptions
这个方法里,另外,第一页面的渲染中也有很多问题。下面咱们分别展开。
didFinishLaunchingWithOptions
上面说到大魔头住在 didFinishLaunchingWithOptions
,如今咱们仔细看一下 didFinishLaunchingWithOptions
方法里的代码耗时,有两行代码的耗时竟然为一秒以上,并且耗时最多的竟然有 1.620
秒之多。
其实 didFinishLaunchingWithOptions
方法里咱们通常都有如下的逻辑:
- 初始化第三方 SDK
- 配置 APP 运行须要的环境
- 本身的一些工具类的初始化
- ...
若是咱们的 UI 架构是上面这样的话。而后咱们在 AppDelegate 里写下这么一段代码:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"didFinishLaunchingWithOptions 开始执行"); self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; BLTabBarController *tabBarVc = [BLTabBarController new]; self.window.rootViewController = tabBarVc; [self.window makeKeyAndVisible]; NSLog(@"didFinishLaunchingWithOptions 跑完了"); return YES; }
而后咱们来到 BLTabBarController
里的 viewDidLoad
方法里进行它的 viewControllers
的设置,而后再进入到每一个 viewController
的 viewDidLoad
方法里进行更多的初始化操做。那么你以为从 didFinishLaunchingWithOptions
到最后显示展现的 viewController
的 viewDidLoad
这些方法的执行顺序是怎么样的呢?
下面是我写的一个 demo,用来展现加载的顺序:
2017-08-15 10:46:57.860 Demo[1404:325698] didFinishLaunchingWithOptions 开始执行 2017-08-15 10:46:57.862 Demo[1404:325698] 开始加载 BLTabBarController 的 viewDidLoad 2017-08-15 10:46:57.874 Demo[1404:325698] didFinishLaunchingWithOptions 跑完了 2017-08-15 10:46:57.876 Demo[1404:325698] 开始加载 BLViewController 的 viewDidLoad, 而后执行一堆初始化的操做
上面的状况是能保证咱们不在 BLTabBarController
中操做 BLViewController
的 view
,若是咱们在BLTabBarController
中操做了 BLViewController
的 view
的话,那么调用顺序将会是这样:
2017-08-15 11:09:03.661 Demo[1458:349413] didFinishLaunchingWithOptions 开始执行 2017-08-15 11:09:03.663 Demo[1458:349413] 开始加载 BLTabBarController 的 viewDidLoad 2017-08-15 11:09:03.664 Demo[1458:349413] 开始加载 BLViewController 的 viewDidLoad, 而后执行一堆初始化的操做 2017-08-15 11:09:03.676 Demo[1458:349413] didFinishLaunchingWithOptions 跑完了
这是很可怕的一件事情,为何呢?由于通常咱们都把界面的初始化、网络请求、数据解析、视图渲染等操做放在了 viewDidLoad
方法里,这样一来每次启动 APP 的时候,在用户看到第一个页面以前,咱们要把这些事件所有都处理完,才会进入到视图渲染阶段。
上面分析了拖慢 t2
的两个因素,它们是 didFinishLaunchingWithOptions
里面的初始化以及第一个页面渲染耗时。对于这两个不一样的方面,咱们的优化思路也是不同的。
didFinishLaunchingWithOptions
对于 didFinishLaunchingWithOptions
,这里面的初始化是必须执行的,可是咱们能够适当的根据功能的不一样对应的适当延迟启动的时机。对于咱们项目,我将初始化分为三个类型:
- 日志、统计等必须在 APP 一块儿动就最早配置的事件
- 项目配置、环境配置、用户信息的初始化 、推送、IM等事件
- 其余 SDK 和配置事件
对于第一类,因为这类事件的特殊性,因此必须第一时间启动,仍然把它留在 didFinishLaunchingWithOptions
里启动。第二类事件,这些功能在用户进入 APP 主体的以前是必需要加载完的,因此咱们能够把它放在第二批,也就是用户已经看到广告页面,再进行广告倒计时的时候再启动。第三类事件,因为不是必须的,因此咱们能够放在第一个界面渲染完成之后的 viewDidAppear
方法里,这里彻底不会影响到启动时间。
就这样,进行过这一轮优化之后,咱们的 t2
事件就从 6 秒多
降到 2 秒多
。
咱们的思路是这样的,用户点击 APP,我先尽快把广告页面加载出来。这样,用户就不会以为启动慢了,同时咱们能够在广告读秒的过程当中进行第二批启动事件的加载,这个加载用户也感受不到。但还没完,等会广告展现完,切到主 APP 的时候,若是一系列 viewDidLoad
里方法里有不少耗时的操做,那用户仍是会感受到卡顿。
因此对于第一个页面渲染的优化思路就是,先立马展现一个空壳的 UI 给用户,而后在 viewDidAppear
方法里进行数据加载解析渲染等一系列操做,这样一来,用户已经看到界面了,就不会以为是启动慢,这个时候的等待就变成等待数据请求了,这样就把这部分时间转嫁出去了。
通过这两轮优化,咱们的 t2
时间就从 6 秒多
变成了 0.1 秒不到
,也便是总共砍掉了 6 秒多
的启动时间。
为此,我专门建了一个类来负责启动事件,为何呢?若是不这么作,那么这次优化之后,之后再引入第三方的时候,别的同事可能很直觉的就把第三方的初始化放到了 didFinishLaunchingWithOptions
方法里,这样长此以往, didFinishLaunchingWithOptions
又变得不堪重负,到时候又要专门花时间来作重复的优化。
下面是这个类的头文件:
/** * 注意: 这个类负责全部的 didFinishLaunchingWithOptions 延迟事件的加载. * 之后引入第三方须要在 didFinishLaunchingWithOptions 里初始化或者咱们本身的类须要在 didFinishLaunchingWithOptions 初始化的时候, * 要考虑尽可能少的启动时间带来好的用户体验, 因此应该根据须要减小 didFinishLaunchingWithOptions 里耗时的操做. * 第一类: 好比日志 / 统计等须要第一时间启动的, 仍然放在 didFinishLaunchingWithOptions 中. * 第二类: 好比用户数据须要在广告显示完成之后使用, 因此须要伴随广告页启动, 只须要将启动代码放到 startupEventsOnADTimeWithAppDelegate 方法里. * 第三类: 好比直播和分享等业务, 确定是用户能看到真正的主界面之后才须要启动, 因此推迟到主界面加载完成之后启动, 只须要将代码放到 startupEventsOnDidAppearAppContent 方法里. */ #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface BLDelayStartupTool : NSObject /** * 启动伴随 didFinishLaunchingWithOptions 启动的事件. * 启动类型为:日志 / 统计等须要第一时间启动的. */ + (void)startupEventsOnAppDidFinishLaunchingWithOptions; /** * 启动能够在展现广告的时候初始化的事件. * 启动类型为: 用户数据须要在广告显示完成之后使用, 因此须要伴随广告页启动. */ + (void)startupEventsOnADTime; /** * 启动在第一个界面显示完(用户已经进入主界面)之后能够加载的事件. * 启动类型为: 好比直播和分享等业务, 确定是用户能看到真正的主界面之后才须要启动, 因此推迟到主界面加载完成之后启动. */ + (void)startupEventsOnDidAppearAppContent; @end NS_ASSUME_NONNULL_END
下面是 .m
文件,这里作了一层自动校验,若是 30 秒
之后,这些启动项有没有被启动的,就会在 DEBUG
环境下弹出警告信息。同时也会将那些没有启动的启动项进行启动。
#import "BLDelayStartupTool.h" static BOOL _isCalledStartupEventsOnAppDidFinishLaunchingWithOptions = NO; static BOOL _isCalledStartupEventsOnADTimeWithAppDelegate = NO; static BOOL _isCalledStartupEventsOnDidAppearAppContent = NO; const NSTimeInterval kBLDelayStartupEventsToolCheckCallTimeInterval = 30; @implementation BLDelayStartupTool + (void)load { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBLDelayStartupEventsToolCheckCallTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self checkStartupEventsDidLaunched]; }); } + (void)checkStartupEventsDidLaunched { NSString *alertString = @""; if (!_isCalledStartupEventsOnAppDidFinishLaunchingWithOptions) { alertString = [alertString stringByAppendingString:@"AppDidFinishLaunching, "]; [self startupEventsOnAppDidFinishLaunchingWithOptions]; } if (!_isCalledStartupEventsOnADTimeWithAppDelegate) { alertString = [alertString stringByAppendingString:@"ADTime, "]; [self startupEventsOnADTime]; } if (!_isCalledStartupEventsOnDidAppearAppContent) { alertString = [alertString stringByAppendingString:@"DidAppearAppContent"]; [self startupEventsOnDidAppearAppContent]; } if (alertString.length > 0) { #if DEBUG alertString = [alertString stringByAppendingString:@" 等延迟启动项没有启动, 这会形成应用奔溃"]; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"注意" message:alertString delegate:nil cancelButtonTitle:@"好的" otherButtonTitles:nil]; [alertView show]; #endif } } + (void)startupEventsOnAppDidFinishLaunchingWithOptions { _isCalledStartupEventsOnAppDidFinishLaunchingWithOptions = YES; } + (void)startupEventsOnADTime { _isCalledStartupEventsOnADTimeWithAppDelegate = YES; } + (void)startupEventsOnDidAppearAppContent { _isCalledStartupEventsOnDidAppearAppContent = YES; } @end
pre-main
时间上面已经将 t2
时间处理好了,接下来看看 pre-main
。
苹果为查看 pre-main
提供了支持,具体配置以下,配置的 key 为:DYLD_PRINT_STATISTICS
。
还须要勾选下面这个选项:
而后再运行项目,Xcode
就会在控制台输出这部分 pre-main
的耗时:
Total pre-main time: 2.2 seconds (100.0%) dylib loading time: 1.0 seconds (45.2%) rebase/binding time: 100.05 milliseconds (4.3%) ObjC setup time: 207.21 milliseconds (9.0%) initializer time: 946.39 milliseconds (41.3%) slowest intializers : libSystem.B.dylib : 8.54 milliseconds (0.3%) libBacktraceRecording.dylib : 46.30 milliseconds (2.0%) libglInterpose.dylib : 187.42 milliseconds (8.1%) beiliao : 896.56 milliseconds (39.1%)
可是这部分不是那么好处理,由于这部分主要是由如下几个方面影响的:
- 用到的系统的动态库的数量,好比
UIKit.framework
等- cocoapods 里引用的第三方框架数量
- 项目中类的数量
load
方法中执行的代码- 组件化
其余还有,请大神补充。上面几点中,咱们能作的也就是把全部类的 load
方法扫一遍,不要在这里面执行耗时的操做。其余的不是短期能改变的。
若是你想在这些方面有所突破的话,请看下面参考文章。
参考文章:
App Startup Time: Past, Present, and Future
iOS App 启动性能优化
WWDC 之优化 App 启动速度
iOS Dynamic Framework 对App启动时间影响实测
优化 App 的启动时间
下面这个连接是我全部文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。若是某篇文章恰好在你的实际开发中帮到你,又或者提供一种不一样的实现思路,让你以为有用,那就看看这句话 “坚持天天点赞的人,99%都是帅哥美女,不再用单身了”。
你还能够关注我本身维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。
若是你有问题,除了在文章最后留言,还能够在微博 @盼盼_HKbuy上给我留言,以及访问个人 Github。
做者:NewPan 连接:http://www.jianshu.com/p/c1734cbdf39b