前言
过长的启动时间对于用户体验是非常的不好的,苹果建议的启动时间不要超过400ms,大约0.4秒,并且启动时间超过20s将被系统直接杀死,所以优化启动时间是极其必要的。
这里有篇写的不错: www.jianshu.com/p/db493e529… 美团: tech.meituan.com/2018/12/06/…
App的两种启动方式:冷/热启动、App完整启动流程以及优化思路
但是,一般提到启动优化和监控,基本上都是指的是:冷启动。所以下面的介绍,也是冷启动优化和监控。
一、冷启动与热启动
冷启动:App点击启动前,此时App的进程还不在系统里。需要系统新创建一个进程分配给App。热启动:App在冷启动后用户将App退回后台,此时App的进程还在系统里。用户重新返回App的过程。
主要区别:
| 名称 | 区别 |
|---|---|
| 冷启动 | 启动时,App的进程不在系统里,需要开启新进程。 |
| 热启动 | 启动时,App的进程还在系统里,不需要开启新进程。 |
热启动流程:
二、启动耗时统计
用户能感知到的启动慢,其实都发生在主线程上。
而主线程慢的原因有很多,比如在主线程上执行了耗时的IO读写操作、在渲染周期中执行了大量计算等。
我们一般统计 App 的启动耗时是从点击 App 开始到首屏渲染完成的过程所消耗的时间。
总结来说,App 的启动主要包括三个阶段:
main()函数执行前;即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。main()函数执行后;即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕.- 首屏渲染完成后;即
didFinishLaunchingWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成
启动耗时:
t(App总启动时间) = t1(main()之前的加载时间) + t2(main()之后到didFinishLaunchingWithOptions的加载时间) + t3首屏渲染完成
注释:pre-main阶段优化是相对比较复杂的,主要优化还是在main()之后
1,main 函数之前称为 pre_main 阶段,main函数执行前,系统会做的事:
App启动后,首先,系统内核(Kernel)创建一个进程。 加载可执行文件。(App里的所有.o文件)- 加载动态链接库(
dyld是苹果的动态链接器),进行rebase指针调整和bind符号绑定。ObjC的runtime初始化。包括:ObjC相关Class的注册、category注册、selector唯一性检查等。- 初始化。包括:执行
+load()方法、用attribute((constructor))修饰的函数的调用、创建C++静态全局变量等。
此过程pre_main(main 函数之前)我们可以使用如下方法去查看时长:
打开需要检测耗时的项目,Xcode -> Product -> Scheme -> Edit Scheme,新增: DYLD_PRINT_STATISTICS 设置值为1
如上图设置完成,运行项目,然后可以看到下面表格数据 :
| 加载过程 | 加载时长 |
|---|---|
| Total pre-main time: | 460.33 milliseconds (100.0%) 总耗时大概:0.46秒 |
| dylib loading time: | 197.97 milliseconds (43.0%)加载动态库 (可以删除不必要的动态库,动态库合并,或将动态库变成静态库)。 |
| rebase/binding time: | 85.82 milliseconds (18.6%) 指针重定位(进行rebase指针调整和bind符号绑定,ASLR随机分配的内存地址+文件偏移地址,用于找到外部方法)(外部方法:可执行文件外的方法)。 |
| ObjC setup time: | 49.00 milliseconds (10.6%) ObjC类初始化(包括ObjC相关Class的注册、category注册、selector唯一性检查等,每20000个类大概增加耗时800ms删除僵尸类。) |
| initializer time: | 127.53 milliseconds (27.7%) 其它初始化(调用每个ObjC类与分类的+load方法,创建C++静态全局变量)。 |
| slowest intializers : | 比较耗时的动态库,根据上面可对pre-main阶段进行优化,同时对比swift和OC,因为swift是静态语言,相对耗时会更少。 |
| libSystem.B.dylib : | 4.10 milliseconds (0.8%) 一个系统的动态库。 |
| libc++.1.dylib : | 13.71 milliseconds (2.9%) |
| libMainThreadChecker.dylib : | 31.80 milliseconds (6.9%) |
还有一个方法获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1就可以:
total time: 1.0 seconds (100.0%)
total images loaded: 243 (0 from dyld shared cache)
total segments mapped: 721, into 93608 pages with 6173 pages pre-fetched
total images loading time: 817.51 milliseconds (78.3%)
total load time in ObjC: 63.02 milliseconds (6.0%)
total debugger pause time: 683.67 milliseconds (65.5%)
total dtrace DOF registration time: 0.07 milliseconds (0.0%)
total rebase fixups: 2,131,938
total rebase fixups time: 37.54 milliseconds (3.5%)
total binding fixups: 243,422
total binding fixups time: 29.60 milliseconds (2.8%)
total weak binding fixups time: 1.75 milliseconds (0.1%)
total redo shared cached bindings time: 29.32 milliseconds (2.8%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 93.76 milliseconds (8.9%)
libSystem.dylib : 2.58 milliseconds (0.2%)
libBacktraceRecording.dylib : 3.06 milliseconds (0.2%)
CoreFoundation : 1.85 milliseconds (0.1%)
Foundation : 2.61 milliseconds (0.2%)
libMainThreadChecker.dylib : 42.73 milliseconds (4.0%)
ModelIO : 1.93 milliseconds (0.1%)
AFNetworking : 18.76 milliseconds (1.7%)
LDXLog : 9.46 milliseconds (0.9%)
libswiftCore.dylib : 1.16 milliseconds (0.1%)
libswiftCoreImage.dylib : 1.51 milliseconds (0.1%)
Bigger : 3.91 milliseconds (0.3%)
Reachability : 1.48 milliseconds (0.1%)
ReactiveCocoa : 1.56 milliseconds (0.1%)
SDWebImage : 1.41 milliseconds (0.1%)
SVProgressHUD : 1.23 milliseconds (0.1%)
total symbol trie searches: 133246
total symbol table binary searches: 0
total images defining weak symbols: 30
total images using weak symbols: 69
那么针对此过程pre_main(main 函数之前)的优化方案:
- (1)减少使用
+load()方法里做事情,尽量把这些事情推迟到+initiailize方案①:如果可能的话,将+load中的内容,放到渲染完成后做。 方案②:使用+initialize()的方法代替+load(),注意把逻辑移动到+initialize()时,要注意避免+initialize()的重复调用问题,可以使用dispatch_once()让逻辑只执行一次。- (2)
合并多个动态库苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。 除了我们App本身的可行性文件,系统中所有的framework比如UIKit、Foundation等都是以动态链接库的方式集成进App中的。系统使用动态链接有几点好处:代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中只有一份。易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。- (3)
优化类、方法、全局变量减少ObjC类(class)、方法(selector)、分类(category)的数量;少用C++全局变量;main函数执行后,减少加载启动后不会去使用的类或者方法。- (4)
优化首屏渲染前的功能初始化main函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。首屏渲染外的其他功能放到首屏渲染完成后去初始化。- (5)
优化主线程耗时操作,防止屏幕卡顿首先检查首屏渲染前,主线程上的耗时操作。将耗时操作滞后或异步处理。通常的耗时操作有:网络加载、编辑、存储图片和文件等资源。针对耗时操作做相对应的优化即可。- (6)
通过重排 Mach-O中的二进制,减少启动流程中的缺页中断次数(Page Fault)
小知识点:+load()与+initialize()两者的区别?
+load()方法会在main()函数调用前就调用,而+initialize()是在类第一次使用时才会调用。
+load方法的调用优先级: 父类 > 子类 > 分类,并且不会被覆盖,均会调用。
+load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类也会加载。+initialize方法的调用优先级:分类 > 子类,父类 > 子类。(父类的分类重写了+initialize方法会覆盖父类的+initialize方法)
2,main函数执行后:
main函数执行后的阶段,指的是:从 main 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions方法里首屏渲染相关方法执行完成。即: 从main函数执行到设置self.window.rootViewController执行完成的阶段。
- 首屏初始化所需配置文件的读写操作;
- 首屏列表大数据的读取;
- 首屏渲染的大量计算;
此过程,时间统计可以如下:
在main函数文件下:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
CFAbsoluteTime startTime;
int main(int argc, char * argv[]) {
startTime = CFAbsoluteTimeGetCurrent();
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
在 AppDelegate.h里面
#import <UIKit/UIKit.h>
extern CFAbsoluteTime startTime;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
在 AppDelegate.m里面
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
double launchTime = CFAbsoluteTimeGetCurrent() - startTime;
NSLog(@"main()阶段测量时间launchTime-----: %.2f秒",launchTime);
return YES;
}
最终打印:
main()阶段测量时间launchTime-----: 0.06秒
那么针对此过程main() 函数执行后的优化项的优化方案:
- 减少在主线程中执行IO读写操作.
- 将各种SDK(二方、三方)初使化工作放到子线程处理.
- 减少首屏渲染的大量计算.
首屏渲染完成后的的优化方案:
这个阶段用户已经能够看到 App 的首页信息了,所以优化的优先级排在最后。但是,那些会卡住主线程的方法还是需要最优先处理的,不然还是会影响到用户后面的交互操作。