之前使用instrument工具進(jìn)行性能檢測(cè)時(shí),總感覺找不到竅門,要浪費(fèi)很多時(shí)間才能定位到有問題的代碼片段。 工作的項(xiàng)目中慢慢重視性能優(yōu)化一塊了,所以想著先將instrument了解一遍,上周是先看了頭號(hào)種子選手 —— Timer profile。
1 性能檢測(cè)工具使用的原因
2 Timer Profile如何使用
2.1 通用技巧
2.2 signpost
3 總結(jié)
1 性能檢測(cè)工具使用的原因
去年在嘗試進(jìn)行性能優(yōu)化的工作時(shí)根本無從下手,只能不斷的從網(wǎng)上找資料,然后跟著資料學(xué)著給所有的陰影加個(gè)shadowPath、更改所有圓角的繪制方法、以及不斷的想怎么減少層級(jí)關(guān)系等等,雖然這些方法都是已經(jīng)總結(jié)有效的處理,但是如同在2018年WWDC關(guān)于如何通往更高效性能中也提到了,性能問題主要可以劃分三大場(chǎng)景:
① 性能衰退 Major Regression
某次回歸測(cè)試的時(shí)候突然出現(xiàn)大幅度的性能問題,這時(shí)候必須回過頭排查新調(diào)整代碼的邏輯,當(dāng)然如果工程中有自動(dòng)化測(cè)試功能那么就能直接細(xì)致的監(jiān)控。
② 偏離目標(biāo) Off Target
目標(biāo)性能為頁面刷新每秒55幀以上,但是實(shí)際卻只有45或者更少的幀率。說明存在待解決的幾個(gè)問題需要進(jìn)行修復(fù)。
③ 拙劣設(shè)計(jì) Poor Design
性能表現(xiàn)的極差,給用戶的已經(jīng)不是體驗(yàn)而是考驗(yàn)了...不再只是解決幾個(gè)問題就能修復(fù)了,需要認(rèn)真的排查架構(gòu)、頁面結(jié)構(gòu)等深一層的問題。
所以真正的性能優(yōu)化首先要知道自己的方向是哪種場(chǎng)景,針對(duì)該場(chǎng)景下采取相對(duì)應(yīng)的手段檢測(cè)存在的問題。雖然別人的經(jīng)驗(yàn)確實(shí)是有用的,但是得想方設(shè)法的找到關(guān)鍵點(diǎn)。
2 Timer Profile如何使用
2.1 通用技巧
打開檢測(cè)工具的界面時(shí),有幾個(gè)我在學(xué)習(xí)時(shí)值得注意的地方:
① Detail View
左下角的詳細(xì)信息中有Weight 和 Self Weight、Symbol Name三列,后面的符號(hào)名稱比較好理解,前面兩個(gè):
Weight表示權(quán)重,從這個(gè)方法開始耗費(fèi)總時(shí)間的權(quán)重
Self Weght表示這個(gè)方法本身耗費(fèi)的時(shí)間權(quán)重,上圖中全是0,表示真正耗時(shí)的在內(nèi)部的調(diào)用方法中
② Extended Detail View
右下角的堆棧追蹤信息,當(dāng)選中左邊的一項(xiàng)時(shí),右邊會(huì)迅速的打開整個(gè)調(diào)用關(guān)系堆棧,而灰色標(biāo)注的是系統(tǒng)方法。
而在尋找耗時(shí)方法們的路上有幾個(gè)挺好的小技巧:
① 快速過濾
過濾系統(tǒng)庫并且將耗時(shí)的方法置頂
② 快速打開層級(jí)關(guān)系
點(diǎn)擊方法的同時(shí)按住option,能快速開啟多層
③ 具體耗時(shí)分析
點(diǎn)擊某一項(xiàng)時(shí),會(huì)進(jìn)入具體某一行代碼的占用權(quán)重分析
③ 降低檢測(cè)成本
當(dāng)檢測(cè)的數(shù)據(jù)過多,instrument默認(rèn)開啟的即時(shí)記錄可能會(huì)導(dǎo)致電腦發(fā)熱卡頓。如果想要在發(fā)現(xiàn)性能問題立刻停止調(diào)試的話是需要開啟即時(shí)記錄,而其他情況下延遲或者最后5秒的處理已經(jīng)能滿足需求了
剩下的就是找啊找啊找啊找,這時(shí)候如果是自己寫的代碼邏輯還好,一眼能知道這個(gè)方法占比較多的原因,但是如果是別人的代碼,你知道是怎么一回事嗎?
是時(shí)候引入蘋果在2018年為OSLog家族引入的性能衡量工具Signpost了。
2.2 signpost
即便在Timer Profile中檢測(cè)的時(shí)候能告訴你是否是方法本身占用耗時(shí),但是卻無法判斷出到底是因?yàn)檎{(diào)用了太多次還是本身方法存在一個(gè)超級(jí)耗時(shí)的操作。而signpost的作用就是告訴你這段代碼占用耗時(shí)比如此大的具體信息。
在代碼中寫入signpost打印的內(nèi)容會(huì)顯示到instrument,寫入的方法十分簡(jiǎn)單:
① signpost先導(dǎo)入 os/signpost.h
① 創(chuàng)建os_signpost_interval_begin/end
os_log_t log = os_log_create("com.lizhou.WorkIdeaDemo", "third");
os_signpost_id_t signid = os_signpost_id_generate(log);
os_signpost_interval_begin(log, signid, "outerTotal");
for(int i = 0; i < 100000; i++) {
os_signpost_interval_begin(log, signid, "signpost");
NSLog(@"打印的第%d遍",i);
os_signpost_interval_end(log, signid, "signpost");
}
os_signpost_interval_end(log, signid, "outerTotal");
以上的邏輯可以分為三步進(jìn)行了解:
① os_log_t
os_log_t os_log_create(const char *subsystem, const char *category);
subsystem自定義的標(biāo)識(shí)符,可以直接使用項(xiàng)目的bundle id
category是用于分類,主要作用于對(duì)相關(guān)聯(lián)的內(nèi)容操作進(jìn)行分類
② os_signpost_id_t
os_signpost_id_t os_signpost_id_generate(os_log_t log);
③ begin/end
記錄一段代碼邏輯執(zhí)行的情況就是從開始到結(jié)束,
os_signpost_interval_begin(log, interval_id, name, ...)
os_signpost_interval_end(log, interval_id, name, ...)
begin/end的name一致,保證能識(shí)別出是同一個(gè)邏輯的開始/結(jié)束,后面的…表示能添加額外的參數(shù),在instrument中顯示。
當(dāng)插入以上邏輯后進(jìn)行檢測(cè)會(huì)顯示以下的內(nèi)容: 代碼的執(zhí)行次數(shù)、時(shí)長(zhǎng)以及每次執(zhí)行時(shí)最大、最短和平均時(shí)長(zhǎng)等內(nèi)容
當(dāng)然在實(shí)際開發(fā)中會(huì)更多的需要檢測(cè)異步線程中的性能問題,注意一下上圖中的顯示: 只使用了os_log的category 和 os_signpost的name進(jìn)行分類,并沒有使用interval_id。解釋一下三種分類字段的含義就能清楚的明白:
category 相關(guān)操作
signpost name 某類相同操作
signpost id 同一類型操作下的不同任務(wù)
所以異步其實(shí)就是相同類型操作下創(chuàng)建了多個(gè)不同的任務(wù),只要三個(gè)標(biāo)識(shí)相同的begin/end成對(duì)出現(xiàn),那么在不同的線程中都可以進(jìn)行檢測(cè)。
for(int i = 0; i < 10; i++) {
os_signpost_id_t signid_1 = os_signpost_id_make_with_pointer(log, (__bridge const void * _Nullable)([NSString stringWithFormat:@"%d",i]));
os_signpost_interval_begin(log, signid_1, "signpost");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"打印的第%d遍",i);
os_signpost_interval_end(log, signid_1, "signpost");
});
}
如果想要在begin和end間添加一些特定的點(diǎn),比如用戶的點(diǎn)擊事件和圖片在某個(gè)特定時(shí)間點(diǎn)的加載信息,就能使用event事件
os_log_t log = os_log_create("com.lizhou.WorkIdeaDemo", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
os_signpost_id_t signid = os_signpost_id_generate(log);
os_signpost_event_emit(log, signid, "點(diǎn)擊事件","點(diǎn)擊第%ld個(gè)",index);
每個(gè)事件的name后都能添加additional information,比如上面的動(dòng)態(tài)字符串格式
os_signpost如何添加到points of interest區(qū)塊中,請(qǐng)看上圖的os_log_t創(chuàng)建方法中的category
看一下instrument的顯示格式,按照分類邏輯顯示清晰明了:
聊了這么多如何使用signpost,最重要的一件事還沒聊: 什么時(shí)候可以去使用?
① 禁止與開啟log
雖然signpost本身是輕量級(jí)的,而且為編譯時(shí)做了大量的優(yōu)化,把大部分工作推遲到instrument中。但是release包確實(shí)沒有必要開啟,直接為log賦值disable
② 使用版本
API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0))
OS_EXPORT OS_NOTHROW OS_WARN_RESULT OS_OBJECT_RETURNS_RETAINED OS_NONNULL_ALL
os_log_t
os_log_create(const char *subsystem, const char *category);
API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0))
OS_EXPORT OS_NOTHROW OS_WARN_RESULT
os_signpost_id_t
os_signpost_id_generate(os_log_t log);
當(dāng)在低版本中使用這些字段時(shí),直接崩潰打印以下的報(bào)錯(cuò)信息:
lazy symbol binding failed: Symbol not found: _os_signpost_id_generate
學(xué)習(xí)的過程中真的發(fā)現(xiàn)很多有意思的地方,但是也發(fā)現(xiàn)了一些還未解決的部分,通往高性能的路上還需要繼續(xù)學(xué)習(xí)。