iOS-drawRect
简单介绍
简介:drawRect 对于iOS开发来说,如果不是有特殊的画板类的APP需求,一般的APP开发很少会使用到这个方法。该方法在创建UIView子类的时候会在.m的实现文件中默认创建,只是默认加注释了。
作用:从注释中可以看出来,需要自定义绘制的时候,重写drawRectOnly override drawRect: if you perform custom drawing.
声明:在UIView的Category UIViewRendering中声明,从名字可以看出主要是跟UIView的渲染相关。
注意事项
主要是在视图第一次显示的时候调用。为系统在UIViewController的loadView和viewDidLoad调用之后自动调用
- 初始化的时候如果没有设置frame,
drawRect不会被调用。 - sizaThatFits,方法调用之后,
drawRect会调用。 drawRect不能直接调用,调用setNeedsDisplay后,drawRect会调用- 做画图程序的时候,实时画图不要用gestureRecognizer,用touchMove的方法,然后调用setNeedsDisplay刷新屏幕。
- layoutSubviews调用顺序先于
drawRect UIImageView不用重写drawRecct去自定义重绘内容,写了也没用,替代方法可以参考用UIView作为UIImageView的子类,然后对UIView进行drawRect

对内存的影响 详细看下一篇
参考:http://bihongbo.com/2016/01/03/memoryGhostdrawRect/
重写drawRect为何会内存暴涨
要想搞明白这个问题,我们需要梳理一下iOS程序上图形显示的原理。在iOS系统中所有显示的视图都是从UIView继承而来的,,同时UIView负责接收用户交互。但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer
CALayer类的概念与UIView非常类似,也具有树形的层级关系,并且可以包含图片文本,背景色等。它与UIView最大的不同在于它不能响应用户的交互,可以说它根本就不知道响应链的存在,它的API虽然提供了“某店是否在图层范围内的方法”,但是它并不具有响应的能力。
在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层,实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级API。
可以说CALayer是UIView的内部实现细节。
CALayer其实只是iOS当中一个普通的类,并不能直接渲染到屏幕上,因为屏幕上你所看到的东西,其实都是一张张图片。而为什么我们能看到CALayer的内容,是因为CALayer内部有一个contents属性。contents默认可以传一个id类型的对象,但是只有你传CGImage的时候,它才能够正常显示在屏幕上。所以,最终我们的图形渲染落点落在content。
contents也被称为寄宿图,除了给它赋值CGImage之外,我们也可以直接对它进行绘制,绘制的方法正式此次问题的关键,通过继承UIView并实现 drawRect方法即可以自定义绘制。drawRect方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,UIView不关心绘制的内容。如果UIView检测到drawRect方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图乘以contentsScale(这个属性与屏幕分辨率有关,我们的画板程序在不同模拟器下呈现的内存用量不同也是因为它)的值。
那么回到画板程序,当画板从屏幕上出现的时候,因为重写了drawRect方法,drawRect方法就会自动调用,生成一张寄宿图后,方法里面的代码利用CoreGraphics去绘制n条黑色的线,然后内容就会缓存起来,等待下次调用setNeedsDisplay时再进行更新。
画板视图的drawRect方法的背后实际上都是底层CALayer进行了重绘和保存中间产生的图片,CALayer的delegate属性默认数显了CALayerDelegate协议,当它需要内容信息的时候会调用协议中的方法来拿。当画板视图重绘时,因为它的支持图层CALayer的代理就是画板视图本身,所以支持图层会请求画板视图给它一个寄宿图来显示,它此刻会调用:
1 | - (void)displayLayer:(CALayer *)layer; |
如果画板视图实现了这个方法,就可以拿到layer来直接设置contents寄宿图,如果这个方法没有实现,支持图层CALayer会尝试调用:
1 | - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; |
这个方法调用之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentScale决定)和一个CoreGraphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。在这异步生成的空寄宿图内存是相当巨大的,它就是本次内存问题的关键,一旦你实现了CALayerDelegate协议中的drawlayer inContext 方法或者UIView中的drawRect方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,,这个上下文需要的内存可以从这个公式得出:(图层宽图层高4字节),宽高的单位均为像素。
1 | _myDrawer = [[BHBMyDrawer alloc] initWithFrame:CGRectMake(0, 0, SCREEN_SIZE.width*5, SCREEN_SIZE.height*2)]; |
该画板程序的画板视图它在iPhone6s plus 机器的上下文内存量就是 1920 2 1080 5 4字节,相当于79MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配,这就是画板程序内存暴增的原因。
内存暴增的原因找出来了,合理的解决方案?
我认为最合理的办法处理类似于画板这样画线条的需求直接用专有图层CAShapeLayer。让我们看看它是什么:
CAShapeLayer是一个荣国矢量图形而不是bitmap来绘制的图层子类,用CGPath来定义想要绘制的图形,CAShapeLayer会自动渲染。它可以完美替代我们直接使用CoreGraphics绘制layer,对比之下使用CAShapelayer有以下几个优点:
- 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比CoreGraphics快很多。
- 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
- 不会被图层边界剪裁掉。
- 不会出现像素化。
总结一下绘制性能优化原则:
- 绘制图形性能的优化最好的办法就是不去绘制。
- 利用专有图层代替绘图要求。
- 不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。
- 异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。
仿写猿题库练题画板功能 demo
答疑
1.内存没有问题啊,为什么我们的代码内存不会暴增?
首先检讨一下,我在原文当中第一个内存暴增例子可能过于简单不够详尽。其实内存的增长与绘图视图的大小和屏幕分辨率有直接关系,你可以尝试将视图范围扩大,并且使用iPhone 6 Plus或者Ipad等高分辨率机型进行测试。由于我们的绘图程序支持两只手指拖动绘图版(很多人忽略了这个功能,导致很多内容存在误解,建议大家用真机玩一下这个功能),实际的绘图区域要比屏幕大出很多倍,所以内存增长特别明显,这也是我们对drawRect使用不好的例子,如果你使用的相当谨慎,内存是不会有很严重的问题(也许只有几MB),但是我们有能力去优化它,就应该尽量让它运行的更好!另外我在github上的BHBDrawBorderDemo仓库上传了一个比较明显的暴增例子,使用drawRect和CAShapeLayer的代码都有,demo很简单就是弹出一个视图,在这个视图上画2个矩形如下图,你可以在调试的时候非常方便的切换,代码戳这里
参考:
https://zsisme.gitbooks.io/ios-/content/index.html
http://bihongbo.com/2016/01/11/memoryGhostMore/