之前就总结过内存管理的内容,但并不系统、全面。所以,一直想找时间好好把这一块内容规整一下,因为说起内存管理,这是一个很庞大而又与开发密不可分的内容,虽然ARC下我们几乎不需要手动管理内存,但我们还是要了解一些内存管理的原理和内存优化的方案。
一.iOS内存管理基本原理
iOS和其它系统一样,内存分页,每页4K。多个页构成一个region统一管理,负责管理的对象是VM object,其中包含了pager、size、resident pages等诸多属性。
不管是Objective-C的[NSObject alloc],还是C代码的对内存分配,最终重任都会落到malloc库上,释放也是如此,最终都将使用malloc库中的free()。
当内存吃紧时,对于可以重新载入的只读数据来说,直接清理掉,而对于可写的数据,只能通过App自己去管理维护。内存紧张时,iOS会向App发起memory warning,不配合释放足够内存者,kill!
二. ARC(引用计数)
1. 何为引用计数机制?
ARC 是 WWDC2011 和 iOS5 引入的变化。ARC 是 LLVM 3.0 编译器的特性,用来自动管理内存。
与 Java 中 GC 不同,ARC 是编译器特性,而不是基于运行时的,所以 ARC 其实是在编译阶段自动帮开发者插入了管理内存的代码,而不是实时监控与回收内存。
以经典的开灯例子举例,在办公室内至少有一人保持开灯状态,无人时保持关灯状态
最早进入办公室的人 ===> 开灯 ==== > 生成对象
之后进入办公室的人 ===> 需要照明 ==== > 持有对象
下班离开办公室的人 ===> 不需要照明 ==== > 释放对象
最后下班离开办公室的人===> 关灯 ==== > 废弃对象
中间无论多少人,只要遵循进来就需要照明,走就不要照明。这样办公室的照明就可以得到很好的管理;同理使用引用计数功能,对象也能够得到很好的管理。
原则:
1) 当对象被创建(通过alloc、new或copy等方法)时,其引用计数初始值为1。
2 ) 持有对象,当对象被强引用时,引发retain方法,引用计数加1。
3) 对象被释放时,引发release操作,引用计数减1。
4) 当引用计数为0时,引发dealloc操作,对象被销毁废弃。
注意:不可对已经被销毁的对象发送消息
2. ARC的修饰符
ARC的修饰符提供成员变量访问方法、权限、环境、内存管理类型的声明。
属性的参数分为三类,基本数据类型默认为(atomic,readwrite,assign),对象类型默认为(atomic,readwrite,strong),其中第三个参数就是该属性的内存管理方式修饰,修饰词可以是以下之一:
- strong:强引用(引用计数+1),持有所指向对象的所有权,无修饰符情况下的默认值。如需强制释放,可置nil。
- weak:弱引用,不持有所指向对象的所有权,引用指向的对象内存被回收之后,引用本身会置nil,避免野指针。
使用set方法赋值时,实质上不保留新值,也不释放旧值,只设置新值。 - retain:release旧值,再retain新值(引用计数+1)
使用set方法赋值时,实质上是会先保留新值,再释放旧值,再设置新值,避免新旧值一样时导致对象被释放的的问题。 - copy:release旧值,再copy新值(拷贝内容)
一般用来修饰String、Dict、Array等需要保护其封装性的对象,尤其是在其内容可变的情况下,因此会拷贝(深拷贝)一份内容給属性使用,避免可能造成的对源内容进行改动。
使用set方法赋值时,实质上是会先拷贝新值,再释放旧值,再设置新值。
实际上,遵守NSCopying的对象都可以使用copy,当然,如果你确定是要共用同一份可变内容,你也可以使用strong或retain。 - assign:直接赋值,一般用来修饰基本数据类型
3. block的内存管理
iOS中使用block必须自己管理内存,错误的内存管理将导致循环引用等内存泄漏问题,这里主要说明在ARC下block声明和使用的时候需要注意的两点:
1) 如果你使用@property去声明一个block的时候,一般使用copy来进行修饰(当然也可以不写,编译器自动进行copy操作),尽量不要使用retain。
@property (nonatomic, copy) void(^block)(NSData * data);
2) block会对内部使用的对象进行强引用,因此在使用的时候应该确定不会引起循环引用,当然保险的做法就是添加弱引用标记。
__weak typeof(self) weakSelf = self;
4. autorelease和autorelaesepool
官方文档解释:
每个线程(包括主线程),都维护了一个管理 NSAutoreleasePool 的栈。当创先新的 Pool 时,他们会被添加到栈顶。当 Pool 被销毁时,他们会被从栈中移除。
autorelease 的对象会被添加到当前线程的栈顶的 Pool 中。当 Pool 被销毁,其中的对象也会被释放。
当线程结束时,所有的 Pool 被销毁释放。
autorelease 顾名思义及时自动释放的意思。它有点类似 C 语言中自动变量的特性。程序执行时,若自动变量超出其作用域,该自动变量将被自动废弃。
1. autorelease具体的使用方法:
- 生成并持有 NSAutoreleasePool 对象
- 调用已分配对象的 autorelease 实例方法
- 废弃 NSAutoreleasePool 对象
使用场景:需要延迟释放某些对象的情况时,可以把他们先放到对应的Autorelease Pool中,等Autorelease Pool生命周期结束时再一起释放。
利用@autoreleasepool优化循环的内存占用,可能是我们会用到的一点。如下面的循环,次数非常多,而且循环体里面的对象都是临时创建使用的,就可以用@autoreleasepool包起来,让每次循环结束时,可以及时的释放临时对象的内存。
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
// 创建对象 TODO:
NSObject *obj = [[NSObject alloc]init];
};
}
2. Autorelease对象什么时候释放?
在没有手加Autorelease Pool
的情况下,Autorelease
对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
ARC 中我们使用@autoreleasepool{}来使用一个AutoreleasePool
的是时候,编译器就将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage
的简单封装,所以自动释放机制的核心就在于这个类。AutoreleasePoolPage
是一个C++实现的类。
* AutoreleasePool并没有单独的结构,而是由若干个``AutoreleasePoolPage``以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
* AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
* ``AutoreleasePoolPage``每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
* 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
* 一个``AutoreleasePoolPage``的空间被占满时,会新建一个``AutoreleasePoolPage``对象,连接链表,后来的``autorelease``对象在新的page加入
所以,若当前线程中只有一个AutoreleasePoolPage
对象,并记录了很多autorelease
对象地址时内存如下图:
![Upload 51530583gw1elj5gvphtqj20dy0cx756.jpg failed. Please try again.]
图中的情况,这一页再加入一个autorelease
对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送-autorelease
消息,就是将这个对象加入到当前AutoreleasePoolPage
的栈顶next指针指向的位置
释放
每当进行一次objc_autoreleasePoolPush
调用时,runtime向当前的AutoreleasePoolPage
中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop
(哨兵对象)作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
- 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
这篇,主要讲了内存管理的原理和概念性的东西,下一篇会针对内存泄漏及内存优化解决方案做详细的介绍
亲,喜欢的话点个赞呗!