iOS 深入理解离屏渲染

前言

离屏渲染(Offscreen Rendering),对于这个概念作为iOS开发者相信大家并不陌生,多多少少会有一些了解,比如“设置圆角、mask、阴影会触发 离屏渲染”。那么先引出一个问题?

设置圆角(cornerRadius)一定会触发离屏渲染吗?

我们不妨敲敲代码来测试一下:
读者也可以一边阅读代码 一边猜想下面4组情况 会不会触发离屏渲染。

    //1.按钮1
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(2, 320, 90, 90);
    btn1.backgroundColor = [UIColor redColor];
    btn1.layer.cornerRadius = 45;
    btn1.layer.masksToBounds = YES;
    [self.view addSubview:btn1];
    
    //2.按钮2
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(96, 320, 90, 90);
    [btn2 setImage:[UIImage imageNamed:@"btn.jpeg"] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 45;
    btn2.layer.masksToBounds = YES;
    [self.view addSubview:btn2];
    
    
    //3.img1
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(190, 320, 90, 90);
    img1.image = [UIImage imageNamed:@"btn.jpeg"];
    img1.layer.cornerRadius = 45;
    img1.layer.masksToBounds = YES;
    [self.view addSubview:img1];
    
    //4.img2
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(284, 320, 90, 90);
    img2.image = [UIImage imageNamed:@"btn.jpeg"];
    img2.backgroundColor = [UIColor blueColor];
    img2.layer.cornerRadius = 45;
    img2.layer.masksToBounds = YES;
    [self.view addSubview:img2];
    

你的答案是什么呢?对于上面4组案例我们均 设置了layer.cornerRadius = 45; 那么是否都会触发离屏渲染呢?

我们先来验证一下答案。

打开Simulator的离屏渲染颜色标记,

开启Color Off-screen Rendered

在模拟器运行,结果如下

测试对比图

如图可见,上面4组测试,我们都设置了layer.cornerRadius = 45;。但是,第1组和第3组图并没有触发离屏渲染。至此对于上面引出的问题,我们至少可以得出一个结论:

设置圆角(cornerRadius)不一定会触发离屏渲染!

如果你认为 同时 设置layer.cornerRadius = 45;layer.masksToBounds = YES; 就会触发。那么第1组和第3组测试已经将这个想法否定了。

那么到底什么情况下才会触发离屏渲染呢?

离屏渲染触发的原因

在这里我们先来简单了解一下图像的渲染流程:

渲染流水线

CPU计算、解码——>GPU渲染——>帧缓存区——>视频控制器逐行读取(位图)——>数模转化——>显示。

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。

通常情况下,CPU对将要显示的图像进行计算、解码,然后交给GPU,GPU将渲染好的内容存入帧缓存区,在下一次Runloop到来的时候,由视频控制器逐行扫描帧缓存区的数据,经过可能的数模转化,最终交给显示器显示。

帧缓存区

但是,当图形需要做额外的渲染处理的时候,(例如对所有的图层切圆角处理),此时不能直接将帧缓存中的数据交由视频控制器读取(还未处理完成),而在正常的渲染流程中,我们无法对所有的图层进行圆角处理,因为渲染完毕的已经被帧缓存丢弃,所以我们就需要额外的一个新的缓冲区(离屏缓冲区)来存储我们不能第一时间交给视频控制器读取的数据,等待离屏缓冲区要处理的全部数据渲染、组合完毕,再交给帧缓存区,然后依次走下面的流程。这种在离屏缓冲区渲染的过程我们称之为 离屏渲染

离屏缓冲区

在上面的渲染流水线示意图中我们可以看到,主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“画家算法”,也就是由远到近的方式将图层绘制到屏幕上,绘制近距离图层会有覆盖远图层的逻辑。绘制完一层,就会将该层从帧缓存区中移除(以节省空间)。

画家算法

然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。

了解了 离屏渲染 的概念,我们再来看一下苹果官方对于cornerRadius的描述。

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES causes the content to be clipped to the rounded corners.

The default value of this property is 0.0.

解释:当设置大于0的cornerRadius值时,角半径仅仅适用于layer的backgroundColorborder ,并不适用于图像,除非同时设置了layer.masksToBoundstrue,才会对layer的contents也设置圆角。

结合下面的图来看,当我们设置了layer的cornerRadius的值,会对layer的背景色以及前景框进行圆角处理,当我们同时设置了layer.masksToBounds=YES的时候,contents也会被切成圆角。

延伸一下:当我们对有图像内容的视图仅设置cornerRadius时,表象上不生效的原因,是因为contents默认不会被切。而对于单一无组合的简单的view,未设置图像的image和button我们也不需要masksToBounds。

有了上述知识储备,我们来回头分析一下我们的4组案例。

  • 案例1:
 //1.按钮1
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(2, 320, 90, 90);
    btn1.backgroundColor = [UIColor redColor];
    btn1.layer.cornerRadius = 45;
    btn1.layer.masksToBounds = YES;
    [self.view addSubview:btn1];

案例1,需要圆角,设置了图层的背景颜色,但并没有contents,属于单一图层。对于单一图层,并不需要开辟离屏缓冲区,仅在帧缓存内直接切圆角就可以了,所以,案例1不会触发离屏渲染。

  • 案例2
    //2.按钮2
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(96, 320, 90, 90);
    [btn2 setImage:[UIImage imageNamed:@"btn.jpeg"] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 45;
    btn2.layer.masksToBounds = YES;
    [self.view addSubview:btn2];

案例2,需要圆角,设置了按钮的图片内容,没有设置背景色或边框,但是不要忘了,UIButton如果设置了image,它的内部还有一个UIImageView的图层,所以属于组合图层,当需要设置圆角的时候,不能一次性在帧缓存中直接读取,需要单独对每个图层进行圆角裁剪,这就需要开辟离屏缓冲区用于保存,组合之后交给FrameBuffer,所以案例2会触发离屏渲染。

  • 案例3
    //3.img1
    UIImageView *img1 = [[UIImageView alloc]init];
    img1.frame = CGRectMake(190, 320, 90, 90);
    img1.image = [UIImage imageNamed:@"btn.jpeg"];
    img1.layer.cornerRadius = 45;
    img1.layer.masksToBounds = YES;
    [self.view addSubview:img1];

案例3,需要圆角,设置了imageView的contents,由于layer.masksToBounds = YES;所以图片将被切成圆角,由于layer.cornerRadius = 45;所以背景色以及边框将被切成圆角,但是并没有设置图层的背景色或边框,所以案例3还是单一图层,不会触发离屏渲染。

  • 案例4
    //4.img2
    UIImageView *img2 = [[UIImageView alloc]init];
    img2.frame = CGRectMake(284, 320, 90, 90);
    img2.image = [UIImage imageNamed:@"btn.jpeg"];
    img2.backgroundColor = [UIColor blueColor];
    img2.layer.cornerRadius = 45;
    img2.layer.masksToBounds = YES;
    [self.view addSubview:img2];

案例4,需要圆角,同时设置了imageView的backgroundColor和contents,属于组合图层,会触发离屏渲染。

对于以上4个案例,我们小结一下什么情况会触发离屏渲染

1、由多个图层组成(不止1个)
2、多个图层同时设置圆角(layer.cornerRadiuslayer.masksToBounds组合使用)

纠其根本原因:如果你无法仅仅使用frame buffer来画出最终结果,需要做额外的计算,,那就只能另开一块内存空间来储存中间结果。 进行离屏渲染

哪些情况会触发离屏渲染

  • 1.需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)

iOS9优化过后,单纯的设置cornerRadius并不会触发离屏渲染。

如果要绘制一个多图层且带有圆角并剪切圆角以外内容的容器,就会触发离屏渲染。

  • 将一个layer的内容裁剪成圆角,可能不存在一次遍历就能完成的方法
  • 容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪

此时我们就不得不开辟一块独立于frame buffer的空白内存。而如果只是设置cornerRadius(如不需要剪切内容,只需要一个带圆角的边框),或者只是需要裁掉矩形区域以外的内容(虽然也是剪切,但是稍微想一下就可以发现,对于纯矩形而言,实现这个算法似乎并不需要另开内存),并不会触发离屏渲染。

  • 2.添加了投影的 layer (layer.shadow)

其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去(这只是我的猜测,实际情况可能更复杂)。

不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。

使用阴影必须保证 layer 的masksToBounds = false,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常这样实现一个阴影:

let imageViewLayer = avatorView.layer
imageViewLayer.shadowColor = UIColor.blackColor().CGColor
imageViewLayer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示
imageViewLayer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响
imageViewLayer.shadowOffset = CGSize(width: 5, height: 5)
//设定路径:与视图的边界相同
let path = UIBezierPath(rect: cell.imageView.bounds)
imageViewLayer.shadowPath = path.CGPath//路径默认为 nil
  • 3.设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)

alpha并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。将一对蓝色和红色layer叠在一起,然后在父layer上设置opacity=0.5,并复制一份在旁边作对比。左边关闭group opacity,右边保持默认(从iOS7开始,如果没有显式指定,group opacity会默认打开),然后打开offscreen rendering的调试,我们会发现右边的那一组确实是离屏渲染了。

CALayer的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。GroupOpacity=YES,子layer 在视觉上的透明度的上限是其父 layer 的opacity。

这个属性的文档说明:

The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.

从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。

GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

当父视图的layer.opacity != 1.0时,会开启离屏渲染,opacity并不是分别应用在每一层之上,而是只有到整个layer树画完之后,再统一加上opacity,最后和底下其他layer的像素进行组合

当父视图的layer.opacity == 1.0时,父视图不用管子视图,只需显示当前视图即可。
为了让子视图与父视图保持同样的透明度,从 iOS 7 以后默认全局开启了这个功能。我们可以设置layer的opacity值为YES,减少复杂图层合成

  • 4.使用了 mask 的 layer (layer.mask)
WWDC中苹果的解释,mask需要遍历至少三次!

mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,只有到整个layer树画完之后,再统一加上mask,最后和底下其他layer的像素进行组合。(和group opacity的原理类似)

  • 5.毛玻璃效果
UIBlurEffect

渲染的位图不能直接交给FrameBuffer等待显示,需要经过模糊处理之后才能将最终的结果交给FrameBuffer。

  • 6.采用了光栅化的 layer (layer.shouldRasterize)

如果开启,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。

使用光栅化时,可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。

关于光栅化的使用建议:

  • 如果layer不能被复用,则不建议开启
  • 如果layer不是静态的,需要频繁的被修改(如处在动画之中,imageview的image需要改变,label的text会发生改变等)此时开启光栅化反而影响效率
  • 离屏渲染有时间的限制,缓存内容如果在100ms内没有被复用,则会被丢弃,无法进行复用
  • 离屏渲染的缓存空间也是有限的,超过屏幕像素大小的2.5倍就会失效。
  • 7.绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

UILabel 和 UITextView 要想显示圆角需要表现出与周围不同的背景色才行。想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的contents呈现透明的背景色,文本视图类的 layer 的contents默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。不过 UILabel 上设置backgroundColor的行为被更改了,不再是设定 layer 的背景色而是为contents设置背景色,UITextView 则没有改变这一点,所以在 UILabel 上实现圆角要这么做:

//不要这么做:label.backgroundColor = aColor 以及不要在 IB 里为 label 设置背景色
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
  • 8.edge antialiasing(抗锯齿)

设置 allowsEdgeAntialiasing 属性为YES(默认为NO).

经过测试,开启 edge antialiasing(旋转视图并且设置layer.allowsEdgeAntialiasing = true) 在 iOS 8 和 iOS 9 上并不会触发离屏渲染,对性能也没有什么影响,也许到现在这个功能已经被优化了。

特殊的离屏渲染:CPU的“离屏渲染”

如果我们在UIView中实现了drawRect方法,就算它的函数体内部实际没有代码,系统也会为这个view申请一块内存区域,等待CoreGraphics可能的绘画操作。

对于类似这种“新开一块CGContext来画图“的操作,有很多文章和视频也称之为“离屏渲染”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实所有CPU进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”。

自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是根据苹果工程师的说法,CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。

其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU

为什么要避免离屏渲染

GPU的操作是高度流水线化的。本来所有计算工作都在有条不紊地正在向frame buffer输出,此时突然收到指令,需要输出到另一块内存,那么流水线中正在进行的一切都不得不被丢弃,切换到只能服务于我们当前的“切圆角”操作。等到完成以后再次清空,再回到向frame buffer输出的正常流程。

由于离屏渲染中的离屏缓冲区,是 额外开辟的一个存储空间,当它将数据转存到Frame Buffer时,也是需要耗费时间的,所以在转存的过程中,仍有掉帧的可能。

在tableView或者collectionView中,滚动的每一帧变化都会触发每个cell的重新绘制,因此一旦存在离屏渲染,上面提到的上下文切换就会每秒发生60次,并且很可能每一帧有几十张的图片要求这么做,对于GPU的性能冲击可想而知(GPU非常擅长大规模并行计算,但是我想频繁的上下文切换显然不在其设计考量之中)

开辟额外的空间
上下文切换

那为什么我们明知有性能问题时,还是要使用离屏渲染呢?

离屏渲染是一种手段,设计的目的本身是解决问题的。离屏缓存区是一个临时的缓冲区,用来存放在后续操作使用,但目前并不使用的数据。

1.一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊等。

2.处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。这种情况开发者通过 CALayer 的 shouldRasterize 主动触发的。

尽管离屏渲染开销很大,但是当我们无法避免它的时候,可以想办法把性能影响降到最低。优化思路也很简单:既然已经花了不少精力把图片裁出了圆角,如果我能把结果缓存下来,那么下一帧渲染就可以复用这个成果,不需要再重新画一遍了。

CALayer为这个方案提供了对应的解法:shouldRasterize。一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。有几个需要注意的点:

  • shouldRasterize的主旨在于降低性能损失,但总是至少会触发一次离屏渲染。如果你的layer本来并不复杂,也没有圆角阴影等等,打开这个开关反而会增加一次不必要的离屏渲染
  • 离屏渲染缓存有空间上限,最多不超过屏幕总像素的2.5倍大小
  • 一旦缓存超过100ms没有被使用,会自动被丢弃
  • layer的内容(包括子layer)必须是静态的,因为一旦发生变化(如resize,动画),之前辛苦处理得到的缓存就失效了。如果这件事频繁发生,我们就又回到了“每一帧都需要离屏渲染”的情景,而这正是开发者需要极力避免的。针对这种情况,Xcode提供了“Color Hits Green and Misses Red”的选项,帮助我们查看缓存的使用是否符合预期
  • 其实除了解决多次离屏渲染的开销,shouldRasterize在另一个场景中也可以使用:如果layer的子结构非常复杂,渲染一次所需时间较长,同样可以打开这个开关,把layer绘制到一块缓存,然后在接下来复用这个结果,这样就不需要每次都重新绘制整个layer树了。

如何避免圆角的离屏渲染

圆角引起离屏渲染的本质是裁剪的叠加,导致 masksToBounds 对 layer 以及所有 sublayer 进行二次处理。那么我们只要避免使用 masksToBounds 进行二次处理,而是对所有的 sublayer 进行预处理,就可以只进行“画家算法”,用一次叠加就完成绘制。

可以用以下方案代替直接设置圆角的操作

  • 【换资源】直接更换资源,让UI提供带圆角的图片。
  • 【mask】使用layer.mask属性,增加一个和背景色相同的mask覆盖在最上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
  • 【UIBezierPath】使用贝塞尔曲线绘制闭合圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
  • 【CoreGraphics】重写drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
  • 【CoreGraphics】方式 用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)];

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

//开始对imageView进行画图

UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);

//使用贝塞尔曲线画出一个圆形图

[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];

[imageView drawRect:imageView.bounds];

imageView.image = UIGraphicsGetImageFromCurrentImageContext();

//结束画图

UIGraphicsEndImageContext();

[self.view addSubview:imageView];

  • 【CAShapeLayer 】方式

使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect方法中画出一些想要的图形,CAShapeLayer动画渲染直接提交GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率高,能大大优化内存使用

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; 
imageView.image = [UIImage imageNamed:@"myImg"]; 
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; 
//设置大小 
maskLayer.frame = imageView.bounds; 
//设置图形样子 
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer; 
[self.view addSubview:imageView];

  • 【参考yyimage的写法】
- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius 
 corners:(UIRectCorner)corners 
 borderWidth:(CGFloat)borderWidth 
 borderColor:(UIColor *)borderColor 
 borderLineJoin:(CGLineJoin)borderLineJoin { 
 if (corners != UIRectCornerAllCorners) { 
 UIRectCorner tmp = 0; 
 if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft; 
 if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight; 
 if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft; 
 if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight; 
 corners = tmp; 
 } 
 UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); 
 CGContextRef context = UIGraphicsGetCurrentContext(); 
 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); 
 CGContextScaleCTM(context, 1, -1); 
 CGContextTranslateCTM(context, 0, -rect.size.height); 
 CGFloat minSize = MIN(self.size.width, self.size.height); 
 if (borderWidth < minSize / 2) { 
 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners 
cornerRadii:CGSizeMake(radius, borderWidth)]; 
 [path closePath]; 
 CGContextSaveGState(context); 
 [path addClip]; 
 CGContextDrawImage(context, rect, self.CGImage); 
 CGContextRestoreGState(context); 
 } 
 if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { 
 CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; 
 CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); 
 CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0; 
 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, 
borderWidth)]; 
 [path closePath]; 
 path.lineWidth = borderWidth; 
 path.lineJoinStyle = borderLineJoin; 
 [borderColor setStroke]; 
}

  • 为UIImage类扩展一个实例函数,仿YYImage做法
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
    /* 当前UIImage的可见绘制区域 */
    CGRect rect = (CGRect){0.f,0.f,size};
    /* 创建基于位图的上下文 */
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    /* 在当前位图上下文添加圆角绘制路径 */
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    /* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */
    CGContextClip(UIGraphicsGetCurrentContext());
    /* 绘制 */
    [self drawInRect:rect];
    /* 取得裁剪后的image */
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    /* 关闭当前位图上下文 */
    UIGraphicsEndImageContext();
    return image;
}

使用方法

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)];
/* 创建并初始化UIImage */
UIImage *image = [UIImage imageNamed:@"icon"];
/* 添加圆角矩形 */
image = [image imageWithCornerRadius:50 ofSize:imageView.frame.size];
[imageView setImage:image];

参考阅读:

关于iOS离屏渲染的深入研究
绘制像素到屏幕上
离屏渲染优化详解:实例示范+性能测试
iOS 渲染原理解析

你可能感兴趣的:(iOS 深入理解离屏渲染)