前兩個月,反饋羣裏逐漸開始透漏出app啓動慢的問題,之前一直忙着作業務,對啓動優化這塊確實比較疏忽,又加上進入Q2以來,組內對項目的性能體驗等方面要求愈發重視起來,以此爲契機,開始着手整理啓動優化這塊。ios
通常而言,啓動時間
是指用戶從點擊APP那一刻開始
到看到第一個界面時
這中間的時間。git
你們都知道 APP 的入口是 main 函數,在 main 以前,咱們本身的代碼是不會執行的。而進入到 main 函數之後,咱們的代碼都是從didFinishLaunchingWithOptions
開始執行的。github
這裏咱們要想知道哪些操做,或者說哪些代碼是耗時的,咱們須要一個打點計時器。經過打點計時器,對每一個方法進行計時分析,再針對性處理。找到個挺好用的三方庫:BLStopwatch數組
查看了BLStopwatch源碼,也並不複雜,主要就是經過互斥鎖
來實現記錄監測時間差。緩存
+ (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;
}
複製代碼
- (void)start {
self.state = BLStopwatchStateRuning;
self.startTimeInterval = CACurrentMediaTime();
self.temporaryTimeInterval = self.startTimeInterval;
}
複製代碼
- (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);
}
複製代碼
- (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];
...
}
複製代碼
性能好的手機效果更明顯,基本實現秒開。
到這,這一部分暫時處理完了。
主要耗時在didFinishLaunchingWithOptions
和首頁加載渲染
兩個地方。
didFinishLaunchingWithOptions
裏作的都是第三方SDK初始化
,加載初始化資源
,環境配置
等這些。
咱們能夠根據輕重緩急,對其進行分配。
首頁加載渲染
這部分,咱們能夠經過優化啓動流程, 好比在UIApplicationDidFinishLaunching時初始化開屏廣告,作到對業務層無干擾。還有開屏廣告使用緩存數據,都能提升加載速度。
還有pre-main的那部分,由於比較難搞,有沒有明顯的效果,就沒怎麼處理了。等有時間再弄吧,目前的反饋效果比較良好了。