寫在前面
本文來(lái)自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序員。這是他的個(gè)人網(wǎng)站:http://www.marcelofabri.com/,你還可以在Twitter上關(guān)注@marcelofabri_。
性能對(duì) iOS 應(yīng)用的開發(fā)尤其重要,如果你的應(yīng)用失去反應(yīng)或者很慢,失望的用戶會(huì)把他們的失望寫滿App Store的評(píng)論。然而由于iOS設(shè)備的限制,有時(shí)搞好性能是一件難事。開發(fā)過(guò)程中你會(huì)有很多需要注意的事項(xiàng),你也很容易在做出選擇時(shí)忘記考慮性能影響。
這正是我寫下這篇文章的原因。這篇文章以一個(gè)方便查看的核對(duì)表的形式整合了你可以用來(lái)提升你app性能的25條建議和技巧。
請(qǐng)耐心讀完這篇文章,為你未來(lái)的app提個(gè)速!
注意:每在優(yōu)化代碼之前,你都要注意一個(gè)問(wèn)題,不要養(yǎng)成”預(yù)優(yōu)化”代碼的錯(cuò)誤習(xí)慣。時(shí)常使用Instruments去profile你的代碼來(lái)發(fā)現(xiàn)需要提升的方面。Matt Galloway寫過(guò)一篇很棒的如何利用Instruments來(lái)優(yōu)化代碼的文章)。
還要注意的是,這里列出的其中一些建議是有代價(jià)的,所建議的方式會(huì)提升app的速度或者使它更加高效,但也可能需要花很多功夫去應(yīng)用或者使代碼變得更加復(fù)雜,所以要仔細(xì)選擇。
目錄
我要給出的建議將分為三個(gè)不同的等級(jí): 入門級(jí)、 中級(jí)和進(jìn)階級(jí):
入門級(jí)(這是些你一定會(huì)經(jīng)常用在你app開發(fā)中的建議)
- 用ARC管理內(nèi)存
- 在正確的地方使用reuseIdentifier
- 盡可能使Views不透明
- 避免龐大的XIB
- 不要block主線程
- 在Image Views中調(diào)整圖片大小
- 選擇正確的Collection
- 打開gzip壓縮
中級(jí)(這些是你可能在一些相對(duì)復(fù)雜情況下可能用到的)
- 重用和延遲加載Views
- Cache, Cache, 還是Cache!
- 權(quán)衡渲染方法
- 處理內(nèi)存警告
- 重用大開銷的對(duì)象
- 使用Sprite Sheets
- 避免反復(fù)處理數(shù)據(jù)
- 選擇正確的數(shù)據(jù)格式
- 正確地設(shè)定Background Images
- 減少使用Web特性
- 設(shè)定Shadow Path
- 優(yōu)化你的Table View
- 選擇正確的數(shù)據(jù)存儲(chǔ)選項(xiàng)
進(jìn)階級(jí)(這些建議只應(yīng)該在你確信他們可以解決問(wèn)題和得心應(yīng)手的情況下采用)
- 加速啟動(dòng)時(shí)間
- 使用Autorelease Pool
- 選擇是否緩存圖片
- 盡量避免日期格式轉(zhuǎn)換
無(wú)需贅述,讓我們進(jìn)入正題吧~
初學(xué)者性能提升
這個(gè)部分致力于一些能提高性能的基本改變。但所有層次的開發(fā)者都有可能會(huì)從這個(gè)記錄了一些被忽視的項(xiàng)目的小小的性能備忘錄里獲得一些提升。
1. 用ARC管理內(nèi)存
ARC(Automatic Reference Counting, 自動(dòng)引用計(jì)數(shù))和iOS5一起發(fā)布,它避免了最常見的也就是經(jīng)常是由于我們忘記釋放內(nèi)存所造成的內(nèi)存泄露。它自動(dòng)為你管理retain和release的過(guò)程,所以你就不必去手動(dòng)干預(yù)了。
下面是你會(huì)經(jīng)常用來(lái)去創(chuàng)建一個(gè)View的代碼段:
UIView *view = [[UIView alloc] init];
// ...
[self.view addSubview:view];
[view release];
忘掉代碼段結(jié)尾的release簡(jiǎn)直像記得吃飯一樣簡(jiǎn)單。而ARC會(huì)自動(dòng)在底層為你做這些工作。
除了幫你避免內(nèi)存泄露,ARC還可以幫你提高性能,它能保證釋放掉不再需要的對(duì)象的內(nèi)存。這都啥年代了,你應(yīng)該在你的所有項(xiàng)目里使用ARC!
這里有一些更多關(guān)于ARC的學(xué)習(xí)資源:
Apple’s official documentation
Matthijs Hollemans’s Beginning ARC in iOS Tutorial
Tony Dahbura’s How To Enable ARC in a Cocos2D 2.X Project
If you still aren’t convinced of the benefits of ARC, check out this article on eight myths about ARC to really convince you why you should be using it!
ARC當(dāng)然不能為你排除所有內(nèi)存泄露的可能性。由于阻塞, retain 周期, 管理不完善的CoreFoundation object(還有C結(jié)構(gòu))或者就是代碼太爛依然能導(dǎo)致內(nèi)存泄露。
這里有一篇很棒的介紹ARC不能做到以及我們?cè)撛趺醋龅奈恼?http://conradstoll.com/blog/2013/1/19/blocks-operations-and-retain-cycles.html。
一個(gè)開發(fā)中常見的錯(cuò)誤就是沒有給UITableViewCells, UICollectionViewCells,甚至是UITableViewHeaderFooterViews設(shè)置正確的reuseIdentifier。
為了性能最優(yōu)化,table view用 tableView:cellForRowAtIndexPath:
為rows分配cells的時(shí)候,它的數(shù)據(jù)應(yīng)該重用自UITableViewCell。 一個(gè)table view維持一個(gè)隊(duì)列的數(shù)據(jù)可重用的UITableViewCell對(duì)象。
不使用reuseIdentifier的話,每顯示一行table view就不得不設(shè)置全新的cell。這對(duì)性能的影響可是相當(dāng)大的,尤其會(huì)使app的滾動(dòng)體驗(yàn)大打折扣。
自iOS6起,除了UICollectionView的cells和補(bǔ)充views,你也應(yīng)該在header和footer views中使用reuseIdentifiers。
想要使用reuseIdentifiers的話,在一個(gè)table view中添加一個(gè)新的cell時(shí)在data source object中添加這個(gè)方法:
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
這個(gè)方法把那些已經(jīng)存在的cell從隊(duì)列中排除,或者在必要時(shí)使用先前注冊(cè)的nib或者class創(chuàng)造新的cell。如果沒有可重用的cell,你也沒有注冊(cè)一個(gè)class或者nib的話,這個(gè)方法返回nil。
如果你有不透明的Views,你應(yīng)該設(shè)置它們的opaque屬性為YES。
原因是這會(huì)使系統(tǒng)用一個(gè)最優(yōu)的方式渲染這些views。這個(gè)簡(jiǎn)單的屬性在IB或者代碼里都可以設(shè)定。
Apple的文檔對(duì)于為圖片設(shè)置不透明屬性的描述是:
(opaque)這個(gè)屬性給渲染系統(tǒng)提供了一個(gè)如何處理這個(gè)view的提示。如果設(shè)為YES, 渲染系統(tǒng)就認(rèn)為這個(gè)view是完全不透明的,這使得渲染系統(tǒng)優(yōu)化一些渲染過(guò)程和提高性能。如果設(shè)置為NO,渲染系統(tǒng)正常地和其它內(nèi)容組成這個(gè)View。默認(rèn)值是YES。
在相對(duì)比較靜止的畫面中,設(shè)置這個(gè)屬性不會(huì)有太大影響。然而當(dāng)這個(gè)view嵌在scroll view里邊,或者是一個(gè)復(fù)雜動(dòng)畫的一部分,不設(shè)置這個(gè)屬性的話會(huì)在很大程度上影響app的性能。
你可以在模擬器中用Debug\Color Blended Layers選項(xiàng)來(lái)發(fā)現(xiàn)哪些view沒有被設(shè)置為opaque。目標(biāo)就是,能設(shè)為opaque的就全設(shè)為opaque!
iOS5中加入的Storyboards(分鏡)正在快速取代XIB。然而XIB在一些場(chǎng)景中仍然很有用。比如你的app需要適應(yīng)iOS5之前的設(shè)備,或者你有一個(gè)自定義的可重用的view,你就不可避免地要用到他們。
如果你不得不XIB的話,使他們盡量簡(jiǎn)單。嘗試為每個(gè)Controller配置一個(gè)單獨(dú)的XIB,盡可能把一個(gè)View Controller的view層次結(jié)構(gòu)分散到單獨(dú)的XIB中去。
需要注意的是,當(dāng)你加載一個(gè)XIB的時(shí)候所有內(nèi)容都被放在了內(nèi)存里,包括任何圖片。如果有一個(gè)不會(huì)即刻用到的view,你這就是在浪費(fèi)寶貴的內(nèi)存資源了。Storyboards就是另一碼事兒了,storyboard僅在需要時(shí)實(shí)例化一個(gè)view controller.
當(dāng)家在XIB是,所有圖片都被chache,如果你在做OS X開發(fā)的話,聲音文件也是。Apple在相關(guān)文檔中的記述是:
當(dāng)你加載一個(gè)引用了圖片或者聲音資源的nib時(shí),nib加載代碼會(huì)把圖片和聲音文件寫進(jìn)內(nèi)存。在OS X中,圖片和聲音資源被緩存在named cache中以便將來(lái)用到時(shí)獲取。在iOS中,僅圖片資源會(huì)被存進(jìn)named caches。取決于你所在的平臺(tái),使用NSImage 或UIImage 的imageNamed:
方法來(lái)獲取圖片資源。
很明顯,同樣的事情也發(fā)生在storyboards中,但我并沒有找到任何支持這個(gè)結(jié)論的文檔。如果你了解這個(gè)操作,寫信給我!
想要了解更多關(guān)于storyboards的內(nèi)容的話你可以看看 Matthijs Hollemans的Beginning Storyboards in iOS 5 Part 1 和 Part 2
永遠(yuǎn)不要使主線程承擔(dān)過(guò)多。因?yàn)閁IKit在主線程上做所有工作,渲染,管理觸摸反應(yīng),回應(yīng)輸入等都需要在它上面完成。
一直使用主線程的風(fēng)險(xiǎn)就是如果你的代碼真的block了主線程,你的app會(huì)失去反應(yīng)。這。。。正是在App Store中拿到一顆星的捷徑 :]
大部分阻礙主進(jìn)程的情形是你的app在做一些牽涉到讀寫外部資源的I/O操作,比如存儲(chǔ)或者網(wǎng)絡(luò)。
你可以使用NSURLConnection
異步地做網(wǎng)絡(luò)操作:
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
或者使用像 AFNetworking這樣的框架來(lái)異步地做這些操作。
如果你需要做其它類型的需要耗費(fèi)巨大資源的操作(比如時(shí)間敏感的計(jì)算或者存儲(chǔ)讀寫)那就用 Grand Central Dispatch,或者 NSOperation 和 NSOperationQueues.
下面代碼是使用GCD的模板
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// switch to a background thread and perform your expensive operation
dispatch_async(dispatch_get_main_queue(), ^{
// switch back to the main thread to update your UI
});
});
發(fā)現(xiàn)代碼中有一個(gè)嵌套的dispatch_async
嗎?這是因?yàn)槿魏蜺IKit相關(guān)的代碼需要在主線程上進(jìn)行。
如果你對(duì) NSOperation 或者GCD 的細(xì)節(jié)感興趣的話,看看Ray Wenderlich的 Multithreading and Grand Central Dispatch on iOS for Beginners, 還有 Soheil Azarpour 的 How To Use NSOperations and NSOperationQueues 教程。
如果要在UIImageView
中顯示一個(gè)來(lái)自bundle的圖片,你應(yīng)保證圖片的大小和UIImageView的大小相同。在運(yùn)行中縮放圖片是很耗費(fèi)資源的,特別是UIImageView
嵌套在UIScrollView
中的情況下。
如果圖片是從遠(yuǎn)端服務(wù)加載的你不能控制圖片大小,比如在下載前調(diào)整到合適大小的話,你可以在下載完成后,最好是用background thread,縮放一次,然后在UIImageView中使用縮放后的圖片。
學(xué)會(huì)選擇對(duì)業(yè)務(wù)場(chǎng)景最合適的類或者對(duì)象是寫出能效高的代碼的基礎(chǔ)。當(dāng)處理collections時(shí)這句話尤其正確。
Apple有一個(gè) Collections Programming Topics 的文檔詳盡介紹了可用的classes間的差別和你該在哪些場(chǎng)景中使用它們。這對(duì)于任何使用collections的人來(lái)說(shuō)是一個(gè)必讀的文檔。
呵呵,我就知道你因?yàn)樘L(zhǎng)沒看…這是一些常見collection的總結(jié):
Arrays: 有序的一組值。使用index來(lái)lookup很快,使用value lookup很慢, 插入/刪除很慢。
Dictionaries: 存儲(chǔ)鍵值對(duì)。 用鍵來(lái)查找比較快。
Sets: 無(wú)序的一組值。用值來(lái)查找很快,插入/刪除很快。
8. 打開gzip壓縮
大量app依賴于遠(yuǎn)端資源和第三方API,你可能會(huì)開發(fā)一個(gè)需要從遠(yuǎn)端下載XML, JSON, HTML或者其它格式的app。
問(wèn)題是我們的目標(biāo)是移動(dòng)設(shè)備,因此你就不能指望網(wǎng)絡(luò)狀況有多好。一個(gè)用戶現(xiàn)在還在edge網(wǎng)絡(luò),下一分鐘可能就切換到了3G。不論什么場(chǎng)景,你肯定不想讓你的用戶等太長(zhǎng)時(shí)間。
減小文檔的一個(gè)方式就是在服務(wù)端和你的app中打開gzip。這對(duì)于文字這種能有更高壓縮率的數(shù)據(jù)來(lái)說(shuō)會(huì)有更顯著的效用。
好消息是,iOS已經(jīng)在NSURLConnection中默認(rèn)支持了gzip壓縮,當(dāng)然AFNetworking這些基于它的框架亦然。像Google App Engine這些云服務(wù)提供者也已經(jīng)支持了壓縮輸出。
如果你不知道如何利用Apache或者IIS(服務(wù)器)來(lái)打開gzip,可以讀下這篇文章。
中級(jí)性能提升
你確信你已經(jīng)掌握了前述那些基礎(chǔ)級(jí)的優(yōu)化方案了嗎?但實(shí)際情況是,有時(shí)一些解決方案并不像那些一樣明顯,它們往往嚴(yán)重依賴于你如何架構(gòu)和書寫你的app。下面的這些建議就是針對(duì)這些場(chǎng)景的。
9. 重用和延遲加載(lazy load) Views
****更多的view意味著更多的渲染,也就是更多的CPU和內(nèi)存消耗,對(duì)于那種嵌套了很多view在UIScrollView里邊的app更是如此。
這里我們用到的技巧就是模仿UITableView
和UICollectionView
的操作: 不要一次創(chuàng)建所有的subview,而是當(dāng)需要時(shí)才創(chuàng)建,當(dāng)它們完成了使命,把他們放進(jìn)一個(gè)可重用的隊(duì)列中。
這樣的話你就只需要在滾動(dòng)發(fā)生時(shí)創(chuàng)建你的views,避免了不劃算的內(nèi)存分配。
創(chuàng)建views的能效問(wèn)題也適用于你app的其它方面。想象一下一個(gè)用戶點(diǎn)擊一個(gè)按鈕的時(shí)候需要呈現(xiàn)一個(gè)view的場(chǎng)景。有兩種實(shí)現(xiàn)方法:
- 創(chuàng)建并隱藏這個(gè)view當(dāng)這個(gè)screen加載的時(shí)候,當(dāng)需要時(shí)顯示它;
- 當(dāng)需要時(shí)才創(chuàng)建并展示。
每個(gè)方案都有其優(yōu)缺點(diǎn)。
用第一種方案的話因?yàn)槟阈枰婚_始就創(chuàng)建一個(gè)view并保持它直到不再使用,這就會(huì)更加消耗內(nèi)存。然而這也會(huì)使你的app操作更敏感因?yàn)楫?dāng)用戶點(diǎn)擊按鈕的時(shí)候它只需要改變一下這個(gè)view的可見性。
第二種方案則相反-消耗更少內(nèi)存,但是會(huì)在點(diǎn)擊按鈕的時(shí)候比第一種稍顯卡頓。
10. Cache, Cache, 還是Cache!
****一個(gè)極好的原則就是,緩存所需要的,也就是那些不大可能改變但是需要經(jīng)常讀取的東西。
我們能緩存些什么呢?一些選項(xiàng)是,遠(yuǎn)端服務(wù)器的響應(yīng),圖片,甚至計(jì)算結(jié)果,比如UITableView的行高。
NSURLConnection默認(rèn)會(huì)緩存資源在內(nèi)存或者存儲(chǔ)中根據(jù)它所加載的HTTP Headers。你甚至可以手動(dòng)創(chuàng)建一個(gè)NSURLRequest然后使它只加載緩存的值。
下面是一個(gè)可用的代碼段,你可以可以用它去為一個(gè)基本不會(huì)改變的圖片創(chuàng)建一個(gè)NSURLRequest并緩存它:
+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // this will make sure the request always returns the cached image
request.HTTPShouldHandleCookies = NO;
request.HTTPShouldUsePipelining = YES;
[request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
return request;
}
注意你可以通過(guò) NSURLConnection 獲取一個(gè)URL request, AFNetworking也一樣的。這樣你就不必為采用這條tip而改變所有的networking代碼了。
如果想了解更多關(guān)于HTTP caching, NSURLCache, NSURLConnection的相關(guān)知識(shí),可以讀下這篇文章()
如果你需要緩存其它不是HTTP Request的東西,你可以用NSCache。
NSCache和NSDictionary類似,不同的是系統(tǒng)回收內(nèi)存的時(shí)候它會(huì)自動(dòng)刪掉它的內(nèi)容。 Mattt Thompson有一篇很棒的關(guān)于它的文章::http://nshipster.com/nscache/
如果你對(duì)HTTP感興趣可以讀下Google的這篇 best-practices document on HTTP caching。
11. 權(quán)衡渲染方法
在iOS中可以有很多方法做出漂亮的按鈕。你可以用整幅的圖片,可調(diào)大小的圖片,uozhe可以用CALayer, CoreGraphics甚至OpenGL來(lái)畫它們。
當(dāng)然每個(gè)不同的解決方法都有不同的復(fù)雜程度和相應(yīng)的性能。有一篇Apple UIKit team中的一員Andy Matuschak推薦過(guò)的很棒的關(guān)于graphic性能的帖子很值得一讀。
簡(jiǎn)單來(lái)說(shuō),就是用事先渲染好的圖片更快一些,因?yàn)槿绱艘粊?lái)iOS就免去了創(chuàng)建一個(gè)圖片再畫東西上去然后顯示在屏幕上的程序。問(wèn)題是你需要把所有你需要用到的圖片放到app的bundle里面,這樣就增加了體積 – 這就是使用可變大小的圖片更好的地方了: 你可以省去一些不必要的空間,也不需要再為不同的元素(比如按鈕)來(lái)做不同的圖。
然而,使用圖片也意味著你失去了使用代碼調(diào)整圖片的機(jī)動(dòng)性,你需要一遍又一遍不斷地重做他們,這樣就很浪費(fèi)時(shí)間了,而且你如果要做一個(gè)動(dòng)畫效果,雖然每幅圖只是一些細(xì)節(jié)的變化你就需要很多的圖片造成bundle大小的不斷增大。
總得來(lái)說(shuō),你需要權(quán)衡一下利弊,到底是要性能能還是要bundle保持合適的大小。
12. 處理內(nèi)存警告
一旦系統(tǒng)內(nèi)存過(guò)低,iOS會(huì)通知所有運(yùn)行中app。在官方文檔中是這樣記述:
如果你的app收到了內(nèi)存警告,它就需要盡可能釋放更多的內(nèi)存。最佳方式是移除對(duì)緩存,圖片object和其他一些可以重創(chuàng)建的objects的strong references.
幸運(yùn)的是,UIKit提供了幾種收集低內(nèi)存警告的方法:
在app delegate中使用applicationDidReceiveMemoryWarning:
的方法
在你的自定義UIViewController的子類(subclass)中覆蓋didReceiveMemoryWarning
注冊(cè)并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到這類通知,你就需要釋放任何不必要的內(nèi)存使用。
例如,UIViewController的默認(rèn)行為是移除一些不可見的view, 它的一些子類則可以補(bǔ)充這個(gè)方法,刪掉一些額外的數(shù)據(jù)結(jié)構(gòu)。一個(gè)有圖片緩存的app可以移除不在屏幕上顯示的圖片。
這樣對(duì)內(nèi)存警報(bào)的處理是很必要的,若不重視,你的app就可能被系統(tǒng)殺掉。
然而,當(dāng)你一定要確認(rèn)你所選擇的object是可以被重現(xiàn)創(chuàng)建的來(lái)釋放內(nèi)存。一定要在開發(fā)中用模擬器中的內(nèi)存提醒模擬去測(cè)試一下。
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它們,比如從JSON或者XML中解析數(shù)據(jù)。
想要避免使用這個(gè)對(duì)象的瓶頸你就需要重用他們,可以通過(guò)添加屬性到你的class里或者創(chuàng)建靜態(tài)變量來(lái)實(shí)現(xiàn)。
注意如果你要選擇第二種方法,對(duì)象會(huì)在你的app運(yùn)行時(shí)一直存在于內(nèi)存中,和單例(singleton)很相似。
下面的代碼說(shuō)明了使用一個(gè)屬性來(lái)延遲加載一個(gè)date formatter. 第一次調(diào)用時(shí)它會(huì)創(chuàng)建一個(gè)新的實(shí)例,以后的調(diào)用則將返回已經(jīng)創(chuàng)建的實(shí)例:
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
if (! _formatter) {
_formatter = [[NSDateFormatter alloc] init];
_formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; // twitter date format
}
return _formatter;
}
還需要注意的是,其實(shí)設(shè)置一個(gè)NSDateFormatter的速度差不多是和創(chuàng)建新的一樣慢的!所以如果你的app需要經(jīng)常進(jìn)行日期格式處理的話,你會(huì)從這個(gè)方法中得到不小的性能提升。
14. 使用Sprite Sheets
你是一個(gè)游戲開發(fā)者嗎,那么Sprite sheets一定是一個(gè)你的最好的朋友了。Sprite sheet可以讓渲染速度加快,甚至比標(biāo)準(zhǔn)的屏幕渲染方法節(jié)省內(nèi)存。
我們有兩個(gè)很好的關(guān)于Sprite的教程:
How To Use Animations and Sprite Sheets in Cocos2D
How to Create and Optimize Sprite Sheets in Cocos2D with Texture Packer and Pixel Formats
第二個(gè)教程涵蓋了可能在很大程度上影響你游戲性能的pixel格式的細(xì)節(jié)。
如果你對(duì)于spirte sheet還不是很熟悉,可以看下這兩個(gè)(youtube)視頻SpriteSheets – The Movie, Part 1 和 Part 2。視頻的作者是創(chuàng)建Sprite sheet很流行的工具之一Texture Packer的作者Andreas L?w。
除了使用Sprite sheets,其它寫在這里的建議當(dāng)然也可以用于游戲開發(fā)中。比如你需要很多的Sprite sheets,像敵人,導(dǎo)彈之類的動(dòng)作類必備元素,你可以重用這些sprites而不用每次都要重新創(chuàng)建。
15. 避免反復(fù)處理數(shù)據(jù)
許多應(yīng)用需要從服務(wù)器加載功能所需的常為JSON或者XML格式的數(shù)據(jù)。在服務(wù)器端和客戶端使用相同的數(shù)據(jù)結(jié)構(gòu)很重要。在內(nèi)存中操作數(shù)據(jù)使它們滿足你的數(shù)據(jù)結(jié)構(gòu)是開銷很大的。
比如你需要數(shù)據(jù)來(lái)展示一個(gè)table view,最好直接從服務(wù)器取array結(jié)構(gòu)的數(shù)據(jù)以避免額外的中間數(shù)據(jù)結(jié)構(gòu)改變。
類似的,如果需要從特定key中取數(shù)據(jù),那么就使用鍵值對(duì)的dictionary。
16. 選擇正確的數(shù)據(jù)格式
從app和網(wǎng)絡(luò)服務(wù)間傳輸數(shù)據(jù)有很多方案,最常見的就是JSON和XML。你需要選擇對(duì)你的app來(lái)說(shuō)最合適的一個(gè)。
解析JSON會(huì)比XML更快一些,JSON也通常更小更便于傳輸。從iOS5起有了官方內(nèi)建的JSON deserialization 就更加方便使用了。
但是XML也有XML的好處,比如使用SAX 來(lái)解析XML就像解析本地文件一樣,你不需像解析json一樣等到整個(gè)文檔下載完成才開始解析。當(dāng)你處理很大的數(shù)據(jù)的時(shí)候就會(huì)極大地減低內(nèi)存消耗和增加性能。
17. 正確設(shè)定背景圖片
在View里放背景圖片就像很多其它iOS編程一樣有很多方法:
使用UIColor的 colorWithPatternImage來(lái)設(shè)置背景色;
在view中添加一個(gè)UIImageView作為一個(gè)子View。
如果你使用全畫幅的背景圖,你就必須使用UIImageView因?yàn)閁IColor的colorWithPatternImage是用來(lái)創(chuàng)建小的重復(fù)的圖片作為背景的。這種情形下使用UIImageView可以節(jié)約不少的內(nèi)存:
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
如果你用小圖平鋪來(lái)創(chuàng)建背景,你就需要用UIColor的colorWithPatternImage來(lái)做了,它會(huì)更快地渲染也不會(huì)花費(fèi)很多內(nèi)存:
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
18. 減少使用Web特性
UIWebView很有用,用它來(lái)展示網(wǎng)頁(yè)內(nèi)容或者創(chuàng)建UIKit很難做到的動(dòng)畫效果是很簡(jiǎn)單的一件事。
但是你可能有注意到UIWebView并不像驅(qū)動(dòng)Safari的那么快。這是由于以JIT compilation 為特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要調(diào)整下你的HTML了。第一件要做的事就是盡可能移除不必要的javascript,避免使用過(guò)大的框架。能只用原生js就更好了。
另外,盡可能異步加載例如用戶行為統(tǒng)計(jì)script這種不影響頁(yè)面表達(dá)的javascript。
最后,永遠(yuǎn)要注意你使用的圖片,保證圖片的符合你使用的大小。使用Sprite sheet提高加載速度和節(jié)約內(nèi)存。
更多相關(guān)信息可以看下 WWDC 2012 session #601 – Optimizing Web Content in UIWebViews and Websites on iOS
19. 設(shè)定Shadow Path
如何在一個(gè)View或者一個(gè)layer上加一個(gè)shadow呢,QuartzCore框架是很多開發(fā)者的選擇:
#import <QuartzCore/QuartzCore.h>
// Somewhere later ...
UIView *view = [[UIView alloc] init];
// Setup the shadow ...
view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);
view.layer.shadowRadius = 5.0f;
view.layer.shadowOpacity = 0.6;
看起來(lái)很簡(jiǎn)單,對(duì)吧。
可是,壞消息是使用這個(gè)方法也有它的問(wèn)題… Core Animation不得不先在后臺(tái)得出你的圖形并加好陰影然后才渲染,這開銷是很大的。
使用shadowPath的話就避免了這個(gè)問(wèn)題:view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
使用shadow path的話iOS就不必每次都計(jì)算如何渲染,它使用一個(gè)預(yù)先計(jì)算好的路徑。但問(wèn)題是自己計(jì)算path的話可能在某些View中比較困難,且每當(dāng)view的frame變化的時(shí)候你都需要去update shadow path.
想了解更多可以看看Mark Pospesel的這篇。
20. 優(yōu)化Table View
Table view需要有很好的滾動(dòng)性能,不然用戶會(huì)在滾動(dòng)過(guò)程中發(fā)現(xiàn)動(dòng)畫的瑕疵。
為了保證table view平滑滾動(dòng),確保你采取了以下的措施:
正確使用reuseIdentifier來(lái)重用cells
盡量使所有的view opaque,包括cell自身
避免漸變,圖片縮放,后臺(tái)選人
緩存行高
如果cell內(nèi)現(xiàn)實(shí)的內(nèi)容來(lái)自web,使用異步加載,緩存請(qǐng)求結(jié)果
使用shadowPath來(lái)畫陰影
減少subviews的數(shù)量
盡量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后緩存結(jié)果
使用正確的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù)
使用rowHeight, sectionFooterHeight和 sectionHeaderHeight來(lái)設(shè)定固定的高,不要請(qǐng)求delegate
21. 選擇正確的數(shù)據(jù)存儲(chǔ)選項(xiàng)
當(dāng)存儲(chǔ)大塊數(shù)據(jù)時(shí)你會(huì)怎么做?
你有很多選擇,比如:
使用NSUerDefaults
使用XML, JSON, 或者 plist
使用NSCoding存檔
使用類似SQLite的本地SQL數(shù)據(jù)庫(kù)
使用 Core Data
NSUserDefaults的問(wèn)題是什么?雖然它很nice也很便捷,但是它只適用于小數(shù)據(jù),比如一些簡(jiǎn)單的布爾型的設(shè)置選項(xiàng),再大點(diǎn)你就要考慮其它方式了
XML這種結(jié)構(gòu)化檔案呢?總體來(lái)說(shuō),你需要讀取整個(gè)文件到內(nèi)存里去解析,這樣是很不經(jīng)濟(jì)的。使用SAX又是一個(gè)很麻煩的事情。
NSCoding?不幸的是,它也需要讀寫文件,所以也有以上問(wèn)題。
在這種應(yīng)用場(chǎng)景下,使用SQLite 或者 Core Data比較好。使用這些技術(shù)你用特定的查詢語(yǔ)句就能只加載你需要的對(duì)象。
在性能層面來(lái)講,SQLite和Core Data是很相似的。他們的不同在于具體使用方法。Core Data代表一個(gè)對(duì)象的graph model,但SQLite就是一個(gè)DBMS。Apple在一般情況下建議使用Core Data,但是如果你有理由不使用它,那么就去使用更加底層的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)這個(gè)庫(kù)來(lái)簡(jiǎn)化SQLite的操作,這樣你就不用花很多經(jīng)歷了解SQLite的C API了。
進(jìn)階性能提示
想要一些是你成為程序猿忍者的精英級(jí)的建議嗎?下面這些提示可以幫你把你的app優(yōu)化到極致!
22. 加速啟動(dòng)時(shí)間
快速打開app是很重要的,特別是用戶第一次打開它時(shí),對(duì)app來(lái)講,第一印象太太太重要了。
你能做的就是使它盡可能做更多的異步任務(wù),比如加載遠(yuǎn)端或者數(shù)據(jù)庫(kù)數(shù)據(jù),解析數(shù)據(jù)。
還是那句話,避免過(guò)于龐大的XIB,因?yàn)樗麄兪窃谥骶€程上加載的。所以盡量使用沒有這個(gè)問(wèn)題的Storyboards吧!
注意,用Xcode debug時(shí)watchdog并不運(yùn)行,一定要把設(shè)備從Xcode斷開來(lái)測(cè)試啟動(dòng)速度
23. 使用Autorelease Pool
NSAutoreleasePool
負(fù)責(zé)釋放block中的autoreleased objects。一般情況下它會(huì)自動(dòng)被UIKit調(diào)用。但是有些狀況下你也需要手動(dòng)去創(chuàng)建它。
假如你創(chuàng)建很多臨時(shí)對(duì)象,你會(huì)發(fā)現(xiàn)內(nèi)存一直在減少直到這些對(duì)象被release的時(shí)候。這是因?yàn)橹挥挟?dāng)UIKit用光了autorelease pool的時(shí)候memory才會(huì)被釋放。
好消息是你可以在你自己的@autoreleasepool里創(chuàng)建臨時(shí)的對(duì)象來(lái)避免這個(gè)行為:
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
這段代碼在每次遍歷后釋放所有autorelease對(duì)象
更多關(guān)于NSAutoreleasePool請(qǐng)參考官方文檔。
24. 選擇是否緩存圖片
常見的從bundle中加載圖片的方式有兩種,一個(gè)是用imageNamed,二是用imageWithContentsOfFile,第一種比較常見一點(diǎn)。
既然有兩種類似的方法來(lái)實(shí)現(xiàn)相同的目的,那么他們之間的差別是什么呢?
imageNamed的優(yōu)點(diǎn)是當(dāng)加載時(shí)會(huì)緩存圖片。imageNamed
的文檔中這么說(shuō):這個(gè)方法用一個(gè)指定的名字在系統(tǒng)緩存中查找并返回一個(gè)圖片對(duì)象如果它存在的話。如果緩存中沒有找到相應(yīng)的圖片,這個(gè)方法從指定的文檔中加載然后緩存并返回這個(gè)對(duì)象。
相反的,imageWithContentsOfFile僅加載圖片。
下面的代碼說(shuō)明了這兩種方法的用法:
UIImage *img = [UIImage imageNamed:@"myImage"]; // caching
// or
UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching
那么我們應(yīng)該如何選擇呢?
如果你要加載一個(gè)大圖片而且是一次性使用,那么就沒必要緩存這個(gè)圖片,用imageWithContentsOfFile
足矣,這樣不會(huì)浪費(fèi)內(nèi)存來(lái)緩存它。
然而,在圖片反復(fù)重用的情況下imageNamed
是一個(gè)好得多的選擇。
25. 避免日期格式轉(zhuǎn)換
如果你要用NSDateFormatter
來(lái)處理很多日期格式,應(yīng)該小心以待。就像先前提到的,任何時(shí)候重用NSDateFormatters
都是一個(gè)好的實(shí)踐。
然而,如果你需要更多速度,那么直接用C是一個(gè)好的方案。Sam Soffes有一個(gè)不錯(cuò)的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用來(lái)解析ISO-8601日期字符串的代碼,簡(jiǎn)單重寫一下就可以拿來(lái)用了。
嗯,直接用C來(lái)搞,看起來(lái)不錯(cuò)了,但是你相信嗎,我們還有更好的方案!
如果你可以控制你所處理的日期格式,盡量選擇Unix時(shí)間戳。你可以方便地從時(shí)間戳轉(zhuǎn)換到NSDate:
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return [NSDate dateWithTimeIntervalSince1970:timestamp];
}
這樣會(huì)比用C來(lái)解析日期字符串還快!
需要注意的是,許多web API會(huì)以微秒的形式返回時(shí)間戳,因?yàn)檫@種格式在javascript中更方便使用。記住用dateFromUnixTimestamp
之前除以1000就好了。
更多閱讀
下列這些WWDC視頻強(qiáng)烈推薦給想要提高app性能的開發(fā)者。你首先需要保證你有使你的Apple ID注冊(cè)為一個(gè)開發(fā)者身份才能看在這里看WWDC 2012的視頻。
406: Adopting Automatic Reference Counting
238: iOS App Performance: Graphics and Animations
242: iOS App Performance: Memory
235: iOS App Performance: Responsiveness
409: Learning Instruments
706: Networking Best Practices
514: OpenGL ES Tools and Techniques
506: Optimizing 2D Graphics and Animation Performance
601: Optimizing Web Content in UIWebViews and Websites on iOS
225: Up and Running: Making a Great Impression with Every Launch
一些01年的WWDC視頻也很有價(jià)值:
308: Blocks and Grand Central Dispatch in Practice
323: Introducing Automatic Reference Counting
312: iOS Performance and Power Optimization with Instruments
105: Polishing Your App: Tips and tricks to improve the responsiveness and performance
121: Understanding UIKit Rendering
其它一些值得看的視頻,大部分來(lái)自iOS 5 Tech Talks:
Your iOS App Performance Hitlist
Optimizing App Performance with Instruments
Understanding iOS View Compositing
基于《Your iOS App Performance Hitlist》這個(gè)Michael Jurewitz的視頻,Ole Begemann寫了一篇文字總結(jié)的文章。
Apple提供了一個(gè)非常有用的叫做“Performance Tuning | 性能調(diào)優(yōu)”的資源。