一、启动类型
- 冷启动:内存中不包含app相关数据的启动,一般可以通过重启手机后打开app来实现冷启动。
- 热启动:杀掉app进程后,数据仍在内存中的启动。
二、启动过程
启动过程大体可分为pre-main阶段
和main函数之后
两个阶段。
pre-main阶段
xcode提供了测试方法,我们可以通过配置 Schemes 中的环境变量 DYLD_PRINT_STATISTICS (简略)或 DYLD_PRINT_STATISTICS_DETAILS (详细)为1,可以看到 pre-main 阶段各个步骤消耗时长。
-
dylib loading time(动态库耗时):主要是递归加载动态库
-
rebase/binding time(偏移修正/符号绑定耗时)
- rebase(偏移修正):修复内部指针。
ASLR+偏移值 = 运行时确定的内存地址
,造成这种现象的原因是因为系统运用了虚拟内存。 - binding(符号绑定): 绑定就是给符号赋值的过程。例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定)。
- rebase(偏移修正):修复内部指针。
-
Objc setup time(OC类注册耗时):对类、类别进行注册,以及选择器的分配
-
initializer time(执行load和__attribute__((constructor))修饰的函数和C++ Static Initializers)
三、时长统计
进程开启时间
#import "StartAppTool.h"
#import <sys/sysctl.h>
#import <mach/mach.h>
@implementation StartAppTool
+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
size_t size = sizeof(*procInfo);
return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}
+ (NSTimeInterval)processStartTime
{
struct kinfo_proc kProcInfo;
if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
} else {
NSAssert(NO, @"无法取得进程的信息");
return 0;
}
}
@end
复制代码
第一个load
+load方法的调用顺序是按照链接顺序执行,如果使用CocoaPod来管理集成库,可以新建一个A开头的Pod库(CocoaPod是按照字母升序),让该Pod库的+load方法第一个被执行;
main开始、didFinishLaunchingWithOptions开始、 didFinishLaunchingWithOptions结束时间都好搞定
启动结束时间
- 方案一:第一个页面显示出来viewDidAppear
- 方案二:首屏首次绘制完成
获取首次可以通过在 didFinishLaunch 中向 Runloop 注册 block 或者 BeforeTimer 的 Observer 来获取上图中两个时间点的回调,代码如下
//注册block
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop block launch end:%f",stamp);
});
//注册kCFRunLoopBeforeTimers回调
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopActivity activities = kCFRunLoopAllActivities;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeTimers) {
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop beforetimers launch end:%f",stamp);
CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);
}
});
CFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);
复制代码
四、启动优化点
pre_main阶段
我们能做的很有限:
- 减少动态库、合并一些动态库(定期清理不必要的动态库),
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- Swift尽量使用struct
- 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load
main后
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 充分利用多线程
另外二进制重拍也是不错的选择。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END