iOS的绘图操作是在UIView类的drawRect方法中完成的,所以如果我们要想在一个UIView中绘图,需要写一个继承UIView 的子类,并重写drawRect方法,在这里进行绘图操作,程序会自动调用此方法进行绘图。
下面先说明一下绘图,比如,你想绘制一个方块,你需要写一个类来扩展UIView并在drawRect方法中填入如下代码:
- (void)drawRect:(CGRect)rect {
// Drawing code.
//获得处理的上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//设置线条样式
CGContextSetLineCap(context, kCGLineCapSquare);
//设置线条粗细宽度
CGContextSetLineWidth(context, 1.0);
//设置颜色
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
//开始一个起始路径
CGContextBeginPath(context);
//起始点设置为(0,0):注意这是上下文对应区域中的相对坐标,
CGContextMoveToPoint(context, 0, 0);
//设置下一个坐标点
CGContextAddLineToPoint(context, 100, 100);
//设置下一个坐标点
CGContextAddLineToPoint(context, 0, 150);
//设置下一个坐标点
CGContextAddLineToPoint(context, 50, 180);
//连接上面定义的坐标点
CGContextStrokePath(context);
}
再说明一下重绘,重绘操作仍然在drawRect方法中完成,但是苹果不建议直接调用drawRect方法,当然如果你强直直接调用此方法,当然是没有效果的。苹果要求我们调用UIView类中的setNeedsDisplay方法,则程序会自动调用drawRect方法进行重绘。
在UIView中,重写drawRect: (CGRect) aRect方法,可以自己定义想要画的图案.drawRect方法依赖Core Graphics框架来进行自定义的绘制
-
缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了
-
这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制
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不提倡
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect ,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
几个坑:
ImageView的子类上重写DrawRect,但是却不生效
苹果的官方文档说了, UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从UIView或自定义的子类里重写。
2.drawRect里面加上super调用行不行?
同学,苹果官方文档是这样说的:
If you subclass UIView directly, your implementation of this method does not need to call super. 所以 , 不用写 [super drawRect] ;
3.drawRect里就算不加任何代码,也会导致内存增长
新建一个demo,添加一个自定义的UIView,宽高为1000*1000,重写drawRect会导致内存增长14.8M
没有重写drawRect , 启动内存 11.2M
重写了drawRect, 里面是空方法, 内存为26M,
增长内存的计算方式为 xxM = 宽*几倍屏*高*几倍屏*4/1024/1024;
尺寸越大计算的误差就越小, 比如一个1000*1000的view在 2倍屏的手机上, 就是1000*2*1000*2*4/1024/1024 = 15.2M, 和上面的计算结果比较接近.
4.那么现在我们分析一下drawRect
导致内存暴增的真正原因:
重写drawRect
为何会导致内存大量上涨?参考链接
要想搞明白这个问题,我们需要撸一撸在 iOS 程序上图形显示的原理。在 iOS 系统中所有显示的视图都是从基类UIView
继承而来的,同时UIView
负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView
的一个实例图层属性来绘制和渲染的,那就是CALayer
。
CALayer
类的概念与UIView
非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView
最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在,它的 API 虽然提供了 “某点是否在图层范围内的方法”,但是它并不具有响应的能力。
在每一个UIView
实例当中,都有一个默认的支持图层,UIView
负责创建并且管理这个图层。实际上 这个CALayer
图层才是真正用来在屏幕上显示的 ,UIView
仅仅是对它的一层封装,实现了CALayer
的delegate
,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。
可以说CALayer
是UIView
的内部实现细节。
脑补了这么多,它与今天的主题drawRect
有何关系呢?别着急,我们既然已经确定CALayer
才是最终显示到屏幕上的,只要顺藤摸瓜,即可分析清楚。CALayer
其实也只是 iOS 当中一个普通的类,它也并不能直接渲染到屏幕上,因为屏幕上你所看到的东西,其实都是一张张图片。而为什么我们能看到CALayer
的内容呢,是因为CALayer
内部有一个contents
属性。contents
默认可以传一个id
类型的对象,但是只有你传CGImage
的时候,它才能够正常显示在屏幕上。 所以最终我们的图形渲染落点落在contents
身上 如图。
contents
也被称为寄宿图,除了给它赋值CGImage
之外,我们也可以直接对它进行绘制,绘制的方法正是这次问题的关键,通过继承UIView
并实现-drawRect:
方法即可自定义绘制。-drawRect:
方法没有默认的实现,因为对UIView
来说,寄宿图并不是必须的,UIView
不关心绘制的内容。但是一旦重写了-drawRect:,
CALayer需要创建一个空寄宿图(有尺寸)和一个Core Graphics 的CGContextRef(上下文),当绘制结束后,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务器进行显示,同时上下文会被不断渲染到屏幕上,直到下次调用setNeedsDisplay。
所以每次重绘都需要抹掉内存重新分配,空寄宿图的产生就消耗了大量内存,这也就是drawRect 内存暴增原因。
总结起来:如果UIView
检测到-drawRect:
方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale
(这个属性与屏幕分辨率有关) 的值。
官方在图像的最佳实践中也提到了- drawRect:
iOS图像最佳实践总结 - 简书 苹果开发者大会 - Image and Graphics Best Practices
先看一种不合理的实现方式
我们先来分析这种方案的问题所在,
UIView是通过CALayer创建FrameBuffer最后显示的。重写了drawRect方法,Calayer会创建一个Backing Store,然后在Backing Store上执行draw函数,最后将内容传递给frameBuffer最终显示。
Backing Store的默认大小和View的大小成正比,以iphone6为例,375*2 * 667*2 * 4 字节 ≈ 3.4 Mb。
iOS 12,对 backing store 有做优化,它的大小会根据图片的色彩空间,动态改变。
在此之前,如果你使用 sRGB 格式,但是实际绘制的内容,只使用了单通道,那么大小会比实际要的大,造成不必要开销。iOS 12 会自动优化这部分。
总结下这种使用drawRect绘制方案的问题:
- Backing Store的创建造成了不必要的内存开销
- UIImage先绘制到Backing Store,再渲染到frameBuffer,中间多了一层内存拷贝
- 背景颜色不需要绘制到Backing Store,直接使用BackGroundColor绘制到FrameBuffer
所以,正确的实现姿势是将这个大的view拆分成小的subview逐个实现。