第一章 自动引用计数
)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 总结
- 至此,我们所探究的 自动引用计数 已经完全讲解完毕,如有疏漏或不正确,不准确的地方,还望大家批评指正,共同进步。