阿里、字节:一套高效的iOS面试题(九 - 视图&图像相关 - 下)

2,024 阅读27分钟

视图 & 图像相关

撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!

原文题目来自:阿里、字节:一套高效的iOS面试题

阿里、字节:一套高效的iOS面试题(九 - 视图&图像相关 - 上

三、UI 绘制

2、UI 显示到屏幕上

先看一下来自 绘制像素到屏幕上 的图:

Display:显示器的主要作用就是显示 RGB 数据,大部分显示器都具有调整自身显示偏移、亮度、饱和度的能力。总结起来就是对传入的 RGB 数据进行处理。

GPU:Display 的上一层是图形处理单元 GPU,GPU 是专门为图形高并发计算而量身定做的处理单元。GPU 可以高效地合成不同的纹理。

GPU Driver:GPU 的驱动, 是直接和 GPU 交流的代码。是它为不同的 GPU 定制了统一的接口。典型的接口由 OpenGL / OpenGL ES,当然 Apple 现在有了自家的 Metal。

OpenGL:全称 Open Graphics Library,是一个和 GPU 直接交流的标准化接口,提供了 2D 与 3D 图像渲染的 API。OpenGL 代码可以直接操作 GPU,实现最高的渲染效率。

OpenGL ES:全称 OpenGL for Embeded System,是 OpenGL 的一个子集,主要针对手机等嵌入式设备。

Core Graphics:Quartz 2D 的一个高级绘图引擎。Core Graphics 是对底层 C 语言的封装,其中提供大量的底层地,轻量级的 2D 渲染 API。(前缀为 CG,如 CGPath,CGColor)

Core Animation:Apple 提供的一套基于绘图的动画框架。但不止是动画,它同样是绘图的根本。(前缀为 CA,如 CALayer)

Core Image:iOS 提供的图形处理框架,主要用于图像识别,给图片添加滤镜。

2.1 像素和点

  • 像素

每一个像素均由三个颜色组件构成:红、绿、蓝。只要根据需要将三个独立的颜色以给定的数值显示到一个屏幕像素上,就可以达到我们想要的结果。幸运的是,我们不需要从这里开始写代码。

通常我们使用一个字节(也即是 8 位,最大 255)来表示一个颜色单位的数值,也就是说,一个颜色单位显示的亮度由一个字节来控制。这样算的话,三个颜色单位就需要 3 个字节,比如:白色的数值是 0xFFFFFF(当红绿蓝三个单元的显示亮度同时达到最大值时,最终合成结果就是白色),黑色就是 0x000000(三个单元的显示亮度同时为 0,就是一片漆黑了)。

关于这一点,既可以在 PS 中验证,也可以将图片读取并转化为 Data 查看:

UIImage *image = [UIImage imageNamed:@"theFox.jpg"];
CFDateRef data = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
NSLog(@"%@", data);
CFRelease(data);


/// 输出:都是白色对不对,因为这张图大部分都是白色啊~~~~
{length = 1890624, bytes = 0xffffffff ffffffff ffffffff ffffffff ... ffffffff ffffffff }

等等,为什么打印出来的是 ffffffff,而不是 ffffff。多出来了 ff 其实就是 透明度 alpha 了,它也由一个字节表示。这张图的分辨率为 687x688,那我们算一下: 687 x 688 x 4 = 1890624,结果跟 log 中的 length 完全相符。

日常开发中,我们进场会指定 UIView.frame = CGRectMake(50, 50, 200, 200),这里的 50 等数值都是开发使用的逻辑坐标系中的点。使用原生绘制 UIKit、Core Animation、Quartz 时,其绘制坐标系与视图坐标系都是逻辑坐标系。而屏幕中的像素点则被称为物理坐标系。

系统会自动根据视图的点坐标映射到设备的像素上去。但由于尺寸等因素,物理坐标系的像素与逻辑坐标系的点并不一定是一一对应的。开发中使用点代替像素的主要目的就是为了保证视图在各类设备上都呈现出合适的效果。具体多少像素对应一个点,这是由系统根据设备硬件决定的。

我们都知道视网膜屏幕这个概念,它首次出现在 iPhone 4 上。在视网膜屏幕中,一条线的绘制对应着多个像素的线条宽度。这种映射关系使普通显示屏与视网膜屏幕上的视图大小基本保持一致。

在 iOS 中,UIScreen、UIView、UIImage、CALayer 都提供用于描述像素和点之间的映射比例。例如 UIView 的 contentScaleFactor,CALayer 的 contentsScale,而 UIScreen 与 UIImage 是 scale。在普通显示屏中,该值为 1.0,视网膜屏幕中为 2.0,而 plus 系统为 3.0。(这些属性都是出现于 iOS 4)

2.2 纹理合成

一个纹理,就是一个包含 RGBA 值得矩形存储空间。了解 AR 或者 U3D 开发的朋友应该明白这个概念。纹理,在 Core Animation 中就相当于 CALayer。

这样理解下来,每一个 layer 都是一个纹理,所有的纹理按照特定层级顺序以某种方式堆叠起来所得到的结果纹理就是最终显示在屏幕上的。对于屏幕上的每一个像素,GPU 都需要计算出具体的 RGB 值。

因此,我们只需要搞清楚一个像素的合成便能理解成哥纹理的合成了。假定两个像素 S 和 D(S 在顶端),那么 (S + D) -> R 的合成算法为:

R = S + D * (1 - S.a)   /// S.a 是 S 像素的透明度

合成结果 = 源色彩(顶端纹理) + 目标色彩(第一层的纹理) * (1 - 源色彩的透明度)

当然,在这个公式里,所有的像素的颜色都已经预先计算过其透明度了。

假定 S 为红色(1, 0, 0),D 为蓝色(0, 0, 1):

  1. S.alpha = 1:

    源色彩完全不透明,S = (1, 0, 0);

    目标色彩 D = (0, 0, 1) * (1 - 1) = (0, 0, 0);

    合成结果为 R = (1, 0, 0),红色。

  2. S.alpha = 0.5:

    源色彩为 50% 透明,此时 S = (0.5, 0, 0);

    目标色彩为 D = (0, 0, 1) * (1 - 0.5) = (0, 0, 0.5);

    合成结果为 R = (0.5, 0, 0.5),紫色。

2.2 图层透明

当源纹理完全不透明时,合成结果就等于原纹理。这可以节省 GPU 很大的工足量,这样只需要单纯的拷贝源纹理而不需要合成所有的像素值。那么,有没有这样一种方法告诉 GPU 纹理上的像素到底是不是透明的呢?

CALayer 都存在一个名为 opaque,类型为 BOOL 的属性。这个单词翻译过来就是 “不透明的”。当我们将这个属性设置为 YES 时,GPU 将不会做任何合成,而是直接从这个 layer 拷贝,完全不考虑其下方的任何东西,这可以大大节省 GPU 的工作量。

所以,UIView 才会将从 layer 包装而来的 opaque 默认为 YES,这是一个相当有用的优化。【但是,CALayer 的 opaque 属性默认值为 NO】

我们至少有两种方便的方式来查看当前布局中哪些 layer 是透明的:

  1. 工具 Instruments 中的 color blended layers 功能【目前已集成到 Xcode -> Debug -> View Debugging -> Rendering 中】;

  2. 模拟器 Simulator 的菜单 Debug -> Color Blended layers

所以,如果知道一个 layer 是不透明的,将他的 opaque 设置为 YES。

如果加载一张没有 alpha 通道的图片并显示在 UIImageView 上,上述操作会自动设置。但一个没有 alpha 通道的图片与一个带有透明通道但任何地方 alpha 都为 1 的图片,这两种情况是完全不同的。在后一种情况下,Core Animation 需要假定是否存在像素的 alpha 值不为 1。

在 Finder 中,可以使用 Get Info(显示简介)并检查 More Info (更多信息)部分,来确定图片是否包含 alpha 通道:

2.3 像素对齐

到现在为止,我们都考虑的是像素完美对齐的情况。当所有像素都是对齐的时候,我们得到相对简单的数学公式。当 GPU 需要计算屏幕上一个像素是什么颜色时,只需要将每个 layers 上对应的单个像素合成到一起就可以了。或者,如果顶层纹理是不透明度的,此时 GPU 简单拷贝顶层纹理的像素即可。

当一个 layer 上的像素与屏幕上的像素完美对齐时,这个 layer 就是像素对齐的。造成不对齐的原因主要有两个。第一个就是 scale,当一个纹理放大或缩小的时候,纹理的像素便不会和屏幕的像素对齐。另一个原因便是纹理的起点不在像素的边界上。

在这两种情况下,CPU 需要做额外的计算。它需要将源纹理上的多个像素混合一起,生成一个用于合成的值。在像素对齐的情况下,GPU 需要做的工作并不多。

有两种方便的方式来检查这个问题:

  1. 工具 Instrucments 中的 Color Misaligned Images 功能【目前已集成到 Xcode -> Debug -> View Debugging -> Rendering 中】;

  2. 模拟器 Simulator 的菜单 Debug -> Color Blended layers

2.4 深入 CALayer

磨刀不误砍柴工,UIView 的绘制其实就是 CALayer 的绘制,我们先从这里开始吧。

如何为 layer 提供 contents

使用 Image

直接将 CGImageRef 对象赋值给 contents 属性即可。其好处在于 layer 直接使用该 CGImageRef 对象,不创建副本(在多个地方使用相同图像的话,可以节省内存)。

但是在 retain 屏幕上,我们需要设置 contentsScale 属性。

让 delegate 提供

当我们使用 delegate 为 layer 提供显示内容的时候。我们可以选择实现 displayLayer: 方法或 drawLayer:InContext 方法。

如果同时重写两个方法,那么只会调用 displayLayer:

  • displayLayer:

此时需要我们自己创建位图进行绘画,最后赋值给 contents 属性。

这里的操作完全可以在后台线程执行,也就是 异步绘制

- (void)display {

    /// self.contents = (__bridge id)[UIImage imageNamed:@"theGirl.JPG"].CGImage;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 100, 100)];
        [[UIColor systemPinkColor] setFill];
        [path fill];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)image.CGImage;
        });
    });
}


LyLayer *theLayer = [LyLayer new];
theLayer.frame = self.view.bounds;
[self.view.layer addSublayer:theLayer];

[theLayer setNeedsDisplay]; /// 记住这句
  • drawLayer:InContext

重写 drawLayer:InContext 方法时,Core Animation 会自动为我们创建好一个位图,和一个图形上下文 CGContextRef。我们需要做的就是使用这个 CGContextRef 来绘制我们想要的内容。

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s", __func__);

    CGContextAddArc(ctx, 80, 80, 10, 0, 2 * M_PI, 1); // 画圆
    CGContextSetLineWidth(ctx, 3); // 设置线粗
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); // 画线的颜色
    CGContextStrokePath(ctx); // 画线
}
子类化 CALayer

这种情况下,可以重写 displaydrawInContext: 方法来绘图。

  • display

这是绘制方法的主入口,重写该方法可以完成掌控绘制流程,但是这就意味着需要我们来创建本来就要分配给 contents 的 CGImageRef。

这里的演示代码可以直接套用 displayLayer: 的。

  • drawInContext:

如果只是想绘制内容,重写这个方法才是较好的选择(layer 会自动给我们创建后备存储)

这里的演示代码也可以套用上边的 drawLayer:InContext:


我来画一个流程图:

NOTE UIView 的 drawRect: 是从 drawInContext: 调用过去的。

如果创建 view 自动创建的 layer

如果这个 layer 是创建 view 而自动创建的,那情况有所不同。

如果 view 没有实现 drawRect: 方法,上述四个方法一个都不会被调用到。

如果 view 实现了 drawRect: 方法,也分两种情况:

  1. view 实现了 displayLayer: 方法,此时的调用栈是:

  2. view 没有实现 displayLayer: 方法,此时的调用栈是:

绘制方法的调用时机

  • 改变 bounds 是不会调用 绘制方法的。

除非其属性 needsDisplayOnBoundsChange 为 YES(就像 UIView 的 contentModeUIViewContentModeRedraw 一样)。

无论通过设置 contents 还是自己在 layer 上绘图,都会快照,这个后备存储里边保存着 layer 的图像缓存。当 layer 改变大小时,只需要拉伸这个图像缓存就好了。

  • layer 不会主动重绘

无论是 display 还是 drawInContext: 方法,CALayer 都不会主动去调用这些绘制回调方法的。需要显示或重绘时,需要我们手动调用 [layer setNeedsDsiplay] 方法。

UIView 首次展示时,系统会自动调用其 setNeedsDisplay,而且会自动传递给这个 view 的 layer,所以 view 自带的 layer 无需调用 setNeedsDisplay 方法。

这里在 视图绘制周期 ,以及 layer 的绘制方法 这两节的说法一致。

  • 绘制回调的优先级

display 系列方法内部是通过给 contents 设置 CGImage 来完成绘制目的的。而 draw系列方法是调用 CoreGraphics 的 API 。

一旦 delegate 响应了 displayLayer: 方法,draw 系列方法是没有出场机会的。

这是由于 两个系列方法的调用机制来决定的。也就是在讲 drawInContext: 方法时的那张图。

CALayer 的后备存储 backing store

WWDC 2012: iOS Performance: Graphics And Animations

这是 WWDC 2012: iOS Performance: Graphics And Animations 的一张图。每一个 CALayer 都有一个像素位图的后备存储,它会被映射成 GPU 上的一个纹理图屏幕上显示出来。

但是并不是所有情况这个后备存储实际存在。当我们使用 displaydrawRect: 准备在 layer 上绘图时,layer 就会自动创建一块与 layer 相同大小的内存区域,在之后绘图的结果就保存在这块区域中,而这块区域就被称为 backing store

准确来说,并不是 displaydrawRect: ,而是 drawRect: 这一个。

看一下 display 这个方法的官方介绍:

Do not call this method directly. The layer calls this method at appropriate times to update the layer’s content. If the layer has a delegate object, this method attempts to call the delegate’s displayLayer: method, which the delegate can use to update the layer’s contents. If the delegate does not implement the displayLayer: method, this method creates a backing store and calls the layer’s drawInContext: method to fill that backing store with content. The new backing store replaces the previous contents of the layer.

重点在斜体加粗那一句。如果 delegate 没有实现 displayLayer: 方法,这个方法将创建一个后备存储

如果我们将一个 CGImage 赋值给 contents,那么 layer 就不会创建这个后备存储,此时 layer 的 contents 就是我们传进去的 CGImage,在渲染时会直接拷贝这个 CGImage 到帧缓冲区中。

2.4 UIView 的绘制流程

图片来自 iOS——图像显示原理以及UI流畅性优化方案

iOS——图像显示原理以及UI流畅性优化方案

调用 view 的 setNeedsDisplay ,该方法内部调用这个 view 的 layer 的同名方法,这个 layer 被标记为 dirty。随后在当前 runlop 快要结束的时候调用 CALayer.display 方法,才会进行当前视图真正的绘制流程。

CALayer.display 方法内部会先判断该 layer 的 delegate 是否响应 displayLayer 方法。如果无法响应,就会进入系统的绘制流程中;如果响应,就会调用异步绘制的接口。

系统绘制流程

这是我自己经过测试,画出的系统绘制流程图:

系统绘制流程

这是证据:

drawRect: 调用栈

另外,如果在 LyView 中不从写 drawRect: 这个方法,就算 layer 重写 drawInContext: 且 LyView 重写 drawLayer:InContext:,重写的这两个方法也不会被调用,同时符号断点 [UIView drawRect:] 也不会进入。

总结下来,系统绘制的流程为:

  1. 判断 view 是否实现了 drawRect: 方法?

  2. 如果没有实现,走不为人知的流程。。。

  3. 如果实现了,就是上边的流程图。

异步绘制流程

如果 layer 的 delegate 实现了 displayLayer: 方法,就可以进入异步绘制的流程中。此时,什么 drawInContext:drawLayer:InContext:drawRect: 都没有出场机会的。

不过,进入异步绘制时,我们需要负责创建对应位图 bitmap,并将内容绘制在这个位图中。绘制完成后,将这个 bitmap 设置为 layer 的 contents。

其流程如下:

异步绘制流程

2.5 离屏渲染

什么是离屏渲染?

  • On-Screen Rendering

在屏渲染:GPU 的渲染操作在当前用于显示的帧缓冲区中进行的。

  • Off-Screen Rendering

离屏渲染:GPU 在当前用于显示的帧缓冲区之外新开辟一个缓冲区进行渲染操作的。

正常情况下,与双缓冲机制 GPU 在当前用于显示的帧缓冲区内渲染下一帧画面。这这种情况下渲染出来的画面可以直接显示在屏幕上。而如果由于我们设置某些特殊的 UI 视图属性,从而触发了在预合成之前无法用于直接显示的指令,就会触发离屏渲染来预处理这部分内容。

因为需要进行预处理操作来预合成这部分无法直接用于显示的内容,GPU 需要先开辟一块另外的缓冲区,并将渲染上下文 Rendering Context 切换到这块区域。然后 GPU 就触发 OpenGL 多通道渲染管线来进行这部分内容的预合成操作,执行完成之后再将上下文切换回原本用于显示的缓冲区。

总结下来:

  1. 创建一块缓冲区:GPU 无法在某一个 layer 渲染完成之后,再回过头来改变其中的某个部分——这一 layer 之前的若干 layer 像素数据已经在渲染过程被永久覆盖了。对于一个 layer,除非能找到一种通过单次遍历就能完成渲染的方法,否则只能另开一片内存来完成多次的修改操作;

  2. 切换渲染上下文:从用于屏幕显示的缓冲区切换到刚刚创建的缓冲区;

  3. 预合成:触发 OpenGL 多通道管线执行需要预合成内容的操作;

  4. 切换渲染上下文:将预合成的结果拷贝至屏幕缓冲区,从创建的缓冲区切换回用于屏幕显示的缓冲区;

这一系列操作会增加 GPU 的工作量,尤其是切换上下文(必须刷新其渲染管线和屏障)。所以,在日常开发中,应尽量避免离屏渲染。

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

如果我们在 UIView 中实现了 drawRect: 方法,就算其函数体内没有实际代码,系统依然会为这个 view 申请一块内存区域和一个图像上下文,等待 Core Graphics 可能的绘画操作。这也就是上边所说的 backing store 和 CGContextRef。

因为不是直接把绘制结果放进用于显示的缓冲区中,而是在其他地方执行这些操作。所有 CPU 进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由 GPU 管理的帧缓冲区中,只能暂时存放在别的内存区域,所以也称为 “离屏渲染”。

但是,根据 Apple 工程师的说法 这并不是真正的离屏渲染。除此之外,还有一个证据:如果我们在 UIView 中实现了 drawRect: ,无论是 Xcode -> Debug -> View Debugging -> Rendering -> Color Offscreen-Rendered Yellow 还是 Simulator -> Debug -> Color off-screen Rendered 都没有把这部分标记为黄色。

有趣的是, UINavigationBarUITabBar 、 辅助触摸 、App 切换器 都是 黄色的。。。就不截图了,有兴趣的朋友自己玩玩哈

离屏渲染到底哪里不好?

2014 WWDC - Advanced Graphics and Animations for iOS Apps 中,Apple 以 UIVisualEffectView 为例描述了 GPU 的处理逻辑,这里有 5 个 Rendering Pass。上边的蓝色为 Tiler 操作的时间分布,红色为 Renderer 操作。

Tiler 是什么?看这个,Apple 也描述了 Core Animation 的渲染机制:

GPU 大部分时间都花在 Renderer 操作上,其中最后一个 Rendering Pass 为在屏渲染,也就是说 UIVisualEffectView 存在四个离屏渲染的 Rendering Pass。

Rendering Pass 之间存在黄色的竖条,它叫无用时间 Idle Time,是上下文转换 Context Switch 的时间。一个 Context Switch 大概会占用 0.1ms - 0.2ms,UIVisualEffectView 有四次 Context Switch,所以其所有 Rendering Pass 会积累 0.4ms - 0.8ms 的 Idle Time。看起来很少,但是每一帧的绘制时间只有 1000 / 60 = 16.67ms。

总结下来,离屏渲染不好的地方在于:

  1. 需要更多的 Rendering Pass,加大 GPU 的工作量;

  2. Rendering Pass 之间需要 Context Switch,导致存在不少的 Idle Time。

哪些操作会触发离屏渲染?

本节全部操作都已开启 Simulator -> Debug -> Color Off-screen Rendered。

前菜:clipsToBounds 与 masksToBounds

clipsToBounds 是 UIView 的属性:subview 是否才叫到这个 view 的边界。

masksToBounds 是 CALayer 的属性:sublayer 是否裁减到这个 layer 的边界。

其实这两个属性的作用是一样。前边我们说到,UIView 封装了 CALayer 的大部分属性,而 clipsToBounds 也是从 masksToBounds 得到的一个数据。

在设置 view 的 clipsToBounds 时,真正设置的就是 layer 的 masksToBounds 。

圆角 cornerRadius(> 0) + masksToBounds(YES)

先看一段 Apple 官网文档对 cornerRadius 的描述:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background.

设置正数半径将使 layer 在其背景中绘制圆角。

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.

默认情况下,cornerRadius 仅仅作用于 layer 背景颜色和边框,不会作用于 layer.contents 上的图像。

However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

然而,masksToBounds = YES 会导致整个 contents 被裁剪为圆角。

也就是说,单单设置 cornerRadius 只能影响 背景颜色 backgroundColor边框 border

for (int i = 0; i < theArray.count; ++i) {
    CGFloat viewX = hMargin + (viewWidth + hMargin) * i;
    CGFloat viewY = 80;
    
    /// 第一行
    UIView *view0 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
    [self.view addSubview:view0];
    view0.backgroundColor = [UIColor grayColor];
    view0.layer.cornerRadius = LyGetHeight(view0) * 0.5;
    view0.layer.borderColor = [UIColor blackColor].CGColor;
    view0.layer.borderWidth = 2;
}

  • 结论一: 单纯设置 cornerRadius 并不会触发离屏渲染。

接下来,我们将左边的 view 设置为 masksToBounds = YES

view0.layer.masksToBounds = (0 == i);

这里也没有触发离屏渲染,这貌似有悖 cornerRadius + masksToBounds 会触发离屏渲染

那我们给这个 view 加一个 subview 试试:

UIView *subview0 = [[UIView alloc] initWithFrame:CGRectMake(20, 0, viewWidth + 40, 80)];
subview0.backgroundColor = [UIColor redColor];
[view0 addSubview:subview0];

UIView with subview

  • 结论二:masksToBounds 不会触发离屏渲染。

针对结论二的解决方案就是:不设置 masksToBounds = YES,大多数 UIView 此属性默认值为 NO(UITextView 为 YES,为了保险可以显式设置)。

常用控件之 UILabel(此时我们需要使用设置 label.layer.backgroundColor 来代替 label.backgroundColor):

/// 第二行  /// UILabel
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[self.view addSubview:label1];
label1.text = theArray[i];
label1.textAlignment = NSTextAlignmentCenter;
label1.layer.cornerRadius = LyGetHeight(label1) * 0.5;
if (0 == i) {
    label1.backgroundColor = [UIColor grayColor];
} else {
    label1.layer.backgroundColor = [UIColor grayColor].CGColor;
}

UILabel

常用控件之 UITextView:

/// 第三行  /// UITextView
UITextView *textView2 = [[UITextView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[self.view addSubview:textView2];
[textView2 setText: @"我是 UITextView"];
textView2.backgroundColor = [UIColor grayColor];
textView2.layer.cornerRadius = LyGetHeight(textView2) * 0.5;
if (1 == i) textView2.layer.masksToBounds = NO;

UITextView

常用控件之 UIImageView:

/// 第四行  /// UIImageView
UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:imageView3];
if (0 == i) {
    imageView3.image = [UIImage imageNamed:@"vortex.jpeg"];
    imageView3.layer.cornerRadius = LyGetHeight(imageView3) * 0.5;
    imageView3.layer.masksToBounds = YES;
} else {
    imageView3.image = [[UIImage imageNamed:@"vortex.jpeg"] drawCornerInRect:imageView3.bounds cornerRadius:LyGetWidth(imageView3) * 0.5];
}

解释:iOS 9.0 之后 UIImageView 设置圆角不会触发离屏渲染,但如果是阴影依然会触发。

常用控件之 UIButton:

/// 第五行 /// UIButton 设置圆角图片
UIButton *button4 = [[UIButton alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:button4];
if (0 == i) {
    [button4 setImage:[UIImage imageNamed:@"vortex.jpeg"] forState:UIControlStateNormal];
    button4.layer.cornerRadius = LyGetHeight(button4) * 0.5;
    button4.layer.masksToBounds = YES;
} else {
    [button4 setImage:[[UIImage imageNamed:@"vortex.jpeg"] drawCornerInRect:button4.bounds cornerRadius:LyGetHeight(button4) * 0.5]
             forState:UIControlStateNormal];
}

  • 结论三:存在 subview 的 UIView,设置 cornerRadius(> 0) + masksToBounds(YES) 会触发离屏渲染。
阴影 shadow

设置阴影时,注意一定更要设置 阴影透明度 shadowOpacity 这个属性,其默认值为 0。

要想设置阴影导致的避免离屏渲染,只需要在正常设置阴影之后为该 layer 设置一个由贝塞尔曲线 UIBezierPath 生成的 shadowPath 即可。

/// 第六行 /// 阴影 shadow
UIImageView *imageView5 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[self.view addSubview:imageView5];
imageView5.image = [UIImage imageNamed:@"vortex.jpeg"];
imageView5.layer.shadowColor = [UIColor blackColor].CGColor;
imageView5.layer.shadowOffset = CGSizeMake(5, 5);
imageView5.layer.shadowRadius = 5;
imageView5.layer.shadowOpacity = 0.8;
if (1 == i) {
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView5.bounds];
    imageView5.layer.shadowPath = path.CGPath;
}

  • shadow 会触发离屏渲染,除非设置 shadowPath
遮罩 mask

先看下 mask 绘制的流程:

Mask 绘制流程

一共有三步,对应三个 Rendering Pass。最后的 Compisiting pass 输出到最后的帧缓存,是在屏渲染。而前面的 pass1 和 pass2 是绘制到纹理 texture 供最后的 Compisiting pass 所用,即离屏渲染。

遮罩最常用的就是 部分圆角 了。部分圆角的原理便是贝塞尔曲线:

我们来改写一下 左边的 label1:

/// 第二行  /// UILabel 部分圆角
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 40)];
[scrollView addSubview:label1];
label1.text = theArray[i];
label1.textAlignment = NSTextAlignmentCenter;
label1.layer.cornerRadius = LyGetHeight(label1) * 0.5;
if (0 == i) {
    label1.backgroundColor = [UIColor grayColor];
    
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:label1.bounds
                                               byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
                                                     cornerRadii:CGSizeMake(20, 20)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.path = path.CGPath;
    label1.layer.mask = maskLayer;
    
} else {
    label1.layer.backgroundColor = [UIColor grayColor].CGColor;
}

通过 mask,虽然实现了圆角,却触发了离屏渲染,有点不值当。

其实 masksToBounds 也是通过 mask 来实现的。

  • mask 会触发离屏渲染
组透明 allowsGroupOpacity(YES) + opacity(< 1)

When the value is true and the layer’s opacity property value is less than 1.0, the layer is allowed to composite itself as a group separate from its parent.

若 allowsGroupOpacity 为 true 且这个 layer 的 opacity 小于 1,这个 layer 会被允许从 superview 独立出来成一个组。

This gives correct results when the layer contains multiple opaque components, but may reduce performance.

若这个 layer 包含多个不透明的组件,这种情况会表现的比较完美,但是会影响性能。

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 true for apps linked against the iOS 7 SDK or later and false for apps linked against an earlier SDK.

默认值从 info.plist 读取 UIViewGroupOpacity ,若不存在则为 YES。后边的不翻译了,谁现在还从 iOS 6 开始支持!!!!!

说人话,当 allowsGrounOpacity 为 true 时,layer 将从 superlayer 继承其 opacity 值,不过这里继承的是最大的 opacity。当 layer 可以设置比 superlayer 更低的值,但无法超过 superlayer。并且独立出来,怎么独立呢?

/// 第七行 /// 组透明 allowsGroupOpacity
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 60)];
[scrollView addSubview:view6];
view6.backgroundColor = [UIColor grayColor];
view6.layer.opacity = 0.5;
view6.layer.allowsGroupOpacity = (0 == i);

UIView *subview6 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 100)];
[view6 addSubview:subview6];
subview6.backgroundColor = [UIColor redColor];

就是这个独立法!!!但是会触发离屏渲染:

测试的时候,注意 superview 本身一定要有点内容,至少设置个背景色。

  • 总结下来:allowsGroupOpacity 触发离屏渲染的条件是 allowsGroupOpacity(YES) + opacity(< 1) + 存在 sublayer 或 背景图
抗锯齿 allowsEdgeAntialiasing(YES) (貌似已优化)

allowsEdgeAntialiasing 决定 layer 是否允许执行反锯齿。默认值从 info.plist -> UIViewEdgeAntialiasing 读取,不存在则为 false。

edgeAntialiasingMask 决定 layer 如何反锯齿(left、right、top、bottom)。默认值为所有边界。

/// 第八行 /// 反锯齿 allowsEdgeAntialiasing / edgeAntialiasingMask
UIImageView *imageView7 = [[UIImageView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, viewWidth)];
[scrollView addSubview:imageView7];
imageView7.image = [UIImage imageNamed:@"vortex.jpeg"];

CATransform3D trans = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
trans = CATransform3DScale(trans, 1.5, 1.5, 1);
imageView7.layer.transform = trans;

if (0 == i) {
    imageView7.layer.allowsEdgeAntialiasing = YES;
    imageView7.layer.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge;
}

经测试,开启 allowsEdgeAntialiasing 并 layer 并不会触发离屏渲染,或许已经优化。(经查阅资料,发现此操作从 iOS 8 已经不会触发离屏渲染了)

毛玻璃 UIBlurEffect
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
blurEffectView.frame = CGRectMake(viewX, viewY, viewWidth, viewWidth);
[scrollView addSubview:blurEffectView];

还是添加了一个 effect 为 UIBlurEffectUIVisualEffectView 而已,其他啥也没做,就黄了!!!

栅格化 shouldRasterize(YES)

shouldRasterize 决定 layer 在组合之前是否渲染成一个位图纹理。

当设置 shouldRasterize = YES 时,layer 将会在自身坐标空间被渲染成一个纹理位图,然后才与其他的内容组合到最终结果。阴影效果和任何 滤镜 都会被栅格化并包含在这个纹理位图中。

当值被设置为 false 时,只要可以,layer 都会被直接混合到最终结果中。但是如果某些合成模型需要,layer 还是可能被提前栅格化,比如滤镜。

默认值为 false。

Rasterization

根据这张图多说一点:

  1. 使用 GPU 一次性混合成图像;

  2. 提前渲染的这个纹理位图会被缓存起来,但是超过 100ms 不适用就会被释放;

  3. 更新内容是会发生额外的离屏渲染流程;

  4. 不要过度使用,缓存大小为 2.5 倍的屏幕尺寸。

/// 第九行  /// shouldRasterize
UIView *view8 = [[UIView alloc] initWithFrame:CGRectMake(viewX, viewY, viewWidth, 60)];
[scrollView addSubview:view8];
[view8 setBackgroundColor:[UIColor grayColor]];
view8.layer.shouldRasterize = (0 == i);

离屏渲染为什么要存在?

先上结论:正确使用离屏渲染可以优化性能。

从上一张图提两句话出来:1. 这个纹理位图会被缓存起来,更新内容才会重绘。这句话意味着:只要我们不更新内容,那就可以直接使用这份缓存来显示。

那么,对于某些静态内容,我们完全可以设置 shouldRasterize = YES。比如 UITableViewCell 的阴影效果与部分圆角效果。

以下内容来自于 从OpenGL再说离屏渲染

我们都知道,GPU 是专门为图形而生的,它非常适合做简单运算,做大量重复的工作。对应 Tiler 中的顶点运算,Renderer 中的混合着色等都很适合在 GPU 上并行运算。GPU 一次只能绘制简单的图元 Primitives,对应到 OpenGL 中就是 点 GL_POINTS、线 GL_LINES、三角形 GL_TRIANGLES

所有复杂图形都是一个个三角形组成的,普通 Layer 由两个三角形组成,GPU 只需要一个 Rendering Pass 就能完成绘制。但是 mask 效果 是将一个 layer 作为 “形状” 来绘制另一个 layer,这种 “形状” 是无法通过点、线、三角形这些基本图元来描述,因此 mask 效果无法用 GPU 一次性绘制出来,只能通过多步组合绘制出来。所以 mask 的绘制流程分为三步。

Rasterization

还是这张图,Rasterization 会使用 GPU 将多个 Layer 绘制到一个纹理位图中,并且这个纹理位图会被缓存起来,以便后续直接使用缓存进行渲染。

在 Rendering 阶段,由一个操作叫 颜色混合 ,对应到每一个像素点,绘制时取 renderBuffer 中的原有颜色与当前颜色按照指定公式计算颜色值,其 OpenGL 代码为:

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

这个操作在 GPU 是比较耗时的。如果 CALayer 的树结构比较复杂,数量大,GPU 每一帧都需要混合所有的 layer,这回消耗 GPU 的大量性能。

而 Rasterization 将刚说的这个操作渲染成一张纹理位图并缓存起来,下次渲染直接使用缓存,从而避免 GPU 的无用消耗。但是这个操作会增加内存的消耗,记得仅使用在静态场景。

参考链接

AutoLayout 的原理性能

《Solving Linear Arithmetic Constraints for User Interface Applications》

Cassowary 网站

Cassowary

Cassowary - Python

单纯算法 Simplex

Masonry

SnapKit

从 Auto Layout 的布局算法谈性能

深入理解 Autolayout 与列表性能 -- 背锅的 Cassowary 和偷懒的 CPU

WWDC 2018:高性能 Auto Layout

Apple 官方教程 VFL

WWDC 2018 - High Performance Auto Layout

Auto Layout Guide

How do you set UILayoutPriority? - stack overflow

IOS开发之自动布局--VFL语言

iOS Auto Layout 中的对齐选项

Apple 官方教程 VFL

UIStackView学习分享, 纯代码实现

自动布局 Auto Layout (原理篇)

详解CALayer 和 UIView的区别和联系

View-Layer 协作

View-Layer Synergy

绘制像素到屏幕上【这篇文章推荐对照英文原版查看,就是下一个】

Getting Pixels onto the Screen

iOS 保持界面流畅的技巧

iOS——图像显示原理以及UI流畅性优化方案

Core Animation Programming Guide

深入理解 iOS Rendering Process

iOS 渲染框架

iOS - 渲染原理

关于iOS离屏渲染的深入研究

iOS-高效设置圆角

How to make a UIView's subviews' alpha change according to it's parent's alpha?

Information Property List Key Reference

Advanced Graphics and Animations for iOS Apps

Advanced Graphics and Animations for iOS Apps.md

iOS-图片高级处理(二、图片的编码解码)

iOS - 图形高级处理 (一、图片显示相关理论)

iOS-图片高级处理(三、图片处理实践)

谈谈 iOS 中图片的解压缩

Which CGImageAlphaInfo should we use?

Quartz 2D Programming Guide

iOS-图片高级处理(二、图片的编码解码)

iOS 图片解码

iOS图片内存优化

iOS图片加载过程以及优化

iOS 图片加载速度优化

iOS 图片渲染及优化