OC经验-启动过程&启动时长统计&启动优化

一、启动类型

  • 冷启动:内存中不包含app相关数据的启动,一般可以通过重启手机后打开app来实现冷启动。
  • 热启动:杀掉app进程后,数据仍在内存中的启动。

二、启动过程

启动过程大体可分为pre-main阶段main函数之后两个阶段。

pre-main阶段

xcode提供了测试方法,我们可以通过配置 Schemes 中的环境变量 DYLD_PRINT_STATISTICS (简略)或 DYLD_PRINT_STATISTICS_DETAILS (详细)为1,可以看到 pre-main 阶段各个步骤消耗时长。

WeChat267fe4226925baeb2cf8d4d5c7057cb7.png

291618904923_.pic_hd.jpg

  • dylib loading time(动态库耗时):主要是递归加载动态库

  • rebase/binding time(偏移修正/符号绑定耗时)

    • rebase(偏移修正):修复内部指针。ASLR+偏移值 = 运行时确定的内存地址,造成这种现象的原因是因为系统运用了虚拟内存
    • binding(符号绑定): 绑定就是给符号赋值的过程。例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定)。
  • Objc setup time(OC类注册耗时):对类、类别进行注册,以及选择器的分配

  • initializer time(执行load和__attribute__((constructor))修饰的函数和C++ Static Initializers)

三、时长统计

image.png

进程开启时间
#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阶段

image.png
我们能做的很有限:

  • 减少动态库、合并一些动态库(定期清理不必要的动态库),
  • 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
  • 减少C++虚函数数量
  • Swift尽量使用struct
  • 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load

main后

  • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
  • 充分利用多线程

另外二进制重拍也是不错的选择。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享