前两天QQ群大佬提出一个问题,如何解决CALayer的contents属性赋值之后的内存暴增问题,百度了一下无头绪,偶然看到了唐巧的文章,感觉很有意思,特此记录一下。原文在此:内存恶鬼drawRect - 谈画图功能的内存优化
正文:
先来说一下contens
是个啥东西:CoreAnimation:CALayer的contents
唐巧文章里写的很清楚了,在这里不再过多的赘述,大概是这样的:
- 搞了一个简易功能的画板,记录手指触摸的轨迹然后绘制在屏幕上。
- 发现两个问题:当画板弹出,其余无任何操作时,内存激增,然后当手指绘制开始时,内存又激增。
- 分析原因可能有两个:一是在手指绘制的过程中创建的大量点对象没有及时释放或者其他资源没有及时释放。这一点因为工程为
ARC
以Instruments
工具得以排除。二是系统在绘制过程中开始大量消耗内存,但是明显可以发现是画板创建之后就会有内存激增,所以矛头直指drawRect
。
这是画板绘制功能的一段代码:
- (void)drawRect:(CGRect)rect
{
if (!self.paths.count) return;
CGContextRef ctx = UIGraphicsGetCurrentContext();
for (BHBPaintPath *path in self.paths) {
CGContextSaveGState(ctx);
[[UIColor blackColor] set];
[path stroke]; // 关键的一步绘制
CGContextRestoreGState(ctx);
}
}
- 分析为何
drawRect
会出现内存暴增的真正原因:
简单来说,咱们所看到的屏幕上的东西,其实都是一张图片,是由CALayer
的contents
属性掌管着,最终图形渲染落点落在了contents
身上。 -
contents
也被称为寄宿图,除了给它赋值CGImage
以外,我们也可以直接对它进行绘制,绘制的方法正式此次问题的关键,通过集成UIView
并实现-drawRect:
方法即可自定义绘制。-drawRect:
方法没有默认的实现,因此对UIView
来说,寄宿图并不是必须的,UIView
不关心绘制的内容。如果UIView
检测到-drawRect:
方法被调用了,他就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale
(这个属性与屏幕分辨率有关,我们的画板程序在不同模拟器下呈现的内存用量不同也是因为它)的值。
那么我们重回画板程序,当画板从屏幕上出现的时候,因为重写了-drawRect:
方法,-drawRect:
方法就会自动调用。生成一张寄宿图后,方法里面的代码利用Core Graphics
去绘制n条黑色的线,然后 内容就会缓存起来,等待下次你调用-setNeedsDisplay
时再进行更新。
画板视图的-drawRect:
方法的背后实际上都是底层的CALayer
进行了重绘和保存中间产生的图片,CALayer
的delegate
属性默认实现了CALyaerDelegate
协议,当它需要内容信息的时候回调用协议中的方法来拿。当画板重绘时,因为他的支持图层CALayer
的代理就是画板视图的本身,所以支持图层会请求画板视图给它一个寄宿图来显示,它此刻会调用:
- (void)displayLayer:(CALayer *)layer;
如果画板试图实现了这个方法,就可以拿到layer
来直接设置contents
寄宿图,如果这个方法没有实现,支持图层CALayer
会尝试调用:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
这个方法调用之前,CALayer
创建了一个合适尺寸的空寄宿图(尺寸由bounds
和contentsScale
决定)和一个Core Graphics
的绘制山下文环境,为绘制寄宿图做准备,它作为ctx
参数传入。在这一步生成的空寄宿图内存是相当巨大的,它就是本次内存问题的关键,一旦你实现了CALayerDelegate
协议中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的内存可从这个公式得出:图层宽图层高4字节,宽高的单位均为像素。所以图层在每次绘制的时候都需要重新抹掉内存然后重新分配。它就是我们画板程序内存暴增的真正原因。
- 解决方法
处理类似于画板这样画线条的需求直接用专用图层CAShapeLayer
。
来看看这是个什么东西:
CAShapeLayer
是一个通过矢量图形而不是bitmap
来绘制的图层子类。用CGPath
来定义想要绘制的图形,CAShapeLayer
会自动渲染。他可以完美替代我们直接使用Core Graphics
绘制layer
,相比之下使用CAShapeLayer
有以下优点:
- 渲染快速。
CAShapeLayer
使用了硬件加速,绘制同一图形会比用Core Graphics
快很多。 - 高效使用内存。一个
CAShapeLayer
不需要像普通CALayer
一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。 - 不会被图形边界剪裁掉。
- 不会出现像素化。
- 总结一下绘制性能优化原则:
- 绘制图形性能的优化最好的办法就是不去绘制。
- 利用专有图层代替绘图需求。
- 不得不用到的绘图尽量缩小试图面积,并且尽量降低重绘频率。
- 异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。
- 最后附上画板-demo