圓角卡頓刨根問底

老文章遷移掘金ios

整篇文章其實說來講去,最後其實只是把卡頓的這個事情用最通俗最簡單最沒技術含量的方案實行了。但那麼多方案,爲何選擇一個方案,每一個方案都有優點,一樣也有弊端,不一樣的case,不一樣的場景,不可能一個方案萬金油適用,這個過程須要咱們刨根問底,去了解簡單的解決方案背後的爲何?性能優化

前言

tableView or collectionView的Cell使用中若是大量出現了view.layer.cornerRadius + ClipToBoundsormasksToBounds的設置,會形成滾動不流暢,滾動起來十分的卡頓。app

這一點相信不少iOS developer都不陌生,相關的搜索圓角卡頓圓角性能優化,都能看到不少文章,思路大體以下異步

  • 這樣切圓角會形成GPU的離屏渲染
  • 離屏渲染會消耗太多GPU資源,可是CPU卻沒有太多的佔用
  • GPU消耗太大拖慢了單幀繪製
  • 解決辦法,採用CPU預先繪製bitmap,交給GPU直接渲染

最普遍的辦法就是預先用CPU,構建圓角路徑貝塞爾曲線UIBezierPath,用原來的圖片填充進圓角路徑,得到自然的自帶圓角透明的bitmap數據UIImage,從而直接交給GPU進行普通渲染ide

有很多的blog,以及大量的demo,都驗證了這一點,但看完後不由有幾個疑問,因而產生了今天的刨根問底函數

  • 什麼纔是卡頓的根本緣由?
  • 離屏渲染是什麼?
  • GPU消耗交給CPU,CPU難道就沒消耗了麼?

每一種解決方案,都是在必定得特定情景下而產生的最優解,針對CPU預繪bitmap的方案,在至關多的使用場景下,是正確的沒有任何問題的。性能

可是這裏要講一個真實的case,若是這個方案都沒法解決問題,依然仍是卡,那麼該怎麼辦?學習

到底卡頓根本緣由是啥?測試

沒法完全緩解卡頓,該怎麼辦?優化

真實Case

咱們的App有一個用UICollectionView製做了書架的功能,能夠放置圖書,多本圖書疊放在一塊兒自動生成文件夾,如圖

icon

能夠看到圖中每一本書都是一個圓角矩形,而一個文件夾,若是圖書超過4本則是4+1個圓角矩形,普通的用戶使用習慣,可能不會有太多的文件夾(也說不許喲!)可是若是文件夾數量超過2個屏幕,每個文件夾都是4本以上的圖書,那麼在2各屏幕內來回滾動,產生了很恐怖的結果。。。

咱們的app,在ip6上,同屏幕最多能夠有9個Item,每一個Item若是都是4個以上圖書的文件夾就是5個圓角矩形,換句話說,一個屏幕內最多有45個圓角矩形。在這樣的極限狀況下,iPhone6上會卡的只有15幀左右!60幀纔是滿幀啊親。。。

更況且咱們的app,在iPhone 6 plus上是4*4個item,因而一個屏幕內最多有80個圓角矩形。

咱們還支持iPad universal

細思極恐

真實Case環境說明

  • 測試中的圖書陰影爲貼圖,不是GPU渲染,排除這部分干擾
  • 測試中的圖書均爲已下載完成,排除後臺下載線程致使的干擾
  • 測試的書架上半部分大概有20本左右的圖書,足夠在2個屏幕的範圍內來回滾動測試純圖書1圓角的幀率
  • 測試的書架下半部分大概有20個4本以上圖書的文件夾,足夠在2個屏幕的範圍內來回滾動測試純文件夾5圓角的幀率

無優化處理狀況

首先在沒有任何優化代碼的狀況下,都是最直接的

bookProfileImageView.layer.cornerRadius = 3.0f;
bookProfileImageView.clipsToBounds = YES;
複製代碼

性能檢測

讓咱們看一組Instrument裏面core Animation的數據結果

圖書範圍內滾動幀率

icon

咱們能夠看出,在全是圖書的狀況下,僅僅9個圓角矩形,並不會影響幀率,至少能保持在55幀左右,滾動流暢度接近100% 而且Cpu的佔用率並不高

文件夾範圍內滾動幀率

icon

可怕地事情來了,在全是文件夾的狀況下,已經達到了單屏45個圓角矩形,幀率已經降到了平均15,這是一種什麼感受,滿幀率60,如今只達到了滾動流暢的25%,簡直慘不忍睹

你們再看下Cpu佔用率,仍是不高

看一下Cpu的消耗狀況

咱們仔細看看Cpu都消耗在哪?

icon

能夠看到,Cpu的消耗在曲線圖上並無陡然增高,消耗也都是一些基本的UICollectionView的處理

性能分析

能夠看到,在極限純文件夾區域滾動的時候,就只有那4個字能夠形容慘不忍睹

這樣的產生緣由,其實在一開始就提到了,由於圓角遮罩,在Gpu運算的時候會發生離屏渲染

離屏渲染

將離屏渲染做爲關鍵詞去搜索一下你會查到不少的信息,好比iOS離屏渲染的研究

當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定爲在未預合成以前不能直接在屏幕中繪製,因此就須要屏幕外渲染被喚起。 屏幕外渲染並不意味着軟件繪製,可是它意味着圖層必須在被顯示以前在一個屏幕外上下文中被渲染(不論Cpu仍是Gpu)。 因此當使用離屏渲染的時候會很容易形成性能消耗,由於在OPENGL裏離屏渲染會單獨在內存中建立一個屏幕外緩衝區並進行渲染,而屏幕外緩衝區跟當前屏幕緩衝區上下文切換是很耗性能的。

離屏渲染能夠是廣義的理解爲,在屏幕外的時候就要進行渲染,不管是Cpu仍是Gpu

在咱們的當前的Case裏,由於過分的使用了Gpu去處理圓角遮罩,所以Gpu發生了大量的離屏渲染,大幅度拖慢了速度,致使幀率如此悲慘

但由於是Gpu那邊的資源被過分消耗,Cpu這邊顯然處於比較悠閒的狀態,所以,這三張圖片裏面,Cpu佔用率一直不高,而且沒有明顯的某個異常函數嚴重消耗Cpu資源

Cpu繪製bitmap優化處理狀況

那麼,咱們就對他進行必定的優化

這段代碼是引用來的,由於前一陣子不少人討論這個話題,已經有了不少優秀的方案,好比 iOS高效添加圓角實戰講解

-(void)kt_addCorner:(CGFloat)radius
{
    if (self.image) {
        self.image = [self.image imageAddCornerWithRadius:radius andSize:self.bounds.size];
    }
    return;
}

- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    CGContextAddPath(ctx,path.CGPath);
    CGContextClip(ctx);
    [self drawInRect:rect];
    CGContextDrawPath(ctx, kCGPathFillStroke);
    UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}
複製代碼

能夠看到,思路就是繪製一個圓角的路徑,而後填充生成一個天生自帶圓角的bitmap

而後注掉了代碼中全部的cornerRadius 換上了

[bookProfileImageView kt_addCorner]
複製代碼

上問題到離屏渲染能夠是廣義的理解爲,在屏幕外的時候就要進行渲染,不管是Cpu仍是Gpu 因此思路就是,既然Gpu目前負荷會比較大,而Cpu則至關空閒,那咱們何不讓Cpu提早處理一下bitmap數據呢,這也算是另外一種Cpu離屏渲染

讓咱們看看效果

性能檢測

讓咱們看一組Instrument裏面core Animation的數據結果

圖書範圍內滾動幀率

icon

什麼鬼!圖書範圍的性能竟然降低了!! 對比前面的截圖能夠明顯看出來,圖書範圍的滾動幀率從平均55左右降低到平均50,雖然很是的細微,但很明顯,在這個優化改動下,圖書範圍滾動性能反而降低了!

仔細看一下Cpu佔用率,經過前面的圖進行對比,仍是能看出來有增高,或許不明顯,咱們繼續看

文件夾範圍內滾動幀率

icon

你們注意看,一樣的代碼下,文件夾區域的滾動性能卻有大幅度提高,從剛纔的15幀左右提高到了35幀,提高效果超過了100%

可是咱們仔細看,Cpu佔用率此次能明顯看出提高了不少不少!

看一下Cpu的消耗狀況

咱們仔細看看Cpu都消耗在哪?

icon

看到了吧,此時此刻,Cpu被大量消耗在了imageAddCornerWithRadius:andSize:這個咱們專門爲優化而寫的函數裏,很明顯的說明,在Cpu的層面,此處已經在重點消耗Cpu資源了

性能分析

能夠看到,咱們專門針對Gpu離屏渲染作的專門的優化,彷佛?並無那麼有效!是否是?

  • 首先,9圓角矩形的量級下,性能反而降低,雖然降低並不明顯
  • 其次,即使是在45圓角矩形的量級下,性能還真提高很多,可是依然停留在35幀做用,直觀感受,仍是卡!

到底爲何會這樣呢?

簡單的來講就一句話

Gpu運算會有消耗,轉移給Cpu去運算必定也會產生消耗

  • 在圖書區域的對比中能夠看到,本來的9個圓角的數量級,在Gpu能夠承受的運算範圍內,此時此刻並無很大的Gpu壓力,因此還算流暢55幀,
  • 可是咱們的優化方向,再減輕Gpu壓力,加大Cpu負荷(雖然也沒多大),所以性能反而略微降低到50幀
  • 在文件夾區域能夠看到,45個圓角的數量級,Gpu已經徹底不可承受了,壓力其大無比,幀率悲劇到15幀
  • 通過咱們的優化,Gpu的壓力被大幅度減小,Cpu的壓力隨之上升,此消彼長,但最終的結果是總體幀率提高到了35幀

爲何會是這樣,仍是得從離屏渲染下手

離屏渲染

上文提到的離屏渲染 有這樣一句話

因此當使用離屏渲染的時候會很容易形成性能消耗,由於在OPENGL裏離屏渲染會單獨在內存中建立一個屏幕外緩衝區並進行渲染,而屏幕外緩衝區跟當前屏幕緩衝區上下文切換是很耗性能的。

另一篇文章有這樣一句話 A Performance-minded take on iOS design

You’d think the GPU would always be faster than the CPU at this sort of thing, but there are some tricky considerations here. It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier), so for simple drawing operations, the setup cost may be greater than the total cost of doing the drawing in CPU via e.g.

咱們能夠理解爲Gpu在處理浮點運算,處理矩陣運算的時候,必定會比Cpu快得,畢竟他天生就是拿來作圖形處理的,因此在離屏渲染的數量比較少的時候,咱們把運算交給Cpu,反而是略微增長了耗時與卡頓

就像引文裏說的,離屏渲染真正的消耗,在於不一樣緩衝區的來回切換,一旦圓角的數量增多,計算量加大,這種切換會更加頻繁,因此當數量龐大的時候,Gpu最終全部的操做就會更加耗時

咱們面臨的特殊問題

要說明的是,咱們此次的case和網上的其餘例子比是有不一樣的

  • 網上的一些demo都是針對UILabel UIBotton一些相對簡單的UI,來進行的Cpu圓角繪製
  • 咱們的狀況是,咱們是針對一張張圖書封面bookcover,一個個真實的豐富多彩的png圖,來進行Cpu圓角繪製,這樣更加的耗時
  • 最終的結果就是,即使咱們採用了Cpu離屏渲染,可是幀率依然只有35幀左右,還卡!怎麼辦!

其餘的優化處理辦法

咱們的核心目的是,消除卡頓,感覺感覺絲般順滑,可是現有的一些手段,雖然有效果,但還遠遠達不到目標怎麼辦?

有人說了,不要切圓角了,直接讓UE出一張圓角切圖,把全部的運算都省了

那咱們來試試

去掉全部圓角代碼

//bookProfileImageView.layer.cornerRadius = 3.0f;
//bookProfileImageView.clipsToBounds = YES;
複製代碼

換上這樣的一張圖,中間透明四個角有背景色

icon

讓咱們測試下幀率

圖書範圍內滾動幀率

icon

文件夾範圍內滾動幀率

icon

不管是圖書,仍是文件夾都已經達到了55幀左右的幀率,接近滿幀

絲般順滑

刨根問底深刻思考

這樣就知足了麼?顯然是不能夠的,由於若是一旦圓角item背後有背景圖,有紋理,那這種貼圖的方式根本不能實現了。難道就這麼讓app卡着湊合用麼

顯然不能夠

深刻思考卡頓的原理

解決問題應該從源頭入手,因此咱們相應地要思考,卡頓是怎麼來的?

這塊就要從ibireme大神的 iOS 保持界面流暢的技巧 這篇博客來深刻學習

圖爲原博客中的圖

icon

iOS設備都是採用雙緩衝區+垂直同步開啓的方式來進行圖形渲染,什麼意思呢?

  • Cpu運算處理結束後將要渲染的任務提交給Gpu
  • Gpu運算渲染完成後講最終圖形放入緩衝區
  • Gpu觸發離屏渲染,會有多緩衝區來回切換管理等複雜耗時操做
  • 視頻控制器在固定的頻率內,從緩衝區取渲染結果,展示到顯示器上
icon

這幅圖更加直觀

  • 每個VSync的點,都是垂直同步做用下,控制器去取渲染結果,準備展示的時間點
  • 當Vsync的點到來時,Cpu藍色+Gpu紅色都運算結束,那麼就沒有發生掉幀,沒有發生卡頓,很順暢的繪製了下去
  • 當Vsync的點到來時,運算沒有結束,那麼說明這一幀尚未渲染完畢,所以沒法順暢繪製,產生了掉幀,也就是卡頓
  • 紅色的Gpu持續時間過長,會致使Vsync點到來時運算沒有結束致使卡頓,這是咱們Gpu離屏渲染15幀的狀況
  • 藍色的Cpu持續時間長,也會致使Vsync點到來時運算沒有結束致使卡頓,這就是咱們Cpu離屏渲染35幀的狀況

當圖形的總運算量在那裏擺着,就是很大,就是很卡怎麼辦呢?

AsyncDisplay

異步繪製

簡單地說,就是已經採用了Cpu離屏渲染,仍是會由於主線程計算耗時很長而卡頓UI,那咱們就把Cpu計算bitmap這個過程放到線程裏去。

運算量大怎麼辦?

  • 優化運算,合併圖層,在需求範圍內替換貼圖
  • 開個後臺線程慢慢算,算好了再回到主線程繪製

但由於咱們面對的是頻繁複用的UICollectionView或者UITableView,因此要有很完善的線程管理機制,再輔助以cache機制

採用圖片的方法已經解決了當下app的卡頓問題,可是後續對AsyncDisplay的支持,等有空了再整理一篇吧。。

其實 facebook開源的 AsyncDisplayKit 就是實現了這些,功能很強大,我還沒用熟,感受有點重

相關文章
相關標籤/搜索