iOS-性能優化之CoreAnimation使用

目錄
序言

iOS為了提高滑動的流暢感,特意在滑動的時候將runloop模式切換到UITrackingRunLoopMode,在這個過程中專心做跟滑動相關的工作。但是還是會遇到滑動不流暢的情況。接下來我們就需要借助CoreAnimation來找出造成不流暢的元兇。

一 CoreAnimation 的介紹

分析圖像動畫性能主要用的是Core Animation這個組件,先簡單介紹一下里面一些經常用到的選項:

模擬器 -> Debug

image.png
  • Color Blended layers

標示混合的圖層會為紅色,不透明的圖層為綠色,通常我們希望綠色的區域越多越好。

  • Color Hits Green and Misses Red (已經找不到了)

假如我們設置viewlayer的shouldRasterize為YES,那些成功被緩存的layer會標注為綠色,反之為紅色。

  • Color copied images

標示那些被Core Animation拷貝的圖片。這主要是因為該圖片的色彩格式不能被GPU直接處理,需要在CPU這邊做轉換,假如在主線層做這個操作對性能會有一定的影響。

  • Color misaligned images

被縮放的圖片會被標記為黃色,像素不對齊則會標注為紫色。

  • Color offscreen-rendered yellow

標示哪些layer需要做離屏渲染(offscreen-render)。

二 遇到性能瓶頸的解決出路

下面這張圖摘自 WWDC2014 大會視頻 Advanced Graphics and Animations for iOS Apps

Performance Investigation Mindset.png

解釋說明如下:

是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的離屏渲染操作?
是否有太多的圖層混合操作?
是否有奇怪的圖片格式或者尺寸?
是否涉及到昂貴的view或者效果?
view的層次結構是否合理?

當你碰到性能問題的時候,可以從這幾方面入手

三 離屏渲染
3.1 CPU 的離屏渲染

通常在以下操作時容易造成離屏渲染

  • drawRect

如果沒有自定義繪制的任務就不要在子類中寫一個空的drawRect方法,因為只要實現了該方法,就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸等于視圖大小乘以 contentsScale的值,造成資源浪費

  • 使用Core Graphics

上面的情況使用的就是CPU離屏渲染,首先分配一塊內存,然后進行渲染操作生成一份bitmap位圖,整個渲染過程會在你的應用中同步的進行,接著再將位圖打包發送到iOS里一個單獨的進程--render server,理想情況下,render server將內容交給GPU直接顯示到屏幕上。

3.2 GPU 的離屏渲染

使用GPU在當前屏幕緩沖區以外開辟一個新的緩沖區進行繪制,通常發生的情況有:

  • 設置 cornerRadius + mask
  • shadows
  • edge antialiasing
  • 設置layer.shouldRasterize = YES
離屏渲染.png

離屏渲染(offscreen-render)對性能到底有什么影響?

通常大家說的離屏渲染指的是GPU這塊(當然CPU這塊也會有影響,也需要消耗一定的資源),比如修改了layer
的陰影或者圓角,GPU需要做額外的渲染操作。通常GPU在做渲染的時候是很快的,但是涉及到offscreen-
render的時候情況就可能有些不同,因為需要額外開辟一個新的緩沖區進行渲染,然后繪制到當前屏幕的過程
需要做onscreen跟offscreen上下文之間的切換,這個過程的消耗會比較昂貴,涉及到OpenGL的pipeline跟
barrier,而且offscreen-render在每一幀都會涉及到,因此處理不當肯定會對性能產生一定的影響,所以可以的
話盡量減少offscreen-render的圖層。

查看哪些圖層需要離屏渲染可以用Instruments的Core Animation工具進行檢測,Color Offscreen-Rendered Yellow選項會將對應的圖層標記為黃色。

四 造成離屏渲染原因分析
4.1 Blending

假如最上層的view是不透明的,那直接使用這個view的對應顏色之就可以,但如果view是透明的,在計算像素的顏色值時就需要計算它下面圖層,透明的視圖越多,計算量就越大,因此也會對圖形的性能產生一定的影響,所以可以的話也盡量減少透明圖層的數目。

4.2 圓角 cornerRadius + masksToBounds
_iconImgView.layer.cornerRadius = 22;
_iconImgView.layer.masksToBounds = YES;
image.png

我們可以通過使用 繪制一個圓形圖片覆蓋或者使用 Quartz2D來實現圓角。
具體的可以參考文章 iOS-裁剪圓角方法匯總(五種)

4.3 shadowPath

設置陰影效果可以通過以下代碼實現

// 設置陰影
CALayer *imageViewLayer = iconImgV.layer;
imageViewLayer.shadowColor = [[UIColor blackColor] CGColor];
imageViewLayer.shadowOpacity = 1.0; //此參數默認為0,即陰影不顯示
imageViewLayer.shadowRadius = 2.0; //給陰影加上圓角,對性能無明顯影響
imageViewLayer.shadowOffset = CGSizeMake(5, 5);

但是當你滑動的時候發現會導致離屏渲染。

一個簡單的不需要離屏渲染的方法就是制定陰影的路徑,也就是設置layer的shadowPath屬性

//設定路徑:與視圖的邊界相同
UIBezierPath *path = [UIBezierPath bezierPathWithRect:iconImgV.bounds];
imageViewLayer.shadowPath = path.CGPath;//路徑默認為 nil
4.4 Rasterization

對于圓角這種類似導致的性能問題,最簡單的就是在列表中不要使用圓角,假如要通過cornerRadius + masksToBounds方式裁剪圓角的話,一種最快提升性能的方式就是設置layer的shouldRasterize為YES。

除了 GroupOpacityEdgeAntialiasing,其他效果觸發的離屏渲染都會對性能產生嚴重影響,離屏渲染真的是一無是處嗎?不,離屏渲染本來是個優化設計。如何物盡其用?答案是:Rasterization

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = cell.layer.contentsScale

shouldRasterize = false時,離屏渲染的黃色特征僅限于上述自動觸發離屏渲染的效果的部分,shouldRasterize = true后該部分和開啟了該屬性的 layer 整體(在這里就是 cell 整體)都有黃色特征,所以開啟 Rasterization 是手動啟動了離屏渲染。

從前面來看,離屏渲染會給 GPU 帶來沉重的負擔,強制啟動豈不是更糟?開啟 Rasterization 后,GPU 只合成一次內容,然后復用合成的結果;合成的內容超過 100ms 沒有使用會從緩存里移除,在更新內容時還會產生更多的離屏渲染。對于內容不發生變化的視圖,原本拖后腿的離屏渲染就成為了助力;如果視圖內容是動態變化的,使用這個方案有可能讓性能變得更糟。

Core Animation Instruments 有個Color Hits Green and Misses Red的選項,開啟 Rasterization 后開啟這個選項,屏幕上綠色的部分表示有渲染緩存可用,紅色的部分表示無渲染緩存可用。

使用該屬性注意事項:

  • 適用于內容基本不變的圖層

假如圖層的內容經常變化,比如cell里面有涉及到動畫之類的,那么緩存的內容就無效了,GPU需要重新創建緩存區,導致離屏渲染,這又涉及到OpenGL的上下文環境切換,反而降低性能。

  • 不要過度使用
    緩存區的大小被設置為屏幕大小的2.5倍,假如過分使用同樣會導致大量的離屏渲染。

  • 如果緩存的內容超過100ms沒有被使用則會被回收。

五 實戰技巧
5.1 裁剪圓角

對于圓角可以使用一張中間圓形透明的圖覆蓋在上面,雖然這會引入blending操作,但是大部分情況下性能會比離屏渲染好。
參考文章 iOS-裁剪圓角方法匯總(五種)

5.2 view 層次結構

讓你的view層次結構平坦一些,因為OpenGL在渲染layer的時候,在碰到有子層級layer的時候可能需要停下來把兩者合成到一個buffer里再接著渲染。(When the OpenGL renderer goes to draw each layer, it may have to stop for some subhierarchies and composite them into a single buffer).

5.3 延遲加載圖片

有時候在邊滾動邊設置圖片的時候可能會有一定的影響,因此可以在滾動的時候imageview不執行setimage的操作,滾動停止的時候才加載圖片,由于滾動的時候NSRunloop是處于UITrackingRunLoopMode模式下,可以采用如下的方式,將設置圖片放到NSDefaultRunLoopMode模式下才進行:

// 延遲加載圖片
__block UIImage *downloadedImage = nil;
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:obj] options:SDWebImageDownloaderScaleDownLargeImages progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
    downloadedImage = image;
}];
[imgView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
5.4 * 圖片加載的極限優化方式:FastImageCache

本文會持續更新,并且進行完善,配備相應的代碼例子,敬請期待。


本文參考
iOS app性能優化的那些事(二)
iOS離屏渲染優化


  • 如有錯誤,歡迎指正,多多點贊,打賞更佳,您的支持是我寫作的動力。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容