基于公司项目的一次启动优化的尝试
一、启动时间统计
在Xcode13前,通过增加key能实现打印启动时间的功能,在Xcode13后,使用DYLD_PRINT_STATISTICS发现并不会打印启动时间。找了个替代方案
//
//  IMIAppLaunchTime.m
//  CMKit_Example
//
//  Created by Roffa Zhou on 2022/7/1.
//  Copyright © 2022 xxx. All rights reserved.
//
#import "IMIAppLaunchTime.h"
#import <sys/sysctl.h>
#import <mach/mach.h>
@implementation IMIAppLaunchTime
double __t1; // 创建进程时间 毫秒
double __t2; // before main
double __t3; // didfinsh
/// 获取进程创建时间
+ (CFAbsoluteTime)processStartTime {
    if (__t1 == 0) {
        struct kinfo_proc procInfo;
        NSProcessInfo *processInfo = [NSProcessInfo processInfo];
        int pid = [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 =  [IMIAppLaunchTime 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:%fs",pret);
        NSLog(@"----------App启动---------耗时:didfinish:%fs",didfinish);
        NSLog(@"----------App启动---------耗时:total:%fs",total);
    });
}
/// 构造方法在main调用前调用
/// 获取pre-main()阶段的结束时间点相对容易,可以直接取main()主函数的开始执行时间点.推荐使用__attribute__((constructor)) 构建器函数的被调用时间点作为pre-main()阶段结束时间点:__t2能最大程度实现解耦:
void static __attribute__((constructor)) before_main() {
    if (__t2 == 0) {
        __t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
    }
}
@end
计算load的时间是通过在Compile Sources中第一个文件增加load时间戳打印,再再main方法中增加时间戳打印,可能不准确,但是也能当一个load的运行时间作为判断了
NSLog(@"-------------App启动 main begin: %f", CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970);
经过统计得到各时间段如下,5次得到的结果(取5次中出现总耗时相对最稳定的一个值为参照基准,此次测试取总耗时1.8s为基准,与此值相差100ms以上的结果剔除)
| 描述 | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 5次平均 | 
|---|---|---|---|---|---|---|
| load方法耗时 | 43ms | 38ms | 43ms | 41ms | 42ms | 41ms | 
| pre-main耗时 | 1.243s | 1.213s | 1.220s | 1.238s | 1.192s | 1.295s | 
| didfinish耗时 | 622ms | 580ms | 582ms | 601ms | 621ms | 601ms | 
| 总耗时 | 1.865s | 1.793s | 1.802s | 1.839s | 1.813s | 1.822s | 
综上,pre-main耗时较多,由于处理pre-main相对更为复杂,因此先从didfinish耗时开始进行优化
didFinish优化过程
- 主线程异步执行可以暂缓执行的方法
//下面方法被移动到异步执行
dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
        ILLogV(@"didFinishLaunching");
        //打印系统信息
        [self printSystemInfo];
        //设置显示样式
        [self initDisplayStyle];
        /// 设置APP更新本地存储
        [self setAppUpdateClickCancel];
        [self push_application:application didFinishLaunchingWithOptions:launchOptions];
    });
通过此次优化后,耗时结果如下
| 描述 | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 5次平均 | 
|---|---|---|---|---|---|---|
| load方法耗时 | 43ms | 38ms | 43ms | 41ms | 42ms | 41ms | 
| pre-main耗时 | 1.275s | 1.204s | 1.199s | 1.192s | 1.205s | 1.215s | 
| didfinish耗时 | 588ms | 604ms | 596ms | 594ms | 585ms | 593ms | 
| 总耗时 | 1.863s | 1.809s | 1.794s | 1.786s | 1.790s | 1.808s | 
此次耗时统计对启动时间的优化不是特别明显,总耗时优化了1帧左右
- 主线程异步创建首页相关视图、第三方sdk
 改造前
 [self initThirdSDKConfigWithApplication:application options:launchOptions];
 [self initManagerConfig];
 [self initWindows];          
 [self.window makeKeyAndVisible];
 [self initViews];
改造后
dispatch_async(dispatch_get_main_queue(), ^{ // 确保didFihish代码执行后调用
     //开始初始化
     [self initThirdSDKConfigWithApplication:application options:launchOptions];
     [self initManagerConfig];
     [self initViews];
 });
 [self initWindows];
UIViewController *launchVC = [[UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil] instantiateViewControllerWithIdentifier:@"launchScreen"];
self.window.rootViewController = launchVC;
[self.window makeKeyAndVisible];
由于视图采用异步绘制,会有瞬时的闪动,为了处理此问题,增加了读取启动图vc,默认先把启动图作为rootview
| 描述 | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 5次平均 | 
|---|---|---|---|---|---|---|
| load方法耗时 | 43ms | 38ms | 43ms | 41ms | 42ms | 41ms | 
| pre-main耗时 | 1.312s | 1.411s | 1.258s | 1.249s | 1.368s | 1.319s | 
| didfinish耗时 | 259ms | 271ms | 286ms | 258ms | 285ms | 272ms | 
| 总耗时 | 1.571s | 1.682s | 1.544s | 1.507s | 1.653s | 1.591s | 
didfinish耗时从最初的601ms下降到272ms,优化了大概339ms,总耗时从1.822s下降到1.591s, 优化了大概231ms