内存恶鬼drawRect


转载于:http://bihongbo.com/
   

内存恶鬼drawRect

标题有点吓人,但是对于drawRect的评价倒是一点都不过分。在平日的开发中,随意覆盖drawRect方法,稍有不慎就会让你的程序内存暴增。下面我们来看一个例子。

去年的某天午后,北京的雾霾依旧像现在这样醇厚,我的同事辉哥像往常一样与我楼下约烟。我见辉哥表情凝重,便询问究竟。辉哥做了一个画板功能,但是苦于内存问题一直得不到解决。画板功能很简单,就是记录手指触摸的轨迹然后绘制在屏幕上。下面我们来看一张效果图:

效果图

如图我们看到左侧内存的状况随着手指的绘制逐渐恶化。另外细心的同学可以观察到,点击图中蓝色矩形按钮之后,便会弹出画板,而这时并没有进行任何的手指绘制,内存就突变为114MB,然后每当手指绘制开始时,内存立即增加到300MB左右稳定下来。对于正常的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所有的代码。运行的效果图如下:

效果图1

效果立竿见影,注释掉drawRect之后,内存立刻恢复正常,我们终于抓到了消耗内存的恶鬼,问题就出在对drawRect方法的覆盖。那么抓到了犯人,本文是否应该完结了?非也非也,我们虽说知道了内存暴增的原因,但是我们并没有深入的去分析drawRect为什么对内存的影响这么大,而且我们也没有给出问题的解决方案。请接着往下看。

那么现在我们分析一下drawRect导致内存暴增的真正原因:

重写drawRect为何会导致内存大量上涨?

要想搞明白这个问题,我们需要撸一撸在iOS程序上图形显示的原理。在iOS系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer

CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在,它的API虽然提供了“某点是否在图层范围内的方法”,但是它并不具有响应的能力。

在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的UIView仅仅是对它的一层封装,实现了CALayerdelegate,提供了处理事件交互的具体功能,还有动画底层方法的高级API。

可以说CALayerUIView的内部实现细节。

脑补了这么多,它与今天的主题drawRect有何关系呢?别着急,我们既然已经确定CALayer才是最终显示到屏幕上的,只要顺藤摸瓜,即可分析清楚。CALayer其实也只是iOS当中一个普通的类,它也并不能直接渲染到屏幕上,因为屏幕上你所看到的东西,其实都是一张张图片。而为什么我们能看到CALayer的内容呢,是因为CALayer内部有一个contents属性。contents默认可以传一个id类型的对象,但是只有你传CGImage的时候,它才能够正常显示在屏幕上。所以最终我们的图形渲染落点落在contents身上如图。

效果图2

contents也被称为寄宿图,除了给它赋值CGImage之外,我们也可以直接对它进行绘制,绘制的方法正是这次问题的关键,通过继承UIView并实现-drawRect:方法即可自定义绘制。-drawRect: 方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,UIView不关心绘制的内容。如果UIView检测到-drawRect:方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale(这个属性与屏幕分辨率有关,我们的画板程序在不同模拟器下呈现的内存用量不同也是因为它)的值。

那么回到我们的画板程序,当画板从屏幕上出现的时候,因为重写了-drawRect:方法,-drawRect :方法就会自动调用。生成一张寄宿图后,方法里面的代码利用Core Graphics去绘制n条黑色的线,然后内容就会缓存起来,等待下次你调用-setNeedsDisplay时再进行更新。

画板视图的-drawRect:方法的背后实际上都是底层的CALayer进行了重绘和保存中间产生的图片,CALayerdelegate属性默认实现了CALayerDelegate协议,当它需要内容信息的时候会调用协议中的方法来拿。当画板视图重绘时,因为它的支持图层CALayer的代理就是画板视图本身,所以支持图层会请求画板视图给它一个寄宿图来显示,它此刻会调用:

(void)displayLayer:(CALayer *)layer;

如果画板视图实现了这个方法,就可以拿到layer来直接设置contents寄宿图,如果这个方法没有实现,支持图层CALayer会尝试调用:

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

这个方法调用之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由boundscontentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。在这一步生成的空寄宿图内存是相当巨大的,它就是本次内存问题的关键,一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的内存可从这个公式得出:图层宽*图层高*4字节,宽高的单位均为像素。而我们的画板程序因为要支持像猿题库一样两指挪动的效果,我们开辟的画板大小为:

_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会自动渲染。它可以完美替代我们的直接使用Core Graphics绘制layer,对比之下使用CAShapeLayer有以下优点:

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
  • 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
  • 不会被图层边界剪裁掉。
  • 不会出现像素化。

所以最终我们的画板程序使用CAShapeLayer来实现线条的绘制,性能非常稳定,效果图如下:

效果图3

总结一下绘制性能优化原则:

  • 1.绘制图形性能的优化最好的办法就是不去绘制。
  • 2.利用专有图层代替绘图需求。
  • 3.不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。
  • 4.异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。

本文最后一个效果图为仿写猿题库练题画板功能,demo请在github搜索BHBDrawBoarderDemo。或者直接戳这里。

好了,就是这么多,文章比较乱,如有纰漏请不吝指出!想看对本文的补充请到这里

  •  

转载于:https://www.cnblogs.com/zhao-jie-li/p/5163654.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/262088.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c#和python同一主机直接udp_为什么Python 如此之慢

原文:https://hackernoon.com/why-is-python-so-slow-e5074b6fe55b​hackernoon.comPython 正在爆炸般流行起来,它被用于DevOps, 数据处理,web开发和安全领域。但是在速度方面却没有取得过什么胜利。 Java在速度方面和C/C/C#/Python比起来如何&#xff…

lldb 调试php,linux系统下如何在vscode中调试C++代码

本篇博客以一个简单的hello world程序,介绍在vscode中调试C代码的配置过程。1. 安装编译器vscode是一个轻量的代码编辑器,并不具备代码编译功能,代码编译需要交给编译器完成。linux下最常用的编译器是gcc,通过如下命令安装&#x…

php负责传递数据,php传递数据的方法有哪些

php传递数据的方法有哪些发布时间:2021-03-19 09:10:54来源:亿速云阅读:58作者:小新这篇文章将为大家详细讲解有关php传递数据的方法有哪些,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅…

除了工作怎么交朋友_夫妻感情不好怎么办?夫妻关系紧张该如何解决?

夫妻感情不好怎么办?夫妻关系紧张该如何解决?1. 象征两人的兴趣随着年龄的增长而多样化。年轻的时候往往因为他们之间的差异而互相吸引,但当他们在中老年则需要更多的安慰或关心,理解,需求就不同了。当你发现自己的志向迥然不同时…

java websocket修改为同步_服务端向客户端推送消息技术之websocket的介绍

websocket的介绍在讲解WebSocket前,我们先来看看下面这种场景,在HTTP协议下,怎么实现。需求:在网站中,要实现简单的聊天,这种情况怎么实现呢?如下图:​当发送私信的时候,…

python random randint_python中random.randint和random.randrange的区别详解

在python中,通过导入random库,就能使用randint 和 randrange 这两个方法来产生随机整数。那这两个方法的区别在于什么地方呢?让我们一起来看看! 区别: randint 产生的随机数区间是包含左右极限的,也就是说左右都是闭区…

How to change max_allowed_packet size

2019独角兽企业重金招聘Python工程师标准>>> How to change max_allowed_packet size up vote 116 down vote favorite 40I am having a problem with BLOB fields in my MySQL database - when uploading files larger than approx 1M…

.net runtime占用cpu_.net 中的StringBuilder和TextWriter区别

最近闲来之余,看了一些开源的类库,看到有些类库喜欢用TextWriter类来记录相关的字符串数据,感到比较好奇,为啥不用StringBuilder类对象。于是在网上搜索了一番,总结了相关笔记。StringBuilder类在 .net 中,字符串作为一…

微信页面弹出窗口,底部不随窗口滑动而滚动

公司是做微信第三方公众平台,有一个购物系统,现在需要将商品页面模仿淘宝的样式,就是点击购物车或购买按钮,会弹出一个窗口,显示sku和数量。本来就是一个做java后台的,前端布局不是很懂,上网搜索…

【GIT】使用Git命令窗口将本地工程提交至远程GitHub

目标: 1.解决的问题是如何通过Git命令窗口将本地工程提交至GitHub。 2.方便园友的同时也方便自己以后解决此类问题。 步骤: 1.首先登陆GitHub网站https://github.com/ 2.新建Repository 3.新建TicketBrushSystem的Repository,其中红框的几处可…

softmax ce loss_手写softmax和cross entropy

import 解释下给定的数据,x假设是fc layer的输出,可以看到这里x是(3,3)的,也就是batch_size3,n_classes3。但是label给出了三个数,取值是0,1,因此这里必须要将label先变成one_hot的形式才能在cr…

kafka php 0.8,php5.6 centos7 kafka0.8.1

# 首先在保证php已经正确安装的情况下:# 安装jdk(本人安装jdk7) 和 scala 因为kafka基于scala开发# 之后解压安装包 进入 运行命令 ./gradlew jar # 会下载一些包 # 首先运行 zookeeper ./bin/zookeeper-server-start.sh ./config/zookeeper.properties# 之后运行k…

day02(下)_运算符

首先了解java运算符的优先级和结合性: 常用转义字符表: 示例1 取余/字符串/转义字符: class operator1 {public static void main(String[] args){//取余System.out.println(-1%5);//-1System.out.println(1%-5);//1与左边的操作数同号System.out.println(3.1%2.5);//字符串:字…

activity 生命周期_如何理解安卓activity的生命周期(on-create篇)?

个人认为用类比的方式来学习新事物比较容易接受。我这里用蝴蝶的一生来做比喻。OnCreate阶段就像是蝴蝶的幼虫刚出卵里孵化出来,蝴蝶的一生只可能出生一次,oncreate只能被创建一次。蝴蝶刚出生的时候还不是蝴蝶,而是一只毛毛虫一样的东西&…

【十大经典数据挖掘算法】k-means

【十大经典数据挖掘算法】系列 C4.5K-MeansSVMAprioriEMPageRankAdaBoostkNNNave BayesCART1. 引言 k-means与kNN虽然都是以k打头,但却是两类算法——kNN为监督学习中的分类算法,而k-means则是非监督学习中的聚类算法;二者相同之处&#xff1…

ajax获取数据用弹窗显示_Vue之 点击返回弹出推荐商品弹窗

阅读本文约需要5分钟大家好,我是你们的导师,我每天都会在这里给大家分享一些干货内容(当然了,周末也要允许老师休息一下哈)。上次老师跟大家分享了VUE 之 v-on指令的知识,今天跟大家分享下Vue之 点击返回弹出推荐商品弹窗的知识。…

Navicat 编辑器自动完成代码功能讲解

2019独角兽企业重金招聘Python工程师标准>>> Navicat 提供广泛的编辑器高级功能,例如:编辑代码功能、智能自动完成代码、设置 SQL 格式及更多。本教程将介绍关于自动完成代码的操作。 Navicat下载地址:http://www.formysql.com/xi…

com 组件调用不起来_AwesomeGithub组件化探索之旅

阅读前请点击右上角“关注”,每天免费获取Android知识解析及面试解答。Android架构解析,只做职场干货,完全免费分享!之前一直听说过组件化开发,而且面试也有这方面的提问,但都未曾有涉及具体的项目。所以就…

将团队迁移到可视化项目管理软件

自2000年代中期,“Scrum”项目管理(PM)一直统治着软件开发方法。它的迭代结构、频繁会议和清晰的层次结构使其成为受频繁变化的客户需求和条件管制的行业的明显选择。因此,大多数团队习惯基于 Scrum项目管理应用管理开发过程。 \\…

一步一步学Silverlight 2系列(3):界面布局_转载

概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, Ironpython,对JSON、Web Service、WCF以及Sockets的支持等一系列新的特性。《一步一步学Silverlig…