关于UI渲染的一些理解

893 阅读5分钟

正常渲染:

  1. CALayer在内部创建一个上下文环境(CGContextRef);
  2. 判断layer是否有代理: 没有代理:调用layer的drawInContext:方法, 有代理:调用delegate的drawLayer:inContext:方法,然后在合适的时机回调代理,在[UIView drawRect]中进行UI的绘制工作,
  3. 最后layer上传backingStore的bitmap位图到GPU 也就是将生成的bitmap位图赋值给layer.content属性,结束系统绘制流程。 疑问1:将生成的bitmap位图赋值给layer.content,这句我有点不理解,我在各个地方打印contents, 都是nil,文档也写了,默认是为nil的,那也就不奇怪了。令我疑惑的是这些视图的元数据位图到底存放在哪里?

我自己有两种猜测:

1、如上所说,layer.content确实是存放位图的,只不过系统用完之后就清空了。

2、layer.content不是保存系统的绘制数据的,而是用于开发者自己创建的位图。

创建视图有frame,执行drawRect。frame=zero,不执行drawRect,再重新设置不为zero的frame,drawRect依然不执行

疑问2:初始化大小为0 不执行drawRect这能解释的通, 毕竟frame为0相当于隐藏了,没必要浪费资源去渲染了,但是首次不为0的时候为什么还是不执行drawRect呢,drawRect一次都没执行,但是视图还是正常的显示了。 系统是怎么渲染的呢?

这两个疑问希望各位能帮忙解答一下,感谢🙏。

异步渲染: 有代理: 1、某个时机调用setNeedsDisplay; 2、runloop将要结束时调用[CALayer display]; 3、若代理实现了displayLayer将会调用此方法,在子线程中做异步绘制的工作; 4、在子线程中创建上下文、绘制控件并生成图片; 5、在主线程中设置layer.contents,将生成的视图展示在layer上。 无代理: 1、某个时机调用setNeedsDisplay; 2、runloop将要结束时调用[CALayer drawInContext:(CGContextRef)ctx], 在子线程中做异步绘制的工作; 3、在子线程中创建上下文、绘制控件并生成图片; 4、在主线程中设置layer.contents,将生成的视图展示在layer上。

这里大家先想一想,如果要实现异步绘制,是否必须按照这个流程呢?

无代理的情况下很好理解。没什么好说的,看一下有代理的情况, 这里可能会有人不知道有代理和无代理的的区别,简单的说就是有没有载体,例如UIView就是UILayer的载体,创建视图的时候默认会创建layer,layer是视图的呈现者,且UIView自身是layer的代理,关于UIView和layer的区别和联系可以在网上翻一番。

UIView关于渲染主要有以下几个方法:

1⃣️- (void)drawRect:(CGRect)rect;

以下两个是CALayer的代理方法

2⃣️- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

3⃣️- (void)displayLayer:(CALayer *)layer;

如果是正常的系统绘制,则会执行1⃣️,然后再执行2⃣️

如果是异步绘制,如果有自定义的layer,并且实现了display方法,则会先执行layer的display方法,再执行UIView中的3⃣️方法

到这里相信大家心里应该对上面的问题有个大致的答案了吧。

这里先说说我自己的理解,如果你大致看过YYLabel和ASDK的源码,你会发现这两个就是用的上面的异步渲染流程,都自定义了自己的CALayer,那也就说明了这个流程确实是实现异步渲染的非常好的方式。有好的方式就说明还有不好的方式😄,那就是在drawRect或者drawLayer:inContext中实现异步渲染,哇,真是好想法,我找了一个demo也实现了,但是现实中应该没有人会这么用,做做demo就行了。

这里有篇文章说了为什么不应该使用drawRect,原文: 当你使用UIImageView在加载一个视图的时候,这个视图虽然依然有CALayer,但是却没有申请到一个后备的存储,取而代之的是使用一个使用屏幕外渲染,将CGImageRef作为内容,并用渲染服务将图片数据绘制到帧的缓冲区,就是显示到屏幕上,当我们滚动视图的时候,这个视图将会重新加载,浪费性能。所以对于使用-drawRect:方法,更倾向于使用CALayer来绘制图层。因为使用CALayer的-drawInContext:,Core Animation将会为这个图层申请一个后备存储,用来保存那些方法绘制进来的位图。那些方法内的代码将会运行在CPU上,结果将会被上传到GPU。这样做的性能更为好些。

那么异步渲染解决了什么问题呢?

众所周知,UI渲染是在主线程中,当一个列表过于复杂的时候,渲染时间就会增加,导致卡顿。苹果用的是Vsync+双缓冲机制,一帧正在显示的时候,就会把下一帧存在备用缓冲器中,当滑动的时候会每隔16.7ms去备用缓冲器取下一帧放到帧缓冲器(frame buffer)中,当下一帧没渲染完成的时候,会抛弃它,不会把它放到frame buffer中,此时显示的还是当前帧,这就相当于两个周期显示的是相同的画面,这就是卡顿。

同样是异步渲染,为什么没有人在drawRect实现呢? 能用到drawRect方法的情况大部分情况都是绘图,自定义图形。但是这是给大家一个建议,有的东西用drawRect和CAShapeLayer都能做到的,优先用CAShapeLayer。首先要知道CPU过高会导致手机耗电快、发热,严重的会导致crash。以下是DrawRect和CAShapeLayer的区别:

drawRect:drawRect属于CoreGraphic框架,占用CPU,消耗性能大,就算是方法里面什么都不做,也不要写!!!。

CAShapeLayer:CAShapeLayer属于CoreAnimation框架,通过GPU来渲染图形,节省性能,由GPU处理,不消耗内存

参考:zhuanlan.zhihu.com/p/35693019

参考:www.jianshu.com/p/1c1b3f7cf…