1.1 什么是自动引用计数
- 概念:在 LLVM 编译器中设置 ARC(Automaitc Reference Counting) 为有效状态,就无需再次键入
retain
或release
代码。
1.2 内存管理 / 引用计数
1.2.1 概要
-
引用计数就像办公室的灯的照明
对照明设备所做的动作 对OC对象所做的动作 开灯 生成对象 需要照明 持有对象 不需要照明 释放对象 关灯 废弃对象
- 其中,A生成对象时,引用计数为 1, 当多一个人需要照明,如B需要照明,则引用计数 +1, 以此类推。当A不需要对象,A释放对象,引用计数 -1.当最后一个持有对象的人都不要这个对象了,则引用计数变为 0,丢弃对象。
1.2.2 内存管理的思考方式
-
客观正确的思考方式:
- 自己生成的对象,自己所持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放该对象
- 非自己持有的对象无法释放
对象操作 OC方法 生成并持有对象 alloc/new/copy/mutableCopy等 持有对象 retain 释放对象 release 废弃对象 dealloc
-
自己生成的对象,自己所持有:持有对象
- (id) allocObject { // 自己生成并持有对象 id obj = [[NSObject alloc] init]; return obj; }
-
需要注意的是: NSMutableArray 类的 array 方法取得的对象不是自己所持有的。其内部实现原理为:
- (id)object { // 自己生成并持有对象 id obj = [[NSObject alloc] init]; // 将对象注册到 autoreleasepool 中, pool结束时会自动调用 release,这样的方法自己就不会持有对象。 [obj autorelease]; // 返回这个自己不持有的对象。 return obj; }
-
非自己生成的对象,自己也能持有:虽然一开始是不持有的,但是可以使用 retain 使其变成被自己所持有的,然后也可以使用 release 方法释放对象。
// 取得非自己生成的对象 id obj = [NSMutableArray array]; // 取得的对象存在了,但是并非自己所持有的,引用计数还为 0, 但是该对象被放到了autoreleasepool 中,可以自动释放 [obj retain]; // 此时,自己就持有了这个对象,引用计数为 1 [obj release]; // 此时释放了这个对象,引用计数变为 0 ,对象就不可以再被访问了,但是对象也没有被立即废弃
-
无法释放非自己持有的对象:例如
// 取得非自己持有的对象 id obj = [NSMutableArray array]; [obj release]; // 会导致程序崩溃
1.2.3 alloc/retain/release/dealloc 实现
-
分析 GNU 源码来理解 NSObject 类中的方法。
-
首先是 alloc
id obj = [[NSObject alloc] init];
+ (id)alloc { // alloc 在内部调用 allocWithZone return [self allocWithZone:NSDefaultMallocZone()]; } + (id)allocWithZone:(NSZone *)zone { // allocWithZone 在内部调用 NSAllocateObject return NSAllocateObject(self, 0, z); } struct obj_layout { NSUInteger retained; }; inline id NSAllocateObject (Class aClass, NSUInteger extreBytes, NSZone *zone) { int size = 计算容纳对象所需内存的大小; // 分配内存空间 id new = NSZoneMalloc(zone, size); // 将该内存空间中的值初始化为 0 memset(new, 0, size); // 返回作为对象而使用的指针 new = (id)&((struct obj_layout *) new)[1]; } /** 其中, NSZoneMalloc, NSDefaultMallocZone() 等名称中包含的 Zone 是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据对象使用的目的,大小,分配内存,从而提高内存管理的效率。 但是现在的运行时系统知识简单的忽略了区域的概念,运行时系统中的内存管理本身已经机具效率,再使用区域来管理内存反而会引起内存使用效率低下的问题。 */
-
去掉NSZone后简化的代码
struct obj_layout { NSUInteger retained; }; + (id)alloc { int size = sizeof(struct obj_layout) + 对象大小; // 这句的意思是,为 struct obj_layout 这个结构体分配一个 size 大小的内存空间,并且函数calloc()会将所分配的内存空间中的每一位都初始化为零,也就是这块内存中所有的值都为 0 struct obj_layout *p = (struct obj_layout *)calloc(1, size); // 返回该对象指针 return (id)(p + 1); }
-
[obj retain];
的实现- (id)retain { NSIncrementExtraRefCount(self); } inline void NSIncrementExtraRefCount(id anObject) { // 首先 (struct obj_layout *) anObject 找到的是这个对象的尾部, 所以需要 [-1] 减去该对象的大小,来寻址到该对象的头部,然后再判断该结构体中 retained 这个变量的值是否已经大于了系统最大值,如果没有,就 retained++, 使得引用计数 +1. if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1) { [NSException raise: NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"]; ((struct obj_layout *) anObject) [-1].retained++; } }
-
[obj release]
的实现- (void)release { if (NSDecrementExtraRefCountWasZero(self)) { [self delloc]; } } BOOL NSDecrementExtraRefCountWasZero(id anObject) { if (((struct obj_layout *) anObject)[-1].retained == 0) { return YES; } else { ((struct obj_layout *) anObject)[-1].retained--; return NO; } }
-
[obj dealloc];
的实现- (void)dealloc { NSDeallocateObject(self); } inLine void NSDeallocateObject (id anObject) { // 指针 o 指向 anObject 的内存地址,然后释放这个指针指向的内存 struct obj_layout *o = &((struct obj_layout *) anObject) [-1]; free(o); }
-
1.2.4 苹果的实现
-
首先看 alloc 的实现:
// 依次调用这四个方法 + alloc + allocWithZone: class_Instance calloc
-
retainCount / retain / release 的实现
- retainCount __CFDoExtrernRefOperation CFBaseicHashGetCountOfKey - retain __CFDoExternRefOperation CFBasicHashAddValue; - release __CFDoExternRefOperation CFBasicHashRemoveValue // 这些函数的前缀 CF 表示他们都包含于 Core Foundation 框架的源代码中
所以其内部实现可能如下:
int __CFDoExternRefOperation(uintptr_r op, id obj) { CFBasicHashRef table = 取得对象的散列表(obj); int count; switch (op) { case OPERATION_retainCount: count = CFBasicHashGetCountOfKey(table, obj); return count; case OPERATION_retain: CFBasicHashAddValue(table, obj); return obj; case OPERATION_release: count = CFBasicHashRmoveValue(table, obj); // 如果count == 0, 返回 YES, 则会调用 dealloc return 0 == count; } } // 举例说明 retainCount - (NSUInteger)retainCount { return (NSUInteger)__CFDExternRefOperation(OPERATION_retainCount, self); }
- 由此可看出,苹果在计数内部大概是以散列表的方式来管理引用计数的。复习散列表
- 比较
-
通过内存块头部管理引用计数的好处:
少量代码即可完成
能够统一管理引用计数需要的内存块和对象所用的内存块
-
通过引用计数表管理引用计数的好处
- 对象所用的内存块的分配不需要考虑它的头部(跟内存块头部管理引用计数相比,就是少了一个用来计数的头部)
- 引用计数表各记录中存有内存块的地址,可以从各个记录追溯到各个对象的内存块。这使得计时出现故障导致了对象所占用的内存块损坏了,在 内存块头部管理引用计数 时,我们这样就没有办法访问这块内存了,但是在 引用计数表管理引用计数 时,我们就可以通过这个计数表来寻址内存块的位置。
- 另外,在利用工具检测内存泄漏时,引用计数表也可以用来检测各个对象是否有持有者
-
1.2.5 autorelease
autorelease 会像 C语言 的自动变量一样来对待对象实例。当其超出作用域时,就会对对象进行release 的调用。
-
autorelease 的具体使用方法:
生成并持有 NSAutoreleasePool 对象
调用已经分配对象的 autorelease 实例方法
-
废弃 NSAutoreleasePool 对象(对对象自动调用 release)
// 代码如下 NSAutoreleasePool pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; => 等价于 [obj release];
我们在编程中,并不需要显式的调用 pool 对象,因为在 RunLoop 中,这一切都为我们处理好了。在一个 RunLoop 循环中,会进行 NSAutoreleasePool 对象的生成,应用程序的主线程进行处理,废弃 NSAutoreleasePool 对象。
-
尽管是这样,我们有的时候也需要显式的调用 NSAutoreleasePool 对象,因为有时会产生大量的 autorelease 对象,只要不废弃 NSAutoreleasePool 对象,那么这些生成的对象就不能被释放,会导致内存疯长的现象。最典型的例子就是在读取大量图像的同时改变它的尺寸。
- 图像文件读到 NSData 对象,并且从中生成 UIImage 对象,改变这个对象的尺寸后,就会生成新的 UIIamge 对象。这种情况下就会产生大量的 autorelease 对象。这时就有必要在合适的地方生成,持有或废弃 NSAutoreleasePool 对象。
-
另外,在 Cocoa 框架中也有很多类方法用于返回 autorelease 对象。比如
id array = [NSMutableArray arrayWithCapasity:1]; // 等价于 id array = [[[NSMuatbleArray alloc] initWithCapasity:1] autorelease];
1.2.6 autorelease 的实现
首先来看 GNU 的源代码
-
首先看一下 autorelease 方法的实现
[obj autorelease]; // 表面上的实现方法 - (id)autorelease { [NSAutoreleasePool addObject:self]; } /** 实际上, autorelease 内部是用 Runtime 的 IMP Caching 方法实现的。在进行方法调用时,为了解决类名/方法名几区的方法运行是的函数指针,要在框架初始化时对他们进行缓存 */ id autorelease_class = [NSAutoreleasePool class]; SEL autorelease_sel = @selector(addObject:); IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel]; // 实际的方法调用时使用缓存的结果值 - (id)autorelease { (*autorelease_imp)(autorelease_class, autorelease_sel, self); }
-
再看 NSAutoreleasePool 的 addObject 类方法实现
+ (void)addObject:(id)obj { NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 对象; if (pool) { [pool addObject:anObj]; } else { NSLog("不存在正在使用的 NSAutoreleasePool 对象"); } }
- 注意:当多个 NSAutoreleasePool 对象嵌套使用时,理所当然会调用最里层的 NSAutoreleasePool 对象
-
addObject 实例方法实现
// 当调用 NSObject类的 autorelease 实例方法时,这个对象就会被加到 NSAutoreleasePool 对象数组中 - (void)addObject:(id)obj { [array addObject:obj]; }
-
drain 实例方法废弃正在使用的 NSAutoreleasePool 对象的过程
// 执行顺序: drain() -> dealloc() -> emptyPool() -> [obj release] -> [emptyPool release] - (void)drain { [self dealloc]; } - (void)dealloc { [self emptyPool]; [array release]; } - (void)emptyPool { for (id obj in array) { [obj release]; } }
1.2.7 苹果的实现
-
C++的实现
class AutoreleasePoolPage { static inline void *push() { // 生成或持有 NSAutoreleasePool 对象 } static inline id autorelease(id obj) { // 对应 NSAutoreleasePool 类的 addObject 类方法 AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的 AutoreleasePoolPage 实例; autoreleasePoolPage -> add(obj); } static inline void *pop(void *token) { // 废弃 NSAutoreleasePool 对象 releaseAll(); } id *add(id obj) { // 添加对象到 AutoreleasePoolPage 的内部数组中 } void releaseAll() { // 调用内部数组对象的 release 类方法 } }; // 具体调用 void *objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void *objc_autoreleasePoolPop(void *ctxt) { return AutoreleasePoolPage::push(ctxt); } id *objc_autorelease(void) { return AutoreleasePoolPage::autorelease(obj); }
-
观察 NSAutoreleasePool 类方法和 autorelease 方法的运行过程
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // == objc_autoreleasePoolPush() id obj = [[NSObject alloc] init]; [obj autorelease]; // == objc_autorelease(obj) [pool drain]; // == objc_autoreleasePoolPop(pool);
另外:
[[NSAutoreleasePool showPools]];
可以用来确认已经被 autorelease 的对象的状况。-
问题: 如果
autorelease NSAutoreleasePool
对象会如何?- 答: 会崩溃。因为通常在使用 Foundation 框架时,无论调用哪个对象的 autorelease 方法,本质都是调用 NSObject 类的 autorelease 方法。 但是 autorelease 方法已经被 NSAutoreleasePool 类所重载。所以运行时会出现错误。
1.3 ARC 规则
1.3.1概要
- 实际上 引用计数式内存管理 的本质部分在 ARC 中并没有改变,就像 自动引用计数 这个名称一样,ARC 所做的,只是自动的帮助我们处理了 引用计数 相关部分。
1.3.2内存管理的思考方式
- 引用计数式内存的思考方式就是思考 ARC 所引起的变化
- 自己生成的对象,自己所持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放该对象
- 非自己持有的对象无法释放
- 本质上和内存管理的思考方式一样,只是实现方式上有些许不同
1.3.3所有权修饰符
-
ARC 有效时,id 类型和对象类型与 C语言 中的其他类型不同,其类型必须附加 所有权修饰符 。共有以下四种
- __strong 修饰符
- __weak 修饰符
- __unsafe_unretained 修饰符
- __autoreleasing 修饰符
-
__strong 修饰符
-
__strong 修饰符是 id 类型和对象类型默认的所有权修饰符
// 在 ARC 有效的环境下 id obj = [[NSObject alloc] init] <==> id __strong obj = [[NSObject alloc] init]; // 在 ARC 无效的环境下 { id obj = [[NSObject alloc] init] [obj release]; }
-
当被__strong 修饰符修饰的,自己生成的,对象在超过其作用域时:
{ // 自己生成并持有对象 id __strong obj = [[NSObject alloc] init]; /** 因为变量 obj 为强引用,所以自己持有对象 */ } // 因为变量超出作用域,强引用失效,所以释放对象,因为对象此时没有其他的所有者了,对象被废弃。 // 正好遵循内存管理的原则
-
当对象的所有者和对象的生命周期是明确的,取得非自己生成并持有的对象时:
{ // 首先取得非自己生成的对象,但是由于__strong修饰符修饰着这个对象,所以自己持有这个对象 id __strong obj = [NSMutableArray array]; } /** 当超出对象的作用域时,强引用失效,所以释放对象。 但是由于 [NSMutableArray array] 所生成的对象并非自己所持有的,而是自动的加到 autoreleasePool 中,所以会在一个 RunLoop 周期结束后,自动废弃对象。 */
-
有 __strong修饰符的变量之间可以相互赋值
// 首先 obj0 强引用指向 对象A , obj1 强引用指向 对象B,表示 obj1 持有 B, obj2 不持有任何对象 id __strong obj0 = [[NSObject alloc] init]; // 对象A id __strong obj1 = [[NSObject alloc] init]; // 对象B id __strong obj2 = nil; // 此时 obj0 与 obj1 强引用同一个对象 B, 没有人持有 对象A 了,所以 对象A 被废弃。 obj0 = obj1; // 此时 obj2 指向 obj0 所持有的对象, 所以 对象B 现在被三个引用所持有。 obj2 = obj0; // 现在 obj1 对 对象B 的强引用失效,所以现在持有 对象B 的强引用变量为 obj0,obj2 obj1 = nil; // 同理,现在只有 obj2 持有对象B obj0 = nil; // 没有引用指向 对象B 了,废弃 对象B obj2 = nil;
-
__strong修饰符 也可以修饰 OC类成员变量,也可以在方法的参数上,使用附有 _strong 修饰符的变量
@interface Test : NSObject { id __strong obj_; } - (void)setObject:(id __strong)obj; @end @implementation Test - (instancetype)init { self = [super init]; return self; } - (void)setObject:(id)obj { obj_ = obj; } @end // 调用函数 { // 首先test 持有 Test 对象的强引用 id __strong test = [[Test alloc] init]; // Test对象 的 obj_ 成员,持有 NSObject 对象的强引用 [test setObject:[[NSObject alloc] init]]; } /** 此时test强引用超出了其作用域,它失效了。 所以此时没有强引用指向 Test对象 了, Test对象会被废弃 废弃 Test 对象的同时, Test对象 的 obj_ 成员也被废弃。 所以它释放了指向 NSObject 的强引用 因为 NSObject 没有其他所有者了,所以 NSObject 对象也被废弃。 */
_strong修饰符 与 _weak, _autoreleasing 修饰符一样,初始化时,即使不明确指出,他们也都会自动将该引用指向nil。通过 _strong修饰符,完美的满足了 引用计数的思考方式
id类型和对象类型的所有权修饰符默认都为 __strong 所以不需要再显式的指明修饰对象的修饰符为 _strong
-
-
__weak 修饰符
-
_weak修饰符 的出现就是为了解决 _strong修饰符在内存管理中所带来的循环引用问题。如上例:
@interface Test : NSObject { id __strong obj_; } - (void)setObject:(id __strong)obj; @end @implementation Test - (instancetype)init { self = [super init]; return self; } - (void)setObject:(id)obj { obj_ = obj; } @end // 调用函数,打印变量结果如下: { // 首先test1 持有 Test 对象的强引用, test2 持有 Test 对象的强引用 id __strong test1 = [[Test alloc] init]; // 对象A id __strong test2 = [[Test alloc] init]; // 对象B /** TestA 对象中的 obj_ 成员变量持有着 test2指向的 对象B, 同时,test2指向的对象B中的 obj_又强引用着 对象A, 所以造成了循环引用。 */ [test1 setObject:test2]; [test2 setObject:test1]; } /** 当跳出作用域后,test1释放它对 对象A 的强引用 test2释放它对 对象B 的强引用 但是此时 对象A中的 obj_A 对 对象B 的强引用本应该被释放,但是由于在 对象B 中强引用了对象A,所以 obj_A 不会被释放,会一直强引用 对象B, 而同理,对象B 中的 obj_B 也不会被释放,所以它将一直强引用着 对象A, 所以此时外部没有谁引用着 对象A 和 对象B, 但是他们自己在互相引用着,这样就造成了内存泄漏!(所谓内存泄漏,指的就是应该被废弃的对象,却在超出其生存周期变量作用域时还继续存在着) */ /** 打印变量结果如下: 其中 test1 对象中强引用 test2 对象, test2对象 又强引用 test1 对象,造成无尽的循环。 */
-
-
而下面这种情况:
{ id test = [[Test alloc] init]; [test setObject:test]; } /** 当强引用test 的作用域结束后,它释放了对 Test 对象的引用。 但是 Test对象 内部还保留着 对 Test对象 的强引用,所以 Test对象 被引用着,所以不会被回收 */ // 也会发生内存泄漏!
-
所以此时,就非常需要一个 __weak修饰符 来避免循环引用
// 弱引用与强引用正好相反,不能够持有对象实例。 // 这样写会发出警告:Assigning retained object to weak variable; object will be released after assignment // 表示因为没有人持有着 NSObject 对象,所以该对象一旦被创建就会立即被销毁 id __weak obj = [[NSObject alloc] init]; // 正确的使用弱引用的方式 { // 自己生成并持有 NSObject 对象 id obj = [[NSObject alloc] init]; // 因为 NSObject 对象已经被 obj 强引用着, 所以此时 obj1 对它使用弱引用也没有关系, // 不会使它的引用计数 +1 id __weak obj1 = obj; } /** 当超出变量的作用域时, obj 对 NSObject对象 的强引用消失, 此时没有人持有 NSObject对象 了。 NSObject对象 被废弃 */
-
对上述循环引用的例子进行修改如下:
@interface Test : NSObject { id __weak obj_; } - (void)setObject:(id __strong)obj; @end @implementation Test - (instancetype)init { self = [super init]; return self; } - (void)setObject:(id)obj { obj_ = obj; } @end // 调用函数,打印变量结果如下: { // 首先test1 持有 Test 对象的强引用, test2 持有 Test 对象的强引用 id __strong test1 = [[Test alloc] init]; // 对象A id __strong test2 = [[Test alloc] init]; // 对象B /** TestA 对象中的 obj_ 成员变量弱引用着 test2指向的 对象B, 同时,test2指向的 对象B 中的 obj_又弱引用着 对象A。 */ [test1 setObject:test2]; [test2 setObject:test1]; } /** 当跳出作用域后,test1释放它对 对象A 的强引用 test2释放它对 对象B 的强引用 此时,由于 对象中的 obj_变量只拥有对对象的弱引用,所以 没有谁持有着 对象A,和对象B,他们被释放,没有造成循环引用! */
-
__weak修饰符 的另一优点:当持有某个对象的弱引用时,如果该对象被废弃,则弱引用将自动失效,并且会被置为 nil的状态(空弱引用)
id __weak obj1 = nil; { // 自己生成并持有对象 id __strong obj0 = [[NSObject alloc] init]; // obj1 现在也指向 NSObject对象 obj1 = obj0; // 此时打印 obj1 有值 NSLog(@"A = %@", obj1); } /** 当变量 obj0 超出作用域,它不再持有 NSObject对象, 由于 obj1 是弱引用,所以它也不持有 NSObject对象 由于没人持有 NSObject对象, NSObject对象被废弃 被废弃的同时, obj1 变量的弱引用失效, obj1 被重新赋值为 nil */ NSLog(@"B = %@", obj1); /** 结果打印如下: 2017-12-14 15:16:39.859875+0800 littleTest[10071:1377629] A = <NSObject: 0x10054da70> 2017-12-14 15:16:39.860432+0800 littleTest[10071:1377629] B = (null) */
-
__unsafe_unretained 修饰符
-
_unsafe_unretained 修饰符 是不安全的修饰符,在 iOS4 以前用来代替 _weak修饰符
id __unsafe__unretained obj1 = nil; { id __strong obj0 = [[NSObject alloc] init]; obj1 = obj0; NSLog(@"A = %@", obj1); } NSLog(@"B = %@", obj1); /** 该源码无法正确执行,因为 __unsafe_unretained修饰符 使变量既不强引用对象,也不弱引用对象。 当 obj0 超出作用域时, NSObject 无引用,所以被释放 在此同时, obj1 有时会错误访问对象,形成下面这种打印 2017-12-14 15:38:21.462724+0800 littleTest[10140:1399554] A = <NSObject: 0x10044eea0> 2017-12-14 15:38:21.463007+0800 littleTest[10140:1399554] B = <NSObject: 0x10044eea0> 有时会发生错误,直接使程序崩溃。 造成这两种情况的本质为: obj1 访问的对象已经被废弃了,造成了 垂悬指针! */
所以,需要注意的是,当使用 _unsafe_unretained 修饰符 访问对象时,必须要确保该对象确实是真实存在的。
-
-
__autoreleasing 修饰符
-
在 ARC 有效时,我们不可以使用如下代码,因为这些代码是在非 ARC 环境下使用的:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain];
-
作为替换,在 ARC 有效时, 我们会使用
@autoreleasepool { id __autoreleasing obj = [[NSObject alloc] init]; }
他们的对应关系如图:
-
我们可以非显式的使用 __autoreleasing 修饰符 。
-
情况一:当取得非自己生成并持有的对象时,虽然可以使用 alloc/new/copy/mutableCopy 以外的方法来取得对象,但是该对象已经被注册到 autoreleasePool 中。这和在 ARC无效 时,调用 autorelease 方法取得的结果相同。这是因为编译器会自动检查方法名是否以alloc/new/copy/mutableCopy 开始,如果不是,则自动将对象注册到 autoreleasePool 中。
@autoreleasepool { // 首先取得非自己生成的对象,因为 obj 的强引用,所以它持有这个对象 // 因为这个对象的方法名不是以 alloc/new/copy/mutableCopy 开头的,所以他被自动注册到 autoreleasePool中了。 { id __strong obj = [NSMutableArray array]; } /** 当变量超出其作用域时,他失去对这个对象的强引用。所以它会释放自己所持有的对象 但是此时 autoreleasePool 中还持有着对这个对象的引用,所以它不会立即被废弃 */ } /** 当 autoreleasePool 的作用域也结束后,没有人持有这个对象了,所以它被废弃了。 */
-
验证上述说法,首先:创建对象的方式为
[NSMutableArray array]
:// 在 autoreleasepool 的作用域外定义一个 obj1 持有弱引用 id __weak obj1 = nil; @autoreleasepool { { id __strong obj = [NSMutableArray array]; obj1 = obj; NSLog(@"%@", obj1); } NSLog(@"%@", obj1); } NSLog(@"%@", obj1); /** 打印结果: 2017-12-14 16:32:07.118513+0800 littleTest[10242:1444479] ( ) 2017-12-14 16:32:07.118819+0800 littleTest[10242:1444479] ( ) 2017-12-14 16:32:07.118850+0800 littleTest[10242:1444479] (null) 结果表明:当obj0超出其作用域时,它失去了对对象的引用。但是由于该对象被自动注册到 autoreleasepool 中,使得第二个 NSLog 打印时 obj1 依旧弱引用着这个对象,当第三个 NSLog 打印时,由于 autoreleasepool 已经被清空,所以这个对象也被销毁了, obj1 又被重置为 nil */
-
-
此时,创建对象的方式为:
[[NSMutableArray alloc] init]
id __weak obj1 = nil; @autoreleasepool { { id __strong obj = [[NSMutableArray alloc] init]; obj1 = obj; NSLog(@"%@", obj1); } NSLog(@"%@", obj1); } NSLog(@"%@", obj1); /** 打印结果如下: 2017-12-14 16:36:09.584864+0800 littleTest[10257:1449554] ( ) 2017-12-14 16:36:09.585131+0800 littleTest[10257:1449554] (null) 2017-12-14 16:36:09.585149+0800 littleTest[10257:1449554] (null) 这是因为,使用 alloc/new/copy/mutableCopy 方法创建对象时,不会将该对象自动的放入 autoreleasePool 中,这就使得当 obj0 超出其作用域后,就没有人强引用着 NSMutableArray 对象了,该对象也就被废弃了。 */
-
以下为 取得非自己生成并持有的对象 时所调用方法:
+ (id)array { return [[NSMutableArray alloc] init]; } /** 这段代码也没有使用 __autorelease修饰符,所以这个方法内部的对象不会被注册到 autoreleasePool 中。 */ // 上述方法也可以写成如下形式: + (id)array { id obj = [[NSMutableArray alloc] init]; return obj; } /** 因为 return 使得 obj 超出作用域,所以它所指向的对象 NSMutableArray 会被自动释放,但是因为 return 将这个对象作为函数的返回值返回给主调函数,所以这个对象不会被废弃。并且由于这个对象的生成方法是将其作为返回值,不是由alloc/new/copy/mutableCopy 方法创建的,所以 NSMutableArray 对象会被自动添加到 autoreleasePool 中 */
-
情况二: 在访问有 __weak修饰符 的变量时,实际上必定会访问注册到 autoreleasePool 中的对象
id __weak obj1 = obj0; NSLog(@"class=%@", [obj1 class]); // 等价于 id __weak obj1 = obj0; id _autoreleasing temp = obj1; NSLog(@"class=%@", [temp class]); /** 出现这种情况的原因是因为:__weak修饰符 只持有对象的弱引用,这样没法保证它访问对象的过程中,对象不被废弃。所以我们将他要访问的对象放到 autoreleasePool 中,这样就会使得 @autoreleasePool块 结束之前都能保证该对象的存在。 */
-
情况三:由于
id obj <==> id __strong obj
所以我们希望能推出id *obj <==> id __strong *obj
但是实际上并非如此,实际情况是id *obj <==> id __autoreleasing obj
同理:NSObject **obj <==> NSObject * __autoreleasing *obj
,像这样的,id 的指针或对象的指针在没有显示的指定时,会被附加上 __autoreleasing修饰符// 例如 NSString 中的这个方法 stringWithContentsOfFile:(nonnull NSString *) encoding:(NSStringEncoding) error:(NSError * _Nullable __autoreleasing * _Nullable) // 使用这个方式的源代码如下: NSError *error = nil; BOOL result = [obj performOperationWithError:&error]; // 函数声明如下 - (BOOL)performOperationWithError:(NSError **)error; // 等价于 - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error; /** 之所以使用 __autoreleasing 作为修饰符,是因为我们这个方法声明的参数的是 error 这个指针变量的指针,也就是 需要传递 error 的地址。而在这个方法内部的执行如下,它改变了 error 这个指针所指向的内容。使其指向了一个由 alloc 生成的对象。而我们需要明白的内存管理的思考方式为:除了由 alloc/new/copy/mutableCopy 生成的对象外,其他方式生成的对象都必需要注册到 autoreleasePool 中。并取得非自己所持有的对象。所以将变量声明为 (NSError * __autoreleasing *)error 就可以实现这一目的。 */ - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error { *error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil]; return NO; } /** 可能对于不熟悉 C语言 的小伙伴来说,不是很明白为什么这里非要将函数参数声明为 “指针的指针”,这是因为当我们仅仅把参数声明为指针时,方法就变为如下,当我们给函数传递指针时,默认会生成跟指针类型相同的实例变量。当我们在这个方法中操作指针时,我们以为操作的是指针,实际上只是这复制的实例变量。也就是说,在这个例子中 error 就是这个复制的实例变量。当这个方法结束时,error 会被释放,其所指向的内容也会一并被释放。所以此时外部的 error 依旧指向 nil。没有任何改变。 而当我们使用 (NSError * __autoreleasing *)error 作为参数时,虽然复制的实例变量情况还是存在,但是这次复制的是“指针的指针”,也就是说,它指向跟参数指针相同指针地址, 在函数内部使用 *error 获取到了指针地址,使其指向了 NSError对象 。这样,虽然函数当出了其作用域时,那个复制的实例变量被销毁了,但是它改变了函数外部 error 指针所指向的对象,使其从 nil 变成了 NSError对象。 */ - (BOOL)performOperationWithError:(NSError *)error { error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil]; return NO; }
对于函数传递指针及指针的指针 还不明白的请看这里
-
👇的代码会产生编译错误:
NSError *error = nil; NSError **perror = &error; //Pointer to non-const type 'NSError *' with no explicit ownership
-
因为
// 需要改变perror的所有权修饰符 NSError *error = nil; NSError *__strong *perror = &error; // 对于其他类型的所有权修饰符也一样 NSError __weak *error = nil; NSError *__weak *perror = &error; /** 可是我们刚刚在调用 - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error 方法时,并没有将 NSError *error 转化为 __autoreleasing修饰符修饰的,这是为什么? */ // 实际上,编译器自动帮我们转化了修饰符 NSError *error = nil; NSError *_autoreleasing *temp = nil; BOOL result = [obj performOperationWithError:&temp]; error = temp;
像 NSAutoreleasePool 一样, autoreleasepool 也可以嵌套使用。例如在 iOS 程序中,整个程序都被包含在 @autoreleasepool块 中。
NSRunLoop等实现无论 ARC 是否有效,都能够随时释放注册到 autoreleasepool 中的对象。
1.3.4规则
-
在 ARC 有效时编译代码,必须遵守以下规则:
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObject
- 必须遵守内存管理方法命名规则
- 不能显示的调用 dealloc
- 使用 @autoreleasepool块 代替 NSAutoreleasePool
- 不能使用区域 NSZone
- 对象型变量不能作为 C语言 结构体的成员
- 显示的转换 id 和 void *
-
不能使用 retain/release/retainCount/autorelease
- 摘自苹果的官方说明 "设置ARC有效时,无需再次键入release或retain代码" 否则就会编译错误。
-
不能使用 NSAllocateObject/NSDeallocateObject
- 我们已经知道了当我们在调用 NSObject 类的 alloc 方法时,会生成并持有 OC 对象,如 GNUstep 所示,实际上 alloc 就是直接调用 NSAllocateObject 函数来生成持有对象的,但是在 ARC 环境下,如果我们也显示的调用 NSAllocateObject 会产生编译错误。
-
必须遵守内存管理方法命名规则
在 ARC 无效时,用于对象生成/持有需要遵循如下规则:alloc/new/copy/mutableCopy。以上述名称开始的方法在返回对象时,必须得返回给调用方应当持有的对象。这点在 ARC 有效时也是一样。
-
在 ARC 有效时,追加的一条命名规则:init
-
以 init 开始的方法规则要比 alloc/new/copy/mutableCopy 更加严格。该方法必须得是实例方法。不允许是类方法。并且返回对象应该为 id 类型或者该方法声明类的对象类型,或者是该类的超类或子类。该返回对象并不注册到 autoreleasepool 中。基本上只是对 alloc 方法返回值的对象进行初始化操作并返回该对象。
// 以下为使用该方法的源代码: init 方法会初始化alloc 方法返回值,并且返回该对象 id obj = [[NSObject alloc] init]; // 下列是不允许的,因为它没有返回对象 - (void)initTheData:(id)data; // 另外,👇方法虽然也有init, 但它不包含在命名规则里,因为他是一个单词 initialize - (void)initialize;
-
-
不能显示的调用 dealloc
- 因为当对象废弃时,无论如何都会调用对象的 dealloc 方法,所以不需要我们手动调用。而当我们手贱一下的去调用时,就会产生编译错误
- dealloc 方法在大多数情况下用于删除已经注册的代理或者观察者对象
- 在 ARC 无效时,必须在 dealloc 方法内部显式的调用其父类的 dealloc 方法
[super dealloc];
- 在 ARC 有效时,这一切都是自动处理的。
-
使用 @autoreleasepool块 代替 NSAutoreleasePool
- 在 ARC 中使用 NSAutoreleasePool 会引起编译错误
-
不能使用区域 NSZone
- 无论 ARC 是否有效,NSZone在现在的运行时系统已经被完全忽略了。
-
对象型变量不能作为 C语言 结构体的成员
C语言 的结构体中如果存在 OC对象型变量 会引起编译错误
-
如果非要将对象加入结构体,则可强制转化为 void * 或者附加 _unsafe_unretained修饰符 ,因为被 _unsafe_unretained修饰符所修饰的对象,已经不属于编译器的内存管理对象了。
struct Data { NSMutableArray __unsafe__unretained *array }
-
显示的转换 id 和 void *
-
当 ARC 无效时,将 id变量 强制转化为 void *变量 不会出现问题
id obj = [[NSObject alloc] init]; void *p = obj; // 将 void * 赋给 id变量 中,调用他的实例方法,运行时也不会出现问题 id o = p; [o release];
-
当 ARC 有效时,会引起编译错误。此时,id型 或 对象型变量 赋值给 void * 或者逆向赋值时都需要进行特定的转换,如果只是想单纯的赋值则可以使用 bridge转换
id obj = [[NSObject alloc] init]; // id转化为 void *,它的安全性比 __unsafe__unretained 还要低,一不小心就会有垂悬指针 void *p = (__bridge void *)obj; // void * 转换为 id id o = (__bridge id)p;
-
__bridge转换 中还包括 _bridge_retained转换, _bridge_transfer转换
id obj = [[NSObject alloc] init]; void *p = (__bridge_retained void *)obj
-
该代码在 非ARC 环境下
id obj = [[NSObject alloc] init]; void *p = obj; // __bridge__retained转变为了 retain,使得 p 和 obj 都持有了这个对象 [(id)p retain];
-
-
一个其他的例子:
void *p = 0; { id obj = [[NSObject alloc] init]; p = (__bridge_retained void *)obj; } NSLog(@"class = %@", [(__bridge_retained)p class]);
-
该代码在 非ARC 环境下
void *p = 0; { id obj = [[NSObject alloc] init]; p = [obj retain]; [obj release]; } /** 此时 p 依旧持有对 NSObject对象 的引用 */ NSLog(@"class = %@", [(__bridge_retained)p class]);
-
-
__bridge_transfer,被转换的变量所持有的对象在该变量被赋值给转换目标变量后释放
id obj = (__bridge_transfer id)p;
-
非ARC 环境下
id obj = (id)p; [obj retain]; [(id)p release];
-
-
Objective-C 对象 与 Foundation对象
- Core Foundation 对象主要使用在用 C语言 编写的 Core Foundation 框架中,并使用引用计数对象。在 ARC无效 时, Core Foundation 框架中的 retain、release 分别是 CFRetain,CFRelease
- Core Foundation 对象与 OC 对象的区别只在于是 Core Foundation 框架 还是 Foundation 框架所生成的。无论是由哪种框架生成的对象,一旦生成之后,就能在其它框架上使用。比如 Foundation 框架的 API 生成并持有的对象可以由 Core Foundation 框架的 API 进行释放。
- Core Foundation 对象与 Objective-C 对象没有区别,所以在 ARC无效 时,只用简单的 C语言的转换也能实现互换。另外这种互换不需要占用 CPU 资源,所以也叫做 "免费桥"(Toll-Free Bridge)
-
1.3.5属性
-
属性声明的属性 与 所有权修饰符 对应的关系
属性声明的属性 所有权修饰符 assign _unsafe_unretained copy __strong(赋值的是被复制的对象) retain __strong strong __strong unsafe_unretained _unsafe_unretained weak __weak
1.3.6数组
-
静态数组的情况:
// 将附有各种修饰符的变量作为静态数组的使用情况 // 比如 id __weak obj[10]; // 除了 __unsafe__unretained修饰符之外的其他修饰符都是会将数组元素的值默认初始化为nil // 当数组超出其变量作用域时,内存管理也同样适用于他之中的各个对象
动态数组:在这种情况下,根据不同的目的选择使用 NSMutableArray, NSMutableDictionary, NSMutableSet 等 Foundation 框架中的容器,这些容器会恰当的持有追加的对象并会为我们管理这些对象。
-
看一下动态数组在 C语言 中的实现
// 首先,声明一个动态数组需要使用指针。来表示指针的地址 id __strong *array = nil; //这里是由于 id * 类型的指针默认修饰符为 id __autoreleasing * 类型, 所以有必要显示的指定为 __strong 修饰符。另外,虽然保证了附有 __strong修饰符 的 id 类型变量被初始化为 nil, 但是不保证 array变量, 也就是 id指针型变量 被初始化为 nil // 当类型是其他类型时,如下: NSObject * __strong *array = nil; // 之后,使用 calloc函数 确保想分配的,附有 __strong修饰符变量 的容量占有的内存块 array = (id __strong *)calloc(entries, sizeof(id)); // 其中 entries 表示内存块的数量。并且 calloc 函数将数组中的每个变量指向的对象都自动初始化为 nil // 注意这里如果使用了 malloc函数 来分配内存, 则需要手动的将每个变量所指向的对象都初始化为 0,注意这里只能使用 memset等函数 来进行初始化赋值 // 然后,通过 calloc函数 分配的动态数组就能完全按照静态数组的方法使用 array[0] = [[NSObject alloc] init]; // 但是在动态数组中操作 __strong修饰符 的变量与静态数组有很大差异,需要自己手动释放数组,但是当它释放时,必须手动的先将数组的每个变量都置为nil,此时不能使用 memset等函数 将数组中的元素值设为 0 。这也会内存泄漏 for (NSInteger i = 0; i < entries; ++i) { array[i] = nil; } free(array);
1.4 ARC 的实现
1.4.1 __strong修饰符
-
观察赋值给附有 __strong修饰符 的变量在实际程序中到底是如何运行的,👇代码(首先是正常的会使引用计数 +1 的 alloc/new/copy/mutableCopy 方法):
{ id __strong obj = [[NSObject alloc] init]; }
-
该段代码转化为汇编代码后,为(具体如何转化为汇编代码,请看我的另一篇文章):
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 13 .globl _main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## BB#0: pushq %rbp Lcfi0: .cfi_def_cfa_offset 16 Lcfi1: .cfi_offset %rbp, -16 movq %rsp, %rbp Lcfi2: .cfi_def_cfa_register %rbp subq $16, %rsp movl $0, -4(%rbp) movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rax, %rdi callq _objc_msgSend leaq -16(%rbp), %rdi xorl %ecx, %ecx movl %ecx, %esi movq %rax, -16(%rbp) callq _objc_storeStrong xorl %eax, %eax addq $16, %rsp popq %rbp retq .cfi_endproc .section __DATA,__objc_classrefs,regular,no_dead_strip .p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_" L_OBJC_CLASSLIST_REFERENCES_$_: .quad _OBJC_CLASS_$_NSObject .section __TEXT,__objc_methname,cstring_literals L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_ .asciz "alloc" .section __DATA,__objc_selrefs,literal_pointers,no_dead_strip .p2align 3 ## @OBJC_SELECTOR_REFERENCES_ L_OBJC_SELECTOR_REFERENCES_: .quad L_OBJC_METH_VAR_NAME_ .section __TEXT,__objc_methname,cstring_literals L_OBJC_METH_VAR_NAME_.1: ## @OBJC_METH_VAR_NAME_.1 .asciz "init" .section __DATA,__objc_selrefs,literal_pointers,no_dead_strip .p2align 3 ## @OBJC_SELECTOR_REFERENCES_.2 L_OBJC_SELECTOR_REFERENCES_.2: .quad L_OBJC_METH_VAR_NAME_.1 .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 64 .subsections_via_symbols
-
-
简化后的模拟代码为:
// 首先发送消息给 NSObject 类,消息内容为 alloc 指令,然后将结果赋值给 obj id obj = objc_msgSend(NSObject, @selector(alloc)); // 然后将 init 消息发送给 obj objc_msgSend(obj, @selector(init)); // 最后释放 obj objc_release(obj); // 由此可知,在 ARC 有效时,自动插入了 release 方法
-
取得 非自己生成的,但是自己持有的 对象:
id __strong obj = [NSMutableArray array];
-
转化成汇编语言:
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 13 .globl _main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## BB#0: pushq %rbp Lcfi0: .cfi_def_cfa_offset 16 Lcfi1: .cfi_offset %rbp, -16 movq %rsp, %rbp Lcfi2: .cfi_def_cfa_register %rbp subq $16, %rsp movl $0, -4(%rbp) movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rax, %rdi callq _objc_msgSend movq %rax, %rdi callq _objc_retainAutoreleasedReturnValue leaq -16(%rbp), %rdi xorl %ecx, %ecx movl %ecx, %esi movq %rax, -16(%rbp) callq _objc_storeStrong xorl %eax, %eax addq $16, %rsp popq %rbp retq .cfi_endproc .section __DATA,__objc_classrefs,regular,no_dead_strip .p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_" L_OBJC_CLASSLIST_REFERENCES_$_: .quad _OBJC_CLASS_$_NSMutableArray .section __TEXT,__objc_methname,cstring_literals L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_ .asciz "array" .section __DATA,__objc_selrefs,literal_pointers,no_dead_strip .p2align 3 ## @OBJC_SELECTOR_REFERENCES_ L_OBJC_SELECTOR_REFERENCES_: .quad L_OBJC_METH_VAR_NAME_ .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 64 .subsections_via_symbols
-
-
简化后的汇编语言:
// 首先发送 array 消息给接收者 NSMutableArray, 然后将结果的返回值赋给 obj id obj = objc_msgSend(NSMutableArray, @selector(array)); /** obj_retainAutoreleasedReturnValue 函数主要用于最优化程序运行,顾名思义 obj_retainAutoreleasedReturnValue 表示的是 “持有 Autorelease 的返回值”,表示的是,它是用于自己持有对象的函数,但他持有的对象应为返回注册在 autoreleasepool 中对象的方法,,或是函数的返回值。像这段源代码一样,也就是 obj 需要被 __strong 所修饰在调用 alloc/new/copy/mutableCopy 以外的方法时,由编译器插入该函数 */ objc_retainAutoreleasedReturnValue(obj); // 释放 obj objc_release(obj);
-
由于,objc_retainAutoreleasedReturnValue 函数总是成对出现的,所以实际上它还有一个姐妹:objc_autoreleaseReturnValue, 它主要用在 alloc/new/copy/mutableCopy 以外的方法生成对象时的返回对象上,也就是如👇所示
+ (id)array { return [[NSMutableArray alloc] init]; } // 转化成汇编后的简化代码 + (id)array { id obj = objc_msgSend(NSMutableArray, @selector(alloc)); objc_msgSend(obj, @selector(init)); // 此时,返回注册到 autoreleasepool 中对象的方法:使用了 obj_autoreleaseReturnValue 函数来返回注册到 autoreleasepool 中的对象,但是 obj_autoreleaseReturnValue 方法与 obj_autorelease 方法不同,一般不仅限于注册对象到 autoreleasepool 中 return objc_autoreleaseReturnValue(obj); } /** objc_autoreleaseReturnValue 方法会检查使用该函数的方法或调用方的执行命令列表, 1.如果方法或函数的调用方在调用了方法或函数后紧接着调用了 objc_retainAutoreleasedReturnValue() 函数,那么就不会将返回的对象注册到 autoreleasepool 中,而是直接传递到方法或函数的调用方去。 2.如果方法或函数的调用方在调用了方法或函数后紧接着没调用objc_retainAutoreleasedReturnValue() 函数,那么就会将返回对象注册到 autoreleasepool 中。 而 objc_retainAutoreleasedReturnValue() 函数与 objc_retain 函数不同,他即便不注册到 autoreleasepool 中,也能正确的获取对象。 通过 objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue 方法的协作,可以不将对象注册到 autoreleasepool 中二直接传递,这一过程达到最优化 */
1.4.2 __weak修饰符
- 就像我们之前看到的:__weak修饰符 所提供的功能如魔法一般
- 若附有 __weak修饰符 的变量所引用的对象被废弃,则将 nil 赋值给该变量
- 使用附有 __weak修饰符 的变量,即是使用注册到 autoreleasepool 中的对象
若附有 __weak修饰符 的变量所引用的对象被废弃,则将 nil 赋值给该变量 原理验证:
-
下面我们来看看 __weak修饰符 原理实现:
{ id __weak obj1 = obj; } /** 编译器的模拟代码 */ id obj1; // 首先通过 obj_initWeak 函数初始化附有 __weak 修饰符的变量 objc_initWeak(&obj1, obj); // 然后在变量作用域结束时,通过 obj_destroyWeak 函数释放该变量 objc_destroyWeak(&obj1); /** 其中,objc_initWeak 函数的作用是:将附有 __weak修饰符 的变量初始化为 0 后,会将赋值的对象作为参数调用 objc_storeWeak 函数 obj_destroyWeak 函数的作用是:将 0 作为参数调用 obj_storeWeak 函数 */ objc_initWeak(&obj1, obj); <==> obj1 = 0; objc_storeWeak(&obj1, obj); objc_destroyWeak(&obj1) <==> objc_storeWeak(&obj1, 0); /** objc_storeWeak 函数把 第二个参数 的赋值对象的 地址 作为 "键值",将 第一个参数 的附有 __weak修饰符 的变量的"地址"注册到 weak 表 中。如果第二个参数为 0 ,则把变量的地址从 weak 表中删除 weak 表与引用计数表相同,实现方式都为"散列表"。如果使用 weak 表,将废弃对象的地址作为键值进行搜索,就能高速的获取对应的附有 weak修饰符 的变量的地址。另外,由于一个对象可以同时赋值给多个附有 weak修饰符 的变量中,所以对于一个键值,可注册多个变量的地址。 */
-
释放对象时,废弃没人持有的对象的同时,程序是如何操作的,下面我们来跟踪观察,对象将通过 objc_release 方法释放
- obj_release
- 引用计数为 0, 所以执行 dealloc
- _objc_RootDealloc
- object_dispose
- objc_destrctInstance
- objc_clear_deallocating
- 其中,objc_clear_deallocating 的动作如下
- 从 weak 表中获取废弃对象的地址作为键值的记录
- 将包含在记录中的所有附有 __weak修饰符 变量的地址赋值为 nil
- 从 weak 表中删除该记录
- 从引用计数表中删除废弃对象的地址作为键值的记录
- 其中,objc_clear_deallocating 的动作如下
根据以上步骤可知:__weak修饰符 所修饰的变量所引用的对象被废弃,该变量被置为 nil 得到实现。但是由此可知,如果大量使用附有 _weak修饰符修饰变量,将会产生性能问题。
-
在使用 __weak修饰符 时, 如果如下方式,会引起警告
id __weak obj = [[NSObject alloc] init]; // 因为该对象刚被创建就会被释放 // 编译器的模拟代码 id obj; id tmp = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp, @selector(init)); // 虽然自己生成并持有的对象通过 objc_initWeak 函数被赋值给附有 __weak修饰符 的变量中,但是编译器判断它没有持有者,所以该对象立即通过 objc_release 方法释放 objc_initWeak(&obj, tmp); objc_release(tmp); objc_destroyWeak(&obj); // 这样一来, nil 就会被赋值给引用废弃对象的附有 __weak修饰符 的变量
-
关于立即释放对象的一些思考
// 已知以下代码会引起编译器的警告,这是因为编译器判断生成并持有的对象不能继续持有,因为没有强引用指向它 id __weak obj = [[NSObject alloc] init]; // --------------------------------------------------------------------------------- // 附有 __unsafe_unretained 修饰符的变量会怎样? 也会产生警告 id __unsafe_unretained obj = [[NSObject alloc] init]; // 转换成编译器的模拟代码: id obj = obj_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); // obj_release 函数立刻释放了生成并持有的对象,这样该对象的垂悬指针被赋给 obj objc_release(obj); // --------------------------------------------------------------------------------- // 如果在生成对象的时候不把它赋给变量会怎样? // 在 非ARC 环境下,必然会发生内存泄漏 // 但是在 ARC 环境下,由于不能继续持有该对象,会立即调用 obj_release 函数,由于 ARC 的处理,这样的代码不会产生内存泄漏 [[NSObject alloc] init]; // ARC 下生成的代码 id tmp = obj_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp, @selector(init)); objc_release(tmp); // --------------------------------------------------------------------------------- // 是否可以调用被立即释放掉的对象的实例方法? (void)[[[NSObject alloc] init] hash]; // 该代码会变成如下形式: id tmp = obj_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp, @selector(init)); objc_msgSend(tmp, @selector(hash)); objc_release(tmp); // 所以,obj_release 方法是在该对象实例方法调用完成后才会被调用,所以可以调用被立即释放的对象的实例方法
使用附有 __weak修饰符 的变量,即是使用注册到 autoreleasepool 中的对象 ,原理验证:
-
看👇代码
{ id __weak obj1 = obj; NSLog(@"%@", obj1); } // 该源码可转化为如下形式 id obj1; objc_initWeak(&obj1, obj); // objc_loadWeakRetained 取出附有 __weak修饰符 变量所引用的对象并 retain id tmp = objc_loadWeakRetained(&obj1); // 将对象注册到 autoreleasepool 中 objc_autorelease(tmp); NSLog(@"%@", tmp); objc_destroyWeak(&obj1);
-
由此可知:因为附有 __weak修饰符 的变量所引用的对象像这样被注册到 autoreleasepool 中,所以在 @autoreleasepool 块结束之前都可以放心的使用 _weak修饰的变量。但是,不能大量的使用附有 _weak修饰符 修饰的变量,否则会引起注册到 autoreleasepool 中的对象大大增加,因此在使用附有 _weak修饰符 的变量时,最好先暂时赋给附有 _strong修饰符 的变量后再使用。若看不太懂则可以只看下面代码:
// 下面这段代码会使变量 o 所赋值的对象被注册到 autoreleasepool 中 5 次 { id __weak o = obj; for (int i = 0; i < 5; ++i) { NSLog(@"%d -- %@", i, o); } } // 而下面这段代码只会使变量 o 所赋值的对象被注册到 autoreleasepool 中 1 次 { id __weak o = obj; id tmp = o; for (int i = 0; i < 5; ++i) { NSLog(@"%d -- %@", i, tmp); } }
不支持 __weak修饰符 的情况
- 在 iOS4 和 OS X Snow Leopard 不支持 __weak修饰符 。
- 不支持 __weak修饰符 的类:NSMachPort等, 这个类重写了 retain/release 方法,并且实现了自己的引用计数。
- 不支持 __weak修饰符 的类在其类中声明附加了 --attribute—((objc_arc_weak_reference_unavailable)) 这一属性,同时定义了 NS_AUTOMATED_REFCOUNT_WEAK_UNAVAALIBLE
- 还有一种情况也不能使用 __weak修饰符 ,那就是当
allocWeakReference/retainWeakReference
实例方法返回 NO 的情况。(这种情况没有被写入 NSObject 类的接口说明文档中),也就是说,这两个方法我们一般不会接触到。
1.4.3 __autoreleasing修饰符
将对象赋值给附有 __autoreleasing修饰符 的变量等同于 ARC 无效时,调用对象的 autorelease 方法。
-
首先看一下使用 alloc/new/copy/mutableCopy 时的情况
@autoreleasepool { id __autoreleasing obj = [[NSObject alloc] init]; } // 模拟代码 id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); objc_autorelease(obj); objc_autoreleasPoolPop(pool);
-
再看一下使用 alloc/new/copy/mutableCopy 以外的方法时的情况
@autoreleasepool { id __autoreleasing obj = [NSMutableArray array]; } // 模拟代码 id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSMutableArray, @selector(array)); objc_retainAutoreleasedReturnValue(obj); objc_autorelease(obj); objc_autoreleasPoolPop(pool); // 虽然 obj 持有对象的方法变为 objc_retainAutoreleasedReturnValue, 但是将 obj 所引用的对象注册到 autoreleasepool 中的方法并没有改变
1.4.4 引用计数
-
引用计数数值本身到底是什么?
// 这个函数为获得引用计数的函数数值 uintptr_t _objc_rootRetainCount(id obj); { id __strong obj = [[NSObject alloc] init]; NSLog(@"retain count = %d", _objc_rootRetainCount); } // 打印结果:retain count = 1
- 我们实际上并不能完全信任 _objc_rootRetainCount 这个函数所取得的数值,因为有时对于已经释放的对象以及不正确的对象地址,有时也会返回 1 。 并且在多线程中使用对象的引用计数数值,因为有竞争状态的问题,所以取得的数值并不一定完全可信
1.5 总结
- 至此,我们所探究的 自动引用计数 已经完全讲解完毕,如有疏漏或不正确,不准确的地方,还望大家批评指正,共同进步。