一、 启动过程分析
1、解析Info.plist
a.加载相关信息,例如闪屏
b.沙箱建立、权限检查
2、Mach-O加载
Mach-O 文件:我们写的程序想要跑起来,肯定它的可执行文件格式要被操作系统所理解。比如ELF是Linux下的可执行文件格式,那么对于OS X / iOS来说,Mach-O 是其可执行文件格式。
Mach-O格式主要包括以下几种文件类型:
Executable:应用的主要二进制
Dylib:动态链接库
Bundle:不能被链接,只能在运行时使用dlopen加载
Image:包含Executable、Dylib和Bundle
Framework:包含Dylib、资源文件和头文件的文件夹
如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为__attribute__((constructor))的C函数
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的 +load 函数
3、程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
二、测量App的启动时间
iOS App的启动有两种方式:冷启动和热启动,下面对这两种启动方式进行简单介绍。
冷启动:App第一次启动或者是被上滑Kill掉之后,再次从点击App应用图标到App完全启动显示主页面为止的过程成为冷启动。
热启动:当用户从当前App按下HOME键退出后,系统并不会立即kill掉App的进程,还会继续后台执行一段时间(至于什么时候会被kill,完全取决于系统内存等资源的使用情况,理想情况下可以保持后台运行15min)。然后当用户再次点击App图标时,会很快加载到上次停留的页面,几乎不需要进行资源载入,这种情况下的启动成为热启动。
下面我们对于App启动时间的测量只讨论在冷启动情形下。根据苹果官方介绍,iOS应用的启动分为两个阶段:pre-main和main,所以App启动的总时间为:Total Time = pre-main Time + main Time。
Pre-main 阶段
Pre-mian 阶段即main()函数被调用之前的加载时间,包括dylib动态库的加载、Mach-O文件加载、Rebase/Bining、Objective-C Runtime 加载等。该过程如下图所示:
main 阶段
main 阶段指的是从调用main()函数开始,调用UIApplicationMain()创建UIApplication,AppDelegate,直到回调App代理方法applicationDidBecomeActive:为止,其中包括application:didFinishLaunchingWithOptions方法中创建keyWindow、三方sdk和其他初始化项所消耗的时间,也包含RootViewController 的 ChildViewController的view完全显示出来所花费的时间。该过程如下图所示:
main 阶段时间测量
那么对于pre-main 和 main 启动阶段的耗时如何测量呢?苹果已为我们提供了一个简单易用的测试方法:在Xcode中选中项目的Scheme→ Edit Scheme...→,然后选择Run→Arguments→Environment Variables中添加Name = DYLD_PRINT_STATISTICSvalue,Value = 1 的环境变量。如下图所示:
然后重新运行项目,注意控制台下面的打印输出,格式为:
Totalpre-main time:655.68milliseconds (100.0%)
dylib loading time:205.67milliseconds (31.3%)
rebase/binding time:320.95milliseconds (48.9%)
ObjCsetup time:63.07milliseconds (9.6%)
initializer time:65.85milliseconds (10.0%)
slowest intializers :
libSystem.B.dylib :3.43milliseconds (0.5%)
libMainThreadChecker.dylib :15.59milliseconds (2.3%)
century :76.56milliseconds (11.6%)
Totalpre-main time: 表示pre-main阶段总的时间消耗为655.68毫秒
dylib loading time: 动态库加载时间
rebase/binding time: 重定位指针指向/指向镜像外部内容 所需时间
ObjCsetup time:ObjC初始化时间
initializer time: 其他初始化时间的总和
slowest intializers : 最慢的三个初始化分别是libSystem.B.dylib、libMainThreadChecker.dylib、century(项目名称)
main 阶段时间测量
由于main阶段涉及到大量的自定义代码,app主体页面的初始化、前期配置、三方库初始化等大量逻辑代理,iOS系统没有直接的测量方式,所以只能通过打点的方式计算启动开始到启动结束的时间差即可。例如下面示例代码:
// main.m 文件
#import"AppDelegate.h"
externCFAbsoluteTimeStartTime;
intmain(intargc,char* argv[]) {
// 记录开始时间
StartTime =CFAbsoluteTimeGetCurrent();
@autoreleasepool{
returnUIApplicationMain(argc, argv,nil,NSStringFromClass([AppDelegateclass]));
}
}
// AppDelegate.m 文件
CFAbsoluteTimeStartTime;
- (void)applicationDidBecomeActive:(UIApplication*)application {
dispatch_async(dispatch_get_main_queue(), ^{
NSUIntegermilliseconds = (NSUInteger)((CFAbsoluteTimeGetCurrent() - StartTime) *1000);
NSLog(@"Loading done in %lu ms", milliseconds);
});
}
影响启动性能的因素
接下来我们讨论在App启动的pre-main阶段和mian阶段,影响启动性能的多种因素。
Pre-main 阶段
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load方法使用越多,启动越慢
我们尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象;至于ObjC的+load方法,似乎大家已经习惯不用它了。任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。
main 阶段
执行main()函数的耗时
执行application:didFinishLaunchingWithOptions的耗时
rootViewController及其childViewController的加载、view及其subviews的加载
一般的App的主体UI架构是,UITabbarViewController 作为 keyWindow的根控制器,然后包含多个ChildViewController作为每个tab的根控制器。然而在didFinishLaunchingWithOptions代理方法中各控制器的初始化顺序是怎样的呢?
答案是:
-[MQQTabBarController viewDidLoad]
-[MQQTab1ViewController viewDidLoad]
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[MQQTab2ViewController viewDidLoad] (点击了第二个tab之后加载)
-[MQQTab3ViewController viewDidLoad] (点击了第三个tab之后加载)
从上面的加载过程可以看出,在main()方法之后的启动中,只要是对页面的渲染,要减少这个阶段的耗时需要在viewDidLoad方法中做优化,减少不必要的逻辑,视图尽量使用懒加载,使用异步方式加载网络数据。
三、启动时间优化
苹果建议在400ms内完成pre-main 阶段的启动,App整体的启动时间不能超过20s,否则系统会被kill掉进程。
由于每个App的体量和类型不一样,对应启动过程中配置的逻辑处理复杂程度也是皆不相同,所有没有一个标准的时间来衡量启动时间是否达到最优。但是可以从用户体验上下功夫预加载占位视图,数据缓存等都可以营造出加载速度提升的感觉。
下面从实践的角度分析可优化的部分:
pre-main 阶段优化
1、移除不需要的动态库framework
2、移除不需要的类、未使用的变量、方法等
对应项目中大量的文件,可以使用工具快速扫描整个项目找出未使用的类、变量、或方法。这里推荐一个开源工具 Fui 能准确的完成此任务,不足之处在于它处理不了动态库或静态库中提供的类,和C++ 的类模板。
Fui使用方法:
在终端中cd到项目根目录,然后执行fui find,就可以得到一个列表,可根据这个列表在Xcode中手动检查并删除无用代码。
3、合并功能类似的类和扩展(Category)
4、+load方法中做的事情可以使用dispatch_once()来代替,因为main()方法被调用之前就会加载+load方法,会影响pre-main阶段启动事件。
5、压缩资源图片(对要求不高的图片可做压缩处理)
main 阶段优化
1、优化application: didFinishLaunchingWithOptions:内部逻辑
各种业务请求配置更新,多个配置更新可合并为单个请求,部分业务的配置延后更新。
对于新版本引导、广告闪屏逻辑,需要重构逻辑清晰
数据迁移,对太旧的无用的逻辑可以删除,不需要立即处理的数据,改为后台加载
2、优化rootViewController加载:在启动过程中只加载tabbarVc,主Vc即可,而且主Vc中的ViewDidLoad方法中也只加载需要立即显示出来的view,其他视图均使用懒加载,数据进行异步加载。
文章转载:https://www.dazhuanlan.com/2020/01/04/5e1055a4e07fa/?__cf_chl_jschl_tk__=7750f38bebb1f21fdaddcddca0415ee71d31c736-1614321013-0-AWpmiwwC2bq_8PHZ6dbnAI7dbzqLC36vJnCCmhi6ThuE9vMnd3LM67aZyDLR_NJL9OD6mRUWHOQyh3g4j0Pu2gfxG2bqnAUKHjusvzyCt9SgFPr_BvqiVVboKbJT2CnMKE17X9TXh29kBG4pUgxySpMoZiroz9gDZaKkCapAOGs88StJKu4rDYYjwNwCfv5apw7KDW2yUwjsu-8JFscQ3j0KlbepXj_f87G1Cq2LI0JosaCI1xKUmw1csldKhrqcDOmp-hMtRnt6tF_8dQ1ggjBaRUb0qne-6PNV4HeCmgyrGT4o2mjAJDBLu8aaGNuROH0camvmKwRQHg5_L0zQT0I