iOS内存管理机制详解
机制
OC采用引用计数器对内存进行管理,当一个对象的引用计数(retainCount)为0,则被释放。
引用计数分为两种:
- 手动引用计数(MRC)
1 | // MRC代码 |
- 自动引用计数(ARC)
比如如下ARC代码:
1 | NSObject * obj; |
OC的内存机制可以简单概括为:谁持有(retain)谁释放(release)。retain
引用计数+1,release
反之。
我们先看看那ratain和release内部是如何实现的。
retain
1 | - (id)retain { |
可以看出retain底层是调用了sidetable_retain()
1 | id objc_object::sidetable_retain() |
SideTable数据结构:
1 | struct SideTable { |
通过代码可以出,SideTable拥有一个自旋锁
,一个引用计数map。这个引用计数的map以对象的地址
作为key,引用计数作为value
release
1 | - (oneway void)release { |
1 | uintptr_t objc_object::sidetable_release(bool performDealloc) |
release过程:查找map,对引用计数减1,如果引用计数小于阈值,则调用SEL_dealloc
自己生成的对象,自己持有
使用以下名称开头的方法意味着生成的对象会被自己持有,也就是内部会对象进行一次retain:
- alloc
- new
- copy
- mutableCopy
比如NSObject的alloc方法:
1 | + (id)alloc { |
对于OC提供的方法,除了上面几种,比如说[NSMutableArray array],通过这些方法获取到的对象并不对其进行持有,内部会将生成的对象放入到自动释放池上
1 | // 取得非自己生成而且不持有的对象 |
再比如如果我们定义一个方法:
1 | - (id)object { |
无法释放非自己持有的对象
就像上面的 id obj1 = [obj1 object]
,obj并没有持有对象,如果这时候我们主动调用[obj1 release]
就会发生崩溃。
还有一种情况就是已经被释放的对象再对其进行release操作的时候也会发生崩溃
1 | id obj = [[NSObject alloc] init]; |
ARC中常见的所有权关键字
assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。
copy对应关键字__strong,只不过在赋值的时候,调用copy方法。
retain对应__strong
strong对应__strong
__strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,以下源代码中的id变量,实际上被附加了所有权修饰词:
1
id obj = [[NSObject alloc] init];
weak 对应 __weak
weak是用来替代unsafe_unretained,weak修饰符的变量(即弱引用)不持有对象,所以在超出其作用域时,对象就会释放,所以因为强引用而造成的循环引用,将其中的成员变量改为弱引用,就不会发生相同情况。
在持有某若引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值状态(空弱引用)。
unsafe_unretained 对应 __unsafe_unretained
unsafe unretained与weak修饰符一样不会增加引用计数,自己生成的对象不能继续为自己所有,所以会立即释放。
iOS4以及OS X Snow Leopard的应用程序中,必须使用unsafe unretained修饰符来替代weak修饰符。赋值给附有__unsafe unretained修饰符变量的对象在通过该变量使用时,如果没有确保其存在,那么应用就会崩溃。
1 | id __unsafe_unretained obj1 = nil; |
如果像上面那样,程序就会崩溃,因为obj0被销毁之后,obj1并不会自动置为nil。
__bridge
1 | id obj = [[NSObject alloc] init]; |
将Objective-C的对象类型用 __bridge
转换为 void* 类型和使用 __unsafe_unretained
关键字修饰的变量是一样的
__bridge
转换中还有另外两种转换,分别是” __bridge_retained
转换”和” __bridge_transfer
转换”
__bridge_retained
转换可使要转换赋值的变量也持有所赋值的对象。
1 | // MRC |
__bridge_transfer
转换提供与次相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。
1 | id p = (__bridge_transfer id)p; |
等效于:
1 | // MRC |
同__bridge_retained
和retain
类似,__bridge_transfer
与release
相似。 在给id obj赋值时retain即相当于__strong修饰符的变量。
如果使用以上两种转换,那么不是用id类型或者对象型变量也可以生成、持有以及释放对象。虽然可以这样做,但是ARC中并不推荐。
1 | void *p = (__bridge_retained void *)[NSObject new]; |
和下面代码等效
1 | // MRC |
Objective-C对象与Core Foundation对象
Core Foundation对象主要使用在C语言编写的Core Foundation框架中,并是用引用计数的对象。在ARC无效时,Core Foundation框架中的retain/release分别是CFRetain/CFRelease。
因为Core Foundation对象与OC对象没有区别,所以在MRC时,只用简单的C语言的转换也能实现互换。另外这种转换不需要使用额外的CPU资源,因此也被称为”Toll-Free Bridge”
以下函数可用于OC对象和Core Foundation对象之间的相互变换,即Toll-Free Bridge转换:
1 | CFTypeRef CFBridgingRetain(id x) { |
1 | CFMutableArrayRef cfObj = NULL; |
效果如下
还可以通过__bridge_retained来替代CFBridgingRetain
1 | CFMutableArrayRef cfObject = (__bridge_retained CFMutableArrayRef)obj; |
反过来
1 | CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); |
打印出来:
和书上的结果好像不一样,如果加上CFRelsease就正常了,这个点没有搞清楚:
如果我们直接用__bridge_transfer进行转换,结果几就过就正常了:
1 | CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); |
ARC运行时的优化
ARC不只是在编译时由编译器进行内存管理,实际上在此基础还借助了OC运行时库。也就是说,ARC由以下工具、库实现:
- clang(LLVM编译器)3.0以上
- objc4 Objective-C运行时库493.9以上
__strong修饰符
赋值给__strong修饰的变量:
1 | { |
可以看作成:
1 | id obj = objc_msgSend(NSObject, @selector(alloc)); |
使用alloc/new/copy/mutableCopy以外的方法:
1 | { |
可以看作:
1 | id obj = objc_msgSend(NSMutableArray, @selector(array)); |
这里和上面区别主要是objc_retainAutoreleaseReturnValue
,该函数主要用于优化程序运行,objc_retainAutoreleaseReturnValue
的入参是返回注册在autoreleasepool
中的对象的方法/函数的返回值
。编译器会在alloc/new/copy/mutableCopy以外
的方法调用外部插入。
上面所说的功能实现的时候是需要objc_retainAutoreleaseReturnValue
和objc_autoreleaseReturnValue
配合完成,任何不在alloc/new/copy/mutableCopy组中的方法必须调objc_autoreleaseReturnValue
。 例如,NSMutableArray类方法“array”调用此函数。
1 | + (id)array { |
1 | /* pseudo code by the compiler */ |
任何返回添加到自动释放池的对象的方法都将调用objc_autoreleaseReturnValue
函数,如上例所示。 它将一个对象添加到自动释放池并返回。 但是实际上objc_autoreleaseReturnValue
不会一直注册到自动释放池。
objc_autoreleaseReturnValue
检查调用者的可执行代码,如果代码在调用此方法后调用objc_retainAutoreleasedReturnValue
函数,它将跳过注册到自动释放池,并将对象返回给调用者。 即使objc_autoreleaseReturnValue
没有将对象注册到自动释放池,objc_retainAutoreleasedReturnValue
函数也可以正确地获得这样的对象。 通过objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
的合作,对象绕过被添加到自动释放池。
一般来说:检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue
,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。
__weak修饰符
- 当引用对象被丢弃时,__weak修饰的变量会赋值为nil。
- 通过__weak限定变量访问对象时,该对象将添加到自动释放池。
1 | { |
1 | /* pseudo code by the compiler */ |
通过objc_initWeak
初始化__weak修饰的变量,在变量作用域结束时通过objc_destroyWeak
释放该变量。
objc4源码中objc_initWeak
和objc_destroyWeak
的具体实现
1 | id objc_initWeak(id *location, id newObj) |
所以,本质上都是调用了storeWeak
函数,storeWeak
函数把第二参数的赋值对象的地址作为键值
,将第一个参数的附有__weak修饰符的变量的地址注册到weak表中。如果第二个参数为0/nil,则把变量的地址从weak表中删。
这个函数总结起来主要做了以下事情:
- 获取存储weak对象的map,这个map的key是对象的地址,value是weak引用的地址
- 当对象被释放的时候,根据对象的地址可以找到对应的weak引用的地址,将其置为nil即可
weak表与引用计数表都是采用散列表
实现。另外,由于一哥对象可以同时赋值给多个__waek对象修饰符的变量中,对于一个键值,可以注册多个变量的地址。
释放对象的时候,一般经历下面几个操作:
- objc_release
- 因为引用计数为0所以执行dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocating
对象被废弃时最后调用的objc_ckear_deallocating函数动作如下:
- 从weak表中获取废弃对象的地址为键值的记录
- 将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil
- 从weak表伤处该记录
- 从应用计数表中删除废弃对象的地址为键值的记录
通过上面的步骤可以看出,如果大量的是用__weak修饰符的变量,会对cpu资源造成相应的消耗,一般只有在需要避免循环引用的时候是用__weak修饰符。
如果我们像上图那样,自己生成对象并复制给__weak变量,自己不能持有该对象,对象会马上被回收,引起编译器警告
编译器处理后代码:
1 | /* pseudo code by the compiler |
然后再来测试一下:使用__weak修饰符的变量是否会将对象注册到autoreleasepool。
1 | { |
1 | /* 编译器的模拟代码 */ |
与被赋值相比,在使用附有__weak修饰变量的情况下,增加了对objc_loadWeakRetained
函数和objc_autorelease
函数的调用。
objc_loadWeakRetained
函数取出附有__weak修饰符变量所引用的对象并ratainobjc_autorelease
函数将对象注册到autoreleasepool中
由于附有__weak修饰符变量所引用的对象能被注册到autoreleasepool中,所以在@autoreleasepool块结束前都能保证对象不被释放。但是,如果大流量地是用__weak变量会导致autoreleasepool的对象也会大量地添加,因此在使用__weak变量最好先暂时赋值给__strong变量再是用后者。
这样就能解释我们平时用到的weak-strong-dance的原理了。以AFN的源码为例子:
1 | __weak __typeof(self)weakSelf = self; |
上面在闭包中利用把weakSelf赋值给strongSelf,保证在callback闭包执行的过程中,self不会被释放。
题外话
- 1、内敛函数
内联函数是指用inline关键字修饰的函数。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。参考资料
参考文章:
黑幕背后的Autorelease
自动释放池的前世今生 —- 深入解析 Autoreleasepool
深入理解Objective C的ARC机制