前言
作为iOS
平台的开发者,是否曾经为内存问题而苦恼过?内存莫名的持续增长,程序莫名的crash
,难以发现的内存泄漏,这些都是iOS
平台内存相关的常见问题;本文将会详细介绍iOS
平台的内存管理机制,autorelease
机制和内存的使用陷阱,这些将会解决iOS
平台内存上的大部分问题,提高了程序的稳定性;
iOS
平台内存管理介绍
iOS
平台的内存管理采用引用计数的机制;当创建一个对象时使用alloc
或者allWithZone
方法时,引用计数就会+1
;当释放对象使用release
方法时,引用计数就是-1
;这就意味着每一个对象都会跟踪有多少其他对象引用它,一旦引用计数为0
,该对象的内存就会被释放掉;另外,iOS
也提供了一种延时释放的机制AutoRelease
,以这种方式申请的内存,开发者无需手动释放,系统会在某一时机释放该内存; 由于iOS
平台的这种内存管理的多样性,导致开发者在内存使用上很容易出现内存泄漏或者程序莫名崩溃的情况,本文会详细介绍iOS
平台内存的使用规范与技巧以及如何利用工具避免或者发现问题;
iOS
平台内存使用原则
1、对象的所有权与销毁
1.谁创建,谁释放;
如果是以alloc
,new
或者copy
,mutableCopy
创建的对象,则必须调用release
或者autorelease
方法释放内存; 如果没有释放,则导致内存泄漏!
2.谁retain
,谁释放;
如果对一个对象发送retain
消息,其引用计数会+1
,则使用完必须发送release
或者autorelease
方法释放内存或恢复引用计数;
如果没有释放,则导致内存泄漏!
3.没创建且没retain
,别释放;
不要释放那些不是自己alloc
或者retain
的对象,否则程序会crash
;
不要释放autorelease
的对象,否则程序会crash
;
2、对象的深拷贝与浅拷贝 一般来说,复制一个对象包括创建一个新的实例,并以原始对象中的值初始化这个新的实例。复制非指针型实例变量的值很简单,比如布尔,整数和浮点数。复制指 针型实例变量有两种方法。一种方法称为浅拷贝,即将原始对象的指针值复制到副本中。因此,原始对象和副本共享引用数据。另一种方法称为深拷贝,即复制指针 所引用的数据,并将其赋给副本的实例变量。
1.深拷贝
深拷贝的流程是 先创建一个新的对象且引用计数为1
,并用旧对象的值初始化这个新对象;ClassA* objA = [[ClassA alloc] init]; ClassA* objB = [objA copy];
objB
是一个新对象,引用计数为1
,且objB
的数据等同objA
的数据;
注意:objB
需要释放,否则会引起内存泄漏!2.浅拷贝
浅拷贝的流程是,无需引入新的对象,把原有对象的引用计数+1
即可ClassA* objA = [[ClassA alloc] init]; ClassA* objB = [objA retain];
注意:
objB
需要释放,恢复objA
的引用计数,否则会引起内存泄漏!
3、对象的存取方法
1.属性声明和实现
变量声明的常用属性类型包括readonly
,assign
,retain
和copy
;且系统会自动为声明了属性的变量生成set
和get
函数;
readonly
属性:只能读,不能写;
assign
属性:是默认属性,直接赋值,没有任何保留与释放问题;
retain
属性:会增加原有对象的引用计数并且在赋值前会释放原有对象,然后在进行赋值;
copy
属性:会复制原有对象,并在赋值前释放原有对象,然后在进行赋值;
2.使用属性声明可能带来的隐患
当一个非指针变量使用retain
(或者copy
)这个属性时,尽量不要显性的release
这个变量;直接给这个变量置空即可;否则容易产生过度释放,导致程序crash
; 例如:
ClassA
类的strName
是NSString*
类型的变量且声明的属性为retain
;ClassA.strName = nil
;
[ClassA.strName release]
;
Assign
这个属性一般是非指针变量(布尔类型,整形等)时用这个类型;属于直接赋值型,不需要考虑内存的保留与释放; 如果一个指针类型的变量使用assign
类型的属性,有可能引用已经释放的变量;导致程序crash
; 例如:
ClassB* obj =[[[ClassB alloc] init] autorelease];
……
ClassA.strName = obj;
后续在使用
ClassA.strName
的时候, 因为obj
是autorelease
的,可能obj的内存已经被回收;导致引用无效内存,程序crash
;
iOS
平台AutoRelease
机制
1.自动释放池的常见问题
大家在开发iOS
程序的时候,是否遇到过在列表滑动的情况内存莫名的增长,频繁访问图片的时候内存莫名的增长,频繁的打开和关闭数据库的时候内存莫名的增长…… 这些都是拜iOS
的autorelease
机制所赐;具体分析如下:
滑动列表的时候,内存出现莫名的增长,原因可能有如下可能
1.没有使用UITableView
的reuse
机制; 导致每显示一个cell
都用autorelease
的方式重新alloc
一次;导致cell
的内存不断的增加; 2.每个cell
会显示一个单独的UIView
, 在UIView
发生内存泄漏,导致cell
的内存不断增长; 频繁访问图片的时候,内存莫名的增长 频繁的访问网络图片,导致iOS
内部API
,会不断的分配autorelease
方式的buffer
来处理图片的解码与显示; 利用图片cache
可以缓解一下此问题;
频繁打开和关闭SQLite
,导致内存不断的增长 在进行SQLite
频繁打开和关闭操作,而且读写的数据buffer
较大,那么SQLite
在每次打开与关闭的时候,都会利用autorelease
的方式分配51K
的内存; 如果访问次数很多,内存马上就会顶到几十兆,甚至上百兆! 所以针对频繁的读写数据库且数据buffer
较大的情况, 可以设置SQLite
的长连接方式;避免频繁的打开和关闭数据库;
2.自动释放池的概念
NSAutoreleasePool
内部包含一个数组(NSMutableArray
),用来保存声名为autorelease
的所有对象。如果一个对象声明为autorelease
,系统所做的工作就是把这个对象加入到这个数组中去。
ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain count = 1
,把此对象加入autorelease pool
中
NSAutoreleasePool
自身在销毁的时候,会遍历一遍这个数组,release
数组中的每个成员。如果此时数组中成员的retain count
为1
,那么release
之后,retain count
为0
,对象正式被销毁。如果此时数组中成员的retain count
大于1
,那么release
之后,retain count
大于0
,此对象依然没有被销毁,内存泄露。
3.自动释放池的作用域与嵌套
AutoreleasePool
是可以嵌套使用的!
池是被嵌套的,嵌套的结果是个栈,同一线程只有当前栈顶pool
实例是可用的:
当短生命周期内,比如一个循环中,会产生大量的临时内存,可以创建一个临时的autorelease pool
,这样可以达到快速回收内存的目的;
4.自动施放池的手动创建与自动创建
1需要手动创建自动释放池
●如果你正在编写一个不是基于Application Kit
的程序,比如命令行工具,则没有对自动释放池的内置支持;你必须自己创建它们。
●如果你生成了一个从属线程,则一旦该线程开始执行,你必须立即创建你自己的自动释放池;否则,你将会泄漏对象。
●如果你编写了一个循环,其中创建了许多临时对象,你可以在循环内部创建一个自动释放池,以便在下次迭代之前销毁这些对象。这可以帮助减少应用程序的最大内存占用量。
2.系统自动创建自动释放池
Application Kit
会在一个事件周期(或事件循环迭代)的开端—比如鼠标按下事件—自动创建一个自动释放池,并且在事件周期的结尾释放它.
iOS平台内存使用陷阱
1.重复释放
在前文已经提到,不要释放不是自己创建的对象;
释放自己的autorelease
对象,app
会crash
;
释放系统的autorelease
对象,app
会crash
;
2.循环引用
循环引用,容易产生野引用,内存无法回收,最终导致内存泄漏!可以通过弱引用的方式来打破循环引用链;所谓的弱引用就是不需要retain
,直接赋值的方式,这样的话,可以避免循环引用的问题,但是需要注意的是,避免重复释放的问题;
iOS平台内存报警机制
由于iOS
平台的内存管理机制,不支持虚拟内存,所以在内存不足的情况,不会去Ram
上创建虚拟内存;所以一旦出现内存不足的情况,iOS
平台会通知所有已经运行的app
,不论是前台app
还是后台挂起的app
,都会收到 memory warning
的notice
;一旦app
收到memory warning的notice
,就应该回收占用内存较大的变量;
1.内存报警处理流程
1.
app
收到系统发过来的memory warning
的notice
;
2.app
释放占用较大的内存;
3.系统回收此app
所创建的autorelease
的对象;
4.app
返回到已经打开的页面时,系统重新调用viewdidload
方法,view
重新加载页面数据;重新显示;
2.内存报警测试方法
在Simulate
上可以模拟低内存报警消息;
iOS
模拟器 -> 硬件 -> 模拟内存警告;
开发者可以在模拟器上来模拟手机上的低内存报警情况,可以避免由于低内存报警引出的app
的莫名crash
问题;
iOS平台内存检查工具
1.编译和分析工具Analyze
iOS
的分析工具可以发现编译中的warning
,内存泄漏隐患,甚至还可以检查出logic
上的问题;所以在自测阶段一定要解决Analyze
发现的问题,可以避免出现严重的bug
;
内存泄漏隐患提示:
Potential Leak of an object allocated on line ……
数据赋值隐患提示:
The left operand of …… is a garbage value;
对象引用隐患提示:
Reference-Counted object is used after it is released;
以上提示均比较严重,可能会引起严重问题,需要开发者密切关注!
2.内存检测工具
1.内存泄漏检测工具—
Leak
Leak
工具可以很容易的统计所有内存泄漏的点,而且还可以显示在那个文件,哪行代码有内存泄漏,这样定位问题比较容易,也比较方面;但是Leak
在统计内存泄漏的时候会把autorelease
方式的内存也统计进来;所以我们在查找内存泄漏情况的时候,可以autorelease
的情况忽略掉;
Leak
工具:
通过Leak
工具可以很快发现代码中的内存泄漏,通过工具也可以很快找到发生内存泄漏的代码段:
2.内存猛增检测工具—Allocations
Allocations
工具可以很容易的列出所有分配内存的点,这样我们可以按照分配内存大小来进行排序,这样可以很容易的发现哪些点分配的内存最多,而且是持续分配,这样我们来针对性的分析这些持续分配较大内存的地方;
此工具会显示出所有申请内存的地方,并统计申请的次数和大小; 从这个列表中可以找出内存申请次数最多且申请内存最大的语句;从而分析出哪些地方使用的内存最多,进而可以优化和改进;
补充:
关于iOS的动态内存检测,Xcode
自带了工具(instruments
):Leaks
。神马,你不知道这个单词是什么意思?google
一下,翻译过来意思是“泄露”。
使用方法:点击Product->Profile
,然后选择那个漏水的水管Leaks
,进入界面后,点击运行,instruments
就会开始自动检测内存泄露的地方了,在这个过程中,可以对手机上运行的测试工程进行操作,图形界面中,上面是Allocations
,下面是Leaks
,当出现了一条红色的小柱子的时候,就是出现了内存泄露;点击界面中间分隔条,选择Call Tree
选项,然后把右边的 Invert Call Tree
和 Hide System Libraries
选项,就可以看到具体是那个类中得哪个方法出现了内存泄露了,双击类名,就出显示出此类此方法中造成的内存泄露代码,ok,接下来就是有针对性的进行代码优化,内存优化了。
由于现在用得都是ARC
模式,所以一般出现泄露的地方都是block
中的self
疏忽了,没有使用weak
类型;或者,两个类之间出现了循环应用这种低级错误引起的。不过,有些第三方框架也可能会引起内存泄露,比如,公司项目中使用的 微客服 这个第三方的客服系统就出现了内存泄露问题。