https://segmentfault.com/a/1190000002568993
「原創(chuàng)譯文」iOS 性能優(yōu)化:Instruments 工具的救命三招
你的 iOS 應(yīng)用,運(yùn)行速度靠譜嗎?中槍的同學(xué)莫要愁,性能優(yōu)化咱有妙招。用 Xcode 自家的調(diào)試工具 Instruments,揪出那些堵線程、占內(nèi)存、耗資源的問題代碼,徹底破掉迷局,讓應(yīng)用揚(yáng)眉吐氣!
對(duì)于每位 iOS 開發(fā)者來說,代碼性能是個(gè)避不開的話題。隨著項(xiàng)目的擴(kuò)大和功能的增多,沒經(jīng)過認(rèn)真調(diào)試和優(yōu)化的代碼,要么任性地卡頓運(yùn)行,要么低調(diào)地崩潰了之……結(jié)果呢,大家用著不高興,開發(fā)者也不開心。
其實(shí)要破這個(gè)局面并不難,只要在 Xcode 自帶的監(jiān)控調(diào)試工具 Instruments 上花點(diǎn)功夫,讓大代碼流暢運(yùn)行也不是神話。Instruments 提供了很多功能,我會(huì)重點(diǎn)介紹一下我最常用的三大類:
Time Profiler:分析代碼的執(zhí)行時(shí)間,找出導(dǎo)致程序變慢的原因。
Allocations:監(jiān)測(cè)內(nèi)存使用/分配情況迅速膨脹的內(nèi)存可以很快讓程序斃命,所以要多加防范。
Leaks:找到引發(fā)內(nèi)存泄漏的起點(diǎn)
即使有 ARC(自動(dòng)引用計(jì)數(shù))內(nèi)存管理機(jī)制,但在現(xiàn)實(shí)中對(duì)象之間引用復(fù)雜,循環(huán)引用導(dǎo)致的內(nèi)存泄漏仍然難以避免,所以關(guān)鍵時(shí)刻還要自力更生。
針對(duì)這三方面的測(cè)試,我寫了個(gè)演示應(yīng)用,放在 GitHub 上,來幫助大家更直觀地了解這些工具的使用方法。好,進(jìn)入正題。
Time Profiler
時(shí)間都去哪兒啦? Time Profiler 可以回答。它會(huì)按照設(shè)定的時(shí)間間隔(默認(rèn) 1 毫秒)來跟蹤每一線程的堆棧信息(stack trace),并通過比較時(shí)間間隔之間的堆棧狀態(tài),來推算出某個(gè)方法執(zhí)行了多久,給出一個(gè)近似值。在演示應(yīng)用頭一項(xiàng)「Time Profiler: System Methods」中,我用插入排序(Insertion Sort)和冒泡排序(Bubble Sort)兩種算法來做性能比較,下面是 Swift 代碼:
/* 引用自:http://waynewbishop.com/swift/sorting-algorithms/ */func insertionSort() { var x, y, key: Int for (x = 0; x < numberList.count; x++) { key = numberList[x] for (y = x; y > -1; y--) { if key < numberList[y] { numberList.removeAtIndex(y + 1) numberList.insert(key, atIndex: y) } } }}func bubbleSort() { var x, y, z, passes, key : Int for (x = 0; x < numberList.count; ++x) { passes = (numberList.count - 1) - x; for (y = 0; y < passes; y++) { key = numberList[y] if (key > numberList[y + 1]) { z = numberList[y + 1] numberList[y + 1] = key numberList[y] = z } } }}
這段代碼主要是對(duì)數(shù)組的添加和刪除,兩種方法執(zhí)行起來耗時(shí)不多,但后臺(tái)發(fā)生的系統(tǒng)動(dòng)作卻多得讓人眼暈。
可以發(fā)現(xiàn),代碼用到了很多間接依賴,這些都是支撐代碼運(yùn)行的系統(tǒng)庫(kù)文件。因?yàn)樘幚泶髷?shù)據(jù)集比較消耗系統(tǒng)資源,所以要盡可能地把繁重的操作放到后臺(tái)去做,上面的代碼就走的后臺(tái)線程。在上圖的 Call Tree 中可以看到,被調(diào)用的堆棧名是 dispatch_worker_thread3。如果把它放到主線程去執(zhí)行,程序肯定會(huì)掛起。不信你注釋掉 dispatch_async 調(diào)用看一下。
再來個(gè)圖片加載的例子。
這兒有三種圖片加載方法:
loadSlowImage1:從指定 URL 下載一張圖片(加載速度慢)
loadImage2:從本地資源庫(kù)加載一張圖片(注意:沒用系統(tǒng)緩存)
loadFastImage3:從系統(tǒng)緩存中加載一張圖片(加載速度快)
我們來看看 Time Profiler 算出的結(jié)果是不是跟預(yù)想的一樣。
進(jìn)入演示應(yīng)用第二項(xiàng)「Time Profiler: Our Methods」,點(diǎn)擊「Reload」十次來重復(fù)加載圖片,這樣能產(chǎn)生足夠的數(shù)據(jù)來分析。然后在 Time Profiler 圖表中通過拖拉鼠標(biāo)選中要放大查看的區(qū)域,從 Call Tree 中雙擊調(diào)用了 .reload 方法那一行(上圖中加亮選中那一行),就會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的代碼行,所用時(shí)間也標(biāo)注出來了。
看到誰最花時(shí)間了吧。雖然代碼沒什么可優(yōu)化的地方,但大家應(yīng)該認(rèn)識(shí)到緩存能發(fā)揮的作用。所以即使有時(shí)還得調(diào)用 loadSlowImage,多數(shù)情況下把圖片緩存下來,還是能省些資源占用。
此外,我想再說說 Call Tree 的選項(xiàng)設(shè)置。
這些選項(xiàng)默認(rèn)是不選的,但把它們勾選上可以幫你更快定位到關(guān)鍵的代碼上,往往這也是問題的源頭。
Separate by Thread:按線程分開做分析,這樣更容易揪出那些吃資源的問題線程。特別是對(duì)于主線程,它要處理和渲染所有的接口數(shù)據(jù),一旦受到阻塞,程序必然卡頓或停止響應(yīng)。
Invert Call Tree:反向輸出調(diào)用樹。把調(diào)用層級(jí)最深的方法顯示在最上面,更容易找到最耗時(shí)的操作。
Hide Missing Symbols:隱藏缺失符號(hào)。如果 dSYM 文件或其他系統(tǒng)架構(gòu)缺失,列表中會(huì)出現(xiàn)很多奇怪的十六進(jìn)制的數(shù)值,用此選項(xiàng)把這些干擾元素屏蔽掉,讓列表回歸清爽。
Hide System Libraries:隱藏系統(tǒng)庫(kù)文件。過濾掉各種系統(tǒng)調(diào)用,只顯示自己的代碼調(diào)用。
Flattern Recursion:拼合遞歸。將同一遞歸函數(shù)產(chǎn)生的多條堆棧(因?yàn)檫f歸函數(shù)會(huì)調(diào)用自己)合并為一條。
Top Functions:找到最耗時(shí)的函數(shù)或方法。
Allocations
我們經(jīng)常需要從服務(wù)器下載大量圖片,特別是開發(fā)照片類的應(yīng)用。但往往稍不注意,內(nèi)存使用就會(huì)暴增,所以得保證把這些圖片緩存下來以便重復(fù)使用。下面來看看演示程序中內(nèi)存分配的例子。
從圖中可以看到,每次點(diǎn)擊「Reload」重新載入圖片時(shí),內(nèi)存都會(huì)出現(xiàn)使用峰值。應(yīng)用先分配大量?jī)?nèi)存來替換原有圖片,然后再釋放掉這部分內(nèi)存,可想而知這樣的操作效率高不了,而且如果要下載更大的文件,呃,局面大概會(huì)失控吧。
看一下堆棧列表第四行,ImageIO_PNG_Data 里有 9 張?zhí)幱诨顒?dòng)狀態(tài)的圖片,占用了12.38 MB 內(nèi)存,這些都是沒被系統(tǒng)釋放或緩存的內(nèi)存,所以導(dǎo)致堆內(nèi)存分配升高。接下來再看看使用緩存后的效果。
使用了緩存庫(kù)(Swift Haneke)后,點(diǎn)「Reload」五次,這回在 Allocations 列表中卻看不到 ImageIO_PNG_Data 對(duì)象了,這說明它是空的,沒有任何圖像數(shù)據(jù)。同時(shí),All Heap Allocations 的大小已從剛才的 14.61 MB 降到了 2.51 MB。Anonymous VM(匿名虛擬內(nèi)存)是系統(tǒng)為程序預(yù)留的、可能會(huì)立即被重復(fù)使用的一部分可用內(nèi)存。要防止程序崩潰,就別讓堆的尺寸增長(zhǎng)太快。
還有就是,例子用的是異步方式來加載圖片,這樣用不著等到所有圖片下載完才能在界面中顯示。大多數(shù)圖像緩存庫(kù)都會(huì)把加載工作放到后臺(tái),以避免延長(zhǎng)主線程的響應(yīng)周期。
All Heap Allocations 是程序真實(shí)的內(nèi)存分配情況,All Anonymous VM則是系統(tǒng)為程序分配的虛擬內(nèi)存,為的就是當(dāng)程序有需要的時(shí)候,能夠及時(shí)為程序提供足夠的內(nèi)存空間,而不會(huì)現(xiàn)用現(xiàn)創(chuàng)建。
Leaks
盡管 Apple 推出的 ARC 可以有效防范內(nèi)存泄漏,但出問題的機(jī)率還是會(huì)有,Swift 也不例外。鑒于篇幅有限,本文就不涉及內(nèi)存和 ARC 的工作原理了,具體可以參考官方文檔。我會(huì)用代碼來觸發(fā)內(nèi)存泄漏。
首先從最底層上說,當(dāng)兩個(gè)對(duì)象相互建立了強(qiáng)引用(strong reference),當(dāng)一個(gè)對(duì)象被釋放,另一個(gè)對(duì)象由于是強(qiáng)引用的關(guān)系不允許被釋放,此時(shí) ARC 無法確定沒被釋放的對(duì)象到底還有沒有用,于是就導(dǎo)致了內(nèi)存泄漏。
要解決這個(gè)問題,可以將其中的一個(gè)對(duì)象中變量設(shè)為 weak,不讓它出現(xiàn)在保留周期中。很多開發(fā)者在管理 view controller 時(shí)常會(huì)在內(nèi)存泄漏上中招,以為換了新的 controller,老的 controller 就被釋放回收了,其實(shí)還沒。這樣代碼一多,就會(huì)造成很多對(duì)象都沒被釋放。所以用這個(gè)工具把整個(gè)應(yīng)用跑一遍,把那些斷鏈的強(qiáng)引用清理干凈,會(huì)大有裨益。
除了上述這三類工具,Instruments 還有很多實(shí)用的工具,推薦大家根據(jù)自己的關(guān)注點(diǎn),花些時(shí)間去學(xué)學(xué)。比如:
Core Data:監(jiān)測(cè)讀取、緩存未命中、保存等操作,能直觀顯示是否保存次數(shù)遠(yuǎn)超實(shí)際需要。
Cocoa Layout:觀察約束變化,找出布局代碼的問題所在。
Network:跟蹤 TCP / IP和 UDP / IP 連接。
Automations:創(chuàng)建和編輯測(cè)試腳本來自動(dòng)化 iOS 應(yīng)用的用戶界面測(cè)試。
最后小總結(jié)下。我倒不想一味夸大 Instruments 的作用,如果應(yīng)用跑得挺痛快,沒出現(xiàn)啥調(diào)皮行為,大可把它忽略,等到問題來了再做優(yōu)化。對(duì)于新手來說,花些時(shí)間了解 Instruments 的功能,多調(diào)試多積累經(jīng)驗(yàn),這樣做出來的應(yīng)用在用戶體驗(yàn)上肯定錯(cuò)不了。
你最常用的 Instruments 工具都有哪些?歡迎與我們分享。
原文:How To Use The 3 Instruments You Should Be Using譯者:LeanCloud



