iOS性能優(yōu)化系列
一:性能優(yōu)化策略
這一系列文章是我的讀書筆記,整理一下,也算是溫故而知新。
性能問(wèn)題的處理流程
- 發(fā)現(xiàn)/重現(xiàn)問(wèn)題
- 利用工具剖析
- 形成假設(shè)
- 改進(jìn)代碼和設(shè)計(jì)
在以上的四個(gè)步驟中循環(huán)反復(fù),直到問(wèn)題解決。
Profile!不要猜!
性能優(yōu)化的主要策略:
- 不要做無(wú)用功:不要在啟動(dòng)時(shí)花幾百ms來(lái)做logging,不要為同樣的數(shù)據(jù)做多次查詢
- 試圖重用:對(duì)于創(chuàng)建過(guò)程昂貴的對(duì)象,要重用而不是重新創(chuàng)建
- Table View的cell
- Date/Number的formatter
- 正則表達(dá)式
- SQLite語(yǔ)句
- 使用更快的方式設(shè)計(jì)、編程:選擇正確的集合對(duì)象和算法來(lái)進(jìn)行編程、選擇適合的數(shù)據(jù)存儲(chǔ)格式(plist、SQLite)、優(yōu)化SQLite查詢語(yǔ)句
- 事先做優(yōu)化
- 對(duì)于昂貴的計(jì)算,要進(jìn)行事先計(jì)算。iCal中的重復(fù)事件,是預(yù)先計(jì)算出來(lái)的,并保存到數(shù)據(jù)庫(kù)中。
- 事先計(jì)算并緩存一些對(duì)象,可能會(huì)占用大量的內(nèi)存。注意不要將這些對(duì)象聲明為static并常駐內(nèi)存。
- 事后做優(yōu)化:異步加載、懶加載
- 為伸縮性而做優(yōu)化:當(dāng)數(shù)據(jù)有10條、100條、1000條甚至更多的時(shí)候,應(yīng)用程序的性能不應(yīng)該對(duì)應(yīng)的呈數(shù)量級(jí)式的增長(zhǎng),否則無(wú)法使用。
說(shuō)起來(lái)慚愧,我真的很少遇到性能問(wèn)題。以前假設(shè)中的性能問(wèn)題,很多是根本不存在的。事前計(jì)劃也杜絕了不了性能問(wèn)題的產(chǎn)生,所以不如暫時(shí)忘記它吧。當(dāng)然對(duì)于一些常識(shí)性的提高性能的設(shè)計(jì),仍然是必須的。
二:iOS應(yīng)用啟動(dòng)速度優(yōu)化
很多app的開(kāi)發(fā)者都不重視app的啟動(dòng)速度,這對(duì)于碎片化使用情景的用戶來(lái)說(shuō),簡(jiǎn)直是災(zāi)難。
iOS應(yīng)用的啟動(dòng)速度
應(yīng)用啟動(dòng)時(shí),會(huì)播放一個(gè)放大的動(dòng)畫。iPhone上是400ms,iPad上是500ms。最理想的啟動(dòng)速度是,在播放完動(dòng)畫后,用戶就可以使用。
如果應(yīng)用啟動(dòng)過(guò)慢,用戶就會(huì)放棄使用,甚至永遠(yuǎn)都不再回來(lái)。拋開(kāi)代碼不談,如果抱著PC端游和單機(jī)游戲的思維,在游戲啟動(dòng)時(shí)強(qiáng)加公司Logo,啟動(dòng)動(dòng)畫,并且用戶不可跳過(guò),也會(huì)使用戶的成功使用率大大降低。
iOS系統(tǒng)的“看門狗"
為了防止一個(gè)應(yīng)用占用過(guò)多的系統(tǒng)資源,開(kāi)發(fā)iOS的蘋果工程師門設(shè)計(jì)了一個(gè)“看門狗”的機(jī)制。在不同的場(chǎng)景下,“看門狗”會(huì)監(jiān)測(cè)應(yīng)用的性能。如果超出了該場(chǎng)景所規(guī)定的運(yùn)行時(shí)間,“看門狗”就會(huì)強(qiáng)制終結(jié)這個(gè)應(yīng)用的進(jìn)程。開(kāi)發(fā)者們?cè)赾rashlog里面,會(huì)看到諸如0x8badf00d
這樣的錯(cuò)誤代碼(“看門狗”吃了壞的食物,它很不高興)。
場(chǎng)景 | “看門狗”超時(shí)時(shí)間 |
---|---|
啟動(dòng) | 20秒 |
恢復(fù)運(yùn)行 | 10秒 |
懸掛進(jìn)程 | 10秒 |
退出應(yīng)用 | 6秒 |
后臺(tái)運(yùn)行 | 10分鐘 |
值得注意的是,Xcode在Debug的時(shí)候,會(huì)禁止“看門狗”。
如何測(cè)試啟動(dòng)時(shí)間
兩種方法:一種使用NSLog,另外一種使用Time Profiler。
- 使用NSLog
- 1 CFAbsoluteTime StartTime;
- 2 int main(int argc, char **argv) {
- 3 StartTime = CFAbsoluteTimeGetCurrent();
- 4 // ... 5 } 6
- 7 - (void)applicationDidFinishLaunching:(UIApplication *)app {
- 8 dispatch_async(dispatch_get_main_queue(), ^{
- 9
- NSLog(@"Launched in %f sec", CFAbsoluteTimeGetCurrent() - StartTime);
- 10
- }); 11 // ... 12 }
- 使用Time Profiler
- Instruments->Time Profiler
- Profile你的app
- 切換到CPU strategy view,找到你的app啟動(dòng)的第一幀
- 搜索
-[UIApplication _reportAppLaunchFinished]
- 找到包含
-[UIApplication _reportAppLaunchFinished]
的最后一幀,即可計(jì)算出啟動(dòng)時(shí)間
iOS App啟動(dòng)過(guò)程
- 鏈接并加載Framework和static lib
- UIKit初始化
- 應(yīng)用程序callback
- 第一個(gè)Core Animation transaction
鏈接并加載Framework及static lib時(shí)需要注意:
- 每個(gè)Framework都會(huì)增加啟動(dòng)時(shí)間和占用的內(nèi)存
- 不必要的Framework,不要鏈接
- 必要的Framework,不要票房為Optional
- 只在使用在Deployment Target之后發(fā)布的Framework時(shí),才使用Optional(比如你的Deployment Target是iOS 3.0,需要鏈接StoreKit的時(shí)候)
- 避免創(chuàng)建全局的C++對(duì)象
初始化UIKit時(shí)需要注意:
- 字體、狀態(tài)欄、user defaults、main nib會(huì)被初始化
- 保持main nib盡可能的小
- User defaults本質(zhì)上是一個(gè)plist文件,保存的數(shù)據(jù)是同時(shí)被反序列化的,不要在user defaults里面保存圖片等大數(shù)據(jù)
應(yīng)用程序的回調(diào):
application:willFinishLaunchingWithOptions:
- 恢復(fù)應(yīng)用程序的狀態(tài)
application:didFinishLaunchingWithOptions:
我一直認(rèn)為設(shè)計(jì)的本質(zhì)是折衷。當(dāng)你為了100ms的啟動(dòng)速度優(yōu)化歡欣不已,而無(wú)視那長(zhǎng)達(dá)10秒的啟動(dòng)動(dòng)畫時(shí),應(yīng)該想想究竟什么是應(yīng)該做的。做正確的事情比把事情做好更重要。
三:事件處理-拯救主線程
用戶經(jīng)常評(píng)論app的一個(gè)用詞是“卡頓”,很大的因素是因?yàn)橹骶€程被占用了。用戶的事件是在主線程被處理的,包括點(diǎn)擊、滾動(dòng)、加速計(jì)、Proximity Sensor。
為了保證事件的平滑處理,需要進(jìn)行如下優(yōu)化:
- 最小化主線程的CPU占用
- 將工作“搬離”主線程
- 不要阻塞主線程
最小化主線程的CPU占用
前面兩篇文章,我們接觸到了Time Profiler。使用它可以剖析不同線程的CPU使用情況,并給出調(diào)用堆棧的CPU時(shí)間占用百分比。如果app“卡頓”,并且在Time Profiler的結(jié)果可以找到明確的高占用堆棧,你需要把它優(yōu)化掉。
將工作“搬離”主線程 - 隱式并發(fā)
為了得到更流暢的交互體驗(yàn),iOS已經(jīng)幫我們做了很多事情,Android就沒(méi)有這么好運(yùn)了。iOS將以下這些事情搬離了主線程:
- View和layer的動(dòng)畫(動(dòng)畫繪制前的計(jì)算,而不是drawing過(guò)程)
- Layer的組合計(jì)算(drawing后的疊加)
- PNG的解碼(是的,你沒(méi)看錯(cuò);而且利用了CPU的多核心)
注意滾動(dòng)(Scrolling)不是一個(gè)動(dòng)畫,而是在Main Run Loop中不斷接收事件并且處理。
將工作“搬離”主線程 - 顯式并發(fā)
這里是需要開(kāi)發(fā)者們搞定的部分。磁盤、網(wǎng)絡(luò)等I/O會(huì)阻塞線程,不要把它們放到主線程里。常用的技術(shù)有:
- Grand Central Dispatch(GCD)
- NSOperationQueue
- NSThread
iOS 4.0后,易用的GCD技術(shù)被廣泛使用。例如:
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
- ^{
- // do something in background dispatch_async(dispatch_get_main_queue(), ^{
- // do something on main thread
- }); });
GCD的陷阱
GCD其實(shí)就是線程,只不過(guò)提供了一個(gè)更高層次的抽象。過(guò)多的線程一定會(huì)帶來(lái)性能損失,因此GCD設(shè)計(jì)了一個(gè)最高允許的線程值(對(duì)開(kāi)發(fā)者透明,不用管到底有多少)。那么如何解決這個(gè)問(wèn)題呢?
- 將隊(duì)列串行化
- 使用Dispatch sources
- 使用帶有限制的NSOperationQueue
- 使用Cocoa Touch提供的異步方法
另外一個(gè)陷阱是線程安全:
- UIKit必須要在主線程使用,除了UIGraphics,UIBezierPath,UIImage
- 大多數(shù)CG、CA、Foundation的類,不是線程安全的
- 如果你使用了ojbc runtime來(lái)進(jìn)行introspection,由于它是thread safe的,可能會(huì)導(dǎo)致競(jìng)爭(zhēng)
此外,iOS 4.3添加了DISPATCH_QUEUE_PRIORITY_BACKGROUND
,它擁有非常低的優(yōu)先級(jí)。這個(gè)優(yōu)先級(jí)只用于不太關(guān)心完成時(shí)間的真正的后臺(tái)任務(wù),如果要表示較低的優(yōu)先級(jí),你通常需要的是DISPATCH_QUEUE_PRIORITY_LOW
。
不要阻塞主線程
即使占用了很少的CPU時(shí)間(如果你在Time Profiler中看到這些的數(shù)據(jù)),也可能會(huì)阻塞主線程。磁盤、網(wǎng)絡(luò)、Lock、dispatch_sync以及向其它進(jìn)程/線程發(fā)送消息都會(huì)阻塞主線 程。Time Profiler只能檢測(cè)出占用CPU過(guò)多的堆棧,但檢測(cè)不了這些IO的問(wèn)題。
大多數(shù)的阻塞事件,都會(huì)伴隨著一個(gè)系統(tǒng)調(diào)用,如:
read/write
- 讀寫文件send/recv
- 收發(fā)網(wǎng)絡(luò)數(shù)據(jù)psynch_mutex_wait
- 獲得鎖mach_msg
- IPC
System Trace
這個(gè)Instrumentor,記錄了所有的系統(tǒng)調(diào)用,以及每次調(diào)用的等待時(shí)間。如果你在System Trace里面發(fā)現(xiàn)了CPU Time很低,但Wait Time很高的調(diào)用,說(shuō)明在主線程處理I/O已經(jīng)嚴(yán)重?fù)p害了app的性能。
保證主線程的低CPU占用,將I/O移至其它線程,可以大大地提高主線程對(duì)交互事件的處理能力。我建議開(kāi)發(fā)者朋友們寫代碼的時(shí)候,除非是以前遇到過(guò)的問(wèn)題,都沒(méi)有必要假設(shè)問(wèn)題存在。80%的優(yōu)化都是不必要的。