关于iOS下的离屏渲染

211 阅读6分钟

离屏渲染几乎iOS面试时必问的问题,也是在应用开发中对app进行性能优化而必须要清楚的一个问题。那么离屏渲染在什么情况下会出现?为什么会有离屏渲染?离屏渲染的原理是什么?我们如何避免使用离屏渲染?什么情况下会触发离屏渲染?带着这些疑问,来开始我们的探索吧。

离屏渲染在什么情况下会出现?

首先我们通过如下代码构建视图

    //1.按钮有背景和图片
    UIButton *button1 = [UIButton buttonWithType:(UIButtonTypeCustom)];
    [button1 setImage:[UIImage imageNamed:@"longin_wechat"] forState:(UIControlStateNormal)];
    button1.backgroundColor = [UIColor redColor];
    button1.frame = CGRectMake(100, 100, 50, 50);
    button1.layer.cornerRadius = 25;
    button1.clipsToBounds = YES;
    [self.view addSubview:button1];
    
    //2.按钮有图片,没有背景
    UIButton *button2 = [UIButton buttonWithType:(UIButtonTypeCustom)];
    
    button2.backgroundColor = [UIColor redColor];
    button2.frame = CGRectMake(100, 170, 50, 50);
    button2.layer.cornerRadius = 25;
    button2.clipsToBounds = YES;
    [self.view addSubview:button2];
    //3.imageView有背景和图片
    UIImageView *imageV1 = [[UIImageView alloc] initWithFrame:CGRectMake(100, 240, 50, 50)];
    imageV1.backgroundColor = [UIColor blueColor];
    imageV1.image = [UIImage imageNamed:@"longin_wechat"];
    imageV1.layer.cornerRadius = 25;
    imageV1.layer.masksToBounds = YES;
    [self.view addSubview:imageV1];
    //4.imageView只有图片
    UIImageView *imageV2 = [[UIImageView alloc] initWithFrame:CGRectMake(100, 310, 50, 50)];
    imageV2.image = [UIImage imageNamed:@"longin_wechat"];
    imageV2.layer.cornerRadius = 25;
    imageV2.layer.masksToBounds = YES;
    [self.view addSubview:imageV2];

以上的代码都有一个共同的特征,就是都切了圆角。不同之处在于1、3设置了背景和图片;2设置了背景;4只设置了图片。那么这些视图中有哪些会触发离屏渲染呢?

用XCode将代码运行到模拟器上,打开离屏渲染,如下图

我们发现,1,3视图下面出现了黄色的图层,也就是触发了离屏渲染。那为什么2,4视图同样是切了圆角,为什么又没有触发离屏渲染呢?

为什么会有离屏渲染?离屏渲染的原理?

通过对比我们可以发现,1、3视图有背景和图片两个图层;2、4视图只有一个背景或图片的图层。那么计算机在渲染一个图层和两个或多个图层时会有什么不同呢?

1.渲染一个图层的情况

当计算机在渲染一个图层的时候,GPU会把这个图层加载到‘Frame Buffer’帧缓冲区,在下一个runloop屏幕会从帧缓冲区中读出图层并渲染显示,渲染完了以后就会把这个图层的数据从帧缓冲区中释放了。

2.渲染两个图层的情况

当一个显示效果是由A、B两个图层叠加后统一处理才能显示相应的效果时,GPU需要将A、B分别读取出来,然后将他们存到一个缓冲区里面,这个缓冲区就是“offscreen Buffer”离屏渲染缓冲区;当A、B两个图层都加载到离屏渲染缓冲区后,再对他们进行混合,将混合后的结果存到帧缓冲区,然后在下一个runloop显示到屏幕上。

类似的,当出现三个图层混合成才能显示效果的时候,也需要进行如上的操作!于是就有了离屏渲染机制的出现。

如上图:它的显示效果类似UImageView设置了背景颜色,image和边框,分别对应backgroundColor、contents和border三个图层;现在我们要对其进行圆角处理,就需要分别将这三个图层加载到离屏渲染缓冲区,然后进行统一的圆角处理后生成新的图层,并加载到帧缓冲区,等到下一个runloop由于视图控制器显示到屏幕上。

离屏渲染的原理

当app需要使用离屏渲染实现某些特效时,GPU需要分别对生成这个特效所需要的图层进行读取,并存储到离屏渲染缓冲区,然后对存储在离屏渲染缓冲区的这些图层进行统一的处理后混合生成新的图层,并存储到帧缓冲区,等到下一个runloop显示到屏幕上。

但是离屏渲染缓冲区的内存大小的有限的,大小为屏幕像素的2.5倍,并且当触发离屏渲染时CPU/GPU需要进行大量的计算,这会导致一些性能问题,比如由于加载的图层过大,加载的图层太多,而导致CPU/GPU计算速度跟不上屏幕刷新帧率(60FPS)而导致掉帧出现的屏幕卡顿问题。

所以离屏渲染虽然能为我们实现很多特效,并且可以对一些图层进行复用,能提升一些渲染上的效率。但同样也伴随着新的性能问题出现,而且离屏渲染是由系统自动触发的,程序员无法控制。当我们没有必要使用离屏渲染的时候,还是要尽量不要触发离屏渲染。

那么是不是所有的多图层的情况都会出现离屏渲染呢?

我们把视图1的圆角去掉,再看看有没有触发离屏渲染

 //1.按钮有背景和图片
    UIButton *button1 = [UIButton buttonWithType:(UIButtonTypeCustom)];
    [button1 setImage:[UIImage imageNamed:@"longin_wechat"] forState:(UIControlStateNormal)];
    button1.backgroundColor = [UIColor redColor];
    button1.frame = CGRectMake(100, 100, 50, 50);
    //button1.layer.cornerRadius = 25;
    //button1.clipsToBounds = YES;
    [self.view addSubview:button1];

看看,把圆角效果去掉后,虽然视图1有背景和图片两个图层,但没有触发离屏渲染。从而可以得出结论,多图层并不会触发离屏渲染,只有当满足触发条件时才会触发离屏渲染,比如圆角。

我们如何在不触发离屏渲染的情况下使用圆角呢?

下面提供一些方案

方案一

方案二

方案三

方案四

方案五

什么情况下会触发离屏渲染?

这里总结了一些触发离屏渲染的情况,当一个多图层视图出现如下操作时,会触发离屏渲染

  • 1.使用了mask的layer(layer.mask);
  • 2.需要进行裁剪的layer(layer.masksToBounds/view.clipsToBounds)
  • 3.设置组透明度为YES,并且透明度不为1的layer(layer.allowsGroupOpacity/layer.opacity)
  • 4.添加了阴影的layer(layer.shadow);
  • 5.采用了光栅化的layer(layer.shouldRasterize);
  • 6.绘制了文字的layer(UILabel,CATextLayer,Core Text等)

多图层下的视图是如何显示这些图层的?

如上图所示的三张图的绘制逻辑如下:

  • 第一步:绘制远处的山;
  • 第二步:绘制中间的草原;
  • 第三步:绘制离得最近的树。

这种由远及近的绘制方式称为油画绘制法,我们的屏幕在绘制视图时也是采用这种方式。先绘制父视图,再绘制子视图,如下图:

最终要显示的效果是(3),在绘制时首先将蓝色背景图层加载到帧缓冲区,然后显示到屏幕上,显示后将其从帧缓冲区清除;然后加载黑色的树到帧缓冲区,显示到屏幕上,再从帧缓冲区移除;最后加载太阳到帧缓冲区,并显示到屏幕,清除帧缓冲区的记录。这样就得到了最后的效果了。 通过这种绘制逻辑,我们发现,图层越多,CPU/GPU需要进行的计算和绘制就越多,所以我们在界面开发时,要尽可能的减少图层和子视图的数量,来提高app的性能。

总结

离屏幕渲染是为了实现多图层的特效而诞生的一种渲染机制,需要开辟屏幕像素2.5倍内存大小的离屏缓冲区,并做大量的计算,非常消耗计算机的性能。所以在日常的开发中我们要尽可能的避免触发离屏渲染,提升app的性能。