老话题——圆角视图优化处理

本篇持续更新。(第二次更新:为UILabel设置圆角、离屏渲染知识)
涉及:各种视图的圆角处理方案、结合网络与存储的图片方案等。

引言

圆角视图效果十分常见,存在于各种时间线、个人中心等等。相对于直角更柔和,让人易于接受。在iOS中进行圆角处理的方法不少,各方法间的适用场景和性能也不尽相同,下文就将目前所看到的各类 文章进行小小的总结。

设置圆角的一般姿势及原理

最简单的设置圆角的方法只需要一行代码:
view.layer.cornerRadius = 5.0f;
这行代码不会带来任何性能损耗,在Instrument中可以看到,例如在一个tableView中显示超过30个圆角View时,帧数一直都是接近60的。
对于内部有子视图的UILabel来说,仅仅这一行代码并不能实现圆角效果。所以很多时候看到的会是下面这样的代码:

label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;

这时圆角实现了,但是在Instrument中,滑动时的帧数却只有35左右了。勾上Color Offscreen-Rendered Yellow,发现label的四个角出现了黄色标记,说明这里出现了离屏渲染。

离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价不算太大,付出最大代价的是上下文切换。

上下文切换,不管是在GPU渲染过程中,还是一直所熟悉的进程切换,上下文切换在哪里都是一个相当耗时的操作。首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering或者再开始一个新的离屏渲染都需要重复之前的操作。

为什么会使用离屏渲染?
当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。
屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

iOS版本上的优化
iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染
iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片>设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
这可能是苹果也意识到离屏渲染会产生性能问题,所以能不产生离屏渲染的地方苹果也就不用离屏渲染了。

更多离屏渲染可以参考 Advanced Graphics and Animations for iOS Apps(session 419)学习与延伸

通过控制变量的方法,可以明确一点:同时写上面这两行代码时,才会出现离屏渲染。虽然出现了离屏渲染,影响了性能,但页面上只有少量的视图进行了这个操作时,这个影响不大。5s上出现15个时,滑动时的帧数依然在55以上。当圆角视图较多时(数目不确定,25个以上?),帧数大幅度下降,只有30左右,严重影响了用户体验。

小总结:当页面内出现了少数的设置此操作的简单视图时,优化或许是不必要的。

高效的设置圆角

为UIImageView添加圆角

UIImageView的圆角需求是最常见的,一个合适的实现方案可以让页面更流畅,用户体验更好。
这里的实现思路是将图片截取为圆角

- (UIImage *)al_setImageCornerRadius:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
    if (image == nil || cornerRadius <= 0.0f) {
        return;
    }

    CGSize size = self.bounds.size;
    CGRect rect = CGRectMake(0, 0, size.width, size.height);

    //创建一个基于位图的上下文(context),并将其设置为当前上下文(context)
    //void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);
    //参数1 size 为新创建的位图上下文的大小。
    //参数2 opaque 透明开关,如果图形完全不用透明,设置为YES以优化位图的存储。
    //参数3 scale 缩放因子
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);

    //获取图形上下文。
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    //圆角路径直接用贝塞尔曲线绘制。
    //在矩形中,针对四角中的某个角加圆角。
    //参数1 rect: 需要画的矩形的Frame。
    //参数2 corners: 哪些部位需要画成圆角,这里四个角都画。
    //参数3 cornerRadii: 圆角的Size。
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect
                                           byRoundingCorners:UIRectCornerAllCorners
                                                      cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];

    //把创建的路径添加到上下文对象中                                                      
    CGContextAddPath(ctx,path.CGPath);
    //裁剪
    CGContextClip(ctx);

    //绘制
    [image drawInRect:rect];
    //绘制路径
    //参数2 填充规则,kCGPathFillStroke表示填充。
    CGContextDrawPath(ctx, kCGPathFillStroke);

    //从当前上下文中获取UIImage对象。
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    //移除栈顶的基于当前位图的图形上下文。
    UIGraphicsEndImageContext(); 

    return newImage;
}

//实现一个便捷方法
- (void)al_setImage:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
    self.image = [self al_setCornerRadiusImage:image cornerRadius:cornerRadius];
}

此方法并不会进行离屏渲染,不仅成功添加了圆角效果,同时还保证了性能不受影响。结合代码中的注释,这个方法并不难理解。
不过值得注意的是,如果图片比较大,使用此方法绘制时效率会十分低下,同时也会造成屏幕卡顿,解决方法后续研究后更新。

为UILabel设置圆角

目前的权宜之计的思路是:将背景颜色的设置放到layer层。代码如下。

self.sLabel.layer.cornerRadius = 5.0f;
self.sLabel.layer.backgroundColor = [UIColor clearColor].CGColor;
self.sLabel.layer.borderWidth = 2.0;
self.sLabel.layer.borderColor = [UIColor darkGrayColor].CGColor;

此处并未调用masksToBounds方法,没有发生离屏渲染,帧数也没有下降。但这里会有一个问题,如果字体大小较大,会出现文字超出圆角/边框的现象。若要采用这种方法,在字体设置时需要注意。
更好的方法后续更新

结合网络和存储

这里结合SDWebImage进行网络请求和储存来简化问题。
使用此方法需在工程中引入SDWebImage。

- (void)al_loadImageUrlStr:(NSString *)urlStr placeHolderImageName:(NSString *)placeHolderStr cornerRadius:(CGFloat)cornerRadius {
    NSURL *url = [NSURL URLWithString:urlStr];;

    if (placeHolderStr == nil) {
        placeHolderStr = @"占位图地址";
    }
    UIImage *placeholderImage = [UIImage imageNamed:placeHolderStr]

    if (radius > 0.0f) {
        //需要手动缓存处理成圆角的图片
        NSString *cacheUrlStr = [urlStr stringByAppendingString:@"radiusCache"];
        UIImage *cacheImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:cacheUrlStr];

        //如果已经存在缓存则直接赋值。
        if (cacheImage != nil) {
            self.image = cacheImage;
        }
        else {    //没有缓存则下载图片。
            [self sd_setImageWithURL:url placeholderImage:placeholderImage completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                if (!error) {
                    //利用上面圆角处理的方法获取圆角图片
                    UIImage *radiusImage = [self al_setImageCornerRadius:image cornerRadius:cornerRadius];
                    self.image = radiusImage;
                    //缓存圆角图片
                    [[SDImageCache sharedImageCache] storeImage:radiusImage forKey:cacheurlStr];
                    //清除原有非圆角图片缓存
                    [[SDImageCache sharedImageCache] removeImageForKey:urlStr];
                }
            }];
        }
    }
    else {
        [self sd_setImageWithURL:url placeholderImage:placeholderImage completed:nil];
    }
}

目前这个方法在某些情况下效果会有点问题,正在继续优化中,持续更新。

总结

  1. 如果能够只用cornerRadius解决问题,就不用优化。
  2. 如果必须设置masksToBounds,可以参考圆角视图的数量,如果数量较少(一页只有几个)也可以考虑不用优化。
  3. UIImageView的圆角通过直接截取图片实现,其它视图的圆角可以通过CoreGraphics画出圆角矩形实现。

参考资料

  1. 小心别让圆角成了你列表的帧数杀手
  2. iOS高效添加圆角效果
请我吃颗糖,鼓励我继续创作!