这部分主要讲一下UI视图和图像相关的技术点,大致包括以下内容
- UI事件传递&响应
- UI图像显示原理
- UI卡顿,掉帧原因和优化方案
- UIView的绘制原理
- 图像相关处理
一、UI事件传递&响应
首先说一下UIView和CALayer的关系。
UIView实际上里面包含这两个属性,一个CALayer类型的layer和backgroundColor,那它里面所包含的layer部分实际上就是CALayer类型的,那么这个layer就指向了一个CALayer类型的变量,backgroundColor实际上是对CALayer同名属性方法的一个包装,实际上,UIView的显示部分是由CALayer的contents决定的,对应的叫做backing store,实际上就是一个bitmap类型的位图,最终显示到屏幕上对应的UI控件可以理解为都是位图。
而UIView除了包含layer负责的显示部分,也包含了触摸传递事件,负责响应链。
所以UIView和CALayer的关系可以总结如下:
UIView不仅仅提供了显示的内容,而且负责处理触摸等事件,参与响应链。
CALayer负责显示的内容contents,也就是具体现在到屏幕上的未图(bitmap)。
这样设计的好处
这体现了系统设计UIView和CALayer中所运用的一个设计原则,就是单一职责。UIView只负责处理触摸事件参与视图响应链,而CALayer只负责内容上的显示,这就体现了职责上的分工。(单一职责原则是设计模式中六大基本原则之一。)UI事件传递
首先说一下UI事件传递的目的:它是为了找到响应事件的视图对象到底是谁?也就是说谁才是事件的响应者。
假如我们点击了C2视图的空白区域,系统是最终怎么的方式才找到了最终的事件响应视图是C2的呢?
关于事件传递主要和下面两个方法有关:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
两个方法的主要作用:
第一个:返回值是UIView,也就是最终哪个视图响应了 事件 ,就把哪个视图给返回
第二个:用来判断某一个点击的位置是否在当前视图范围内,如果在的话就会返回YES
事件传递的流程:
假如我们点击了 屏幕的某一个位置,这个事件会传递给UIApplication,然后UIApplication传递给当前的UIWindow,UIWindow里就会判断hitTest:withEvent:来返回最终的响应视图。在这个系统实现内部,会调用pointInside:withEvent:来判断当前点击的point是否在UIWindow范围内,如果是的话,它会遍历它的子视图来查找最终响应这个事件的视图,而遍历的方式是以倒序方式去遍历,也就是说最后添加到UIWindow上的视图最优先被遍历到,在每个UIView中都会调用它们对应的hitTest:withEvent:,可以理解为是一个递归调用,比如UIWindow所有的子视图都会以倒序方式分别调用对应子视图的hitTest:withEvent:,而hitTest:withEvent:对于一个指定的子视图而言,它内部实现又会调用它的所有子视图的hitTest:withEvent:,最终会返回一个响应视图hit,如果返回有值的话这个hit就作为这个事件的响应视图,如果没有的话,假如它在当前UIWindow范围内,UIWindow本身就作为事件的响应视图。
![](http://img.e-com-net.com/image/info10/896474764d82458c99f4e9524f1972a7.jpg)
hitTest:withEvent:
系统实现流程图-
image
优先判断当前视图的hidden
属性,包括它是否可交互以及它的alpha
值,如果说当前视图不是可隐藏的并且可以交互且透明度>0.01,在这种情况下才会走后续的路程,否则会返回nil(也就是当前视图不作为事件最终响应者,然后再由它的父视图去遍历同级兄弟节点视图)。
假如三个判断都通过的话,会调用当前视图的pointInside:withEvent:
来判断点击的点是否在视图范围内,如果不在的话返回nil(也就是当前视图不作为事件最终响应者,然后再由它的父视图去遍历同级兄弟节点视图)。
如果pointInside:withEvent:
返回的YES,那会以倒序方式遍历当前视图的所有子视图,遍历的过程当中,会调用当前视图所有子视图的pointInside:withEvent:
,假如某一个子视图返回了最终事件响应视图的话,就会把对应的视图作为最终的响应视图返回给调用方,假如某一个子视图pointInside:withEvent:
返回的是nil的话,就会继续遍历当前视图的下一子视图,然后也是以pointInside:withEvent:
方法去调用,如果全部遍历结束都没有对应的子视图去响应这个时间的话,那么由于当前点击位置在当前范围内,就会把当前视图作为最终的事件响应视图返回给调用方。
-
hitTest:withEvent:
代码实现逻辑
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.userInteractionEnabled ||
[self isHidden] ||
self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
//遍历当前对象的子视图
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 坐标转换
CGPoint vonvertPoint = [self convertPoint:point toView:obj];
//调用子视图的hittest方法
hit = [obj hitTest:vonvertPoint withEvent:event];
// 如果找到了接受事件的对象,则停止遍历
if (hit) {
*stop = YES;
}
}];
if (hit) {
return hit;
}
else{
return self;
}
}
else{
return nil;
}
}
- 视图响应链
Responder Object
响应者对象是能够响应并处理事件的对象,是构成Responder Chain(响应链)和事件传递链的节点
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,称之为"响应者对象",UIApplication/UIView/UIViewControl都是UIResponder的子类,UIResponder声明了用于处理事件的接口,并定义了默认行为.
注:CALayer不是UIResponder的子类,这说明CALayer无法响应事件,这也是UIView与CALayer的重要区别之一
- (UIResponder )nextResponder;
- (BOOL)canBecomeFirstResponder; // default is NO
- (BOOL)becomeFirstResponder;
UIResponder内部提供了以下方法来处理事件触摸事件
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
注意有个很重要的方法,nextResponder
,很明显可以看出来响应是一条链表结构,通过nestResponder找到下一个responder。这里是从第一个responder开始通过nextresponder传递事件,直到有responder响应了事件就停止传递;如果传到最后一个responder都没有被响应,那么该事件就被抛弃。
- 通过以下代码可以看出响应链的传递过程:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%@ touch begin", self.class);
UIResponder *next = [self nextResponder];
while (next) {
NSLog(@"下一个响应者%@",next.class);
next = [next nextResponder];
}
}
打印代码如下:
2021-02-02 12:48:05.252104+0800 Event[83273:6199169] ViewController touch begin
2021-02-02 12:48:05.252333+0800 Event[83273:6199169] 下一个响应者UIDropShadowView
2021-02-02 12:48:05.252492+0800 Event[83273:6199169] 下一个响应者UITransitionView
2021-02-02 12:48:05.252657+0800 Event[83273:6199169] 下一个响应者UIWindow
2021-02-02 12:48:05.252790+0800 Event[83273:6199169] 下一个响应者UIWindowScene
2021-02-02 12:48:05.252918+0800 Event[83273:6199169] 下一个响应者UIApplication
2021-02-02 12:48:05.253049+0800 Event[83273:6199169] 下一个响应者AppDelegate
可以看到在ViewController上进行点击,通过UIResponder 的 nextResponder 这个方法一层一层找到下一个响应着,直到AppDelegate。
如果传递到UIApplication也没有响应,则这个事件作废.
- 怎么样才算是对事件做出了响应呢
在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
总结:
事件的传递方向(就是hittest就是事件的传递):
UIApplication -> UIWindow ->ViewController-> UIView -> initial view
而responder(响应)传递的方向:
Initial View -> Parent View -> ViewController -> Window -> Application-> ApplicationDelegate
如果最终传递到Application对象,依然没有对事件作出响应,事件就会被舍弃掉。
二、UI图像显示原理
首先我们看一下如下图:
以上就是图片的显示原理:
1、关于CPU和GPU两个硬件都是通过总线连接起来的,我们在CPU输出的结果往往是一个位图,再经由总线在合适的时期上传给GPU。
2、GPU拿到位图以后会做相应位图的图层渲染,包括纹理合成。
3、之后把结果放到帧缓冲区Frame Buffer中。
4、由视频控制器根据VSync信号在指定时间之前,去提取帧缓冲区中屏幕显示内容。
5、最终显示到显示器上。
这个是比较笼统的介绍,那么在这个过程中CPU、GPU分别做了哪些工作呢?让我们一一说来。
从上图可以看出:
1、首先当我们创建一个UIView控件之后呢,UIView的显示是CALayer负责(这个我们在UI事件的传递和响应中已经讲过了)。
2、CALayer有个contents属性,就是最终要绘制到屏幕上的位图。
3、假如绘制的是label,那么contents里最终放置的就是hello world文字的位图。
4、系统会在合适的时机回调给我们drawRect方法,我们可以在此的基础之上,去绘制一些自定义想要绘制的内容。
5、绘制好的位图,最终会经由Core Animation这个框架,提交给GPU部分的OpenGL渲染管线。
6、GPU进行位图的最终渲染,包括纹理合成,然后显示到屏幕上面。
-
cup的工作
CPU需要完成UI的布局(layout),包括显示或者说绘制(Display),之后做一些准备工作(PrePare),然后把对应的位图提交到GPU上面(Commit)
- layout:UI布局和文本计算包括控件Frame的设置,对于控件的文字或者size的计算。
- Display:就是绘制过程,drawRect方法发生在这一步骤
- PrePare:准备阶段,假如有UIImageView,那么设置它的image的时候,图片是不能直接显示到屏幕上去的,需要对图片进行解码,解码的动作就发生在这一过程当中
- Commit:对CPU最终的输出结果位图进行提交。
GPU渲染管线过程
GPU渲染管线指的是OpenGL的渲染管线
首先顶点着色
,指的是对位图进行处理
做完上面渲染管线的五个步骤之后,会把最终的像素点提交到对应的帧缓冲区(FrameBuffer)
中,由视频控制器
在Vsync信号
到来之前,去帧缓冲区
中提取最终要显示到屏幕上的内容,最终显示到显示器上。
通过以上的分析,我们就大致了知道图像的生成过程和原理了,在我们使用APP的过程中会发生卡顿的现象,这是由什么原因造成的呢?下一章我们就来讲解一下。
三、UI卡顿,掉帧原因和优化方案
上面是VSync垂直信号
- 我们一般说的页面滑动的流畅性是60fps,指的是每一秒会有60帧的画面更新,那么人眼看到的就是流畅的效果。基于此,相当于每隔16.7毫秒(60分之1)就要产生一帧画面,在16.7ms之内,需要CPU和GPU共同协同完成最终的一帧的数据.
- 灰方块表示CPU花费一定的时间进行文本的布局,UI计算,视图的绘制,图片解码等
- 然后把位图提交给GPU,GPU进行图层的合成,纹理渲染,准备好要显示的画面,那么在下一次VSync信号到来时,就可以显示准备好的这一帧画面了。
假如CPU做工作时时长特别长的时候,留给GPU的时间就比较少了,等GPU将工作做完,总时长就超出16.7了,在下一帧VSync信号到来的时候,没有准备好当下这一帧的画面,就产生了掉帧,那么我们肉眼看到的效果就是滑动的卡顿。
所以,掉帧的原因是:在规定的16.7ms内,在下一帧VSync信号到来之前,CPU和GPU并没有完成下一帧画面的合成,于是导致了卡顿掉帧。
基于这个掉帧原因,我们可以分析如何提高Tableview等控件的滑动流畅性优化的方案。
滑动优化方案
优化分为两个层面:CPU和GPU两个方面。
基于CPU:
文本的布局,UI计算,视图的绘制,图片解码等。
1、在子线程中进行对象的创建,调整和销毁,节省一部分CPU的时间。
2、在子线程中预排版(布局计算,文本计算),让主线程有更多的时间去响应用户的交互。
3、预渲染(文本等异步绘制,图片编解码等)。基于GPU:(图层的合成,纹理渲染等)
纹理渲染方面的优化:
假如我们触发了离屏渲染,就会产生一些layer的圆角,masksToBounds的设置,阴影蒙层,这些都会触发GPU层面的离屏渲染,这种情况下,GPU做纹理渲染的工作量就会很大.优化就是尽量避免离屏渲染,还可以依托CPU的异步绘制机制来减轻GPU的压力。
视图混合方面的优化:
若视图层级复杂,GPU就要做每一个视图的合成,合成每个像素点对应的像素值,需要做大量的计算,可以减轻视图层级复杂性,就能减轻GPU的压力,也可以通过CPU层面的异步绘制机制,来达到提交的位图本身就是一个层级少的视图,这样也可以减轻GPU的压力。
离屏渲染
On-Screen Rendering:
意为在屏渲染(当前屏幕渲染),指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。(屏幕缓冲区我们刚才讲了:就是在GPU和CPU合成的图像保存在这个屏幕缓冲区中)。
Off-Screen Rendering:
意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
离屏渲染:当我们设置某一些UI视图的图层属性,标记为它在未预合成之前,不能用于当前屏幕上面直接显示的时候,就会触发离屏渲染。离屏渲染的概念,起源于GPU层面,指的是在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
什么场景下会触发离屏渲染
- 设置视图的圆角属性(必须同时maskToBounds为YES才会触发)
- 设置视图的图层蒙版
- 设置阴影
- 设置光栅化
为何要避免离屏渲染
1、离屏渲染是发生在GPU层面上的,离屏渲染使GPU层面上面触发了OpenGL的多通道渲染管线,产生了额外的开销,所以我们要避免离屏渲染。
2、离屏渲染会创建新的渲染缓冲区,会有内存上的开销。包括对上下文的切换(因为有多通道的渲染管线,所以会需要把多通道的渲染结果做渲染合成,就涉及到了上下文切换),就会有GPU的额外开销。
3、触发离屏渲染时,会增加GPU的工作量,很可能导致CPU和GPU的总耗时加起来超过了16.7ms,就会导致UI的卡顿和掉帧,所以需要避免离屏渲。染
总结一下解决页面卡顿的技巧
1、UIImageView尽量设置为不透明
opque尽量设置为YES
当UIImageView的opque设置为YES的时候其alpha的属性就会无效,UIImageView的半透明取决于其图片半透明或者UIImageView本身的背景色合成的图层view是半透明的。
如果图片全部不是半透明就不会触发图层的blend操作,整个图层就会不透明。
如果叠加的图片有出现半透明的,就会立马触发图层的blend操作,整个图层不透明。
背景色尽可能设为alpha值为1
当某一块图层的alpha和其superView的背景色alpha不一样的时候会触发alpha合成操作,这是一项看似很简单但却是非常消耗CPU性能的操作。
2、UIView的背景色设置
UIView的背景色尽量不要设置为clearColor,这样也会触发alpha叠加,在UITableView滑动的时候是非常消耗性能的。子视图的背景色尽可能设置成其superView的背景色,这样图层合成的时候不会触发blend操作。
最好不使用带alpha通道的图片,如果有alpha尽量让UI设计人员取消alpha通道。
3、cell上layer尽量避免使用圆角
在工作中关于滑动界面我们会时常遇到cell行设置头像为圆角等需求,这时候我们尽量避免使用layder.cornerRadius,因为这会触发离屏渲染。离屏渲染很耗时间。
4、优化图片的加载方式
图片的加载方式有两种形式:
UIImageView *image = [UIImageView imageView:@"1.png"];
UIImageView *image = [UIImageView imageWithContentOfFile:@"1.png"];
两种加载图片方式的区别:
第一种:当我们经常需要这张图片并且仅仅是小图的时候,我们可以使用此种方式加载图片。
这种方式是把图片缓存在图片缓存区,当我们使用的时候会通过图片的名字也就是通过key的方式去查找图片在缓存区的内存地址。
当我们使用很多图片的时候系统就会开辟很多内存来存储图片,所以qq、微信我们很多时候都会去清除缓存操作。
第二种:当我们使用工程里面的一张大图并且使用次数很少甚至为1次的时候,我们优先会采用这种方式加载图片,这种方式当使用完图片的时候会立即丢弃释放资源,所以对性能不会带来负担。
5、尽量延迟图片的加载
当我们在滑动页面的时候尤其对于那种布局特别复杂的cell,滑动的时候不要加载图片,当滑动停止得时候再进行图片的加载。
我们都知道不管是UITableView还是UIScrollView在滚动的时候需要显示东西都是通过runLoop去拿。
当滚动的时候runLoop会处于NSRunLoopTrackingMode的模式,我们可以通过一个主线程队列dispatch_after或者selfPerformSelector设置runLoop的模式为NSDefaultRunLoopMode模式,就可以做到停止滚动再加载图片。
注:其实严格意义上selfPerformSelector的事件就是在主线程队列中等待。
优先加载理念
采用优先加载的理念,既先展示一部分,当滑动的时候再加载下面的一部分这样就保持流畅。
6、避免阻塞主线程
让图片的绘制、图片的下载、对象的创建、文本的渲染等这些耗时的操作尽可能采用子线程异步的方式去处理,对于layer及UI的操作不得不在主线程里面,只能想办法优化。
7).xib、storyBoard、纯代码的问题
苹果推出storyboard确实为开发者节省了大量的时间,提高了开发效率,但是对于那种
复杂的滑动界面,利用storyboard是非常消耗资源的,不信的可以试试用性能工具timeProfie看看CPU所占的性能百分比,其CPU的资源远远大于纯代码布局。
其他卡顿问题解决方案
页面Push卡顿优化
通过iOS的生命周期我们可以了解到,在push操作和pop操作中会调用对应的生命周期函数。
push的时候:在页面02显示之前(---页面02 -viewWillAppear
将要显示---),分别调用了三个方法:(1)页面02 -loadView
创建View ,(2)---页面02 -viewDidLoad
初始化完毕--- (3)---页面01 -viewWillDisappear
将要消失--- 如果在这三个方法中有耗时操作,那么就会造成卡顿。
pop的时候:在页面01再次显示的时候(---页面01 - viewWillAppear将要显示---),会调用页面02的viewWillDisappear方法,如果在这个方法中有耗时操作,就会引起Pop时卡顿。
所以,在开发时,不要将耗时操作放在页面显示之前调用。比如:如果一个页面消失时需要执行一些操作,可以放在viewDidDisappear
方法中执行,不要放在viewWillDisappear
中。借助三方库保持界面流畅度
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 开源的一个用于保持 iOS 界面流畅的库
ASDK的基本原理
ASDK主要是利用上面说的使界面更流畅的机制,从而对页面流畅进行优化。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKit 和 Core Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。
ASDK 的图层预合成
有时一个 layer 会包含很多 sub-layer,而这些 sub-layer 并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK 为此实现了一个被称为 pre-composing 的技术,可以把这些 sub-layer 合成渲染为一张图片。开发时,ASNode 已经替代了 UIView 和 CALayer;直接使用各种 Node 控件并设置为 layer backed 后,ASNode 甚至可以通过预合成来避免创建内部的 UIView 和 CALayer。
通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU 避免了创建 UIKit 对象的资源消耗,GPU 避免了多张 texture 合成和渲染的消耗,更少的 bitmap 也意味着更少的内存占用。
ASDK 异步并发操作
自 iPhone 4S 起,iDevice 已经都是双核 CPU 了,现在的 iPad 甚至已经更新到 3 核了。充分利用多核的优势、并发执行任务对保持界面流畅有很大作用。ASDK 把布局计算、文本排版、图片/文本/图形渲染等操作都封装成较小的任务,并利用 GCD 异步并发执行。如果开发者使用了 ASNode 相关的控件,那么这些并发操作会自动在后台进行,无需进行过多配置。
Runloop 任务分发
ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。
通过这种机制,ASDK 可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。
其他
ASDK 中还有封装很多高级的功能,比如滑动列表的预加载、V2.0添加的新的布局模式等。ASDK 是一个很庞大的库,它本身并不推荐你把整个 App 全部都改为 ASDK 驱动,把最需要提升交互性能的地方用 ASDK 进行优化就足够了。
四、UIView的绘制原理
UIView的绘制流程
流程说明
当我们调用[UIView setNeedsDisplay]方法时,并没有执行立即执行绘制工作。
而是马上调用[view.layer setNeedsDisplay]方法,给当前layer打上脏标记。
在当前RunLoop快要结束的时候调用layer 的display方法,来进入到当前视图的真正绘制当中。
在layer的display方法内部,系统会判断layer的layer.delegate是否实现了displayLayer:方法,a.如果没有实现,则执行系统的绘制流程;b.如果实现了则会进入异步绘制的入口。
最后把绘制完的backing store(可以理解为位图)提交给GPU。
从上面的流程我们可以看出在绘制分为两种情况:一种是系统的绘制,另一种是异步绘制,下面我们就分别讲一下系统绘制和异步绘制两种的绘制流程。
异步绘制流程
基于layer的delegate,如果实现了displayLayer方法,就可以进入到异步绘制流程当中
在异步绘制过程中, 需要代理去负责生成对应的bitmap
设置该bitmap作为layer.contents属性的值
异步绘制的机制和流程
左侧是主队列,右侧是全局并发队列
假如在某一时机调用了setNeedsDiaplay方法后
在当前runloop将要结束的时候,会有系统调用视图所对应layer的display方法
如果代理实现了displayLayer方法,会调用这个代理的displayLayer这个方法
然后通过子线程的切换,我们会在子线程中去做位图的绘制,此时主线程可以去做些其他的工作
然后再回到主队列中,提交这个位图,设置给CALayer的contents属性
子线程的绘制
通过CGBitmapContextCreat方法,来创建一个位图的上下文
通过CoreGraphic的相关API,可以做当前UI控件的一些绘制工作
之后通过CGBitmapContextCreatImage方法,根据所绘制的上下文,生成一张CGImage图片
系统绘制流程
首先CALayer内部会创建一个CGContextRef,在drawRect方法中,可以通过上下文堆栈当中的取出这个context,拿到的就是当前控件或者说视图的上下文或者说是backing store
然后layer会判断它是否有代理,若没有,则调用CALayer的drawInContext。
若有则调用代理方法,然后做当前视图的绘制工作(这一步发生在系统内部当中),再在合适的时机,基于drawRect回调方法,
drawRect默认操作是什么都不做,而之所以有这个接口,就是为了让我们在系统绘制之上,可以做些自定义的绘制工作。
最后再由CALayer上传对应的backing store给GPU,这里的backing store我们可以理解为位图。