啓動優化--設計個`打點計時器`

前兩個月,反饋羣裏逐漸開始透漏出app啓動慢的問題,之前一直忙着作業務,對啓動優化這塊確實比較疏忽,又加上進入Q2以來,組內對項目的性能體驗等方面要求愈發重視起來,以此爲契機,開始着手整理啓動優化這塊。ios

通常而言,啓動時間是指用戶從點擊APP那一刻開始看到第一個界面時這中間的時間。git

你們都知道 APP 的入口是 main 函數,在 main 以前,咱們本身的代碼是不會執行的。而進入到 main 函數之後,咱們的代碼都是從didFinishLaunchingWithOptions開始執行的。github

這裏咱們要想知道哪些操做,或者說哪些代碼是耗時的,咱們須要一個打點計時器。經過打點計時器,對每一個方法進行計時分析,再針對性處理。找到個挺好用的三方庫: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是中間值仍是連續值來計算時間間隔,中間值就是當前瞬時時間減去每次執行此方法後保存的臨時時間,連續值是當前瞬時時間減去開始時的時間bash

而後再上鎖,往數組裏保存執行到的步驟和執行所耗時間,解鎖。經過互斥鎖來保證多線程操做時的數據安全。關於各類鎖性能的測試,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。這其中也是用互斥鎖來保證遍歷時的數據安全。app

這樣,打點計時器就設計完成了。異步

怎麼使用

而後在項目裏使用,在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的那部分,由於比較難搞,有沒有明顯的效果,就沒怎麼處理了。等有時間再弄吧,目前的反饋效果比較良好了。

相關文章
相關標籤/搜索