生產(chǎn)中怎樣處理masksToBounds離屏渲染帶來的性能損耗


github鏈接: https://github.com/liuzhiyi1992/ZYCornerRadius
本文地址:http://zyden.vicp.cc/zycornerradius/
歡迎轉載,請注明出處,謝謝。

這篇文章介紹ZYCornerRadius(一句代碼,圓角風雨無阻)解決生產(chǎn)中圓角帶來的離屏渲染問題的思路。


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

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

imageView.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設置圓角會觸發(fā)離屏渲染的解決思路,有什么問題和建議還請大家發(fā)issues指導更正。

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




另外內(nèi)存使用的對比(使用ZYCornerRadius處理大量的圓角圖片幾乎沒有帶來內(nèi)存增長):

既然我們要避免讓GPU觸發(fā)離屏,那么只能把兵符交給CPU,雖然CPU對圖形的處理能力不及GPU,但由于這種處理的難度不大,且代價肯定遠小于上下文切換。
其實一開始的想法就是從-drawRect下手,但是看了某篇文章(找不回來了)后打消了這個念頭,-drawRect的確存在很多性能坑。

既然不能讓控件masksToBounds,ZYCornerRadius就從圖片本身下手,我使用在UIKit中對Core Graphics有一定封裝的應用層類UIBezierPath,對圖片進行破壞性的切角,破壞性僅僅是對切去部分而言,當然這操作是在CPU內(nèi)完成的,而后我只需要取到處理完成的bitmap(可為UIImage對象)交給GPU顯示于屏幕即可。

/**
 * @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);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        UIGraphicsBeginImageContextWithOptions(size, YES, scale);
        if (nil == UIGraphicsGetCurrentContext()) {
            return;
        }
        UIBezierPath *cornerPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCornerType cornerRadii:cornerRadii];
        [cornerPath addClip];
        [image drawInRect:self.bounds];
        id processedImageRef = (__bridge id _Nullable)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
        UIGraphicsEndImageContext();
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = processedImageRef;
        });
    });
}

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

順便一提這里還存在一個性能問題,==Color Blended Layers==,UIGraphicsBeginImageContextWithOptions(<#CGSize size#>, <#BOOL opaque#>, <#CGFloat scale#>)的第二個參數(shù)是透明通道的開關,true則為不透明。以下兩張圖是參數(shù)傳NO or YES在模擬器中打開了Color Blended Layers Debug所看見的區(qū)別:

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

/**
 * @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);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        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];
        id processedImageRef = (__bridge id _Nullable)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
        UIGraphicsEndImageContext();
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = processedImageRef;
        });
    });
}

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


prefect!


實際生產(chǎn)

目前我們解決了離屏渲染的問題,可這并不符合實際生產(chǎn),在app中顯示的網(wǎng)絡圖片我們不可能事先知道并且調(diào)用- (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:來進行切角,也不可能每次都還要寫個SDWedImage的complete回調(diào)去做這個操作,我決定用swizzleMethod的辦法來處理,關于對swizzleMethod的認識,可以看看我這篇文章

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

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

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

- (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];
}


同時結合KVO維持切角效果做持久化:

UIImageView的內(nèi)容image可能會因為許多動作而導致被重新設置,我們的切角效果就會因此而消失掉,我們需要對image屬性進行監(jiān)聽,一旦發(fā)生變化即對改變后的新值再次作切角處理,再次賦值給imageView.layer.content顯示。KVO響應部分源碼:

#pragma mark - KVO for .image
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"image"]) {
        UIImage *newImage = change[NSKeyValueChangeNewKey];
        if ([newImage isMemberOfClass:[NSNull class]]) {
            return;
        } else if ([objc_getAssociatedObject(newImage, &kZYProcessedImage) intValue] == 1) {
            return;
        }
        if (_isRounding) {
            [self zy_cornerRadiusWithImage:newImage cornerRadius:self.frame.size.width/2 rectCornerType:UIRectCornerAllCorners];
        } else if (0 != _cornerRadius && _rectCornerType && nil != self.image) {
            [self zy_cornerRadiusWithImage:newImage cornerRadius:_cornerRadius rectCornerType:_rectCornerType];
        }
    }
}

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

issues

收到一個朋友的issues: (在0.6.1解決)
當tableViewCell被selected后(去除selected效果則不會),里面被切角的imageView效果會丟失,觸發(fā)重用后也不能恢復,除非重新setImage。

思考這個問題從這里出發(fā):我們將處理后的圖片顯示于屏幕之上是通過KVO對imageView.image的監(jiān)聽,再將處理后的圖片上下文賦值給imageView的mainLayer,這個過程完成后其實imageView持有的image就沒有被玷污的,同時_image也是,當tableViewCell被selected后觸發(fā)了subViews的重繪后,小三還是會被正室所取代。

解決這個問題:
回到過去,使用setImage讓處理后的圖片顯示于屏幕,使用runtime對image對象綁定一個標識符,因KVO存在的無限遞歸。


Usage(一句代碼,圓角風雨無阻)

ZYCornerRadius提供兩種使用方式

Category方式:
導入頭文件

#import "UIImageView+CornerRadius.h"

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

//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
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方式同理:
導入頭文件

#import "ZYImageView.h"

使用方式同理


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

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

+ (UIImageView *)zy_cornerRadiusAdvance:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType;
- (instancetype)initWithCornerRadiusAdvance:(CGFloat)cornerRadius rectCornerType:(UIRectCorner)rectCornerType;

配置一個圓形的UIImageView

+ (UIImageView *)zy_roundingRectImageView;
- (instancetype)initWithRoundingRectImageView;

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

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


CocoaPods:

pod 'ZYCornerRadius', '~> 0.8.1'



附帶一個25行500個圓角的UITabelView性能測試gif



近期更新:

0.8.1 - 解決更新圖片時圖片內(nèi)容閃動問題。
0.7.1 - 去除部分api,保持使用簡潔的設計理念,加入帶邊框功能
0.6.1 - 解決在TableViewCell被selected后,其中UIImageView的image被重置的問題
0.5.1 - 解決SDWebImage使用placeholder為nil時發(fā)生的crash
0.4.1 - 發(fā)布第一個較完善版本


以下記錄失敗過程。。。

  • 嘗試在-drawRect中做切角操作
    1.內(nèi)存使用過大,造成更多的性能損耗
  • 嘗試從init出發(fā)
    1.需要事先傳入Image,而且當Image改變后無效,不適合實際生產(chǎn)
  • 嘗試從-layoutSubviews下手
    1.在Category中重寫該方法會造成不可挽回的結果
  • 在setImage中設置好標識符開關,在layoutSubviews中判斷開關狀態(tài)再執(zhí)行操作
    1.雖然解決了對其他UIImageView的影響,可實現(xiàn)方式過于投機取巧過于費力。
  • 嘗試直接從重寫-setImage下手
    1.直接重寫會導致無限遞歸
    2.自己重寫為UIImageView顯示圖片的機制,不熟悉源碼實現(xiàn),擔心造成什么遺漏。
  • 最壞的打算,大膽使用swizzleMethod。


Relation:

@liuzhiyi1992 on Github


License:

ZYCornerRadius is released under the MIT license.

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

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

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