Xcode13 & iOS15 适配之打印启动时间

5,886 阅读3分钟

iOS15上统计启动时长的变量DYLD_PRINT_STATISTICS失效了。团队中需要保留每次的启动时间以作检验优化标准。在网上找到下面文章,写了个获取启动时间工具类。

参考文章:blog.csdn.net/yanhaijunya…

#import "AppLaunchTime.h"

#import <sys/sysctl.h>

#import <mach/mach.h>


@implementation AppLaunchTime


double __t1; // 创建进程时间

double __t2; // before main

double __t3; // didfinsh

/// 获取进程创建时间
+ (CFAbsoluteTime)processStartTime {

    if (__t1 == 0) {

        struct kinfo_proc procInfo;

        int pid = [[NSProcessInfo processInfo] processIdentifier];

        int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};

        size_t size = sizeof(procInfo);

        if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &procInfo, &size, NULL, 0) == 0) {

            __t1 = procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;

        }

    }

    return __t1;

}
/// 开始记录:在DidFinish中调用

+ (void)mark {

    double __t1 =  [LGAppLaunchTime processStartTime];
    
    
    dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用

        if (__t3 == 0) {

            __t3 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;

        }

        double pret = __t2 - __t1 / 1000;

        double didfinish = __t3 - __t2;

        double total = __t3 - __t1 / 1000;

        

        NSLog(@"----------App启动---------耗时:pre-main:%f",pret);

        NSLog(@"----------App启动---------耗时:didfinish:%f",didfinish);

        NSLog(@"----------App启动---------耗时:total:%f",total);

    });
}

/// 构造方法在main调用前调用 
/// 获取pre-main()阶段的结束时间点相对容易,可以直接取main()主函数的开始执行时间点.推荐使用__attribute__((constructor)) 构建器函数的被调用时间点作为pre-main()阶段结束时间点:__t2能最大程度实现解耦:

void static __attribute__((constructor)) before_main() {

    if (__t2 == 0) {

        __t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;

    }

}

@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [LGAppLaunchTime mark];
    ...

20201015155605911.png

pre-main()阶段开始时间:__t1 苹果公司并没有直接向开发者提供内部统计时间字段以供开发者直接获取App的启动开始时刻点,目前行业内主要有两种标准作为App的启动时间点:

第一种标准:以名称+(void)load方法被调用时的时间点 由于+(void)load 方法被调用的时间点发生Initializer初始化配置阶段, 根据(CompileSources)编译资源规则下动态库的加载顺序顺序的调用相应类下的+(void)load方法,因为动态库的加载顺序是递归加载的,所以我们只要找到最内部的叶子节点的动态库,然后在这个最内部叶子结点动态库中+(void)load方法重构以记录启动时间作为开始时间点。这种方式没有统计到Initializer初始化配置阶段前面部分所消耗的那些时间,比如在Initializer初始化配置阶段前面增加动态库、Category等造成的耗时并不能被及时发现统计。

第二种标准:获取整个进程创建(从开始到结束)消耗时间 App从源头配置直至运行整个过程实际上是一个逻辑进程,如果能获取到逻辑进程的起步创建时间即exec()可执行函数触发阶段的触动时间点作为整个app逻辑进程的开始时间点,能够更提前记录到App的启动开始时间点。

pre-main()阶段结束时间点:__t2

获取pre-main()阶段的结束时间点相对容易,可以直接取main()主函数的开始执行时间点。 推荐使用__attribute__((constructor)) 构建器函数的被调用时间点作为pre-main()阶段结束时间点:__t2

为什么不用最后一个load方法执行时间作为pre-main()阶段的结束时间点?因为在超大型工程中我们没办法确定哪个名称load方法是最后一个被执行load方法。。。

启动动作正式完成对应的时间点:__t3 启动动作正式完成对应的时间点一般以didFinishLaunchingWithOptions:已完成启动对应的代理协议函数的结束时间点,但didFinishLaunchingWithOptions:已完成启动对应的代理协议函数的结束时间点(仅仅对应着光点初步渲染出现)其实不包括光点出现之后启动图动画渲染的时间消耗,而启动图动画执行完成后的时间点更加接近于用户的感官。可以在执行didFinishLaunchingWithOptions: 的runloop循环的后面的循环来获取时间.