由于一直没有好好学习UIView的绘制流程,关于UIView的drawRect一直以来都有两个疑问:
1 为什么只在drawRect方法里才能获取当前图层的上下文
2 drawRect不是号称自定义实现UIView吗,为什么我重写了drawRect原先设置的背景颜色和frame等等都没变,不是应该是我在drawRect写了什么就只显示什么吗?如:
代码:
// ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.roosterView = [[RoosterView alloc] initWithFrame:self.view.bounds];
self.roosterView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.roosterView];
}
// RoosterView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage *myImage = [UIImage imageNamed:@"rooster"];
CGRect myRect = CGRectMake(0, 0, myImage.size.width, myImage.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextTranslateCTM(context, 0, -(myRect.size.height-(myRect.size.height-2*myRect.origin.y-myRect.size.height)));//向上平移
CGContextTranslateCTM (context, myRect.size.width/4, 0);
CGContextScaleCTM (context, .25, .5);
CGContextRotateCTM (context, radians ( 22.));
CGContextDrawImage(context, myRect, myImage.CGImage);
}

不是只应该显示形变之后的图片吗?!为什么还是占满整个屏幕白色背景还在?不是说重写drawRect对UIView进行自定义嘛!!!
这里需要了解:真正被显示的是layer,每一个在 UIKit 中的 view 都有它自己的 CALayer。每一个layer都有个content,这个content指向的是一块缓存,叫做backing store(后备存储),backing store有点像一个图像。这个后备存储正是被渲染到显示器上的。
绘图流程大概是:
- 每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。
- UIView的绘制和渲染是两个过程,当UIView被绘制时,CPU执行drawRect,通过context将数据写入backing store。
- 当backing store写完后,通过render server交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上
CALayer被绘制时方法调用栈:

首先:Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。从图中可以看到监听回调。
接着:当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用 CGContextRef 绘制。
上图监听事件到来后出发一系列事件一直到-[CALayer display],根据微软开源WinObjc,display的主要工作有:
- (void)display {
.......
// 判断contents是否有值
if (priv->contents == NULL || priv->ownsContents || [self isKindOfClass:[CAShapeLayer class]]) {
.......
// 创建当前图层上下文
CGContextRef drawContext = CreateLayerContentsBitmapContext32(width, height);
priv->ownsContents = TRUE;
CGImageRef target = CGBitmapContextGetImage(drawContext);
CGContextRetain(drawContext);
CGImageRetain(target);
priv->savedContext = drawContext;
......
// 设备坐标和UIKit坐标之间的转换
CGContextScaleCTM(drawContext, 1.0f, -1.0f);
CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);
CGContextSetDirty(drawContext, false);
[self drawInContext:drawContext];
if (priv->delegate != 0) {
if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
[priv->delegate displayLayer:self];
} else {
[priv->delegate drawLayer:self inContext:drawContext];
}
}
CGContextReleaseLock(drawContext);
CGContextRelease(drawContext);
// If we've drawn anything, set it as our contents
if (!CGContextIsDirty(drawContext)) {
CGImageRelease(target);
CGContextRelease(drawContext);
priv->savedContext = NULL;
priv->contents = NULL;
} else {
priv->contents = target;
}
} else if (priv->contents) {
priv->contentsSize.width = float(priv->contents->Backing()->Width());
priv->contentsSize.height = float(priv->contents->Backing()->Height());
}
}
从调用栈截图看出layer是在drawInContext:方法里调用了layer代理实现的
drawLayer:inContext:方法和以上代码关于drawInContext:和代理函数[priv->delegate displayLayer:self];和[priv->delegate drawLayer:self inContext:drawContext];的调用时机有出入(待详查)不过整体流程操作还是可以明白的。
代码先判断contents属性是否有值,如果没有就开始创建自己的图层关联上下文,从上下文创建CGImageRef,最后赋值给contents属性,这与文档关于contents属性的描述一致。
文档:
If you are using the layer to display a static image, you can set this property to the CGImageRef containing the image you want to display. (In macOS 10.6 and later, you can also set the property to an NSImage object.) Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.
那么这些和只在drawRect方法里才能获取当前图层的上下文有什么关系呢,依然看源码:
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
UIGraphicsPushContext(context);
CGRect bounds;
bounds = CGContextGetClipBoundingBox(context);
[self drawRect:bounds];
UIGraphicsPopContext();
}
drawRect方法在drawLayer:inContext:里被调用,并且被调用前有个UIGraphicsPushContext(context);方法将视图图层对应上下文压入栈顶,然后drawRect执行完后,将视图图层对应上下文执行出栈操作。
系统会维护一个CGContextRef的栈,而UIGraphicsGetCurrentContext()会取栈顶的CGContextRef,当前视图图层的上下文的入栈和出栈操作恰好将drawRect的执行包裹在其中,所以说只在drawRect方法里才能获取当前图层的上下文。
第一个问题知道了答案,那么是时候总结下第二道个问题的答案了:对于view的frame,backgroundColor各种设置是通过view间接操作了layer,继而存储到backing store,view给暴露出drawRect接口只是一个询问补充的目的,layer自己会装配它的后备存储,生成了上下文,已经玩的红红火火了,为了表示对你的尊重,再问你一句:大爷还有要补充的吗?你重写了drawRect说有。大家都说drawRect自定义view说白了其实只是一个补充的作用。
把drawRect说的这么不堪,其实不是没有凭据的,因为苹果说:
这听起来貌似有点低俗,但是最快的绘制就是你不要做任何绘制。
大多数时间,你可以不要合成你在其他视图(图层)上定制的视图(图层),这正是我们推荐的,因为 UIKit 的视图类是非常优化的 (就是让我们不要闲着没事做,自己去合并视图或图层) 。
最后:图层的后备存储将会被不断的渲染到屏幕上。直到下次再次调用视图的 -setNeedsDisplay ,将会依次将图层的后备存储更新到视图上。
在调用中drawRect之前的都在cpu中执行,然后GPU将bitmap从RAM移动到VRAM将按像素计算将一层层图层合成成一张图然后显示:

// 以下待进一步验证:
drawRect调是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
1.如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。
2.该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3.通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4.直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0.
以上1,2推荐;而3,4不提倡
update - 2025-03-25
Backing Store:所有
CALayer的默认渲染缓冲区,存储图层的 原始内容(bitmap)(无论是否光栅化)。Backing Store 是 Core Animation 框架管理的显存(或内存)区域,而非开发者的私有变量或可见属性contents属性是Backing Store的输入源之一
Rasterization Cache:当 shouldRasterize = YES 时,Core Animation 会将图层及其子图层 合成后的结果 渲染成一个位图,存储在独立的显存区域(称为「光栅化缓存(Rasterization Cache)」 注意 矢量图转 bitmap 的过程也叫光栅化这是渲染管线的通用步骤,光栅化缓存(Rasterization Cache)是Core Animation 的优化策略, )
渲染流程
一、CALayer 的 backing store 本质与数据存储
-
backing store存储内容
CALayer的backing store确实存储 二进制像素数据(位图),其本质是用于暂存渲染结果的缓冲区。当通过contents属性直接赋值CGImage或通过drawRect:等方法绘制内容时,系统会将生成的位图数据缓存到 backing store 中 - 位图的生成逻辑
- CPU 参与阶段:修改
CALayer的布局属性(如frame、alpha)或触发setNeedsDisplay时,CPU 会先完成布局计算、绘制指令生成等操作,最终生成位图数据并存储到backing store - 离屏渲染场景:当需要复杂效果(如阴影、圆角)时,系统会创建额外的
backing store缓存中间位图数据,再交由 GPU 处理
二、GPU 在渲染流程中的核心作用
-
数据合并与合成
GPU 的主要任务是 合并所有CALayer的backing store位图数据,并根据图层的透明度、混合模式(Blend Mode)等信息,生成最终屏幕大小的像素数据块。这一过程称为图层合成(Compositing)
2 渲染管线中的分工
- CPU 职责:负责布局计算、绘制指令生成、位图数据生成(如文本渲染、矢量图形光栅化)
-
GPU 职责:处理位图合成、透明度混合、纹理映射、光照计算等图形密集型操作,最终将结果输出到帧缓冲区(Frame Buffer)
3 透明度与性能影响 - 透明度合成开销:若多个 CALayer 存在透明度叠加(如半透明视图叠加),GPU 需逐像素混合颜色值,可能导致性能下降16。
-
优化手段:通过减少图层数量、避免不必要的透明区域、预合成静态内容(如
shouldRasterize)降低 GPU 负载46。
离屏渲染
屏渲染,本质是GPU 需要先在一个「临时画布」完成中间步骤,再将结果合并到「主画布」(屏幕缓冲区)。核心原因是 无法直接通过常规图层叠加顺序完成效果,需缓存中间结果。
一、用「画布比喻」理解离屏渲染的必要性
常规渲染流程(无离屏渲染)
想象你在一张画布上绘制一幅画,图层叠加顺序是 从底层到顶层:先画背景(如蓝天);
再画中间层(如山丘);
最后画顶层(如人物)。
所有步骤都在同一张画布上直接完成,不需要中间缓存。这就是常规渲染的逻辑:所有图层按顺序直接合成到屏幕缓冲区(Frame Buffer)。阴影效果的矛盾:为何需要离屏渲染?
假设你要给顶层的人物添加阴影:
- 阴影必须绘制在人物下方(否则会被人物遮挡);
- 但阴影的形状和位置取决于人物的轮廓(没有人物就无法确定阴影)。
这就导致一个矛盾:
- 必须知道人物的形状才能画阴影;
- 但人物本身需要最后绘制(否则会被后续图层覆盖)。
为了解决这个问题,系统需要 分两步操作:
- 先在一个临时画布(离屏缓冲区)上绘制人物;
- 根据临时画布上的人物轮廓生成阴影;
- 将阴影绘制到主画布(屏幕缓冲区);
- 再将人物绘制到主画布。
二技术细节:阴影的离屏渲染流程
以 CALayer 的阴影效果(shadowOffset、shadowOpacity)为例:
- CPU 阶段
-
生成主体内容:CPU 将图层内容(如
UIImage或矢量图形)光栅化为位图,存入backing store; - 提交到 GPU:将位图数据传递给 GPU。
- GPU 阶段(离屏渲染)
-
步骤 1:创建临时缓冲区 :
GPU 分配一块离屏缓冲区(Offscreen Buffer)。 -
步骤 2:渲染主体到临时缓冲区
将主体的位图数据绘制到离屏缓冲区,但不直接显示到屏幕。 -
步骤 3:生成阴影
根据临时缓冲区中的主体轮廓,计算阴影的形状和渐变(如高斯模糊)。 -
步骤 4:合成到主缓冲区
- 先将阴影绘制到主缓冲区;
- 再将主体从临时缓冲区复制到主缓冲区(覆盖阴影的对应区域)。
三、为什么必须用离屏渲染?
1. 数据依赖性问题
阴影的形状、位置、模糊度完全依赖于主体的几何信息。如果直接在屏幕缓冲区绘制:
- 无法先画阴影(不知道主体位置);
- 后画主体会覆盖阴影(无法保留阴影)。
2. GPU 的并行性限制
GPU 的渲染管线是高度并行的,但某些操作(如模糊、裁剪)需要完整的位图数据才能处理。离屏渲染本质上是 强制同步点,确保先完成主体渲染,再处理阴影。
四 离屏渲染场景
1. 阴影(shadowOffset、shadowOpacity)
画布理论解释:
-
问题:
阴影必须绘制在主体下方,但阴影的形状、位置、模糊度完全依赖主体的轮廓。如果直接在「主画布」绘制,流程会矛盾:- 先画阴影 → 不知道主体形状(无法确定阴影);
- 后画阴影 → 主体已经覆盖了阴影区域(阴影不可见)。
解决方案:
- 创建临时画布:单独绘制主体到临时画布,获取其轮廓;
- 生成阴影:根据主体轮廓计算阴影形状和模糊效果;
- 合并到主画布:先画阴影,再覆盖主体。
GPU 处理过程:
- GPU 分配一个离屏缓冲区(临时画布);
- 将主体内容(如 UIView 的位图)渲染到离屏缓冲区;
- 根据离屏缓冲区的主体轮廓,生成阴影位图;
- 将阴影绘制到主画布(屏幕缓冲区);
- 将离屏缓冲区中的主体复制到主画布,覆盖阴影对应区域。
2. 圆角 + 裁剪(cornerRadius + masksToBounds)
画布理论解释
问题:
圆角裁剪需要将图层内容限制在圆角范围内,但直接在主画布裁剪会导致后续叠加的图层破坏裁剪区域。
例如:若先裁剪一个圆角图片,再在其上叠加文字,文字可能超出圆角范围。-
解决方案:
- 创建临时画布:在临时画布上绘制原始内容
- 应用圆角蒙版:裁剪掉圆角外的像素;
- 合并到主画布:将裁剪后的结果复制到主画布。
GPU 处理过程:
- GPU 分配离屏缓冲区;
- 将原始内容(如图片)渲染到离屏缓冲区;
- 应用圆角蒙版(通过 Alpha 通道或模板测试裁剪圆角外的像素);
- 将处理后的位图复制到主画布。
3. 图层蒙版(CALayer.mask)
画布理论解释:
问题:
蒙版需要与图层内容逐像素混合(Alpha 通道运算)。
如果直接在主画布混合,后续叠加的图层可能破坏混合结果。解决方案:
- 创建临时画布:将图层内容和蒙版单独绘制到临时画布;
- 逐像素混合:根据蒙版的 Alpha 值裁剪或混合图层内容;
- 合并到主画布:将混合后的结果复制到主画布。
GPU 处理过程:
- GPU 分配离屏缓冲区;
- 将图层内容和蒙版渲染到离屏缓冲区;
- 对每个像素执行 Alpha 混合运算(如 sourceAlpha * maskAlpha);
- 将混合后的位图复制到主画布。
4. 高斯模糊(如 UIVisualEffect)
画布理论解释:
-
问题:
模糊效果需要基于完整的位图数据计算(如对周围像素加权平均)。
如果直接在主画布处理,无法实时获取完整数据(主画布可能包含未渲染的内容)。 -
解决方案:
- 创建临时画布:将待模糊的内容渲染到临时画布;
- 计算模糊效果:在临时画布上对位图进行多次采样和混合;
- 合并到主画布:将模糊后的结果复制到主画布。
- GPU 处理过程:
- GPU 分配离屏缓冲区;
- 将原始内容(如下层视图)渲染到离屏缓冲区;
- 对离屏缓冲区的位图进行高斯模糊(多次采样、混合);
- 将模糊结果复制到主画布。
五、性能影响与优化
1. 离屏渲染的开销
- 内存占用:临时缓冲区可能占用大量显存(尤其是 Retina 屏幕);
- GPU 负载:多次缓冲区切换和额外渲染步骤增加 GPU 负载。
2. 优化策略
-
预合成静态内容:
对不变化的图层(如圆角图片)提前生成最终位图,避免实时离屏渲染。 -
慎用 masksToBounds:
圆角裁剪(cornerRadius + masksToBounds)会强制离屏渲染,可用预裁剪的图片替代。 -
减少图层数量:
合并多个图层的绘制内容(如用 drawRect: 统一绘制)。
光栅化
定义
光栅化(Rasterization) 的本质是 把“矢量描述”变成“像素点阵图”。
想象你要在屏幕上显示一个红色圆形按钮,但屏幕只能处理一个个像素点。光栅化就是计算出这个圆形在每个像素点的颜色值(比如边缘抗锯齿的渐变红色),最终生成一张由像素组成的位图(Bitmap)。
为什么需要光栅化
- 矢量描述:代码中的 UIBezierPath、UILabel 的文本、CAShapeLayer 的路径都是“数学公式”(如圆心坐标、半径、颜色)。
-
像素显示:屏幕需要的是每个像素的具体颜色值(如 RGB(255,0,0))。
光栅化就是中间的“翻译过程”,把数学公式转换成像素点阵。
CPU 的光栅化 vs GPU 的光栅化
虽然两者都涉及“矢量转像素”,但分工不同:
CPU 的光栅化
-
场景
当你用 Core Graphics(如 drawRect:、UIGraphicsImageRenderer)绘制文本、矢量图形时,CPU 负责光栅化。
// CPU 光栅化:将矢量路径转换为位图
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 100, height: 100))
let image = renderer.image { context in
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
UIColor.red.setFill()
path.fill()
}
// 文本图层(需要光栅化)
let textLayer = CATextLayer()
textLayer.string = "Hello"
textLayer.fontSize = 16
- 步骤:
- CPU 计算圆形的数学路径;
- CPU 确定每个像素是否在路径内;
- CPU 生成红色位图数据(含抗锯齿边缘的渐变红色像素);
- 将位图数据交给 GPU 显示。
GPU 的光栅化
-
场景
当 GPU 处理 3D 模型时(如游戏中的三角形网格),需要将 3D 坐标转换为屏幕上的 2D 像素,确定每个像素的颜色。
为什么光栅化会影响性能?
CPU 光栅化:
如果频繁用drawRect:绘制复杂矢量图形(如曲线、文本),会占用 CPU 资源,导致主线程卡顿。
优化方案:将结果缓存为位图(如生成UIImage后重复使用)。GPU 光栅化:
通常是 3D 渲染的瓶颈(如顶点过多),但普通 UIKit 开发中更需关注离屏渲染和图层混合。
问题
通常指 CPU 将矢量内容(文本、路径)转换为位图的过程————那么 CALayer 也和矢量图一样接收一些描述数据:位置、大小,背景颜色、透明度,最终这些数据会被转成bitmap(bitmap在用来称谓二进制像素信息的名词吧)存储到backing store中,这个过程叫光栅化吗
回答: 光栅化 ≠ 所有位图生成,只有矢量数据(路径、文本)转像素的过程才叫光栅化。
概念澄清:
2.1 什么是 Backing Store?
- 定义:Backing Store 是 Core Animation 为 CALayer 分配的一块内存区域,用于缓存图层内容最终的 位图数据(即像素点阵)。
- 作用:避免重复渲染,提升性能(如果图层内容不变,直接复用缓存)。
- 存储内容:可能是纯色、图片、或通过 CPU/GPU 光栅化生成的位图。
2.2 什么是光栅化(Rasterization)?
- 核心:将 矢量描述(如路径、文本)转换为 像素点阵。
-
误区:不是所有生成位图的过程都叫光栅化!
-
纯色图层(如
backgroundColor):直接由 GPU 合成,不需要光栅化。 -
矢量内容(如
CAShapeLayer的path):需要光栅化。 -
文本内容(如
UILabel的文本):需要光栅化。
-
纯色图层(如
CALayer 的渲染流程
1. 场景 1:纯色图层(无光栅化)
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
layer.backgroundColor = UIColor.red.cgColor // 纯色描述
-
过程:
- CALayer 存储描述数据(位置、颜色);
- 无需生成位图;
- GPU 直接根据颜色值合成到屏幕缓冲区(无光栅化)。这里就回答了苹果为什么不鼓励我们自己
drawRect:直接赋值CALayer的属性值,然后GPU接手,GPU就是快啊~
Backing Store:可能不分配,或分配一个极小的缓存(仅存储颜色值)。
2. 场景2:矢量路径(需要光栅化)
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100)).cgPath
shapeLayer.fillColor = UIColor.red.cgColor
-
过程:
- CALayer 存储描述数据(路径、颜色);
- 光栅化:由 CPU 或 GPU 将路径转换为像素点阵;
- 结果存储到 Backing Store;
- GPU 将 Backing Store 的位图合成到屏幕缓冲区。
3. 场景 3:文本图层(需要光栅化)
let textLayer = CATextLayer()
textLayer.string = "Hello"
textLayer.fontSize = 16
-
过程:
- CALayer 存储描述数据(文本、字体、颜色);
- 光栅化:由 Core Text 框架将文本转换为像素点阵(抗锯齿处理)
- 结果存储到 Backing Store;
- GPU 合成位图到屏幕缓冲区。
光栅化性能优化
shouldRasterize 的作用
强制将图层内容(无论是否光栅化)缓存为位图,减少重复渲染开销
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale // 避免模糊
- 适用场景 图层内容复杂且不频繁变化(如静态阴影+圆角组合)。复用的cell里如果细节不同就不适合使用,因为位图无法复用,反而增加负担
- 陷阱:过度使用会导致显存暴涨,且缓存失效时重新光栅化更耗时。
优化光栅化:
对需要光栅化的内容(如文本、路径)提前生成位图(UIGraphicsImageRenderer),避免主线程卡顿
// 提前将矢量路径转为位图
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 100, height: 100))
let image = renderer.image { context in
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
UIColor.red.setFill()
path.fill()
}
let layer = CALayer()
layer.contents = image.cgImage // 直接使用位图,无需运行时光栅化
参考:
ObjC中国
iOS开发之图形渲染分析、离屏渲染、当前屏幕渲染、On-Screen Rendering、Off-Screen Rendering
iOS开发笔记--iOS 事件处理机制与图像渲染过程