iOS之性能優化

要提一下,“過早的優化是萬惡之源”,在需求未定,性能問題不明顯時,沒必要嘗試做優化,而要儘量正確的實現功能。做性能優化時,也最好是走修改代碼 -> Profile -> 修改代碼這樣一個流程,優先解決最值得優化的地方。

入門級(這是些你一定會經常用在你app開發中的建議)

- 在正確的地方使用reuseIdentifier

一個開發中常見的錯誤就是沒有給UITableViewCells, UICollectionViewCells,甚至UITableViewHeaderFooterViews設正確的reuseIdentifier。爲了性能最優化,table view用 tableView:cellForRowAtIndexPath: 爲rows分配cells的時候,它的數據應該重用自UITableViewCell。一個table view維持一個隊列的數據可重用的UITableViewCell對象。 不使用reuseIdentifier的話,每顯示一行table view就不得不設置全新的cell。這對性能的影響可是相當大的,尤其會使app的滾動體驗大打折扣。
自iOS6起,除了UICollectionView的cells和補充views,你也應該在header和footer views中使用reuseIdentifiers。 想要使用reuseIdentifiers的話,在一個table view中添加一個新的cell時在data source object中添 加這個方法:

static NSString *CellIdentifier = @"Cell"; 
UITableViewCell  * cell  =  [tableView  dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];  

這個方法把那些已經存在的cell從隊列中排除,或者在必要時使用先前註冊的nib或者class創造新的cell。如果沒有可重用的cell,你也沒有註冊一個class或者nib的話,這個方法返回nil。

- 儘可能使Views透明

UIView的alpha、hidden和opaque屬性之間的關係和區別;
(1)、alpha
液晶顯示器是由一個個的像素點組成的,每個像素點都可以顯示一個由RGBA顏色空間組成的一種色值。其中的A就表示透明度alpha,UIView中alpha是一個浮點值,取值範圍0~1.0,表示從完全透明到完全不透明。
當把alpha的值設置成0以後:
1、當前的UIView和subview都會被隱藏,而不管subview的alpha值爲多少。
2、當前UIView會從響應者鏈中移除,而響應者鏈中的下一個會成爲第一響應者
alpha的默認值是1.0。另外,更改alpha值時,默認是有動畫效果的,這是因爲圖層在Cocoa中是由Core Animation中CALayer表示的,該動畫效果是CALayer的隱含動畫。當然也有辦法禁用此動畫效果,在這就不多述了,感興趣的同學可以繼續關注後續的博客。
(2)、hidden
該屬性爲BOOL值,用來表示UIView是否隱藏,默認值是NO。
當值設爲YES時:
1、當前的UIView和subview都會被隱藏,而不管subview的hidden值爲多少。
2、當前UIView會從響應者鏈中移除,而響應者鏈中的下一個會成爲第一響應者
總之,同alpha爲0時的顯示效果相同。具體兩者之間有什麼區別就不清楚了,如果有知道的還望不吝賜教!
(3)、opaque
該屬性爲BOOL值,UIView的默認值是YES,但UIButton等子類的默認值都是NO。opaque表示當前UIView是否不透明,不過搞笑的是事實上它卻決定不了當前UIView是不是不透明,比如你將opaque設爲NO,該UIView照樣是可見的(上文說過,是否可見是由alpha或hidden屬性決定的),照理說爲NO就表示透明,那就應該是不可見的呀?前面講過,顯示器中的每個像素點都可以顯示一個由RGBA顏色空間組成的色值,比如有紅色(上層的紅色圖層)和綠色(下層的綠色圖層)兩個交叉的圖層色塊,對於沒有交叉的部分,即純紅色和綠色部分來說,對應位置的像素點只需要簡單的顯示紅或綠,對應的RGBA爲(1,0,0,1)和(0,1,0,1)就行了,負責圖形顯示的GPU需要很小的計算量就可以確定像素點對應的顯示內容。
問題是紅色和綠色還有相交的一塊,其相交的顏色爲黃色。這裏的黃色是怎麼來的呢?原來,GPU會通過圖層一和圖層二的顏色進行圖層混合,計算出混合部分的顏色,最理想情況的計算公式如下:
R = S + D * ( 1 – Sa )
其中,R表示混合結果的顏色,S是源顏色(位於上層的紅色圖層一),D是目標顏色(位於下層的綠色圖層二),Sa是源顏色的alpha值,即透明度。公式中所有的S和D顏色都假定已經預先乘以了他們的透明度。
知道圖層混合的基本原理以後,再回到正題說說opaque屬性的作用。當UIView的opaque屬性被設爲YES以後,按照上面的公式,也就是Sa的值爲1,這個時候公式就變成了:
R = S
即不管D爲什麼,結果都一樣。因此GPU將不會做任何的計算合成,不需要考慮它下方的任何東西(因爲都被它遮擋住了),而是簡單從這個層拷貝。這節省了GPU相當大的工作量。由此看來,opaque屬性的真實用處是給繪圖系統提供一個性能優化開關!
按照前面的邏輯,當opaque屬性被設爲YES時,GPU就不會再利用圖層顏色合成公式去合成真正的色值。因此,如果opaque被設置成YES,而對應UIView的alpha屬性不爲1.0的時候,就會有不可預料的情況發生,這一點蘋果在官方文檔中有明確的說明:
An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content,the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.
大家切記!!!!
(4)、最後
當把UIView的alpha屬性設成0,或者把hidden設成YES的時候,當前UIView和它所包含的子UIView都會變成不可見,同時也不會再響應event事件。注意這裏是或的關係,即只要設置了其中的一個都會有此效果,而不管另外一個屬性的值是什麼。

- 避免龐大的XIB

iOS5中加入的Storyboards(分鏡)正在快速取代XIB。然而XIB在一些場景中仍然很有用。比如你的app需要適應iOS5之前的設備,或者你有一個自定義的可重用的view,你就不可避免地要用到他們。 如果你不得不XIB的話,使他們儘量簡單。嘗試爲每個Controller配置一個單獨的XIB,儘可能把一個View Controller的view層次結構分散到單獨的XIB中去。 需要注意的是,當你加載一個XIB的時候所有內容都被放在了內存裏,包括任何圖片。如果有一個不會即刻用到的view,你這就是在浪費寶貴的內存資源了。Storyboards就是另一碼事兒了,storyboard僅在需要時實例化一個view controller. 當家在XIB是,所有圖片都被chache,如果你在做OS X開發的話,聲音文件也是。Apple在相關文檔中的記述是:
當你加載一個引用了圖片或者聲音資源的nib時,nib加載代碼會把圖片和聲音文件寫進內存。在OS X中,圖片和聲音資源緩存在named cache中以便將來用到時獲取。在iOS中,僅圖片資源會被存進named caches。取決於你所在的平臺,使用NSImage 或UIImage 的imageNamed:方法來獲取圖片資源。

- 不要阻塞主線程

永遠不要使主線程承擔過多。因爲UIKit在主線程上做所有工作,渲染,管理觸摸反應,迴應輸入等都需要在它上面完成。
一直使用主線程的風險就是如果你的代碼真的block了主線程,你的app會失去反應。這。。。正是在App Store中拿到一顆星的捷徑大部分阻礙主進程的情形是你的app在做一些牽涉到讀寫外部資源的I/O操作,比如存儲或者網絡。你可以使用NSURLConnection異步地做網絡操作: + (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler 或者使用像AFNetworking這樣的框架來異步地做這些操作。如果你需要做其它類型的需要耗費巨大資源的操作(比如時間敏感的計算或者存儲讀寫)那就用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

    }); 
}); 

發現代碼中有一個嵌套的dispatch_async嗎?這是因爲任何UIKit相關的代碼需要在主線程上進行。

- 在Image Views中調整圖片大小

如果要在UIImageView中顯示一個來自bundle的圖片,你應保證圖片的大小和UIImageView的大小相同。在運行中縮放圖片是很耗費資源的,特別是UIImageView嵌套在UIScrollView中的情況下。如果圖片是從遠端服務加載的你不能控制圖片大小,比如在下載前調整到合適大小的話,你可以在下載完成後,最好是用background thread,縮放一次,然後在UIImageView中使用縮放後的圖片。

- 選擇正確的Collection

一些常見collection的總結:
Arrays: 有序的一組值。使用index來lookup很快,使用value lookup很慢,插入/刪除很慢。
Dictionaries: 存儲鍵值對,用鍵來查找比較快。
Sets: 無序的一組值。用值來查找很快,插入/刪除很快。

- 打開gzip壓縮

大量app依賴於遠端資源和第三方API,你可能會開發一個需要從遠端下載XML, JSON, HTML或者其它格式的app。 問題是我們的目標是移動設備,因此你就不能指望網絡狀況有多好。一個用戶現在還在edge網絡,下一分鐘可能就切換到了3G。不論什麼場景,你肯定不想讓你的用戶等太長時間。 減小文檔的一個方式就是在服務端和你的app中打開gzip。這對於文字這種能有更高壓縮率的數據來說會有更顯著的效用。 好消息是,iOS已經在NSURLConnection中默認支持了gzip壓縮,當然AFNetworking這些基於它的框架亦然。像Google App Engine這些雲服務提供者也已經支持了壓縮輸出。

中級性能提升

你確信你已經掌握了前述那些基礎級的優化方案了嗎?但實際情況是,有時一些解決方案並不像那些一樣明顯,它們往往嚴重依賴於你如何架構和書寫你的app。下面的這些建議就是針對這些場景的。

- 重用和延遲加載(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和內存消耗,對於那種嵌套了很多view在UIScrollView裏邊的app更是如此。這裏我們用到的技巧就是模仿UITableView和UICollectionView的操作: 不要一次創建所有的subview,而是當需要時才創建,當它們完成了使命,把他們放進一個可重用的隊列中。這樣的話你就只需要在滾動發生時創建你的views,避免了不划算的內存分配。創建views的能效問題也適用於你app的其它方面。想象一下一個用戶點擊一個按鈕的時候需要呈現一個view
的場景。有兩種實現方法:
- 創建並隱藏這個view當這個screen加載的時候,當需要時顯示它;
- 當需要時才創建並展示。
每個方案都有其優缺點。 用第一種方案的話因爲你需要一開始就創建一個view並保持它直到不再使用,這就會更加消耗內存。然而這也會使你的app操作更敏感因爲當用戶點擊按鈕的時候它只需要改變一下這個view的可見性。第二種方案則相反消耗更少內存,但是會在點擊按鈕的時候比第一種稍顯卡頓。

- Cache, Cache, 還是Cache!

一個極好的原則就是,緩存所需要的,也就是那些不大可能改變但是需要經常讀取的東西。 我們能緩存些什麼呢?一些選項是,遠端服務器的響應,圖片,甚至計算結果,比如UITableView的行高。 NSURLConnection默認會緩存資源在內存或者存儲中根據它所加載的HTTP Headers。你甚至可以手動創建一個NSURLRequest然後使它只加載緩存的值。下面是一個可用的代碼段,你可以可以用它去爲一個基本不會改變的圖片創建一個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; 
} 

注意你可以通過NSURLConnection 獲取一個URLrequest, AFNetworking也一樣的。這樣你就不必爲採用這條tip而改變所有的networking代碼了。如果你需要緩存其它不是HTTP Request的東西,你可以用NSCache。 NSCache和NSDictionary
類似,不同的是系統回收內存的時候它會自動刪掉它的內容。

- 權衡渲染方法

在iOS中可以有很多方法做出漂亮的按鈕。你可以用整幅的圖片,可調大小的圖片,可以用CALayer,CoreGraphics甚至OpenGL來畫它們。當然每個不同的解決方法都有不同的複雜程度和相應的性能。簡單來說,就是用事先渲染好的圖片更快一些,因爲如此一來iOS就免去了創建一個圖片再畫東西上去然後顯示在屏幕上的程序。問題是你需要把所有你需要用到的圖片放到app的bundle裏面,這樣就增加了體積–這就是使用可變大小的圖片更好的地方了: 你可以省去一些不必要的空間,也不需要再爲不同的元素(比如按鈕)來做不同的圖。
然而,使用圖片也意味着你失去了使用代碼調整圖片的機動性,你需要一遍又一遍不斷地重做他們,這樣就很浪費時間了,而且你如果要做一個動畫效果,雖然每幅圖只是一些細節的變化你就需要很多的圖片造成bundle大小的不斷增大。
總得來說,你需要權衡一下利弊,到底是要性能能還是要bundle保持合適的大小。

- 處理內存警告

一旦系統內存過低,iOS會通知所有運行中app。在官方文檔中是這樣記述: 如果你的app收到了內存警告,它就需要儘可能釋放更多的內存。最佳方式是移除對緩存,圖片object和其他一些可以重創建的objects的strong references. 幸運的是,UIKit
提供了幾種收集低內存警告的方法:
1.在app delegate中使用applicationDidReceiveMemoryWarning: 的方法
2.在你的自定義UIViewController的子類(subclass)中覆蓋didReceiveMemoryWarning
3.註冊並接收 UIApplicationDidReceiveMemoryWarningNotification 的通知一旦收到這類通知,你就需要釋放任何不必要的內存使用。
例如,UIViewController的默認行爲是移除一些不可見的view,它的一些子類則可以補充這個方法,刪掉一些額外的數據結構。一個有圖片緩存的app可以移除不在屏幕上顯示的圖片。這樣對內存警報的處理是很必要的,若不重視,你的app就可能被系統殺掉。然而,當你一定要確認你所選擇的object是可以被重現創建的來釋放內存。一定要在開發中用模擬器中的內存提醒模擬去測試一下。

- 重用大開銷對象

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它們,比如從JSON或者XML中解析數據。想要避免使用這個對象的瓶頸你就需要重用他們,可以通過添加屬性到你的class裏或者創建靜態變量來實現。 注意如果你要選擇第二種方法,對象會在你的app運行時一直存在於內存中,和單例(singleton)很相似。下面的代碼說明了使用一個屬性來延遲加載一個date formatter. 第一次調用時它會創建一個新的實例,以後的調用則將返回已經創建的實例:

@property (nonatomic, strong) NSDateFormatter *formatter; 

 - (NSDateFormatter *)formatter { 

if(! _formatter) {  

        _formatter = [[NSDateFormatter alloc] init];  
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
    }  

return _formatter; 
} 

還需要注意的是,其實設置一個NSDateFormatter的速度差不多是和創建新的一樣慢的!所以如果你的app需要經常進行日期格式處理的話,你會從這個方法中得到不小的性能提升。

- 選擇正確的數據格式

從app和網絡服務間傳輸數據有很多方案,最常見的就是JSON和XML。你需要選擇對你的app來說最合適的一個。 解析JSON會比XML更快一些,JSON也通常更小更便於傳輸。從iOS5起有了官方內建的JSON deserialization就更加方便使用了。但是XML也有XML的好處,比如使用SAX 來解析XML就像解析本地文件一樣,你不需像解析json一樣等到整個文檔下載完成纔開始解析。當你處理很大的數據的時候就會極大地減低內存消耗和增加性能。

- 正確設定背景圖片

在View裏放背景圖片就像很多其它iOS編程一樣有很多方法:
1.使用UIColor的 colorWithPatternImage來設置背景色;
2.在view中添加一個UIImageView作爲一個子View。
如果你使用全畫幅的背景圖,你可以用application bundle的頂層文件夾尋找由供應的名字的圖象。

UIImageView * imageView = [[UIImageView alloc] initWithImage: [UIImage imageNamed:@"icon.png"]];//會緩存圖片(理論上)放在內存裏作爲cache的,這樣圖片會佔用大量的內存。

UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:@"icon.png"]];// 不會緩存圖片,根據path去取圖片所有比較節省內存,速度相對之前慢。

如果用來創建小的重複的圖片作爲背景的。這種情形下使用UIImageView
可以節約不少的內存,如果你用小圖平鋪來創建背景,你就需要用UIColor的colorWithPatternImage來做了,它會更快地渲染也不會花費很多內存: self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];

- 減少使用Web特性

UIWebView很有用,用它來展示網頁內容或者創建UIKit很難做到的動畫效果是很簡單的一件事。但是你可能有注意到UIWebView並不像驅動Safari的那麼快。這是由於以JIT compilation爲特色的Webkit的Nitro Engine的限制。所以想要更高的性能你就要調整下你的HTML了。第一件要做的事就是儘可能移除不必要的javascript,避免使用過大的框架。能只用原生js就更好了。另外,儘可能異步加載例如用戶行爲統計script這種不影響頁面表達的javascript。 最後,永遠要注意你使用的圖片,保證圖片的符合你使用的大小。使用Sprite sheet提高加載速度和節約內存。更多相關信息:https://developer.apple.com/videos/wwdc2012/

- 優化Table View

Table view需要有很好的滾動性能,不然用戶會在滾動過程中發現動畫的瑕疵。

爲了保證table view平滑滾動,確保你採取了以下的措施:

正確使用`reuseIdentifier`來重用cells

儘量使所有的view opaque,包括cell自身

避免漸變,圖片縮放,後臺選人

緩存行高

如果cell內現實的內容來自web,使用異步加載,緩存請求結果

使用`shadowPath`來畫陰影

減少subviews的數量

儘量不適用`cellForRowAtIndexPath:`,如果你需要用到它,只用一次然後緩存結果

使用正確的數據結構來存儲數據

使用`rowHeight`, `sectionFooterHeight` 和 `sectionHeaderHeight`來設定固定的高,不要請求delegate

- 選擇正確的數據存儲選項

當存儲大塊數據時你會怎麼做?

你有很多選擇,比如:

使用`NSUerDefaults`

使用XML, JSON, 或者 plist

使用NSCoding存檔

使用類似SQLite的本地SQL數據庫

使用 Core Data

NSUserDefaults的問題是什麼?雖然它很nice也很便捷,但是它只適用於小數據,比如一些簡單的布爾型的設置選項,再大點你就要考慮其它方式了XML這種結構化檔案呢?總體來說,你需要讀取整個文件到內存裏去解析,這樣是很不經濟的。使用SAX又是一個很麻煩的事情。NSCoding?不幸的是,它也需要讀寫文件,所以也有以上問題。在這種應用場景下,使用SQLite 或者 Core Data比較好。使用這些技術你用特定的查詢語句就能只加載你需要的對象。在性能層面來講,SQLite和Core Data是很相似的。他們的不同在於具體使用方法。Core Data代表一個對象的graph model,但SQLite就是一個DBMS。Apple在一般情況下建議使用Core Data,但是如果你有理由不使用它,那麼就去使用更加底層的SQLite吧。

進階性能提示

- 加速啓動時間

快速打開app是很重要的,特別是用戶第一次打開它時,對app來講,第一印象太太太重要了。你能做的就是使它儘可能做更多的異步任務,比如加載遠端或者數據庫數據,解析數據。還是那句話,避免過於龐大的XIB,因爲他們是在主線程上加載的。所以儘量使用沒有這個問題的Storyboards吧!注意,用Xcode debug時watchdog並不運行,一定要把設備從Xcode斷開來測試啓動速度

- 使用Autorelease Pool

NSAutoreleasePool負責釋放block中的autoreleased objects。一般情況下它會自動被UIKit調用。但是有些狀況下你也需要手動去創建它。假如你創建很多臨時對象,你會發現內存一直在減少直到這些對象被release的時候。這是因爲只有當UIKit用光了autorelease pool的時候memory纔會被釋放。好消息是你可以在你自己的@autoreleasepool裏創建臨時的對象來避免這個行爲:這段代碼在每次遍歷後釋放所有autorelease對象

- 避免日期格式轉換

如果你要用NSDateFormatter來處理很多日期格式,應該小心以待。就像先前提到的,任何時候重用NSDateFormatters都是一個好的實踐。然而,如果你需要更多速度,那麼直接用C是一個好的方案。Sam Soffes有一個不錯的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)裏面有一些可以用來解析ISO-8601日期字符串的代碼,簡單重寫一下就可以拿來用了。嗯,直接用C來搞,看起來不錯了,但是你相信嗎,我們還有更好的方案!如果你可以控制你所處理的日期格式,儘量選擇Unix時間戳。你可以方便地從時間戳轉換到NSDate:

發佈了5 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章