一、App啟動(dòng)耗時(shí)在線監(jiān)控
在大型的多團(tuán)隊(duì)合作的項(xiàng)目中,往往不經(jīng)意間的一個(gè)個(gè)改動(dòng),可能就會(huì)直接或累加式的拖慢App的啟動(dòng)速度,測(cè)試人員通過(guò)本地的錄屏或者開(kāi)發(fā)工具測(cè)量啟動(dòng)耗時(shí)由于受測(cè)試機(jī)器的狀態(tài)和樣本數(shù)量的原因數(shù)據(jù)往往有波動(dòng),并不能真正反饋App啟動(dòng)時(shí)間的真實(shí)變化。所以加入在線的數(shù)據(jù)監(jiān)控變得非常重要。
在介紹App啟動(dòng)耗時(shí)監(jiān)控之前,我們先大概回顧下App啟動(dòng)過(guò)程:

main()函數(shù)之前的階段我們成為pre-main(),至于pre-main()中每個(gè)階段的具體作用,這邊就不再贅述,網(wǎng)上資料較多。
pre-main()開(kāi)始時(shí)間:__t1
蘋(píng)果并沒(méi)有直接提供App啟動(dòng)的開(kāi)始時(shí)間,目前業(yè)內(nèi)主要有兩種標(biāo)準(zhǔn)作為App的啟動(dòng)時(shí)間:
-
第一個(gè)
+(void)load被調(diào)用時(shí)我們知道
+(void)load方法調(diào)用發(fā)生Initializer階段, 根據(jù)動(dòng)態(tài)庫(kù)的加載順序調(diào)用+(void)load方法,而動(dòng)態(tài)庫(kù)的加載順序是遞歸加載的,我們只要找到葉子節(jié)點(diǎn)的動(dòng)態(tài)庫(kù),然后在這個(gè)動(dòng)態(tài)庫(kù)中的添加+(void)load方法來(lái)記錄啟動(dòng)時(shí)間。很明顯,這種方式?jīng)]有統(tǒng)計(jì)到Initializer前面的時(shí)間,比如增加動(dòng)態(tài)庫(kù),Category等造成的啟動(dòng)耗時(shí)并不能被及時(shí)發(fā)現(xiàn)。 -
獲取進(jìn)程創(chuàng)建時(shí)間
我們的App實(shí)際上是一個(gè)進(jìn)程,如果能獲取到進(jìn)程的創(chuàng)建時(shí)間,也就是
exec()階段的時(shí)間,更能提前記錄到App的啟動(dòng)開(kāi)始時(shí)間。#import <sys/sysctl.h> #import <mach/mach.h> + (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; }
pre-main()結(jié)束時(shí)間:__t2
獲取pre-main()結(jié)束時(shí)間相對(duì)容易,可以main()函數(shù)的開(kāi)始執(zhí)行時(shí)間,而我更推薦使用__attribute__((constructor)) 函數(shù)調(diào)用作為pre-main()的結(jié)束時(shí)間,這樣能最大程度的實(shí)現(xiàn)解耦:
void static __attribute__((constructor)) before_main() {
if (__t2 == 0) {
__t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
}
至于為什么不用最后一個(gè)load方法執(zhí)行時(shí)間?因?yàn)樵诖笮凸こ讨形覀儧](méi)辦法確定哪個(gè)load方法是最后一個(gè)...
啟動(dòng)完成時(shí)間:__t3
啟動(dòng)完成時(shí)間一般可以通過(guò)獲取didFinishLaunchingWithOptions:的結(jié)束時(shí)間,但是didFinishLaunchingWithOptions:的結(jié)束時(shí)間其實(shí)不包括啟動(dòng)圖動(dòng)畫(huà)的時(shí)間,啟動(dòng)圖動(dòng)畫(huà)執(zhí)行完成后的時(shí)間更接近用戶的感官。我們可以在執(zhí)行didFinishLaunchingWithOptions: 的runloop循環(huán)的后面的循環(huán)來(lái)獲取時(shí)間:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//do somethings
dispatch_async(dispatch_get_main_queue(), ^{
if (__t3 == 0) {
__t3 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
});
return YES;
}
二、AppDelegate管控
在iOS項(xiàng)目中,AppDelegate類(lèi)里面的代碼往往都是雜亂無(wú)章,各模塊只要有需要,都會(huì)往AppDelegate中添加各種各樣的方法,特別是didFinishLaunchingWithOptions: 方法更是重災(zāi)區(qū)。為什么要把AppDelegate的管控和啟動(dòng)耗時(shí)監(jiān)控寫(xiě)在一起呢?因?yàn)?,合理且統(tǒng)一的模塊調(diào)用方式,可以更好的去統(tǒng)計(jì)每個(gè)模塊的調(diào)用耗時(shí),進(jìn)而實(shí)現(xiàn)更精細(xì)化的監(jiān)控。
-
抽象統(tǒng)一的方法;
如需要在
didFinishLaunchingWithOptions:被調(diào)用的模塊都在自己的模塊中實(shí)現(xiàn)+ (void)didFinishLaunchingWithOptions的靜態(tài)方法。 -
在
didFinishLaunchingWithOptions:中使用rouer或者動(dòng)態(tài)調(diào)用各模塊的對(duì)應(yīng)的靜態(tài)方法。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //do somethings //... NSArray *modules = @[@"Module1",@"Module2",@"Module3",@"Module4",@"Module5"]; for (NSString *module in modules) { //1.獲取開(kāi)始時(shí)間 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); //2.調(diào)用模塊 NSString *url = [NSString stringWithFormat:@"%@://didFinishLaunchingWithOptions",module]; [Router openUrl:url]; //3.獲取消耗的時(shí)間 CFAbsoluteTime cost = CFAbsoluteTimeGetCurrent() - start; //4.記錄、上報(bào) ... } return YES; } -
將AppDelegate制作成靜態(tài)庫(kù),如AppDelegate.framework。main函數(shù)中修改(非pods工程,記得在
Other Linker Flags中添加-force_load,避免鏈接時(shí)被優(yōu)化):int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, @"AppDelegate")); } }
AppDelegate這樣模塊可以交由專(zhuān)人維護(hù),進(jìn)而實(shí)現(xiàn)了AppDelegate的管控。