前言
本文將從發(fā)現(xiàn)問(wèn)題、解決問(wèn)題和預(yù)防問(wèn)題進(jìn)行總結(jié)
如何發(fā)現(xiàn)性能問(wèn)題
業(yè)務(wù)性能監(jiān)控个束,是指在
App
本地,業(yè)務(wù)的開(kāi)始和結(jié)束處打點(diǎn)上報(bào)聊疲,然后后臺(tái)統(tǒng)計(jì)達(dá)到監(jiān)控目的茬底;卡頓監(jiān)控∈鄱茫卡頓監(jiān)控的實(shí)現(xiàn)一般有兩種方案:
1)主線程卡頓監(jiān)控桩警。通過(guò)子線程監(jiān)測(cè)主線程的 runLoop,判斷兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否達(dá)到一定閾值昌妹。具體原理和實(shí)現(xiàn)捶枢,這篇文章介紹得比較詳細(xì)。
(2)FPS監(jiān)控飞崖。要保持流暢的UI交互烂叔,App 刷新率應(yīng)該當(dāng)努力保持在 60fps。監(jiān)控實(shí)現(xiàn)原理比較簡(jiǎn)單固歪,通過(guò)記錄兩次刷新時(shí)間間隔蒜鸡,就可以計(jì)算出當(dāng)前的 FPS。
- 在實(shí)際應(yīng)用過(guò)程牢裳,無(wú)論是主線程監(jiān)控逢防,還是 FPS 監(jiān)控,抖動(dòng)都比較大蒲讯。因此忘朝,一套綜合的判斷方法,結(jié)合了主線程監(jiān)控判帮、FPS監(jiān)控局嘁,以及CPU使用率等指標(biāo)溉箕,作為判斷卡頓的標(biāo)準(zhǔn)。
性能問(wèn)題的解決方法
- 優(yōu)化業(yè)務(wù)流程悦昵。
性能優(yōu)化看似高深肴茄,真正落到實(shí)處才會(huì)發(fā)現(xiàn),最大的坑往往都隱藏在于業(yè)務(wù)不斷累積和頻繁變更之處但指。 - 合理的線程分配
由于GCD
實(shí)在太方便了寡痰,如果不加控制,大部分需要拋到子線程操作都會(huì)被直接加到global
隊(duì)列枚赡,這樣會(huì)導(dǎo)致兩個(gè)問(wèn)題:
(1). 開(kāi)的子線程越來(lái)越多氓癌,線程的開(kāi)銷(xiāo)逐漸明顯谓谦,因?yàn)殚_(kāi)啟線程需要占用一定的內(nèi)存空間(默認(rèn)的情況下贫橙,主線程占1M,子線程占用512KB)。
(2).多線程情況下反粥,網(wǎng)絡(luò)回調(diào)的時(shí)序問(wèn)題卢肃,導(dǎo)致數(shù)據(jù)處理錯(cuò)亂,而且不容易發(fā)現(xiàn)才顿。為此莫湘,我們項(xiàng)目定了一些基本原則。
- UI 操作和 DataSource 的操作一定在主線程郑气。
- DB 操作幅垮、日志記錄、網(wǎng)絡(luò)回調(diào)都在各自的固定線程尾组。
- 不同業(yè)務(wù)忙芒,可以通過(guò)創(chuàng)建隊(duì)列保證數(shù)據(jù)一致性。
合理的線程分配讳侨,最終目的就是保證主線程盡量少的處理非UI操作呵萨,同時(shí)控制整個(gè)App的子線程數(shù)量在合理的范圍內(nèi)。
- 預(yù)處理和延時(shí)加載
預(yù)處理:是將初次顯示需要耗費(fèi)大量線程時(shí)間的操作跨跨,提前放到后臺(tái)線程進(jìn)行計(jì)算潮峦,再將結(jié)果數(shù)據(jù)拿來(lái)顯示。
延時(shí)加載:是指首先加載當(dāng)前必須的可視內(nèi)容勇婴,在稍后一段時(shí)間內(nèi)或特定事件時(shí)忱嘹,再觸發(fā)其他內(nèi)容的加載。這種方式可以很有效的提升界面繪制速度耕渴,使體驗(yàn)更加流暢拘悦。(UITableView 就是最典型的例子)
這兩種方法都是在資源比較緊張的情況下,優(yōu)先處理馬上要用到的數(shù)據(jù)萨螺,同時(shí)盡可能提前加載即將要用到的數(shù)據(jù)窄做。
- 緩存
cache
可能是所有性能優(yōu)化中最常用的手段愧驱,但也是我們極不推薦的手段。cache
建立的成本低椭盏,見(jiàn)效快组砚,但是帶來(lái)維護(hù)的成本卻很高。如果一定要用掏颊,也請(qǐng)謹(jǐn)慎使用糟红,并注意以下幾點(diǎn):
- 并發(fā)訪問(wèn)
cache
時(shí),數(shù)據(jù)一致性問(wèn)題乌叶。
-
cache
線程安全問(wèn)題盆偿,防止一邊修改一邊遍歷的 crash。 -
cache
查找時(shí)性能問(wèn)題准浴。 -
cache
的釋放與重建事扭,避免占用空間無(wú)限擴(kuò)大,同時(shí)釋放的粒度也要依實(shí)際需求而定乐横。
- 使用正確的API
使用正確的 API求橄,是指在滿足業(yè)務(wù)的同時(shí),能夠選擇性能更優(yōu)的API葡公。
- 選擇合適的容器;
- 了解 imageNamed: 與 imageWithContentsOfFile:的差異(imageNamed: 適用于會(huì)重復(fù)加載的小圖片罐农,因?yàn)橄到y(tǒng)會(huì)自動(dòng)緩存加載的圖片,imageWithContentsOfFile: 僅加載圖片)
- 緩存 NSDateFormatter 的結(jié)果催什。
- 尋找 (NSDate *)dateFromString:(NSString )string 的替換品涵亏。
//#include <time.h> time_t t; struct tm tm; strptime([iso8601String cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:> %M:%S%z", &tm); tm.tm_isdst = -1; t = mktime(&tm); [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
不要隨意使用 NSLog().
當(dāng)試圖獲取磁盤(pán)中一個(gè)文件的屬性信息時(shí),使用
[NSFileManager attributesOfItemAtPath:error:]
會(huì)浪費(fèi)大量時(shí)間讀取可能根本不需要的附加屬性蒲凶。這時(shí)可以使用stat
代替NSFileManager
气筋,直接獲取文件屬性:#import <sys/stat.h> struct stat statbuf; const char *cpath = [filePath fileSystemRepresentation]; if (cpath && stat(cpath, &statbuf) == 0) { NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size]; NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime]; NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime]; // etc }
如何預(yù)防性能問(wèn)題
內(nèi)存泄露檢測(cè)工具
MLeakFinder是團(tuán)隊(duì)成員zepo在github開(kāi)源的一款內(nèi)存泄露檢測(cè)工具,具體原理和使用方法可以參見(jiàn)這篇文章豹爹。在此之前裆悄,內(nèi)存泄露引起的性能問(wèn)題是很難被察覺(jué)的,只有泄露到了相當(dāng)嚴(yán)重的程度臂聋,然后通過(guò)Instrument工具光稼,不斷嘗試才得以定位。MLeakFinder能在開(kāi)發(fā)階段孩等,把內(nèi)存泄露問(wèn)題暴露無(wú)遺艾君,減少了很多潛在的性能問(wèn)題。FPS/SQL性能監(jiān)測(cè)工具條
該工具條是在DEBUG模式下肄方,以浮窗的形式冰垄,實(shí)時(shí)展示當(dāng)前可能存在問(wèn)題的FPS次數(shù)和執(zhí)行時(shí)間較長(zhǎng)的SQL語(yǔ)句個(gè)數(shù),是團(tuán)隊(duì)成員tower
的杰作权她。FPS監(jiān)測(cè)的原理并不復(fù)雜虹茶,前文也有介紹逝薪,雖然并不百分百準(zhǔn)確,但非常實(shí)用蝴罪,因?yàn)榭梢噪S時(shí)查看FPS低于某個(gè)閾值時(shí)的堆棧信息董济,再結(jié)合當(dāng)時(shí)的使用場(chǎng)景,開(kāi)發(fā)人員使用起來(lái)非常便利要门,可以很快定位到引起卡頓的場(chǎng)景和原因虏肾。SQL語(yǔ)句的監(jiān)測(cè)也非常實(shí)用.UI / DataSource主線程檢測(cè)工具。
該工具是為了保證所有的UI的操作和 DataSource 操作一定是在主線程進(jìn)行欢搜,同樣是由tower同學(xué)貢獻(xiàn)封豪。實(shí)現(xiàn)原理是通過(guò)hook UIView
的-setNeedsLayout,-setNeedsDisplay炒瘟,-setNeedsDisplayInRect
三個(gè)方法吹埠,確保它們都是在主線程執(zhí)行。子線程操作UI可能會(huì)引起什么問(wèn)題唧领,蘋(píng)果說(shuō)得并不清楚藻雌,實(shí)際開(kāi)發(fā)中我們遇到幾種神奇的問(wèn)題似乎都是跟這個(gè)有關(guān)。
app 突然丟動(dòng)畫(huà)斩个,似乎 iOS 系統(tǒng)也有這個(gè) bug。雖然沒(méi)有確切的證據(jù)驯杜,但使用這個(gè)工具受啥,改完所有的問(wèn)題后,bug 也好了(不止一次是這樣)鸽心。
UI 操作偶爾響應(yīng)特別慢滚局,從代碼看沒(méi)有任何耗時(shí)操作,只是簡(jiǎn)單的 push 某個(gè) controller顽频。
莫名的 crash藤肢,這當(dāng)然是因?yàn)?UI 操作非線程安全引起的。
更多時(shí)候糯景,子線程操作 UI 也并不一定會(huì)發(fā)生什么問(wèn)題嘁圈,也正因?yàn)椴恢罆?huì)發(fā)生什么,所以更需要我們警惕蟀淮,這個(gè)工具替我們掃除了這些隱患最住。雖然,蘋(píng)果表示怠惶,現(xiàn)在部分的 UI 操作也已經(jīng)是線程安全了涨缚,但畢竟大部分還不是。DataSource 的監(jiān)測(cè)是因?yàn)槲覀儤I(yè)務(wù)定下的原則策治,保證列表 DataSource 的線程安全脓魏。
卡頓產(chǎn)生的原因
界面卡頓的原因:在
VSync
信號(hào)到來(lái)后兰吟,系統(tǒng)圖形服務(wù)會(huì)通過(guò) CADisplayLink
等機(jī)制通知 App,App 主線程開(kāi)始在CPU
中計(jì)算顯示內(nèi)容茂翔,比如視圖的創(chuàng)建揽祥、布局計(jì)算、圖片解碼檩电、文本繪制等拄丰。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU
去,由 GPU 進(jìn)行變換俐末、合成料按、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去卓箫,等待下一次VSync
信號(hào)到來(lái)時(shí)顯示到屏幕上载矿。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi)烹卒,CPU 或者 GPU 沒(méi)有完成內(nèi)容提交闷盔,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示旅急,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變逢勾。在開(kāi)發(fā)中,CPU和GPU中任何一個(gè)壓力過(guò)大藐吮,都會(huì)導(dǎo)致掉幀現(xiàn)象溺拱,所以在開(kāi)發(fā)時(shí),也需要分別對(duì)
CPU和GPU壓力進(jìn)行評(píng)估和優(yōu)化
谣辞。
CPU:加載資源迫摔,對(duì)象創(chuàng)建,對(duì)象調(diào)整泥从,對(duì)象銷(xiāo)毀句占,布局計(jì)算,
Autolayout
躯嫉,文本計(jì)算纱烘,文本渲染,圖片的解碼和敬, 圖像的繪制(Core Graphics
)都是在CPU
上面進(jìn)行的凹炸。
GPU:
GPU
是一個(gè)專門(mén)為圖形高并發(fā)計(jì)算而量身定做的處理單元,比CPU
使用更少的電來(lái)完成工作并且GPU的浮點(diǎn)計(jì)算能力要超出CPU很多昼弟。
GPU的渲染性能要比CPU高效很多啤它,同時(shí)對(duì)系統(tǒng)的負(fù)載和消耗也更低一些,所以在開(kāi)發(fā)中,我們應(yīng)該盡量讓CPU負(fù)責(zé)主線程的UI調(diào)動(dòng)变骡,把圖形顯示相關(guān)的工作交給GPU來(lái)處理离赫,當(dāng)涉及到光柵化等一些工作時(shí),CPU也會(huì)參與進(jìn)來(lái)塌碌,這點(diǎn)在后面再詳細(xì)描述渊胸。
相對(duì)于CPU來(lái)說(shuō),GPU能干的事情比較單一:接收提交的紋理(Texture)和頂點(diǎn)描述(三角形)台妆,應(yīng)用變換(transform)翎猛、混合(合成)并渲染,然后輸出到屏幕上接剩。通常你所能看到的內(nèi)容切厘,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類(lèi)。
CPU 和 GPU 的協(xié)作:要在屏幕上顯示視圖懊缺,需要CPU和GPU一起協(xié)作疫稿,CPU計(jì)算好顯示的內(nèi)容提交到GPU,GPU渲染完成后將結(jié)果放到幀緩存區(qū)鹃两,隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù)遗座,經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。
緩沖機(jī)制:
iOS
使用的是雙緩沖機(jī)制俊扳。即GPU會(huì)
預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi)(前幀緩存)
途蒋,讓視頻控制器讀取,當(dāng)下一幀渲染好后拣度,GPU
會(huì)直接把視頻控制器的指針指向第二個(gè)緩沖器(后幀緩存)碎绎。當(dāng)你視頻控制器已經(jīng)讀完一幀,準(zhǔn)備讀下一幀的時(shí)候抗果,GPU
會(huì)等待顯示器的VSync信號(hào)發(fā)出后,前幀緩存和后幀緩存會(huì)瞬間切換奸晴,后幀緩存會(huì)變成新的前幀緩存冤馏,同時(shí)舊的前幀緩存會(huì)變成新的后幀緩存。
iOS 保持界面流暢的技巧
LLDebugTool是一款針對(duì)開(kāi)發(fā)者和測(cè)試者的調(diào)試工具
iOS 性能優(yōu)化總結(jié)
iOS性能優(yōu)化系列篇之“優(yōu)化總體原則”
iOS應(yīng)用UI線程卡頓監(jiān)控
UITableView-FDTemplateLayoutCell - 緩存
繪制像素到屏幕上
iOS圖形原理與離屏渲染
iOS 保持界面流暢的技巧
Advanced Graphics and Animations for iOS Apps(session 419)
使用 ASDK 性能調(diào)優(yōu) - 提升 iOS 界面的渲染性能
Designing for iOS: Graphics & Performance
iOS離屏渲染之優(yōu)化分析
iOS視圖渲染以及性能優(yōu)化總結(jié)
iOS 離屏渲染
深刻理解移動(dòng)端優(yōu)化之離屏渲染
iOS 流暢度性能優(yōu)化寄啼、CPU逮光、GPU、離屏渲染
iOS 圖形性能優(yōu)化錦集
離屏渲染優(yōu)化詳解:實(shí)例示范+性能測(cè)試