本文是<<iOS開發(fā)高手課>> 第十六篇學(xué)習(xí)筆記.
通常情況下访圃,App 的性能問題雖然不會(huì)導(dǎo)致 App 不可用厨幻,但依然會(huì)影響到用戶體驗(yàn)。
如果這個(gè)性能問題不斷累積,達(dá)到臨界點(diǎn)以后况脆,問題就會(huì)爆發(fā)出來饭宾。這時(shí),影響到的就不僅僅是用戶了格了,還有負(fù)責(zé) App 開發(fā)的我們看铆。
為了能夠主動(dòng)、高效地發(fā)現(xiàn)性能問題盛末,避免 App 質(zhì)量進(jìn)入無人監(jiān)管的失控狀態(tài)弹惦,我們就需要對(duì) App 的性能進(jìn)行監(jiān)控。
對(duì) App 的性能監(jiān)控悄但,主要是從線下和線上兩個(gè)維度展開棠隐。
Instruments
Instruments 是蘋果公司官方的性能監(jiān)控工具。被集成在 Xcode 里檐嚣,專門用來在線下進(jìn)行性能分析助泽。
Instruments 的功能非常強(qiáng)大,
- Energy Log 就是用來監(jiān)控耗電量的嚎京,
- Leaks 就是專門用來監(jiān)控內(nèi)存泄露問題的报咳,
- Network 就是用來專門檢查網(wǎng)絡(luò)情況的,
- Time Profiler 就是通過時(shí)間采樣來分析頁(yè)面卡頓問題的挖藏。
除了對(duì)各種性能問題進(jìn)行監(jiān)控外暑刃,還有以下兩大優(yōu)勢(shì):
- Instruments 基于 os_signpost 架構(gòu),可以支持所有平臺(tái)膜眠。
- Instruments 由于標(biāo)準(zhǔn)界面(Standard UI)和分析核心(Analysis Core)技術(shù)岩臣,使得我們可以非常方便地進(jìn)行自定義性能監(jiān)測(cè)工具的開發(fā)。當(dāng)你想要給 Instruments 內(nèi)置的工具換個(gè)交互界面宵膨,或者新創(chuàng)建一個(gè)工具的時(shí)候架谎,都可以通過自定義工具這個(gè)功能來實(shí)現(xiàn)。
從整體架構(gòu)來看辟躏,Instruments 包括 Standard UI
和 Analysis Core
兩個(gè)組件谷扣,它的所有工具都是基于這兩個(gè)組件開發(fā)的。而且捎琐,你如果要開發(fā)自定義的性能分析工具的話会涎,完全基于這兩個(gè)組件就可以實(shí)現(xiàn)。
線上性能監(jiān)控
對(duì)于線上性能監(jiān)控瑞凑,有兩個(gè)原則:
- 監(jiān)控代碼不要侵入到業(yè)務(wù)代碼中末秃;
- 采用性能消耗最小的監(jiān)控方案。
線上性能監(jiān)控籽御,主要集中在 CPU 使用率练慕、FPS 的幀率和內(nèi)存這三個(gè)方面惰匙。
CPU 使用率的線上監(jiān)控方法
App 作為進(jìn)程運(yùn)行起來后會(huì)有多個(gè)線程,每個(gè)線程對(duì) CPU 的使用率不同铃将。各個(gè)線程對(duì) CPU 使用率的總和项鬼,就是當(dāng)前 App 對(duì) CPU 的使用率。
在 iOS 系統(tǒng)中劲阎,你可以在 usr/include/mach/thread_info.h 里看到線程基本信息的結(jié)構(gòu)體绘盟,其中的 cpu_usage 就是 CPU 使用率哪工。結(jié)構(gòu)體的完整代碼如下所示:
struct thread_basic_info {
time_value_t user_time; // 用戶運(yùn)行時(shí)長(zhǎng)
time_value_t system_time; // 系統(tǒng)運(yùn)行時(shí)長(zhǎng)
integer_t cpu_usage; // CPU 使用率
policy_t policy; // 調(diào)度策略
integer_t run_state; // 運(yùn)行狀態(tài)
integer_t flags; // 各種標(biāo)記
integer_t suspend_count; // 暫停線程的計(jì)數(shù)
integer_t sleep_time; // 休眠的時(shí)間
};
因?yàn)槊總€(gè)線程都會(huì)有這個(gè) thread_basic_info 結(jié)構(gòu)體奥此,只需要定時(shí)(比如,將定時(shí)間隔設(shè)置為 2s)去遍歷每個(gè)線程程奠,累加每個(gè)線程的 cpu_usage 字段的值,就能夠得到當(dāng)前 App 所在進(jìn)程的 CPU 使用率了距境。實(shí)現(xiàn)代碼如下:
+ (integer_t)cpuUsage {
thread_act_array_t threads; //int 組成的數(shù)組比如 thread[1] = 5635
mach_msg_type_number_t threadCount = 0; //mach_msg_type_number_t 是 int 類型
const task_t thisTask = mach_task_self();
//根據(jù)當(dāng)前 task 獲取所有線程
// task_threads 方法能夠取到當(dāng)前進(jìn)程中的線程總數(shù) threadCount 和所有線程的數(shù)組 threads申尼。
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return 0;
}
integer_t cpuUsage = 0;
// 我們就可以通過遍歷這個(gè)數(shù)組來獲取單個(gè)線程的基本信息。其中垫桂,線程基本信息的結(jié)構(gòu)體是 thread_basic_info_t师幕,這個(gè)結(jié)構(gòu)體里就包含了我們需要的 CPU 使用率的字段 cpu_usage。然后诬滩,我們累加這個(gè)字段就能夠獲取到當(dāng)前的整體 CPU 使用率霹粥。
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
// 獲取 CPU 使用率
threadBaseInfo = (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBaseInfo->cpu_usage;
}
}
}
assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t)) == KERN_SUCCESS);
return cpuUsage;
}
FPS 線上監(jiān)控方法
FPS 是指圖像連續(xù)在顯示設(shè)備上出現(xiàn)的頻率。FPS 低疼鸟,表示 App 不夠流暢蒙挑,還需要進(jìn)行優(yōu)化。
和前面對(duì) CPU 使用率和內(nèi)存使用量的監(jiān)控不同愚臀,iOS 系統(tǒng)中沒有一個(gè)專門的結(jié)構(gòu)體忆蚀,用來記錄與 FPS 相關(guān)的數(shù)據(jù)矾利。但是,對(duì) FPS 的監(jiān)控也可以比較簡(jiǎn)單的實(shí)現(xiàn):通過注冊(cè) CADisplayLink 得到屏幕的同步刷新率馋袜,記錄每次刷新時(shí)間男旗,然后就可以得到 FPS。具體的實(shí)現(xiàn)代碼如下:
- (void)start {
self.dLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsCount:)];
[self.dLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
// 方法執(zhí)行幀率和屏幕刷新率保持一致
- (void)fpsCount:(CADisplayLink *)displayLink {
if (lastTimeStamp == 0) {
lastTimeStamp = self.dLink.timestamp;
} else {
total++;
// 開始渲染時(shí)間與上次渲染時(shí)間差值
NSTimeInterval useTime = self.dLink.timestamp - lastTimeStamp;
if (useTime < 1) return;
lastTimeStamp = self.dLink.timestamp;
// fps 計(jì)算
fps = total / useTime;
total = 0;
}
}
內(nèi)存使用量的線上監(jiān)控方法
通常情況下欣鳖,我們?cè)讷@取 iOS 應(yīng)用內(nèi)存使用量時(shí)察皇,都是使用 task_basic_info 里的 resident_size 字段信息。但這樣獲得的內(nèi)存使用量和 Instruments 里看到的相差很大泽台。后來什荣,在 2018 WWDC Session 416 iOS Memory Deep Dive,蘋果公司介紹說 phys_footprint 才是實(shí)際使用的物理內(nèi)存怀酷。
struct task_vm_info {
mach_vm_size_t virtual_size; // 虛擬內(nèi)存大小
integer_t region_count; // 內(nèi)存區(qū)域的數(shù)量
integer_t page_size;
mach_vm_size_t resident_size; // 駐留內(nèi)存大小
mach_vm_size_t resident_size_peak; // 駐留內(nèi)存峰值
...
/* added for rev1 */
mach_vm_size_t phys_footprint; // 物理內(nèi)存
...
開發(fā)一款自定義 Instruments 工具
Instruments 通過提供 os_signpost API 的方式使得開發(fā)者監(jiān)控自定義的性能指標(biāo)時(shí)更方便稻爬,從而解決了在此之前只能通過重新建設(shè)工具來完成的問題。并且蜕依,Instruments 是通過 XML 標(biāo)準(zhǔn)數(shù)據(jù)接口解耦展示和數(shù)據(jù)分析
主要包括以下這幾個(gè)步驟:
- 在 Xcode 中桅锄,點(diǎn)擊 File > New > Project;
- 在彈出的 Project 模板選擇界面样眠,將其設(shè)置為 macOS友瘤;
- 選擇 Instruments Package,點(diǎn)擊后即可開始自定義工具的開發(fā)了檐束。
創(chuàng)建之后僅有一個(gè)源文件(.instrpkg)
運(yùn)行后會(huì)彈出一個(gè) Instruments 頁(yè)面,在菜單欄 -> Instruments -> Preferences -> Packages
開發(fā)過程主要是對(duì) instrpkg
文件的配置工作辫秧。這些配置工作中最主要的是要完成 Standard UI 和 Analysis Core 的配置。
蘋果公司還提供了大量的代碼片段被丧,幫助你進(jìn)行個(gè)性化的配置盟戏。你可以查看官方指南中的詳細(xì)教程:https://help.apple.com/instruments/developer/mac/current/
配置 instrpkg 文件
Xcode提供的instrpkg模板中注釋很多,核心的代碼沒有多少晚碾。
但是基本上可以知道這個(gè)代碼是 XML 格式的抓半,通過不同的標(biāo)簽標(biāo)示不同的功能,package 標(biāo)簽標(biāo)示一個(gè)包格嘁,緊接著是其子標(biāo)簽:id笛求、title 與 owner 等等。
<?xml version="1.0" encoding="UTF-8" ?>
<package>
<id>com.forping.Test</id>
<title>Test</title>
<owner>
<name>forping</name>
</owner>
<!-- 可以理解成一個(gè)數(shù)據(jù)來源 -->
<os-signpost-interval-schema>
<id>json-parse</id>
<title>JSON Decode</title>
<!-- 這三個(gè)是與項(xiàng)目中代碼一一對(duì)應(yīng) -->
<subsystem>"com.forping.forping"</subsystem>
<category>"jsonDecode"</category>
<name>"Parsing"</name>
<!-- 開始匹配-->
<start-pattern>
<message>"Parsing started"</message>
</start-pattern>
<!-- 結(jié)束匹配-->
<end-pattern>
<message>"Parsing end SIZE:" ?data-size-value</message>
</end-pattern>
<!-- 表中的一列 -->
<column>
<!-- 助記符標(biāo)識(shí), 在 graph 與 list 中只認(rèn)這個(gè)標(biāo)識(shí) -->
<mnemonic>data-size</mnemonic>
<title>JSON Data Size</title>
<!-- 數(shù)據(jù)的類型 size-in-bytes -->
<type>size-in-bytes</type>
<!-- 顯示 data-size 的值 -->
<expression>?data-size-value</expression>
</column>
<!-- https://help.apple.com/instruments/developer/mac/current/#/dev66257045 -->
<column>
<mnemonic>impact</mnemonic>
<title>Impact</title>
<type>event-concept</type>
<expression>(if (> ?data-size-value 80) then "High" else "Low")</expression>
</column>
</os-signpost-interval-schema>
<!-- 導(dǎo)入 tick 模塊 可以使用 tick作為數(shù)據(jù)來源 相當(dāng)于 `import` -->
<!-- <import-schema>tick</import-schema>-->
<!-- 開始構(gòu)建一個(gè) instrument -->
<instrument>
<id>com.forping.ticksinstrument</id>
<!-- 在 instrument中的title-->
<title>FPTicks</title>
<category>Behavior</category>
<purpose>tickDemo</purpose>
<icon>Generic</icon>
<!-- 創(chuàng)建一個(gè)表, 這個(gè)表中使用到了 `tick` -->
<create-table>
<id>json-parse</id>
<!-- os-signpost-interval-schema 的 id -->
<schema-ref>json-parse</schema-ref>
</create-table>
<!-- 軌道視圖 為您的儀器定義要繪制的圖形(可選) -->
<graph>
<title>JSON Decode</title>
<lane>
<title>JSON Analyz</title>
<table-ref>json-parse</table-ref>
<!-- 繪圖糕簿、繪圖模板或直方圖元素 -->
<plot>
<value-from>data-size</value-from>
<color-from>impact</color-from>
</plot>
</lane>
</graph>
<!-- 詳情視圖 - 為您的儀器定義至少一個(gè)詳細(xì)視圖 -->
<list>
<title>data-info</title>
<table-ref>json-parse</table-ref>
<column>data-size</column>
<column>impact</column>
<column>duration</column>
</list>
</instrument>
<!-- Instruments Developer Help: https://help.apple.com/instruments/developer/mac/current/ -->
<!-- MARK: Schema Definitions -->
<!-- Define point and interval schemas needed to represent the input and output tables your package will use. -->
<!-- Two kinds are available: schemas with automatically generated modelers, and schemas that require custom modelers -->
<!-- Generated modelers: 'os-log-point-schema', 'os-signpost-interval-schema', 'ktrace-point-schema', 'ktrace-interval-schema' -->
<!-- Custom modeler required: 'point-schema', 'interval-schema' -->
<!-- To use existing schemas from other packages, declare 'import-schema' elements -->
<!-- MARK: Modeler Declarations -->
<!-- If there are schemas defined that require a custom modeler, each can be declared with a 'modeler' element -->
<!-- Modelers are based on CLIPS rules and may define 1..n output schemas, each requiring 1..n input schemas -->
<!-- MARK: Instrument Definitions -->
<!-- Instruments record and display data, creating concrete table requirements that instance modelers and data streams. -->
<!-- Any number of 'instrument' elements can be defined; each instrument should provide a cohesive graph and detail experience. -->
<!-- MARK: Embed Templates -->
<!-- Templates may be included and represent a collection of tools configured for a specific tracing workflow -->
<!-- Each 'template' element specifies the relative path to a .tracetemplate file in the project -->
<!-- To create a template: start with a blank document, configure with instruments desired, and choose "File -> Save as Template" -->
</package>
這就是核心實(shí)現(xiàn) Instruments 功能的代碼了探入,詳細(xì)解釋如下:
- 使用了 Instrument 之后依舊需要添加對(duì)應(yīng)的標(biāo)識(shí)、標(biāo)題等基本信息懂诗。
- 需要?jiǎng)?chuàng)建一個(gè)對(duì)這個(gè)自定義的 Instrument 需要有一張對(duì)應(yīng)的表(table)蜂嗽,故需要使用 create-table,值得注意的是這個(gè)表所需要的數(shù)據(jù)是直接來自于 tick schema。
- 開始創(chuàng)建一個(gè)軌道視圖殃恒,這個(gè)軌道視圖的數(shù)據(jù)來自 tick-table 這張表植旧,由于這張表引用系統(tǒng)的 tick schema辱揭,tick 中有一個(gè) time 屬性,所以可以直接使用這個(gè)時(shí)間戳字段病附。
- 詳情視圖问窃,使用 list 標(biāo)簽主要是在詳情視圖中顯示數(shù)據(jù)的。這個(gè) list 相當(dāng)于我們開發(fā)中的 UITableView完沪,tick-table 相當(dāng)于數(shù)據(jù)源(dataSource)域庇。
使用方法
選擇 Blank , 點(diǎn)擊新視圖右側(cè)的 +
號(hào),選擇我們 instrument 標(biāo)題的 title
Analysis Core
如果你想要更好地進(jìn)行個(gè)性化定制,就還需要再了解 Instruments 收集和處理數(shù)據(jù)的機(jī)制覆积,也就是分析核心(Analysis Core )的工作原理听皿。Analysis Core 收集和處理數(shù)據(jù)的過程,可以大致分為三步:
處理我們配置好的各種數(shù)據(jù)表宽档,并申請(qǐng)存儲(chǔ)空間 store尉姨;
store 去找數(shù)據(jù)提供者,如果不能直接找到雌贱,就會(huì)通過 Modeler 接收其他 store 的輸入信號(hào)進(jìn)行合成啊送;
store 獲得數(shù)據(jù)源后偿短,會(huì)進(jìn)行 Binding Solution 工作來優(yōu)化數(shù)據(jù)處理過程欣孤。
在通過 store 找到的這些數(shù)據(jù)提供者中,對(duì)開發(fā)者來說最重要的就是 os_signpost昔逗。
os_signpost 的主要作用降传,是讓你可以在程序中通過編寫代碼來獲取數(shù)據(jù)。你可以在工程中的任何地方通過 os_signpost API 勾怒,將需要的數(shù)據(jù)提供給 Analysis Core婆排。
模擬代碼
os_log_t parsingLog = os_log_create("com.forping.forping", "jsonDecode");
os_signpost_id_t signid = os_signpost_id_generate(parsingLog);
os_signpost_interval_begin(parsingLog, signid, "Parsing started");
// 模擬耗時(shí)操作
[self jsonDecode];
os_signpost_interval_end(parsingLog, signid, "Parsing end");
運(yùn)行效果
上面的代碼,主要是獲取項(xiàng)目中耗時(shí)操作的開始與結(jié)束的笔链。其中在結(jié)束的時(shí)候會(huì)匹配出項(xiàng)目中的元數(shù)據(jù):解析字符的大小段只。這里主要使用的就是 CLIPS 語(yǔ)言的變量。
接著就是 column
, 這個(gè)標(biāo)簽為 shema
定義一些字段鉴扫, schema
是一個(gè)數(shù)據(jù)庫(kù)赞枕。其中這個(gè)是數(shù)據(jù)庫(kù)中有兩個(gè) key:data-size
與 impact
,其中 impact
是由 data-size-value
的值決定的坪创,大于 80
時(shí)值是 High
炕婶, 否則為 Low
。
可以很清楚的看到每次 JSON 解析的開始與結(jié)束莱预,以及執(zhí)行所花的時(shí)間柠掂。
在實(shí)際開發(fā)中可能還會(huì)同時(shí)選中其它的調(diào)試模塊,比如 Time Profiler依沮、內(nèi)存檢測(cè) 等涯贞,這樣能很好的全方位的分析當(dāng)前的運(yùn)行環(huán)境以及運(yùn)行狀態(tài)枪狂。
其他示例
官方示例
蘋果公司在 WWDC 2018 Session 410 Creating Custom Instruments 里提供了一個(gè)范例:https://developer.apple.com/videos/play/wwdc2018/410
通過 os_signpost API 將圖片下載的數(shù)據(jù)提供給 Analysis Core 進(jìn)行監(jiān)控觀察。這個(gè)示例在 App 的代碼如下所示:
//os_signpost 的 begin 和 end 需要成對(duì)出現(xiàn)宋渔。
os_signpost(.begin, log: parsinglog, name:"Parsing", "Parsing started SIZE:%ld", data.count)
// Decode the JSON we just downloaded
let result = try jsonDecoder.decode(Trail.self, from: data)
os_signpost(.end, log: parsingLog, name:"Parsing", "Parsing finished")
上面這段代碼就是使用 os_signpost 的 API 獲取程序里的數(shù)據(jù)摘完。
Instruments 是如何通過配置數(shù)據(jù)表來使用這些數(shù)據(jù)的。配置的數(shù)據(jù)表的 XML 設(shè)計(jì)如下所示:
<os-signpost-interval-schema>
<id>json-parse</id>
<title>Image Download</title>
<subsystem>"com.apple.trailblazer</subsystem>
<category>"Networking</category>
<name>"Parsing"</name>
<start-pattern>
<message>"Parsing started SIZE:" ?data-size</message>
</start-pattern>
<column>
<mnemonic>data-size</mnemonic>
<title>JSON Data Size</title>
<type>size-in-bytes</type>
<expression>?data-size</expression>
</column>
</os-signpost-interval-schema>
配置數(shù)據(jù)表是要對(duì)數(shù)據(jù)輸出進(jìn)行可視化配置傻谁,從而可以將代碼中的數(shù)據(jù)展示出來孝治。如下圖所示,就是對(duì)下載圖片大小監(jiān)控的效果审磁。
參考鏈接: