本文主要以下几个方面详细介绍了iOS中所涉及的内存管理相关的机制
- 数据结构中的堆和栈
- 内存分配中的堆和栈
- 引用计数介绍
- MRC详解
- @autoreleasepool
- ARC详解
- 内存管理相关关键字的底层实现
- 内存泄漏及检测
Heap(堆)和stack(栈)
堆是什么
引自维基百科堆)(英语:Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。
堆(Heap)又被为优先队列(priority queue)。尽管名为优先队列,但堆并不是队列。回忆一下,在队列中,我们可以进行的限定操作是dequeue和enqueue。dequeue是按照进入队列的先后顺序来取出元素。而在堆中,我们不是按照元素进入队列的先后顺序取出元素的,而是按照元素的优先级取出元素。
这就好像候机的时候,无论谁先到达候机厅,总是头等舱的乘客先登机,然后是商务舱的乘客,最后是经济舱的乘客。每个乘客都有头等舱、商务舱、经济舱三种个键值(key)中的一个。头等舱->商务舱->经济舱依次享有从高到低的优先级。
总的来说,堆是一种数据结构,数据的插入和删除是根据优先级定的,他有几个特性:
- 任意节点的优先级不小于它的子节点
- 每个节点值都小于或等于它的子节点
- 主要操作是插入和删除最小元素(元素值本身为优先级键值,小元素享有高优先级)
举个例子,就像叠罗汉,体重大(优先级低、值大)的站在最下面,体重小的站在最上面(优先级高,值小)。 为了让堆稳固,我们每次都让最上面的参与者退出堆,也就是每次取出优先级最高的元素。
栈是什么
引自维基百科栈是计算机科学中一种特殊的串列形式的抽象资料型别,其特殊之处在于只能允许在链接串列或阵列的一端(称为堆叠顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外栈也可以用一维数组或连结串列的形式来完成。堆叠的另外一个相对的操作方式称为伫列。 由于堆叠数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
举个例子,一把54式手枪的子弹夹,你往里面装子弹,最先射击出来的子弹肯定是最后装进去的那一个。 这就是栈的结构,后进先出。
栈中的每个元素称为一个frame
。而最上层元素称为top frame
。栈只支持三个操作, pop, top, push。
- pop取出栈中最上层元素(8),栈的最上层元素变为早先进入的元素(9)。
- top查看栈的最上层元素(8)。
- push将一个新的元素(5)放在栈的最上层。
栈不支持其他操作。如果想取出元素12, 必须进行3次pop
操作。
内存分配中的栈和堆
内存分为5个区域,分别指的是—–>栈区/堆区/BSS段/数据段/代码段
栈:存储局部变量,当其作用域执行完毕之后,就会被系统立即收回
堆:存储OC对象,手动申请的字节空间,需要调用free来释放
BSS段:未初始化的全局变量和静态变量,一旦初始化就会从BSS段中回收掉,转存到数据段中
数据段:存储已经初始化的全局变量和静态变量,以及常量数据,直到结束程序时才会被立即收回
代码段:代码,直到结束程序时才会被立即收回
堆栈空间分配
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
堆栈缓存方式
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。 堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
一般情况下程序存放在Rom
(只读内存,比如硬盘)或Flash
中,运行时需要拷到RAM
(随机存储器RAM)中执行,RAM
会分别存储不同的信息,如下图所示:
内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的。
栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。
也就是说,在iOS
中,我们的值类型是放在栈空间的,内存分配和回收不需要我们关系,系统会帮我处理。在堆空间的对象类型就要有程序员自己分配,自己释放了。
引用计数
引用计数是什么
引自维基百科引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。 当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。
正常情况下,当一段代码需要访问某个对象时,该对象的引用的计数加1;当这段代码不再访问该对象时,该对象的引用计数减1,表示这段代码不再访问该对象;当对象的引用计数为0时,表明程序已经不再需要该对象,系统就会回收该对象所占用的内存。
- 当程序调用方法名以
alloc
、new
、copy
、mutableCopy
开头的方法来创建对象时,该对象的引用计数加1
。 - 程序调用对象的
retain
方法时,该对象的引用计数加1
。 - 程序调用对象的
release
方法时,该对象的引用计数减1
。
NSObject
中提供了有关引用计数的如下方法:
- —
retain
:将该对象的引用计数器加1
。 - —
release
:将该对象的引用计数器减1
。 - —
autorelease
:不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。 - —
retainCount
:返回该对象的引用计数的值。
引用计数内存管理的思考方式
看到“引用计数”这个名称,我们便会不自觉地联想到“某处有某物多少多少”而将注意力放到计数上。但其实,更加客观、正确的思考方式:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
引用计数式内存管理的思考方式仅此而已。按照这个思路,完全不必考虑引用计数。 上文出现了“生成”、“持有”、“释放”三个词。而在Objective-C
内存管理中还要加上“废弃”一词。各个词标书的Objective-C
方法如下表。
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release方法 |
废弃对象 | dealloc方法 |
这些有关Objective-C
内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa框架中用于macOS
、iOS
应用开发。Cocoa
框架中Foundation
框架类库的NSObject
类担负内存管理的职责。Objective-C
内存管理中的alloc/retain/release/dealloc
方法分别指代NSObject
类的alloc
类方法、retain
实例方法、release
实例方法和dealloc
实例方法。
Cocoa框架、Foundation框架和NSObject类的关系
MRC(手动管理引用计数)
顾名思义,MRC
就是调用Objective-C
的方法(alloc/new/copy/mutableCopy/retain/release
等)实现引用计数的增加和减少。
下面通过Objective-C
的方法实现内存管理的思考方式。
自己生成的对象,自己持有
使用以下名称开头的方法名意味着自己生成的对象只有自己持有:
- alloc
- new
- copy
- mutableCopy
alloc的实现
1 | // 自己生成并持有对象 |
使用NSObject
类的alloc
方法就能自己生成并持有对象。指向生成并持有对象的指针被赋给变量obj
。
new的实现
1 | // 自己生成并持有对象 |
[NSObject new]
与[[NSObject alloc] init]
是完全一致的。
copy的实现
copy
方法利用基于NSCopying
方法约定,由各类实现的copyWithZone:
方法生成并持有对象的副本。
1 | #import "ViewController.h" |
打印结果: 2018-03-28 23:01:32.321661+0800 ocram[4466:1696414] p对象<Person: 0x1c0003320> 2018-03-28 23:01:32.321778+0800 ocram[4466:1696414] obj对象<Person: 0x1c0003370>
从打印可以看到obj
是p
对象的副本。两者的引用计数都是1
。
说明:在
- (id)copyWithZone:(NSZone *)zone
方法中,一定要通过[self class]
方法返回的对象调用allocWithZone:
方法。因为指针可能实际指向的是Person
的子类。这种情况下,通过调用[self class]
,就可以返回正确的类的类型对象。
mutableCopy的实现
与copy
方法类似,mutableCopy
方法利用基于NSMutableCopying
方法约定,由各类实现的mutableCopyWithZone:
方法生成并持有对象的副本。
1 | #import "ViewController.h" |
打印结果: 2018-03-28 23:08:17.382538+0800 ocram[4476:1699096] p对象<Person: 0x1c4003c20> 2018-03-28 23:08:17.382592+0800 ocram[4476:1699096] obj对象<Person: 0x1c4003d70>
从打印可以看到obj
是p
对象的副本。两者的引用计数都是1
。
copy
和mutableCopy
的区别在于,copy
方法生成不可变更的对象,而mutableCopy
方法生成可变更的对象。
浅拷贝和深拷贝
既然讲到copy
和mutableCopy
,那就要谈一下深拷贝和浅拷贝的概念和实践。
什么是浅拷贝、深拷贝?
简单理解就是,浅拷贝是拷贝了指向对象的指针, 深拷贝不但拷贝了对象的指针,还在系统中再分配一块内存,存放拷贝对象的内容。
浅拷贝:拷贝对象本身,返回一个对象,指向相同的内存地址。 深层复制:拷贝对象本身,返回一个对象,指向不同的内存地址。
如何判断浅拷贝、深拷贝?
深浅拷贝取决于拷贝后的对象的是不是和被拷贝对象的地址相同,如果不同,则产生了新的对象,则执行的是深拷贝,如果相同,则只是指针拷贝,相当于retain一次原对象, 执行的是浅拷贝。
深拷贝和浅拷贝的判断要注意两点:
- 源对象类型是否是可变的
- 执行的拷贝是
copy
还是mutableCopy
浅拷贝深拷贝的实现
- NSArray调用copy方法,浅拷贝
1 | id obj = [NSArray array]; |
打印结果: 2018-03-29 20:48:56.087197+0800 ocram[5261:2021415] obj是0x1c0003920 2018-03-29 20:48:56.087250+0800 ocram[5261:2021415] obj1是0x1c0003920
指针一样obj
是浅拷贝。
- NSArray调用mutableCopy方法,深拷贝
1 | id obj = [NSArray array]; |
打印结果: 2018-03-29 20:42:16.508134+0800 ocram[5244:2018710] obj是0x1c00027d0 2018-03-29 20:42:16.508181+0800 ocram[5244:2018710] obj1是0x1c0453bf0
指针不一样obj
是深拷贝。
- NSMutableArray调用copy方法,深拷贝
1 | id obj = [NSMutableArray array]; |
打印结果: 2018-03-29 20:50:36.936054+0800 ocram[5265:2022249] obj是0x1c0443f90 2018-03-29 20:50:36.936097+0800 ocram[5265:2022249] obj1是0x1c0018580
指针不一样obj
是深拷贝。
- NSMutableArray调用mutableCopy方法,深拷贝
1 | id obj = [NSMutableArray array]; |
打印结果: 2018-03-29 20:52:30.057542+0800 ocram[5268:2023155] obj是0x1c425e6f0 2018-03-29 20:52:30.057633+0800 ocram[5268:2023155] obj1是0x1c425e180
指针不一样obj
是深拷贝。
- 深拷贝的数组里面的元素依然是浅拷贝
1 | id obj = [NSMutableArray arrayWithObject:@"test"]; |
打印结果: 2018-03-29 20:55:18.196597+0800 ocram[5279:2025743] obj是0x1c0255120 2018-03-29 20:55:18.196647+0800 ocram[5279:2025743] obj内容是0x1c02551e0 2018-03-29 20:55:18.196665+0800 ocram[5279:2025743] obj1是0x1c0255210 2018-03-29 20:55:18.196682+0800 ocram[5279:2025743] obj1内容是0x1c02551e0
可以看到obj
和obj1
虽然指针是不一样的(深拷贝),但是他们的元素的指针是一样的,所以数组里的元素依然是浅拷贝。
其他实现
使用上述使用一下名称开头的方法,下面名称也意味着自己生成并持有对象。
- allocMyObject
- newThatObject
- copyThis
- mutableCopyYourObject
使用驼峰拼写法来命名。
1 | #import "ViewController.h" |
打印结果: 2018-03-28 23:33:37.044327+0800 ocram[4500:1706677] p对象<Person: 0x1c0013770>
allocObject
名称符合上面的命名规则,因此它与用alloc
方法生成并持有对象的情况完全相同,所以使用allocObject
方法也意味着“自己生成并持有对象”。
非自己生成的对象,自己也能持有
1 | //非自己生成的对象,暂时没有持有 |
上述代码中NSMutableArray
通过类方法array
生成了一个对象赋给变量obj
,但变量obj
自己并不持有该对象。使用retain
方法可以持有对象。
不再需要自己持有的对象时释放
自己持有的对象,一旦不再需要,持有者有义务释放该对象。释放使用release
方法。
自己生成并持有对象的释放
1 | // 自己生成并持有对象 |
如此,用alloc
方法由自己生成并持有的对象就通过realse
方法释放了。自己生成而非自己所持有的对象,若用retain
方法变为自己持有,也同样可以用realse
方法释放。
非自己生成的对象持有对象的释放
1 | //非自己生成的对象,暂时没有持有 |
非自己生成的对象本身的释放
像调用[NSMutableArray array]
方法使取得的对象存在,但自己并不持有对象,是如何实现的呢?
1 | + (id)array { |
上例中,我们使用了autorelease
方法。用该方法,可以使取得的对象存在,但自己不持有对象。autorelease
提供这样的功能,使对象在超出指定的生存范围时能够自动并正确的释放(调用release
方法)。
使用NSMutableArray
类的array
类方法等可以取得谁都不持有的对象,这些方法都是通过autorelease
实现的。根据上文的命名规则,这些用来取得谁都不持有的对象的方法名不能以alloc/new/copy/mutableCopy
开头,这点需要注意。
非自己持有的对象无法释放
对于用alloc/new/copy/mutableCopy
方法生成并持有的对象,或是用retain
方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在程序中释放了非自己所持有的对象就会造成崩溃。
1 | // 自己生成并持有对象 |
释放了非自己持有的对象,肯定会导致应用崩溃。因此绝对不要去释放非自己持有的对象。
autorelease
autorelease介绍
说到Objective-C内存管理,就不能不提autorelease。 顾名思义,autorelease就是自动释放。这看上去很像ARC,单实际上它更类似于C语言中自动变量(局部变量)的特性。
在C语言中,程序程序执行时,若局部变量超出其作用域,该局部变量将被自动废弃。
1 | { |
autorelease
会像C
语言的局部变量那样来对待对象实例。当其超出作用域时,对象实例的release
实例方法被调用。另外,同C
语言的局部变量不同的是,编程人员可以设置变量的作用域。
autorelease
的具体使用方法如下:
- 生成并持有
NSAutoreleasePool
对象。 - 调用已分配对象的
autorelease
实例方法。 - 废弃
NSAutoreleasePool
对象。
1 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
上述代码中最后一行的[pool drain]
等同于[obj release]
。
autorelease实现
调用NSObject
类的autorelease
实例方法。
1 | [obj autorelease]; |
调用autorelease
方法的内部实现
1 | - (id) autorelease { |
autorelease
实例方法的本质就是调用NSAutoreleasePool
对象的addObject
类方法。
Autorelease pool
上文提到了,autorelease方法的作用是把对象放到autorelease pool中,到pool drain的时候,会释放池中的对象。举个例子
1 | __weak NSObject * obj; |
放到auto release pool中,
1 | __weak NSObject * obj; |
可以看到,放到自动释放池的对象是在超出自动释放池作用域后立即释放的。事实上在iOS 程序启动之后,主线程会启动一个Runloop,这个Runloop在每一次循环是被自动释放池包裹的,在合适的时候对池子进行清空。
对于Cocoa框架来说,提供了两种方式来把对象显式的放入AutoReleasePool.
- NSAutoreleasePool(只能在MRC下使用)
- @autoreleasepool {}代码块(ARC和MRC下均可以使用)
那么AutoRelease pool又是如何实现的呢?
我们先从autorelease方法源码入手
1 | //autorelease方法 |
可以看到,把一个对象放到auto release pool中,是调用了AutoreleasePoolPage::autorelease这个方法。
我们继续查看对应的实现:
1 | public: static inline id autorelease(id obj) |
到这里,autorelease方法的实现就比较清楚了,
autorelease方法会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象删除。所以,AutoreleasePoolPage就是自动释放池的内部实现。
autorelease注意
autorelease
是NSObject
的实例方法,NSAutoreleasePool
也是继承NSObject
的类。那能不能调用autorelease
呢?
1 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
运行结果发生崩溃。通常在使用Objective-C
,也就是Foundation
框架时,无论调用哪一个对象的autorelease
实例方法,实现上是调用的都是NSObject
类的autorelease
实例方法。但是对于NSAutoreleasePool
类,autorelease
实例方法已被该类重载,因此运行时就会出错。
ARC(自动管理引用计数)
ARC介绍
上面讲了“引用计数内存管理的思考方式”的本质部分在ARC中并没有改变。就像“自动引用计数”这个名称表示的那样,ARC只是自动地帮助我们处理“引用计数”的相关部分。
在编译单位上,可设置ARC
有效或无效,即设置特定类是否启用ARC
。 在Project
里面找到Build Phases
-Compile Sources
,这里是所有你的编译文件。指定编译器属性为-fobjc-arc
即为该文件使用ARC
,指定编译器属性为-fno-objc-arc
即为该文件不使用ARC
,如下图所示。
编译器在编译时会帮我们自动插入,包括 retain
、release
、copy
、autorelease
、autoreleasepool
ARC有效的代码实现
所有权修饰符
Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中,id类型和对象类其类型必须附加所有权修饰符。
- __strong修饰符
- __weak修饰符
- __unsafe_unretained修饰符
- __autoreleasing修饰符
__strong修饰符
__strong
修饰符是id
类型和对象类型默认的所有权修饰符。也就是说,不写修饰符的话,默认对象前面被附加了__strong
所有权修饰符。
比如,对于Test.m文件,当源代码如下时:
1 | #import "Test.h" |
转换后的汇编代码大概缩减为如下:
1 | _objc_msgSend // alloc |
在结合Runtime的源码,我们看看最关键的objc_storeStrong的实现
1 | void objc_storeStrong(id *location, id obj) |
__strong
修饰符的变量obj
在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。 __strong
修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
当然,__strong
修饰符也可以用在Objective-C类成员变量和方法参数上。
1 | @interface Test: NSObject |
无需额外的工作便可以使用于类成员变量和方法参数中。__strong
修饰符和后面要讲的__weak
修饰符和__autoreleasing
修饰符一起,可以保证将附有这些修饰符的自动变量初始化为nil
。
正如苹果宣称的那样,通过__strong
修饰符再键入retain
和release
,完美地满足了“引用计数式内存管理的思考方式”。
__weak修饰符
通过__strong
修饰符并不能完美的进行内存管理,这里会发生“循环引用”的问题。
循环引用容易发生内存泄露。所谓内存泄露就是应当废弃的对象在超出其生命周期后继续存在。
__weak
修饰符可以避免循环引用,与__strong
修饰符相反,提供弱引用。弱引用不能持有对象实例,所以在超出其变量作用域时,对象即被释放。
将上面例子中Test.m修改成为weak,同样我们分析其汇编实现,精简后我们可以看到
,__weak本身实现的核心就是以下两个方法
- _objc_initWeak
- _objc_destroyWeak
我们通过Runtime的源码分析这两个方法的实现:
1 | id objc_initWeak(id *location, id newObj) |
所以,本质上都是调用了storeWeak函数,这个函数内容较多,主要做了以下事情
- 获取存储weak对象的map,这个map的key是对象的地址,value是weak引用的地址。
- 当对象被释放的时候,根据对象的地址可以找到对应的weak引用的地址,将其置为nil即可。
__unsafe_unretained修饰符
__unsafe_unretained
修饰符是不安全的修饰符,尽管ARC
式的内存管理是编译器的工作,但附有__unsafe_unretained
修饰符的变量不属于编译器的内存管理对象。__unsafe_unretained
和__weak
一样不能持有对象。
如果在使用__unsafe_unretained
修饰符时,赋值给__strong
修饰符的变量时有必要确保被赋值的对象确实存在。
__autoreleasing修饰符
在ARC
中,我也可以使用autorelease
功能。指定“@autoreleasepool
块”来代替“NSAutoreleasePool
类对象生成、持有以及废弃这一范围,使用附有__autoreleasing
修饰符的变量替代autorelease
方法。
其实我们不用显示的附加 __autoreleasing
修饰符,这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy
开始,如果不是则自动将返回值的对象注册到autoreleasepool
。
有时候__autoreleasing
修饰符要和__weak
修饰符配合使用。
1 | id __weak obj1 = obj0; |
为什么访问附有__weak
修饰符的变量时必须访问注册到autoreleasepool
的对象呢?这是因为__weak
修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把访问的对象注册到autoreleasepool
中,那么在@autoreleasepool
块结束之前都能确保该对象存在。
属性与所有权修饰符的对应关系
以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有copy
不是简单的赋值,它赋值的是通过NSCopying
接口的copyWithZone:
方法复制赋值源所生成的对象。
ARC规则
在ARC
有效的情况下编译源代码,必须遵守一定的规则。
不能使用retain/release/retainCount/autorelease
ARC
有效时,实现retain/release/retainCount/autorelease
会引起编译错误。代码会标红,编译不通过。
不能使用NSAllocateObject/NSDeallocateObject
须遵守内存管理的方法命名规则
alloc
,new
,copy
,mutableCopy
,init
以init
开始的方法的规则要比alloc
,new
,copy
,mutableCopy
更严格。该方法必须是实例方法,并且要返回对象。返回的对象应为id
类型或方法声明类的对象类型,抑或是该类的超类型或子类型。该返回对象并不注册到autoreleasepool
上。基本上只是对alloc
方法返回值的对象进行初始化处理并返回该对象。
1 | //符合命名规则 |
不要显式调用dealloc
当对象的引用计数为0
,所有者不持有该对象时,该对象会被废弃,同时调用对象的dealloc
方法。ARC
会自动对此进行处理,因此不必书写[super dealloc]
。
使用@autoreleasepool
块替代NSAutoreleasePool
不能使用区域(NSZone)
对象型变量不能作为C语言结构体(struct、union)的成员
C语言结构体(struct、union)的成员中,如果存在Objective-C对象型变量,便会引起编译错误。
1 | struct Data { |
显示警告: ARC forbids Objective-C objects in struct
在C
语言的规约上没有方法来管理结构体成员的生命周期。因为ARC
把内存管理的工资分配给编译器,所以编译器必须能够知道并管理对象的生命周期。例如C
语言的局部变量可使用该变量的作用域管理对象。但是对于C
语言的结构体成员来说,这在标准上就是不可实现的。
要把对象类型添加到结构体成员中,可以强制转换为void *
或是附加__unsafe_unretained
修饰符。
1 | struct Data { |
__unsafe_unretained
修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。
显示转换id
和void *
在MRC时,将id
变量强制转换void *
变量是可以的。
1 | id obj = [[NSObject alloc] init]; |
但是在ARC时就会编译报错,id
型或对象型变量赋值给void *
或者逆向赋值时都需要进行特定的转换。如果只想单纯的赋值,则可以使用“__bridge
转换”
bridge转换中还有另外两种转换,分部是“`bridge_retained”和“
bridge_transfer转换”
bridge_retained转换与
retain类似,
__bridge_transfer`转换与release类似。
1 | void *p = (__bridge_retained void *)[[NSObject alloc] init]; |
ARC内存的泄露和检测
ARC内存泄露常见场景
对象型变量作为C语言结构体(struct、union)的成员
1 | struct Data { |
__unsafe_unretained
修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。
循环引用
循环引用常见有三种现象:
- 两个对象互相持有对象,这个可以设置弱引用解决。
1 | @interface Test: NSObject |
- block持有self对象,这个要在block块外面和里面设置弱引用和强引用。
1 | __weak __typeof(self) wself = self; |
- NSTimer的target持有self
NSTimer会造成循环引用,timer会强引用target即self,一般self又会持有timer作为属性,这样就造成了循环引用。 那么,如果timer只作为局部变量,不把timer作为属性呢?同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timer被invalidate之前,self也就不会被释放。
单例属性不释放
严格来说这个不算是内存泄露,主要就是我们在单例里面设置一个对象的属性,因为单例是不会释放的,所以单例会有一直持有这个对象的引用。
1 | [Instanse shared].obj = self; |
可以看到单例持有了当前对象self
,这个self
就不会释放了。
ARC内存泄露的检测
使用Xcode自带工具Instrument
具体过程略(大家可以参考网上别的资料,比较简单)
在对象dealloc中进行打印
我们生成的对象,在即将释放的时候,会调用dealloc
方法。所以我们可以在dealloc
打印当前对象已经释放的消息。如果没有释放,对象的dealloc
方法就永远不会执行,此时我们知道发生了内存泄露。
-