這篇文章介紹ZYCornerRadius解決生產中圓角帶來的離屏渲染問題的思路。
日常生產中app佈局離不開美麗的圓角(RounderCorner),特別是用圓角UIImageView來做資料呈現互動,但是這種柔和易於讓人接受的檢視效果並不僅僅是改變了一個形狀那麼簡單,需要付出一定的效能代價。
相信這已經是總所周知的問題了,日常我們使用layer的兩個屬性,簡單的兩行程式碼就能實現圓角的呈現
1 2 |
imageView.layer.cornerRadius = CGFloat(10); imageView.layer.masksToBounds = YES; |
由於這樣處理的渲染機制是GPU在當前螢幕緩衝區外新開闢一個渲染緩衝區進行工作,也就是離屏渲染,這會給我們帶來額外的效能損耗,如果這樣的圓角操作達到一定數量,會觸發緩衝區的頻繁合併和上下文的的頻繁切換,效能的代價會巨集觀地表現在使用者體驗上—-掉幀。這也是我親身體驗過的,有一次朋友在玩我手機的時候問我為什麼會卡,看了後才發現原來是一個充滿圓形頭像的TableView。
螢幕的渲染機制這裡就不copy了,很多朋友的文章也討論過這樣的問題。這篇文章有深入介紹螢幕顯示機制。這裡順便貼一下我筆記裡記錄的會引發離屏渲染的操作,給大家做個記憶捆綁,正確與否大家可以自己思量。
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.
因為這些效果均被認為不能直接呈現於螢幕,而需要在別的地方做額外的處理預合成。具體的檢測我們可以使用Instruments的CoreAnimation。
ZYCornerRadius
以下介紹ZYCornerRadius(以Category的方式工作)對UIImageView設定圓角會觸發離屏渲染的解決思路,有什麼問題和建議還請大家發issues指導更正。
先上一張效能對比圖
測試裝置6P,螢幕中有40張尺寸為20*20的小圖片,使用masksToBounds切角處理時幀率大大下降至20+,使用ZYCornerRadius時幀率保持在57+,效能接近0損耗。
既然我們要避免讓GPU觸發離屏,那麼只能把兵符交給CPU,雖然CPU對圖形的處理能力不及GPU,但由於這種處理的難度不大,且代價肯定遠小於上下文切換。
其實一開始的想法就是從-drawRect下手,但是看了某篇文章(找不回來了)後打消了這個念頭,-drawRect的確存在很多效能坑。
既然不能讓控制元件masksToBounds,ZYCornerRadius就從圖片本身下手,我使用在UIKit中對Core Graphics有一定封裝的應用層類UIBezierPath,對圖片進行破壞性的切角,破壞性僅僅是對切去部分而言,當然這操作是在CPU內完成的,而後我只需要取到處理完成的bitmap(可為UIImage物件)交給GPU顯示於螢幕即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * @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。操作沒有觸發GPU離屏渲染,過程在CPU內完成,而後我在Demo中證實了這個方法。
順便一提這裡還存在一個效能問題,==Color Blended Layers==,UIGraphicsBeginImageContextWithOptions(, , )
的第二個引數是透明通道的開關,true則為不透明。以下兩張圖是引數傳NO or YES在模擬器中開啟了Color Blended Layers Debug所看見的區別:
一些沒有被設定為opacity的圖層,因為透明通道的存在,系統需要去計算圖層堆疊後畫素點的真實顏色,在Instruments的測試中也是可以高亮標顯出來,這種效能的損耗程度我還沒有專門去測試。但是在上圖可以看見如果設定為不包含透明通道,我們圖片被剪去的部分就沒有了顏色(黑漆漆一片),這裡使用的解決方案就是在圖片上下文中先畫一層backgroundColor,缺點就是需要傳入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * @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與原先對比:
prefect!
實際生產
目前我們解決了離屏渲染的問題,可這並不符合實際生產,在app中顯示的網路圖片我們不可能事先知道並且呼叫- (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:
來進行切角,也不可能每次都還要寫個SDWedImage的complete回撥去做這個操作,我決定用swizzleMethod的辦法來處理,關於對swizzleMethod的認識,可以看看我這篇文章。
我們把對self.image切角處理放在每次layoutSubviews的時候完成,大家看到這裡頓時把我臭罵了一頓。。。在Category裡重寫-layoutSubviews的致命的,這的確會導致整個專案下所有的UIImageView都會去執行這個山寨的-layoutSubviews,別慌關掉文章,給個機會繼續看下去。
首先我們需要將使用者傳入的切角引數儲存起來,供-layoutSubviews切角時使用,因為category不支援擴充套件屬性,所以我們可以用runtime
來做:
1 2 3 4 5 6 7 8 9 10 |
/** * @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
,我將呼叫了- (void)zy_cornerRadiusAdvance:cornerRadius:rectCornerType:
的UIImageView物件的-layoutSubviews
方法的實現轉移到了我自己的方法-zy_LayoutSubviews
上,也就是說我不需要去重寫-layoutSubviews,而主動呼叫過-zy_cornerRadiusAdvance
的UIImageView物件的-layoutSubviews的實現卻被我換成了-zy_LayoutSubviews
,原始碼在Demo中有。ok,於是在-zy_LayoutSubviews中收官:
1 2 3 4 5 6 7 |
- (void)zy_LayoutSubviews { [super layoutSubviews]; 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就完成了,有問題或建議歡迎發issues交流,還希望大家star以支援啊,謝謝!
Usage:
ZYCornerRadius提供兩種使用方式
Category方式:
匯入標頭檔案
1 |
#import "UIImageView+CornerRadius.h" |
建立圓角半徑為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"]; |
建立圓形的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設定圓角圖片,傳入UIImage,圓角半徑和圓角型別,當次有效!
1 |
- (void)zy_cornerRadiusWithImage:(UIImage *)image cornerRadius:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType; |
近期更新:
0.6.1 – 解決在TableViewCell被selected後,其中UIImageView的image被重置的問題
0.5.1 – 解決SDWebImage使用placeholder為nil時發生的crash
0.4.1 – 釋出第一個較完善版本
以下記錄失敗過程。。。
- 嘗試在-drawRect中做切角操作
1.記憶體使用過大,造成更多的效能損耗 - 嘗試從init出發
1.需要事先傳入Image,而且當Image改變後無效,不適合實際生產 - 嘗試從-layoutSubviews下手
1.在Category中重寫該方法會造成不可挽回的結果 - 在setImage中設定好識別符號開關,在layoutSubviews中判斷開關狀態再執行操作
1.雖然解決了對其他UIImageView的影響,可實現方式過於投機取巧過於費力。 - 嘗試直接從重寫-setImage下手
1.直接重寫會導致無限遞迴
2.自己重寫為UIImageView顯示圖片的機制,不熟悉原始碼實現,擔心造成什麼遺漏。 - 最壞的打算,大膽使用swizzleMethod。
Relation:
@liuzhiyi1992 on Github
License:
ZYCornerRadius is released under the MIT license.