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

视图 & 图像相关

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

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

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

3、UI 绘制

二、UI 显示到屏幕上

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

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,实现最高的渲染效率。bash

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

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

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-图片高级处理(2、图片的编码解码)

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

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

谈谈 iOS 中图片的解压缩

Which CGImageAlphaInfo should we use?

Quartz 2D Programming Guide

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

iOS 图片解码

iOS图片内存优化

iOS图片加载过程以及优化

iOS 图片加载速度优化

iOS 图片渲染及优化

相关文章
相关标签/搜索