iOS圓角圖片的實現(xiàn)

demo地址

一:相對簡便的圓角圖片的實現(xiàn)方式

imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;

由于這樣的處理機制是GPU在當(dāng)前緩沖區(qū)以外新開辟一個渲染緩沖區(qū)進行工作,也就是我們常說的離屏渲染;
這會給我們帶來額外的性能消耗,如果這樣的圓角達到一定數(shù)量,會觸發(fā)渲染緩沖區(qū)的頻煩合并和上下文的頻繁切換,性能的代價會很明顯的表現(xiàn)在用戶體驗上,因為這些效果均被認為不能直接呈現(xiàn)于屏幕,而需要在別的地方做額外的處理預(yù)合成。具體的檢測我們可以使用Instruments的CoreAnimation

二: 使用ZYCornerRadius解決設(shè)置圓角時GPU會觸發(fā)離屏渲染的解決思路的問題

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

ZYCornerRadius性能對比.png

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

內(nèi)存使用對比.jpg

既然我們要避免讓GPU觸發(fā)離屏,那么只能把兵符交給CPU,雖然CPU對圖形的處理能力不及GPU,但由于這種處理的難度不大,且代價肯定遠小于上下文切換。
其實一開始的想法就是從-drawRect下手,但是看了某篇文章(找不回來了)后打消了這個念頭,-drawRect的確存在很多性能坑。
思路
既然不能讓控件masksToBounds,ZYCornerRadius就從圖片本身下手,我使用在UIKit中對Core Graphics有一定封裝的應(yīng)用層類UIBezierPath,對圖片進行破壞性的切角,破壞性僅僅是對切去部分而言,當(dāng)然這操作是在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ù)是透明通道的開關(guān),true則為不透明。以下兩張圖是參數(shù)傳NO or YES在模擬器中

左邊圖片傳入的參數(shù)為YES,右邊的圖片傳入的參數(shù)為NO

組合演示.jpg

打開打開了Color Blended Layers Debug所看見的區(qū)別:

屏幕快照 2017-02-16 下午2.19.00.png

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

組合演示2.png

實際生產(chǎn)

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

  • (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:

來進行切角,也不可能每次都還要寫個SDWedImage的complete回調(diào)去做這個操作,我決定用swizzleMethod的辦法來處理

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

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


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

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

** Usage(一句代碼,圓角風(fēng)雨無阻)**

ZYCornerRadius提供兩種使用方式

Category方式:
導(dǎo)入頭文件

#import "UIImageView+CornerRadius.h"

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

//1
    UIImageView *imageView1 = [[UIImageView alloc]init];
  [imageView1 zy_cornerRadiusAdvance:75.0f rectCornerType:UIRectCornerAllCorners];
    [imageView1 setFrame:CGRectMake(130, 80, 150, 150)];

    imageView1.image = [UIImage imageNamed:@"mac_dog"];
    
    [self.view addSubview:imageView1];

子類ZYImageView方式同理:
導(dǎo)入頭文件

#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設(shè)置圓角圖片,傳入UIImage,圓角半徑和圓角類型,當(dāng)次有效!

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

CocoaPods:

pod 'ZYCornerRadius', '~> 0.8.1'

以下記錄失敗過程。。。

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

轉(zhuǎn)發(fā):http://www.itdecent.cn/p/2fbb4c8fb1fa

繪圖iOS Quarat2D http://www.itdecent.cn/p/8cf8d4b724d2

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

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

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