前语
过长的发动时刻关于用户体验是非常的欠好的,苹果建议的发动时刻不要超越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 的主页信息了,所以优化的优先级
排在最终。但是,那些会卡住主线程的办法仍是需求最优先处理的,否则仍是会影响到用户后边的交互操作。