drawRect - 谈画图功能的内存优化
作者介绍
作者:毕洪博 ( @毕洪博 ),iOS 开发者,pop Art 追随者。现在正在鼓捣 AVFoundation,博客 bihongbo.com, 欢迎大家找我讨论技术。
作者已将本文在微信公众平台的发表权「独家代理」给 iOS 开发微信公共号,本文的打赏归毕洪博所有,以下是文章正文。
正文
标题有点吓人,但是对于drawRect
的评价倒是一点都不过分。在平日的开发中,随意覆盖drawRect
方法,稍有不慎就会让你的程序内存暴增。下面我们来看一个例子。
去年的某天午后,北京的雾霾依旧像现在这样醇厚,我的同事辉哥像往常一样与我楼下约烟。我见辉哥表情凝重,便询问究竟。辉哥做了一个画板功能,但是苦于内存问题一直得不到解决。画板功能很简单,就是记录手指触摸的轨迹然后绘制在屏幕上。下面我们来看一张效果图:
如图我们看到左侧内存的状况随着手指的绘制逐渐恶化。另外细心的同学可以观察到,点击图中蓝色矩形按钮之后,便会弹出画板,而这时并没有进行任何的手指绘制,内存就突变为 114 MB ,然后每当手指绘制开始时,内存立即增加到 300 MB 左右稳定下来。对于正常的 iOS App 来讲,这么大的内存消耗是不能容忍的。
下面分析一下原因:
可能的原因有两个,一是在手指绘制的过程中创建的大量点对象没有及时释放或者其他资源没有及时释放。
二是系统在绘制的过程中开始大量消耗内存。
第一个原因,手指绘制的过程中创建的大量点对象没有及时释放或者其他资源没有及时释放。这一点我们暂时排除以节省时间,因为这个画板功能工程是用ARC
写的,并且我们已经做过代码检查和使用Instruments
工具来检测内存使用情况,这里并没有所谓的对象没有及时释放的问题存在。
第二个原因,系统在绘制的过程中开始大量消耗内存。首先我们曾经注意到一个诡异并且不寻常的事情就是,当黄色的画板刚刚弹出的时候内存就瞬间从 18MB 暴增至 114MB 。这一点更加说明第一个原因不是问题所在,因为这时手指还没有进行任何绘制,也就是说不存在任何点与线的对象,那么内存怎么会暴增呢?
这时我们要考虑这个画板功能是如何实现的,画板分为两步,第一步记录用户手指的轨迹,这一步会生成大量点的对象(已排除嫌疑)。第二步绘制到视图或者图层上,我们平常使用频繁的绘图方式基本上是 Quarz2D 的那套 C 语言框架,而绘制代码所在的地点在哪呢?我们今天的主角终于上场了--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);
}
}
去掉绘图上下文栈和其余判断边界的代码,我们只是在当前view
上绘制了n
条黑色的线。看起来普普通通的绘图方式,怎么会导致内存的剧增呢?我们现在说罪魁祸首是drawRect
证据并不充分。我们回想画板刚弹出时的内存状况,接下来我们注释掉drawRect
所有的代码。运行的效果图如下:
效果立竿见影, 注释掉drawRect
之后,内存立刻恢复正常,我们终于抓到了消耗内存的恶鬼,问题就出在对drawRect
方法的覆盖。 那么抓到了犯人,本文是否应该完结了?非也非也,我们虽说知道了内存暴增的原因,但是我们并没有深入的去分析drawRect
为什么对内存的影响这么大,而且我们也没有给出问题的解决方案。请接着往下看。
那么现在我们分析一下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
不关心绘制的内容。如果UIView
检测到-drawRect:
方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale
(这个属性与屏幕分辨率有关,我们的画板程序在不同模拟器下呈现的内存用量不同也是因为它) 的值。
那么回到我们的画板程序,当画板从屏幕上出现的时候,因为重写了-drawRect:
方法,-drawRect :
方法就会自动调用。 生成一张寄宿图 后,方法里面的代码利用Core Graphics
去绘制 n 条黑色的线,然后内容就会缓存起来,等待下次你调用-setNeedsDisplay
时再进行更新。
画板视图的-drawRect:
方法的背后实际上都是底层的CALayer
进行了重绘和保存中间产生的图片,CALayer
的delegate
属性默认实现了CALayerDelegate
协议,当它需要内容信息的时候会调用协议中的方法来拿。当画板视图重绘时,因为它的支持图层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 字节
,宽高的单位均为像素。而我们的画板程序因为要支持像猿题库一样两指挪动的效果,我们开辟的画板大小为:
_myDrawer = [[BHBMyDrawer alloc] initWithFrame:
CGRectMake(, , SCREEN_SIZE.width*, SCREEN_SIZE.height*)];
我们的画板程序的画板视图它在iPhone6s plus
机器上的上下文内存量就是 1920*2
*1080*5
*4 字节
, 相当于79MB
内存 ,图层每次重绘的时候都需要重新抹掉内存然后重新分配。它就是我们画板程序内存暴增的真正原因。
最终我们将内存暴增的原因找出来了,那么我们有没有合理的解决方案呢?
我认为最合理的办法处理类似于画板这样画线条的需求直接用专有图层CAShapeLayer
。让我们看看它是什么:
CAShapeLayer
是一个通过矢量图形而不是bitmap
来绘制的图层子类。用CGPath
来定义想要绘制的图形,CAShapeLayer
会自动渲染。它可以完美替代我们的直接使用Core Graphics
绘制layer
,对比之下使用CAShapeLayer
有以下优点:
渲染快速。CAShapeLayer 使用了硬件加速,绘制同一图形会比用 Core Graphics 快很多。
高效使用内存。一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
不会被图层边界剪裁掉。
不会出现像素化。
所以最终我们的画板程序使用CAShapeLayer
来实现线条的绘制,性能非常稳定,效果图如下:
总结一下绘制性能优化原则:
绘制图形性能的优化最好的办法就是不去绘制。
利用专有图层代替绘图需求。
不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。
异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。
本文最后一个效果图为仿写猿题库练题画板功能,demo请在github搜索 BHBDrawBoarderDemo
。
或者直接戳这里: https://github.com/bb-coder/BHBDrawBoarderDemo。
好了,就是这么多,如有纰漏请不吝指出!
good luck!
参考:iOS Core Animation: Advanced Techniques
感谢:AttackOnDobby 及其翻译团队。
drawRect - 谈画图功能的内存优化的更多相关文章
- Django项目:CRM(客户关系管理系统)--14--06PerfectCRM实现King_admin注册功能获取内存优化处理
<th >{% get_app_name admin_class.model %}{{ admin_class }} </th> #kingadmin_tags.py # —— ...
- 浅谈C51内存优化
对 51 单片机内存的认识,很多人有误解,最常见的是以下两种 超过变量128后必须使用compact模式编译,实际的情况是只要内存占用量不超过 256.0 就可以用 small 模式编译 128以上的 ...
- Impala内存优化(转载)
一. 引言 Hadoop生态中的NoSQL数据分析三剑客Hive.HBase.Impala分别在海量批处理分析.大数据列式存储.实时交互式分析各有所长.尤其是Impala,自从加入Hadoop大家庭以 ...
- 使用内存映射文件MMF实现大数据量导出时的内存优化
前言 导出功能几乎是所有应用系统必不可少功能,今天我们来谈一谈,如何使用内存映射文件MMF进行内存优化,本文重点介绍使用方法,相关原理可以参考文末的连接 实现 我们以单次导出一个excel举例(csv ...
- 试试SQLSERVER2014的内存优化表
试试SQLSERVER2014的内存优化表 SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据库管理系统,而使用先进内存技 ...
- [经验] Win7减肥攻略(删文件不删功能、简化优化系统不简优化性能)
[经验] Win7减肥攻略(删文件不删功能.简化优化系统不简优化性能) ☆心梦无痕☆ 发表于 2014-1-24 11:15:04 https://www.itsk.com/thread-316471 ...
- JavaScript内存优化
JavaScript内存优化 相对C/C++ 而言,我们所用的JavaScript 在内存这一方面的处理已经让我们在开发中更注重业务逻辑的编写.但是随着业务的不断复杂化,单页面应用.移动HTML5 应 ...
- [WP8.1UI控件编程]Windows Phone大数据量网络图片列表的异步加载和内存优化
11.2.4 大数据量网络图片列表的异步加载和内存优化 虚拟化技术可以让Windows Phone上的大数据量列表不必担心会一次性加载所有的数据,保证了UI的流程性.对于虚拟化的技术,我们不仅仅只是依 ...
- Unity3D 游戏开发之内存优化
项目的性能优化主要围绕CPU.GPU和内存三大方面进行. 无论是游戏还是VR应用,内存管理都是其研发阶段的重中之重. 然而,在我们测评过的大量项目中,90%以上的项目都存在不同程度的内存使用问题.就目 ...
随机推荐
- web项目部署后动态编译无法找到依赖的jar包
很纳闷的一个问题,通过配置文件生成的java源码在本地动态编译没有问题,但是部署服务器后编译不通过,找不到依赖的jar包. 通过网上查资料,找到一个兄弟提供的方法,问题解决了:下面贴出代码以供参考: ...
- 设置eclipse的Maven插件引入依赖jar包后自动下载并关联相应的源码(转)
好多用 Maven 的时候会遇到这样一个棘手的问题: 就是添加依赖后由于没有下载并关联源码,导致自动提示无法出现正确的方法名,而且不安装反编译器的情况下不能进入方法内部看具体实现 . 其实 eclip ...
- php输出变量加{}的作用
之前在输出字符串中有变量如 echo “中间有”; echo $i; echo "变量"; 现在发现一个好方法,把变量用{}括起来 echo "中间有{$i}变量&quo ...
- Python网络编程之基础
计算机网络基础 网络到底是什么?计算机之间如何通信的? 早期:联机 以太网:局域网与交换机 ******广播 主机之间“一对所有”的通讯模式,网络对其中每一台主机发出的信号都进行无条件复制并转发, 所 ...
- hdu2510-符号三角形(dfs+打表)
n只有24 可以写个暴力搜索,然后打表,不然这个很难通过剪枝直接优化到1s以内. #include<bits/stdc++.h> #define inf 0x3f3f3f3f ; usin ...
- python2 学习 数据类型和变量
数据类型和变量 数据类型 整数 Python可以处理任意大小的整数,当然包括负整数,在程序中的表示方法和数学上的写法一模一样,例如:1,100,-8080,0,等等. 计算机由于使用二进制,所以,有时 ...
- ssis-oracle 数据流任务
[OLE DB 源 1 [16]] 错误: SSIS 错误代码 DTS_E_CANNOTACQUIRECONNECTIONFROMCONNECTIONMANAGER.对连接管理器“F360DB”的 A ...
- 2、linux基础知识与技能
2.1.linux内核.发行版linux本身指的是一个操作系统内核,只有内核是无法直接使用的.我们需要的,可以使用的操作系统是一个包含了内核和一批有用的应用程序的一个集合体,这个就叫linux发行版. ...
- [原]Maven项目编译后classes文件中没有.xml问题
在做spring+mybatiss时,自动扫描都配置正确了,却在运行时出现了如下错误.后来查看target/classes/.../dao/文件夹下,发现只有mapper的class文件,而没有xml ...
- eclipse下 Failed to find an AVD compatible with target 的解决方法
第一个Android测试环境下的程序出现这个问题: [2012-04-24 13:18:29 - xxxx] ------------------------------ [2012-04-24 13 ...