启动优化--设计个`打点计时器`

3,166 阅读5分钟

前两个月,反馈群里逐渐开始透漏出app启动慢的问题,以前一直忙着做业务,对启动优化这块确实比较疏忽,又加上进入Q2以来,组内对项目的性能体验等方面要求愈发重视起来,以此为契机,开始着手整理启动优化这块。

一般而言,启动时间是指用户从点击APP那一刻开始看到第一个界面时这中间的时间。

大家都知道 APP 的入口是 main 函数,在 main 之前,我们自己的代码是不会执行的。而进入到 main 函数以后,我们的代码都是从didFinishLaunchingWithOptions开始执行的。

这里我们要想知道哪些操作,或者说哪些代码是耗时的,我们需要一个打点计时器。通过打点计时器,对每个方法进行计时分析,再针对性处理。找到个挺好用的三方库:BLStopwatch

查看了BLStopwatch源码,也并不复杂,主要就是通过互斥锁来实现记录监测时间差。

简单分析下原理:

1. 单例创建,声明一个互斥锁

+ (instancetype)sharedStopwatch {
    static BLStopwatch* stopwatch;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        stopwatch = [[BLStopwatch alloc] init];
    });

    return stopwatch;
}

- (void)dealloc {
    pthread_mutex_destroy(&_lock);
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _mutableSplits = [NSMutableArray array];
        pthread_mutex_init(&_lock, NULL);
    }

    return self;
}

2. 开启打点: 状态设置run 记录开始时间。

- (void)start {
    self.state = BLStopwatchStateRuning;
    self.startTimeInterval = CACurrentMediaTime();
    self.temporaryTimeInterval = self.startTimeInterval;
}

3. 计算耗时

- (void)splitWithType:(BLStopwatchSplitType)type description:(NSString * _Nullable)description {
    if (self.state != BLStopwatchStateRuning) {
        return;
    }

    NSTimeInterval temporaryTimeInterval = CACurrentMediaTime();
    CFTimeInterval splitTimeInterval = type == BLStopwatchSplitTypeMedian ? temporaryTimeInterval - self.temporaryTimeInterval : temporaryTimeInterval - self.startTimeInterval;

    NSInteger count = self.mutableSplits.count + 1;

    NSMutableString *finalDescription = [NSMutableString stringWithFormat:@"#%@", @(count)];
    if (description) {
        [finalDescription appendFormat:@" %@", description];
    }

    pthread_mutex_lock(&_lock);
    [self.mutableSplits addObject:@{finalDescription : @(splitTimeInterval)}];
    pthread_mutex_unlock(&_lock);
    // 保存每次执行此方法后保存的临时时间
    self.temporaryTimeInterval = temporaryTimeInterval;
}

type有两种枚举,BLStopwatchSplitTypeMedian为记录中间值,即上个方法到这个方法中间所耗时间。 BLStopwatchSplitTypeContinuous为记录连续值,即从开始计时到最后打印这期间所用的总时间。

在此方法里,记录当前瞬时时间,再根据type是中间值还是连续值来计算时间间隔,中间值就是当前瞬时时间减去每次执行此方法后保存的临时时间,连续值是当前瞬时时间减去开始时的时间

然后再上锁,往数组里保存执行到的步骤和执行所耗时间,解锁。通过互斥锁来保证多线程操作时的数据安全。关于各种锁性能的测试,YYKit的作者ibireme大神在他的博客中进行了阐述。YYCache就是通过在方法中添加互斥锁的逻辑,来保证多线程操作缓存时数据的同步。

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    pthread_mutex_lock(&_lock);
    //操作链表,写缓存数据
    pthread_mutex_unlock(&_lock);
}
- (id)objectForKey:(id)key {
    pthread_mutex_lock(&_lock);
    //访问缓存数据
    pthread_mutex_unlock(&_lock);
}

4. 弹框打印出各步骤耗时

- (void)stop {
    self.state = BLStopwatchStateStop;
    self.stopTimeInterval = CACurrentMediaTime();
}

- (void)reset {
    self.state = BLStopwatchStateInitial;
    pthread_mutex_lock(&_lock);
    [self.mutableSplits removeAllObjects];
    pthread_mutex_unlock(&_lock);
    self.startTimeInterval = 0;
    self.stopTimeInterval = 0;
    self.temporaryTimeInterval = 0;
}

- (void)stopAndPresentResultsThenReset {
    [[BLStopwatch sharedStopwatch] stop];
#ifdef DEBUG
    [[[UIAlertView alloc] initWithTitle:@"App启动打点计时结果"
                                message:[[BLStopwatch sharedStopwatch] prettyPrintedSplits]
                               delegate:nil
                      cancelButtonTitle:@"确定"
                      otherButtonTitles:nil] show];
#endif
    [[BLStopwatch sharedStopwatch] reset];
}

// 每个打印步骤展示的信息
- (NSString *)prettyPrintedSplits {
    NSMutableString *outputString = [[NSMutableString alloc] init];
    pthread_mutex_lock(&_lock);
    [self.mutableSplits enumerateObjectsUsingBlock:^(NSDictionary<NSString *, NSNumber *> *obj, NSUInteger idx, BOOL *stop) {
        [outputString appendFormat:@"%@: %.3f\n", obj.allKeys.firstObject, obj.allValues.firstObject.doubleValue];
    }];
    pthread_mutex_unlock(&_lock);

    return [outputString copy];
}

调用stopAndPresentResultsThenReset来结束计时并打印出保存起来的每个步骤的计时结果。取结果时,声明字符串outputString,再遍历保存的数组 用outputString来拼接保存的每个步骤名称及对应的耗时,最后输出outputString。这其中也是用互斥锁来保证遍历时的数据安全。

这样,打点计时器就设计完成了。

怎么使用

然后在项目里使用,在didFinishLaunchingWithOptions里开启,在每个方法后面添加打印步骤,在首页加载完成时结束打印,即可看到App的启动时间。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[BLStopwatch sharedStopwatch] start];
    // 初始化程序配置
    [self prepareCustomSetting];
    [[BLStopwatch sharedStopwatch] splitWithDescription:@"初始化程序配置耗时y打印"];
    // 注册Umeng,Growing,wxApi等
    [self socialSetup];
    [[BLStopwatch sharedStopwatch] splitWithDescription:@"注册Umeng,Growing,wxApi耗时打印"];
    // bugly设置
    [self setupBugly];
    [[BLStopwatch sharedStopwatch] splitWithDescription:@"bugly耗时打印"];
    // DoraemonKit设置
    [self setupDoraemonKit];
    [[BLStopwatch sharedStopwatch] splitWithDescription:@"DoraemonKit设置耗时打印"];
    ...
    [[BLStopwatch sharedStopwatch] splitWithType:BLStopwatchSplitTypeContinuous description:@"didFinish完成花费时间打印"];
    return YES;
}

// 首页didLoad
- (void)viewDidLoad {
    [super viewDidLoad];

    [[BLStopwatch sharedStopwatch] splitWithDescription:@"首页加载完成时间打印"];
        [[BLStopwatch sharedStopwatch] splitWithType:BLStopwatchSplitTypeContinuous description:@"启动总时间打印"];
    [[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset];
    ...
}

下面是在iPhone6sPlus上的打印测试图:

未优化时的耗时打印:

主要针对第三方初始化做了延迟或异步处理,重构了开屏广告页的启动流程。

优化后的耗时打印:

性能好的手机效果更明显,基本实现秒开。

到这,这一部分暂时处理完了。

主要策略

主要耗时在didFinishLaunchingWithOptions首页加载渲染两个地方。

didFinishLaunchingWithOptions里做的都是第三方SDK初始化,加载初始化资源,环境配置等这些。

我们可以根据轻重缓急,对其进行分配。

  • 必须要在启动时加载的,仍然留在didFinishLaunchingWithOptions中,不过可以做异步处理。
  • 不需要在启动完成的,做延迟处理--放在首页加载之后再去加载。这样就能很大部分时间。

首页加载渲染这部分,我们可以通过优化启动流程, 比如在UIApplicationDidFinishLaunching时初始化开屏广告,做到对业务层无干扰。还有开屏广告使用缓存数据,都能提高加载速度。

还有pre-main的那部分,因为比较难搞,有没有明显的效果,就没怎么处理了。等有时间再弄吧,目前的反馈效果比较良好了。