解決常見的masksToBounds離屏渲染帶來的性能損耗

-------》(轉(zhuǎn)發(fā))


這篇文章介紹ZYCornerRadius解決生產(chǎn)中圓角帶來的離屏渲染問題的思路。

日常生產(chǎn)中app布局離不開美麗的圓角(RounderCorner),特別是用圓角UIImageView來做數(shù)據(jù)呈現(xiàn)交互,但是這種柔和易于讓人接受的視圖效果并不僅僅是改變了一個形狀那么簡單,需要付出一定的性能代價。

相信這已經(jīng)是總所周知的問題了,日常我們使用layer的兩個屬性,簡單的兩行代碼就能實現(xiàn)圓角的呈現(xiàn)

1

2imageView.layer.cornerRadius?=?CGFloat(10);

imageView.layer.masksToBounds?=?YES;

由于這樣處理的渲染機制是GPU在當前屏幕緩沖區(qū)外新開辟一個渲染緩沖區(qū)進行工作,也就是離屏渲染,這會給我們帶來額外的性能損耗,如果這樣的圓角操作達到一定數(shù)量,會觸發(fā)緩沖區(qū)的頻繁合并和上下文的的頻繁切換,性能的代價會宏觀地表現(xiàn)在用戶體驗上----掉幀。這也是我親身體驗過的,有一次朋友在玩我手機的時候問我為什么會卡,看了后才發(fā)現(xiàn)原來是一個充滿圓形頭像的TableView。

屏幕的渲染機制這里就不copy了,很多朋友的文章也討論過這樣的問題。這篇文章有深入介紹屏幕顯示機制。這里順便貼一下我筆記里記錄的會引發(fā)離屏渲染的操作,給大家做個記憶捆綁,正確與否大家可以自己思量。

The following will trigger offscreen rendering:

Any layer with a mask (layer.mask)

Any layer with layer.masksToBounds / view.clipsToBounds being true

Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0

Any layer with a drop shadow (layer.shadow*).

Any layer with layer.shouldRasterize being true

Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing

Text (any kind, including UILabel, CATextLayer, Core Text, etc).

Most of the drawing you do with CGContext in drawRect:. Even an empty implementation will be rendered offscreen.

因為這些效果均被認為不能直接呈現(xiàn)于屏幕,而需要在別的地方做額外的處理預合成。具體的檢測我們可以使用Instruments的CoreAnimation。

ZYCornerRadius

以下介紹ZYCornerRadius(以Category的方式工作)對UIImageView設(shè)置圓角會觸發(fā)離屏渲染的解決思路,有什么問題和建議還請大家發(fā)issues指導更正。

先上一張性能對比圖

測試設(shè)備6P,屏幕中有40張尺寸為20*20的小圖片,使用masksToBounds切角處理時幀率大大下降至20+,使用ZYCornerRadius時幀率保持在57+,性能接近0損耗。

既然我們要避免讓GPU觸發(fā)離屏,那么只能把兵符交給CPU,雖然CPU對圖形的處理能力不及GPU,但由于這種處理的難度不大,且代價肯定遠小于上下文切換。

其實一開始的想法就是從-drawRect下手,但是看了某篇文章(找不回來了)后打消了這個念頭,-drawRect的確存在很多性能坑。

既然不能讓控件masksToBounds,ZYCornerRadius就從圖片本身下手,我使用在UIKit中對Core

Graphics有一定封裝的應用層類UIBezierPath,對圖片進行破壞性的切角,破壞性僅僅是對切去部分而言,當然這操作是在CPU內(nèi)完成的,而后我只需要取到處理完成的bitmap(可為UIImage對象)交給GPU顯示于屏幕即可。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17/**

*?@brief?clip?the?cornerRadius?with?image,?UIImageView?must?be?setFrame?before,?no?off-screen-rendered

*/

-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?{

CGSize?size?=?self.bounds.size;

CGFloat?scale?=?[UIScreen?mainScreen].scale;

CGSize?cornerRadii?=?CGSizeMake(cornerRadius,?cornerRadius);

UIGraphicsBeginImageContextWithOptions(size,?NO,?scale);

if(nil?==?UIGraphicsGetCurrentContext())?{

return;

}

UIBezierPath?*cornerPath?=?[UIBezierPath?bezierPathWithRoundedRect:self.bounds?byRoundingCorners:rectCornerType?cornerRadii:cornerRadii];

[cornerPath?addClip];

[image?drawInRect:self.bounds];

self.image?=?UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

}

可見,我對圖片進行了切角處理后,將得到的含圓角UIImage通過-setImage傳給了UIImageView。操作沒有觸發(fā)GPU離屏渲染,過程在CPU內(nèi)完成,而后我在Demo中證實了這個方法。

順便一提這里還存在一個性能問題,Color

Blended Layers,UIGraphicsBeginImageContextWithOptions(<#cgsize>,

<#bool>, <#cgfloat>)的第二個參數(shù)是透明通道的開關(guān),true則為不透明。以下兩張圖是參數(shù)傳NO or

YES在模擬器中打開了Color Blended Layers Debug所看見的區(qū)別:

一些沒有被設(shè)置為opacity的圖層,因為透明通道的存在,系統(tǒng)需要去計算圖層堆疊后像素點的真實顏色,在Instruments的測試中也是可以高亮標顯出來,這種性能的損耗程度我還沒有專門去測試。但是在上圖可以看見如果設(shè)置為不包含透明通道,我們圖片被剪去的部分就沒有了顏色(黑漆漆一片),這里使用的解決方案就是在圖片上下文中先畫一層backgroundColor,缺點就是需要傳入:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20/**

*?@brief?clip?the?cornerRadius?with?image,?draw?the?backgroundColor?you?want,?UIImageView?must?be?setFrame?before,?no?off-screen-rendered

*/

-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?backgroundColor:(UIColor?*)backgroundColor?{

CGSize?size?=?self.bounds.size;

CGFloat?scale?=?[UIScreen?mainScreen].scale;

CGSize?cornerRadii?=?CGSizeMake(cornerRadius,?cornerRadius);

UIGraphicsBeginImageContextWithOptions(size,?YES,?scale);

if(nil?==?UIGraphicsGetCurrentContext())?{

return;

}

UIBezierPath?*cornerPath?=?[UIBezierPath?bezierPathWithRoundedRect:self.bounds?byRoundingCorners:rectCornerType?cornerRadii:cornerRadii];

UIBezierPath?*backgroundRect?=?[UIBezierPath?bezierPathWithRect:self.bounds];

[backgroundColor?setFill];

[backgroundRect?fill];

[cornerPath?addClip];

[image?drawInRect:self.bounds];

self.image?=?UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

}

傳入紅色的背景顏色,打開Color Blended Layers Debug與原先對比:

目前我們解決了離屏渲染的問題,可這并不符合實際生產(chǎn),在app中顯示的網(wǎng)絡(luò)圖片我們不可能事先知道并且調(diào)用-

(void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:來進行切角,也不可能每次都還要寫個SDWedImage的complete回調(diào)去做這個操作,我決定用swizzleMethod的辦法來處理,關(guān)于對swizzleMethod的認識,可以看看我這篇文章。

我們把對self.image切角處理放在每次layoutSubviews的時候完成,大家看到這里頓時把我臭罵了一頓。。。在Category里重寫-layoutSubviews的致命的,這的確會導致整個項目下所有的UIImageView都會去執(zhí)行這個山寨的-layoutSubviews,別慌關(guān)掉文章,給個機會繼續(xù)看下去。

首先我們需要將使用者傳入的切角參數(shù)保存起來,供-layoutSubviews切角時使用,因為category不支持擴展屬性,所以我們可以用runtime來做:

1

2

3

4

5

6

7

8

9/**

*?@brief?set?cornerRadius?for?UIImageView,?no?off-screen-rendered

*/

-?(void)zy_cornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType?{

objc_setAssociatedObject(self,?&kRadius,?@(cornerRadius),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);

objc_setAssociatedObject(self,?&kRoundingCorners,?@(rectCornerType),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);

objc_setAssociatedObject(self,?&kIsRounding,?@(0),?OBJC_ASSOCIATION_RETAIN_NONATOMIC);

[self.class?swizzleMethod:@selector(layoutSubviews)?anotherMethod:@selector(zy_LayoutSubviews)];

}

細心的朋友可以看見上面這段代碼里的+swizzleMethod,我將調(diào)用了-

(void)zy_cornerRadiusAdvance:cornerRadius:rectCornerType:的UIImageView對象的-layoutSubviews方法的實現(xiàn)轉(zhuǎn)移到了我自己的方法-zy_LayoutSubviews上,也就是說我不需要去重寫-layoutSubviews,而主動調(diào)用過-zy_cornerRadiusAdvance的UIImageView對象的-layoutSubviews的實現(xiàn)卻被我換成了-zy_LayoutSubviews,源代碼在Demo中有。ok,于是在-zy_LayoutSubviews中收官:

1

2

3

4

5

6-?(void)zy_LayoutSubviews?{

[superlayoutSubviews];

NSNumber?*radius?=?objc_getAssociatedObject(self,?&kRadius);

NSNumber?*roundingCorners?=?objc_getAssociatedObject(self,?&kRoundingCorners);

[self?zy_cornerRadiusWithImage:self.image?cornerRadius:radius.floatValue?rectCornerType:roundingCorners.unsignedLongValue];

}

這樣不需要離屏渲染的UIImageView圓角工具ZYCornerRadius就完成了,有問題或建議歡迎發(fā)issues交流,還希望大家star以支持啊,謝謝!

Usage:ZYCornerRadius提供兩種使用方式

Category方式:

導入頭文件

1

#import?"UIImageView+CornerRadius.h"

創(chuàng)建圓角半徑為6的UIImageView(三種方式):

1

2

3

4

5

6

7

8

9

10

11

12//1

UIImageView?*imageView?=?[UIImageView?zy_cornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

//2

UIImageView?*imageView?=?[[UIImageView?alloc]?initWithCornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

//3

UIImageView?*imageView?=?[[UIImageView?alloc]?init];

[imageView?zy_cornerRadiusAdvance:6.0f?rectCornerType:UIRectCornerAllCorners];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

創(chuàng)建圓形的UIImageView(三種方式):

1

2

3

4

5

6

7

8

9

10

11

12//1

UIImageView?*imageView?=?[UIImageView?zy_roundingRectImageView];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

//2

UIImageView?*imageView?=?[[UIImageView?alloc]?initWithRoundingRectImageView];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

//3

UIImageView?*imageView?=?[[UIImageView?alloc]?init];

[imageView?zy_cornerRadiusRoundingRect];

imageView.image?=?[UIImage?imageNamed:@"mac_dog"];

子類ZYImageView方式同理:

導入頭文件

1

#import?"ZYImageView.h"

使用方式同理

以下列出ZYCornerRadius所開放的主要的func:

配置一個圓角UIImageView,傳入圓角半徑和圓角類型

1

2+?(UIImageView?*)zy_cornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;

-?(instancetype)initWithCornerRadiusAdvance:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;

配置一個圓形的UIImageView

1

2+?(UIImageView?*)zy_roundingRectImageView;

-?(instancetype)initWithRoundingRectImageView;

直接為UIImageView設(shè)置圓角圖片,傳入UIImage,圓角半徑和圓角類型,當次有效!

1

-?(void)zy_cornerRadiusWithImage:(UIImage?*)image?cornerRadius:(CGFloat)cornerRadius?rectCornerType:(UIRectCorner)rectCornerType;

以下記錄失敗過程...

嘗試在-drawRect中做切角操作

1.內(nèi)存使用過大,造成更多的性能損耗

嘗試從init出發(fā)

1.需要事先傳入Image,而且當Image改變后無效,不適合實際生產(chǎn)

嘗試從-layoutSubviews下手

1.在Category中重寫該方法會造成不可挽回的結(jié)果

在setImage中設(shè)置好標識符開關(guān),在layoutSubviews中判斷開關(guān)狀態(tài)再執(zhí)行操作

1.雖然解決了對其他UIImageView的影響,可實現(xiàn)方式過于投機取巧過于費力。

嘗試直接從重寫-setImage下手

1.直接重寫會導致無限遞歸

2.自己重寫為UIImageView顯示圖片的機制,不熟悉源碼實現(xiàn),擔心造成什么遺漏。

最壞的打算,大膽使用swizzleMethod。

Relation:@liuzhiyi1992on Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • github鏈接: https://github.com/liuzhiyi1992/ZYCornerRadius本...
    zhiyi閱讀 3,668評論 13 38
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,698評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,273評論 5 13
  • 實際開發(fā)中,大多會遇到圓角或者圓形的控件的情況。通常,簡便的解決方案主要是: 1.讓美工做一個圓角的圖片...
    LanWor閱讀 1,776評論 1 5
  • 沉夜千百,思著舊事萬般。 朦朧浮生,哪里一人心安? 夢斷一世,卻為情字傷感。 相思明月,何須告問廣寒? 無心留戀,...
    Hello大壯閱讀 189評論 0 0

友情鏈接更多精彩內(nèi)容