總結(jié)一下平時優(yōu)化app性能用到的方法
1.合理的分配線程,不要濫用多線程。開辟線程,以及線程之間的上下文切換是消耗內(nèi)存的,主線程會占用1MB的內(nèi)存空間,子線程會占用512KB的內(nèi)存空間。但是注意,凡事涉及到UI操作的,一定要放回主線程做,不然會引發(fā)崩潰。
2.預(yù)加載以及延遲加載。預(yù)處理,是將初次顯示需要耗費大量線程時間的操作,提前放到后臺線程進行計算,再將結(jié)果數(shù)據(jù)拿來顯示。延時加載,是指首先加載當(dāng)前必須的可視內(nèi)容,在稍后一段時間內(nèi)或特定事件時,再觸發(fā)其他內(nèi)容的加載。這種方式可以很有效的提升界面繪制速度,使體驗更加流暢。(UITableView 就是最典型的例子)
3.緩存。也就是一種空間換時間的做法。使用緩存,一定要注意下面幾點:
- 并發(fā)訪問 cache 時,數(shù)據(jù)一致性問題。
- cache 線程安全問題,防止一邊修改一邊遍歷的 crash。
- cache 查找時性能問題。
- cache 的釋放與重建,避免占用空間無限擴大,同時釋放的粒度也要依實際需求而定。
4.在不同的業(yè)務(wù)場景中,選擇合適的API。比如:
- 了解 imageNamed: 與 imageWithContentsOfFile:的差異(imageNamed: 適用于會重復(fù)加載的小圖片,因為系統(tǒng)會自動緩存加載的圖片,imageWithContentsOfFile: 僅加載圖片)
- 緩存 NSDateFormatter 的結(jié)果。
- 尋找 (NSDate *)dateFromString:(NSString )string 的替換品。
- 不要隨意使用 NSLog().
5.將views設(shè)置為不透明,也就是設(shè)置layer的opaque屬性為YES。這樣做可以避免圖層混合時引發(fā)的渲染耗時。此外還有以下一些技巧:
- 確保控件的opaque屬性設(shè)置為true,確保backgroundColor和父視圖顏色一致且不透明
- 如無特殊需要,不要設(shè)置低于1的alpha值
= 確保UIImage沒有alpha通道
6.imageview設(shè)置圖片的時候,一定要使用跟imageview尺寸一致的。避免在imageview中做縮放操作。
7.處理內(nèi)存警告。UIKit中提供了幾種可以監(jiān)測到內(nèi)存警告的方法:
- 在app delegate中使用applicationDidReceiveMemoryWarning: 的方法
- 在你的自定義UIViewController的子類(subclass)中覆蓋didReceiveMemoryWarning
- 注冊并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知
當(dāng)收到通知的時候,需要將可以重用的重量級model移除出內(nèi)存。以及移除一些當(dāng)前頁面不可 見的頁面元素。
8.避免日期格式的轉(zhuǎn)換。如果你要用NSDateFormatter來處理很多日期格式,應(yīng)該小心以待。就像先前提到的,任何時候重用NSDateFormatters都是一個好的實踐.
9.避免可能會引發(fā)離屏渲染的操作:
- 重寫drawRect方法
- 有mask或者是陰影(layer.masksToBounds, layer.shadow*),模糊效果也是一種mask
- layer.shouldRasterize = true
- 設(shè)置cornerRadius本身并不會導(dǎo)致離屏渲染,但很多時候它還需要配合layer.masksToBounds = true使用。根據(jù)之前的總結(jié),設(shè)置masksToBounds會導(dǎo)致離屏渲染。解決方案是盡可能在滑動時避免設(shè)置圓角,如果必須設(shè)置圓角,可以使用光柵化技術(shù)將圓角緩存起來:
// 設(shè)置圓角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
10.IMP Caching
在 Objective-C 的消息分發(fā)過程中,所有 [receiver message:…] 形式的方法調(diào)用最終都會被編譯器轉(zhuǎn)化為 obj_msgSend(recevier, @selector(message), …) 的形式調(diào)用。在運行時,Runtime 會去根據(jù) selector 到對應(yīng)方法列表中查找相應(yīng)的 IMP 來調(diào)用,這是一個動態(tài)綁定的過程。為了加速消息的處理,Runtime 系統(tǒng)會緩存使用過的 selector 對應(yīng)的 IMP 以便后面直接調(diào)用,這就是 IMP Caching。通過 IMP Caching 的方式,Rumtime 能夠跳過 obj_msgSend 的過程直接調(diào)用方法的實現(xiàn),從而提高方法調(diào)用效率。下面對應(yīng)的是方法的實現(xiàn)
#define LOOP 1000000
#define START { clock_t start, end; start = clock();
#define END end = clock(); printf("Cost: %f ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000); }
- (NSDateFormatter *)dateFormatter:(NSString *)format {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:format];
return dateFormatter;
}
- (void)testIMPCaching {
[self normalCall];
[self impCachingCall];
}
- (void)normalCall {
START
for (int32_t i = 0; i < LOOP; i++) {
NSDateFormatter *d =[self dateFormatter:@"yyyy-MM-dd a HH:mm:ss EEEE"];
d = nil;
}
END
// Print: Cost: 1328.845000 ms
}
- (void)impCachingCall {
START
SEL sel = @selector(dateFormatter:);
NSDateFormatter *(*imp)(id, SEL, id) = (NSDateFormatter *(*)(id, SEL, id)) [self methodForSelector:sel];
for (int32_t i = 0; i < LOOP; i++) {
NSDateFormatter *d = imp(self, sel, @"yyyy-MM-dd a HH:mm:ss EEEE");
d = nil;
}
END
// Print: Cost: 1130.200000 ms
}
可見相差并不太大,在 impCachingCall 中是直接手動做 IMP Caching 來跳過 obj_msgSend 調(diào)用方法的實現(xiàn)。normalCall 則是在 Runtime 通過系統(tǒng)自己的 IMP Caching 機制來運行。通常我們不需要做 IMP Caching,但是如果有時候哪怕一點點的速度提升也是你需要的,你可以考慮考慮這點。
CPU 資源消耗原因和解決方案
1.對象創(chuàng)建。
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應(yīng)觸摸事件的控件,用 CALayer 顯示會更加合適。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建,但可惜的是包含有 CALayer 的控件,都只能在主線程創(chuàng)建和操作。通過 Storyboard 創(chuàng)建視圖對象時,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術(shù)選擇。
盡量推遲對象創(chuàng)建的時間,并把對象的創(chuàng)建分散到多個任務(wù)中去。盡管這實現(xiàn)起來比較麻煩,并且?guī)淼膬?yōu)勢并不多,但如果有能力做,還是要盡量嘗試一下。如果對象可以復(fù)用,并且復(fù)用的代價比釋放、創(chuàng)建新對象要小,那么這類對象應(yīng)當(dāng)盡量放到一個緩存池里復(fù)用。
2.對象調(diào)整
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性,當(dāng)調(diào)用屬性方法時,它內(nèi)部是通過運行時 resolveInstanceMethod 為對象臨時添加一個方法,并把對應(yīng)屬性值保存到內(nèi)部的一個 Dictionary 里,同時還會通知 delegate、創(chuàng)建動畫等等,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調(diào)整時,消耗的資源要遠大于一般的屬性。對此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改。
當(dāng)視圖層次調(diào)整時,UIView、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時,應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
3.對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當(dāng)容器類持有大量對象時,其銷毀時的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了。
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
- 布局計算
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存,那么這個地方基本就不會產(chǎn)生性能問題了。
不論通過何種技術(shù)對視圖進行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調(diào)整上。上面也說過,對這些屬性的調(diào)整非常消耗資源,所以盡量提前計算好布局,在需要時一次性調(diào)整好對應(yīng)屬性,而不要多次、頻繁的計算和調(diào)整這些屬性。
5.AutoLayout
Autolayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對于復(fù)雜視圖來說常常會產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升。具體數(shù)據(jù)可以看這個文章:http://pilky.me/36/。 如果你不想手動調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
6.文本計算
如果一個界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計算會占用很大一部分資源,并且不可避免。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內(nèi)部的實現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本。盡管這兩個方法性能不錯,但仍舊需要放到后臺線程進行以避免阻塞主線程。
如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對象,然后自己計算了,并且 CoreText 對象還能保留以供稍后繪制使用。
7.文本渲染
屏幕上能看到的所有文本內(nèi)容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進行的,當(dāng)顯示大量文本時,CPU 的壓力會非常大。對此解決方案只有一個,那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實現(xiàn)起來非常麻煩,但其帶來的優(yōu)勢也非常大,CoreText 對象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計算(調(diào)整 UILabel 大小時算一遍、UILabel 繪制時內(nèi)部再算一遍);CoreText 對象占用內(nèi)存較少,可以緩存下來以備稍后多次渲染。
8.圖片編碼
當(dāng)你用 UIImage 或 CGImageSource 的那幾個方法創(chuàng)建圖片時,圖片數(shù)據(jù)并不會立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會得到解碼。這一步是發(fā)生在主線程的,并且不可避免。如果想要繞開這個機制,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見的網(wǎng)絡(luò)圖片庫都自帶這個功能。
9.圖形繪制
圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創(chuàng)建圖片并顯示這樣一個過程。這個最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進行。一個簡單異步繪制的過程大致如下(實際情況會比這個復(fù)雜得多,但原理基本一致):
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
GPU 資源消耗原因以及解決方案
1.紋理渲染
所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調(diào)整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當(dāng)在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
當(dāng)圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預(yù)處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096x4096,更詳細(xì)的資料可以看這里:iosres.com。所以,盡量不要讓圖片和視圖的大小超過這個值。
2.視圖的混合 (Composing)
當(dāng)多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結(jié)構(gòu)過于復(fù)雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無用的 Alpha 通道合成。當(dāng)然,這也可以用上面的方法,把多個視圖預(yù)先渲染為一張圖片來顯示。
3.圖形的生成
CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中。當(dāng)一個列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動時,可以觀察到 GPU 資源已經(jīng)占滿,而 CPU 資源消耗很少。這時界面仍然能正常滑動,但平均幀數(shù)會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉(zhuǎn)嫁到 CPU 上去。對于只需要圓角的某些場合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。
屬性字符串 盡量少用 因為會造成CPU負(fù)荷重,掉幀
https://github.com/vedon/iOS-tech/blob/master/UITableViewOpt2/UITableViewOpt2.md
盡量像素不要使用小數(shù)點,這樣會造成像素不對齊,增加GPU負(fù)擔(dān),掉幀
出現(xiàn)像素不對齊的情況,會導(dǎo)致在GPU渲染時,對沒對齊的邊緣,需要進行插值計算,這個插值計算的過程會有性能損耗。