第一章自动引用计数,第二章 block,第三章 GCD。
转换代码的命令:
clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 Test.m
一、自动引用计数
ARC 全称是 automatic Reference counting,编译器自动加入内存管理代码,无需手动输入 retain 或 release 代码了。
1.2 内存管理、引用计数
1.2.1 概要
OC 的内存管理,也就是引用计数,可以用开关房间的灯来说明。对象的引用计数为 0,就会被废弃。
1.2.2 内存管理的思考方式
- 自己生成的对象,自己持有;
- 非自己生成的对象,自己也能持有;
- 不再需要持有对象时要释放;
- 非自己持有的对象不要释放。
解释一下:
自己生成并持有对象,是指调用 alloc、new、copy、mutableCopy 等开头驼峰命名的方法群创建对象,比如 allocMyObject;
非自己生成的对象,是指调用 非 alloc/new/copy/mutableCopy 方法群创建对象;
持有对象,是指调用 retain 方法,或自己生成的对象;
释放对象,是指调用 release 方法;
废弃对象,是指 dealloc 方法。
// 自己生成并持有对象
id obj = [NSObject new];
// 用完后。。。
// 释放自己持有的对象
[obj release];
// 非自己生成的对象,并不持有
NSArray *array = [NSArray array];
// 持有它
[array retain];
// 用完后。。。
// 释放它
[array release];
下面讲解如何实现创建对象的方法。两种情况,自己持有的、非自己持有的。
第一种,自己持有的,实现 allocMyObject 方法:
- (id)allocMyObject {
id obj = [[NSObject alloc] init];
return obj;
}
第二种,非自己持有的,实现 myObject 方法:
- (id)myObject {
id obj = [[NSObject alloc] init];
// 变成非自己持有,自己是指调用方
[obj autorelease];
return obj;
}
调用 autorelease 会把对象放入自动释放池,使得对象超出指定的生存范围也能正确释放,pool 释放的时候会自动对里面的对象调用 release 方法。像 NSMutableArray 的 array 类方法就是这样实现的。
自己总结何时需要调用 release:
- 调用 alloc、new、copy、mutableCopy 等开头驼峰命名的方法创建的对象,用完后要调用 release;
- 调用非 alloc/new/copy/mutableCopy 方法群获得的对象,使用前先 retain,用完后调用 release;
- 实现非 alloc/new/copy/mutableCopy 开头的方法,返回创建的对象前要调用 autorelease。
1.2.3 alloc/retain/release/dealloc 实现
alloc 或 retain 会让引用计数值加 1,release 会让引用计数值减 1。引用计数值为 0 时,对象会被废弃 dealloc。
苹果应该是采用散列表管理引用计数,key 是内存地址,值是引用计数。
NSDefaultMallocZone、NSZoneMalloc、NSZone 是为防止内存碎片化而引入的结构。对内存分配的区域进行多重化管理,根据使用对象的目的、大小分配内存,从而提高了内存管理的效率。但是现在已经不需要了,运行时系统对内存管理的效率更加高效。
可以参考 programming with arc release notes。
1.2.5 autorelease
ARC 不支持 NSAutoreleasePool,改用 @autoreleasepool。
// MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
NSRunLoop 每次循环都会生成和废弃 NSAutoreleasePool 对象,废弃释放池对象的时候也会废弃里面的对象。用于循环可以减小内存峰值,代码如下:
for (int i = 0; i < count; i++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
}
上面的代码,不用等到整个循环结束才废弃 autorelease 对象,可以减小内存峰值。
1.3 ARC 规则
1.3.3 所有权修饰符
4 个修饰符:
- __strong
- __weak
- __unsafe_unretained
- __autoreleasing
__strong、__weak、__autoreleasing 修饰的变量,会初始化为 nil。
__strong 修饰符
__strong 是默认的修饰符,这两句代码是等效的:
id __strong obj = [[NSObject alloc] init];
id obj = [[NSObject alloc] init];
__strong 是强引用,可以持有对象。
__strong 修饰的变量指向一个对象,可以使对象的引用数+1。变量超出作用域失效时,引用的对象会释放,如果对象的引用数为 0,就会废弃。
__weak 修饰符
__weak 是弱引用,不能使对象的引用数 +1,而且对象废弃后,变量会置 nil。用于解决循环引用。
id __weak obj = [[NSObject alloc] init];
上面的代码会警告:Assigning retained object to weak variable; object will be released after assignment。
alloc 后引用数应该是 1,编译器可能在赋值后加了一句 release,所以赋值给 weak 变量后,obj 会被废弃?(这里赋值后,编译器判断对象没有持有者,会通过插入 release 释放它。具体看 1.4.2 节。)
其实也不用想这么复杂,没有强引用指向它,所以就被废弃了。
__unsafe_unretained 修饰符
__weak 要求 iOS 5 以上,iOS5 之前用 __unsafe_unretained 代替。
__unsafe_unretained 修饰的变量,不属于编译器内存管理对象。
__unsafe_unretained 不能持有对象,对象废弃后不会置 nil,继续访问可能崩溃。
id __unsafe_unretained obj = [[NSObject alloc] init];
上面的代码会警告:Assigning retained object to unsafe_unretained variable; object will be released after assignment。
这里在赋值后,编译器判断对象没有持有者,会通过插入 release 释放它。
__autoreleasing修饰符
__autoreleasing 修饰的变量指向的对象相当于调用 autorelease,会注册到自动释放池。
在 ARC,__autoreleasing 代替 autorelease,@autoreleasepool 代替 NSAutoreleasePool:
// ARC 无效
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
// ARC
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
// 如果使用的是 __strong,alloc 创建的对象不会放入释放池?
}
__autoreleasing 很少显式使用。在 @autoreleasepool 代码块里面,通过 alloc/new/copy/mutableCopy 以外的方法创建的对象,会注册到自动释放池。
@autoreleasepool {
// 不使用 __autoreleasing,也能使对象注册到自动释放池。
// 编译器判断方法名后,自动注册到自动释放池。
id __strong obj = [NSMutableArray array];
}
下面的代码,编译器也会自动注册到释放池:
+ (id)array {
id obj = [[NSMutableArray alloc] init];
return obj;
}
id obj 和 id __strong obj 是一样的,由于 return 使得对象超出作用域会被自动释放,所以编译器会自动将其注册到释放池。
虽然 __weak 是为了解决循环引用,但在访问 __weak 修饰的变量时,其实是访问自动释放池里的对象。
理由是 __weak 修饰的对象会随时废弃,__autoreleasing 确保在池子释放前可以访问该对象。具体后文会解释。代码如下所示:
// 其实是访问释放池里的对象
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
// 和上面的代码段一样的
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [tmp class]);
还有一种非显式使用 __autoreleasing 的情况:
id 或对象的指针,默认是 __autoreleasing 修饰,比如 NSError **perror,和 NSError * __autoreleasing *perror 是一样的。
- (void)test1 {
NSError *error = nil; // &error 的类型是 'NSError *__strong *'
NSError * __strong *p3 = &error; // 编译通过
// p1 的类型是 'NSError *__autoreleasing *'。
// 编译报错,Pointer to non-const type 'NSError *' with no explicit ownership。
NSError **p1 = &error;
// 编译报错,Initializing 'NSError *__autoreleasing *' with an expression
// of type 'NSError *__strong *' changes retain/release properties of pointer
NSError * __autoreleasing *p2 = &error;
// 编译通过,但为何不报错呢,实参是 __strong,形参是 __autoreleasing。
// 编译通过,是因为编译器做了处理:
// NSError __autoreleasing *tmp = error;
// [self testError:&tmp];
// error = tmp;
[self testError:&error];
}
// perror 的类型是 'NSError *__autoreleasing *'
- (void)testError:(NSError **)perror {
*perror = [NSError errorWithDomain:@"" code:0 userInfo:nil];
}
上面 testError 的形参是 __autoreleasing 的,所以能够返回注册到释放池的对象。形参改用 __strong 修饰也能够返回对象,使用 __autoreleasing 是为了遵守内存管理的规则:使用 alloc/new/copy/mutableCopy 创建的对象是自己创建并持有,其他方法创建的是非自己创建并持有的对象,类似 NSMutableArray 的 array 方法。
另外,虽然可以非显式使用 __autoreleasing,如果显式使用的话,对象变量必须是自动变量(包括局部变量、函数参数)。
无论 ARC 是否有效,调试使用非公开函数 _objc_autoreleasePoolPrint() 可以查看注册到释放池上的对象。
1.3.4 规则
- 不能使用 retain/release/retainCount/autorelease
- 不能调用 dealloc
- 必须遵守内存管理的函数命名
- 对象不能作为 C语言结构体的成员
- 使用 __bridge 转换 'id' 和 'void *'
以 alloc/new/copy/mutableCopy 开头的函数返回的对象,必须是调用方持有的,这是 MRC 的规则,ARC 也要遵守。
ARC 还得加一条:init 开头的函数,必须是实例方法,必须返回对象,该对象并不注册到释放池,基本上只是对 alloc 创建的对象进行初始化。像 -(void)initTheObject 这样的命名不要使用,因为没有返回对象。
对象不能作为 C语言结构体的成员,我试了没有报错而且能运行。这条规则有点迷:
struct Data {
NSMutableArray *array;
};
ARC 显式转换 'id' 和 'void *',直接转会报错,应该使用 __bridge 转换:
- (void)testVoid {
id obj = [NSObject new];
// 报错,Implicit conversion of Objective-C pointer type 'id' to C pointer type 'void *' requires a bridged cast。
// Use __bridge to convert directly (no change in ownership)。
// Use CFBridgingRetain call to make an ARC object available as a +1 'void *'。
void *p = obj; // MRC 是可以的,不会报错。
// 使用 __bridge 转换
void *p = (__bridge void *)obj;
id obj2 = (__bridge id)p;
[obj2 description];
}
__bridge 转换还有两种:__bridge_retained、__bridge_transfer。
__bridge_retained 相当于转换后,再调用一次 retain,两者同时持有对象:
// ARC
id obj = [NSObject new];
void *p = (__bridge_retained void *)obj;
ARC 的代码相当于 MRC:
// MRC
id obj = [NSObject new];
void *p = obj;
[(id)p retain];
__bridge_transfer 相当于转换后,再调用一次 retain 和 release,旧变量不再持有对象:
// ARC
id obj = (__bridge_transfer id)p;
ARC 的代码相当于 MRC:
// MRC
id obj = (id)p;
[obj retain];
[(id)p release];
关于 OC 对象和 Core Foundation 对象
cf 对象用于 C语言写的 Core Foundation 框架中,使用引用计数管理,CFRetain 和 CFRelease 可以持有、释放对象。
OC 对象和 CF 对象区别很小,不同之处在于是由哪一种框架生成的。因此转换不需要使用额外的 CPU 资源,被称为免费桥(toll-free bridge)。
以下函数用于 OC 对象和 CF 对象转换:
// 调用 CFBridgingRetain 后需用调用 CFRelease 释放对象。
// After using a CFBridgingRetain on an NSObject,
// the caller must take responsibility for calling CFRelease at an appropriate time.
CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
return (__bridge_retained CFTypeRef)X;
}
// 调用 CFBridgingRelease 后,就不要再调用 CFRelease 了。
id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) {
return (__bridge_transfer id)X;
}
CFBridgingRetain 把 Objective-C pointer 转换为 Core Foundation pointer 并转移内存管理职责,之后要调用 CFRelease 释放对象:
NSString *string = @"Get a string";
CFStringRef cfString = (CFStringRef)CFBridgingRetain(string);
// Use the CF string.
CFRelease(cfString);
CFBridgingRelease 把 Core Foundation-style object 转换为 Objective-C object,并转移内存管理职责给 ARC,之后不用再调用 CFRelease(Moves a non-Objective-C pointer to Objective-C and also transfers ownership to ARC):
CFStringRef cfName = ABRecordCopyValue(person, kABPersonFirstNameProperty);
NSString *name = (NSString *)CFBridgingRelease(cfName);
1.3.5 属性
ARC 有效时:
属性 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained 修饰符 |
copy | __strong 修饰符 |
retain | __strong 修饰符 |
strong | __strong 修饰符 |
unsafe_unretained | __unsafe_unretained 修饰符 |
weak | __weak 修饰符 |
weak、strong、retain 只能修饰对象。assign 也可以修饰对象,和 weak 的区别是不会置 nil。
各个修饰符会在 1.4 节讲解。
1.3.6 数组
这里讲的是 C语言的动态数组:
- (void)testCArray {
// 声明动态数组指针
id __strong *array = nil; // 默认是 __autoreleasing
// NSObject * __strong *array = nil;
// 分配内存
size_t count = 5;
array = (id __strong *)calloc(count, sizeof(id));
// 使用数组
array[0] = [[NSArray alloc] init];
// 释放数组
for (int i = 0; i < count; i++) {
array[i] = nil; // 因为编译器不能确定生命周期
}
free(array); // 注意,free 前要设置为 nil。
// 注意,下面的代码是危险的
// malloc、memcpy 都是危险的,不要使用
// malloc 分配的内存没有初始化为 0
array = (id __strong *)malloc(count * sizeof(id));
for (int i = 0; i < count; i++) {
array[i] = nil; // 可能会释放一个不存在的对象
}
free(array);
}
1.4 ARC 的实现
ARC 是由编译器进行内存管理的。实际上,只有编译器是无法完全胜任的,还需要 OC 的运行时库的协助。
1.4.1 __strong 修饰符
先看 alloc/new/copy/mutableCopy 的方法:
{
id __strong obj = [[NSObject alloc] init];
}
// 编译器的模拟代码
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
obcj_release(obj);
编译器自动插入了 release。
再看不是 alloc/new/copy/mutableCopy 开头的方法:
{
id __strong obj = [NSMutableArray array];
}
// 编译器的模拟代码
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
obcj_release(obj);
编译器插入了 objc_retainAutoreleasedReturnValue 函数,用来持有(retain)对象 obj,对象 obj 是一个返回值并且被注册到释放池。
objc_retainAutoreleasedReturnValue 和 objc_autoreleaseReturnValue 是成对出现的。前者是持有对象,后者可能会把对象注册到自动释放池。
这对函数优化的地方在于,如果调用 objc_autoreleaseReturnValue 后,紧接着又调用了 objc_retainAutoreleasedReturnValue,那么 objc_autoreleaseReturnValue 不会把对象注册到自动释放池,objc_retainAutoreleasedReturnValue 也能正确的获取到对象。如图 1-22 所示。
1.4.2 __weak 修饰符
__weak 修饰的变量,指向的对象废弃后,会被置 nil。
使用 __weak 修饰的变量,即是使用注册到自动释放池的对象。
下面看看它是如何实现的。
- (void)testWeak {
id __strong obj = [[NSObject alloc] init];
{
// 假设 obj 被 __stong 修饰,指向一个对象
id __weak objWeak = obj;
}
// 编译器的模拟代码
id objWeak;
objc_initWeak(&objWeak, obj);
objc_destroyWeak(&objWeak);
}
通过 objc_initWeak 初始化变量,超出范围时通过 objc_destroyWeak 释放。
objc_initWeak 先把变量置 0,然后调用 objc_storeWeak 函数:
objWeak = 0;
objc_storeWeak(&objWeak, obj);
objc_destroyWeak 将 0 作为参数调用 objc_storeWeak 函数:
objc_storeWeak(&objWeak, 0);
也就是 testWeak 函数相当于:
// 编译器的模拟代码
id objWeak;
objWeak = 0;
objc_storeWeak(&objWeak, obj);
objc_storeWeak(&objWeak, 0);
散列表也叫哈希表,通过散列函数,把 key 映射为一个位置来访问记录,存放记录的数组叫做散列表。
weak 表是个散列表,应该是以对象的地址作为 key,根据散列函数得到的位置,保存所有指向该对象的 __weak 变量的地址。
比如 objc_storeWeak 函数把 obj 的地址作为 key,映射得到位置后,把变量 objWeak、objWeak2、objWeak3 的地址都保存到这个位置。
如果 objc_storeWeak 的第二个参数为 0,则把 objWeak 的地址从 weak 表中删除 (传 0 是怎么找到变量地址的???)。
废弃对象的步骤:
- objc_release
- 因为引用计数为 0 所以执行 dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocating
对象被废弃时,调用的 objc_clear_deallocating 的动作如下:
- 以废弃对象的地址为 key,从 weak 表获取记录。
- 将记录中所有 __weak 变量赋值为 nil。
- 从 weak 表中删除该记录。
- 从引用计数表中,删除以废除对象地址为 key 的记录。
以上步骤实现了对象废弃时,__weak 变量赋值为 nil 的功能。
下面讲解 __weak 的另一功能:使用 __weak 修饰的变量,即是使用注册到自动释放池的对象。
{
id __weak objWeak = obj;
NSLog(@"%@", objWeak);
}
// 编译器的模拟代码
id objWeak;
objc_initWeak(&objWeak, obj);
id tmp = objc_loadWeakRetained(&objWeak);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&objWeak);
上面的代码增加了 objc_loadWeakRetained 和 objc_autorelease 的调用,
objc_loadWeakRetained 取出 weak 对象并 retain,
objc_autorelease 将对象注册到自动释放池中。
由此可知,在 @autoreleasepool 块结束前,__weak 修饰的变量指向的对象都可以放心使用。但是,每次使用 weak 变量,都会把它注册到释放池,比如 NSLog 5 次,就会注册 5 次,所以最好先暂时赋值给 __strong 修饰的变量后,再使用它:
id __weak o = obj;
id tmp = o; // 只有这句会把对象 o 注册到自动释放池
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
下面讲解对象的立即释放:
{
id __weak obj = [[NSObject alloc] init];
// NSLog(@"obj = %@", obj); 这里会输出 obj = (null)
}
以上代码,编译器会警告:Assigning retained object to weak variable; object will be released after assignment。
对象赋值给 __weak 变量后,编译器判断它没有持有者,会立即释放和废弃它,然后变量就会被赋值 nil:
// 编译器的模拟代码
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp); // 赋值给 weak 变量
obcj_release(tmp); // 判断没有持有者,释放它
objc_destroyWeak(&obj); // 超出作用域
如果不赋值给变量呢,能调用被立即释放的对象的实力方法吗:
// 加 void 是为了避免编译器警告
(void)[[[NSObject alloc] init] hash];
// 编译器的模拟代码
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_msgSend(tmp, @selector(hash));
obcj_release(tmp);
可见在调用了实例方法后,对象才被释放。看来“由编译器进行内存管理”这句话是正确的。
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_autoreleasePoolPop(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_autoreleasePoolPop(pool);
可见注册到自动释放池的方法都是 objc_autorelease。
1.4.4 引用计数
书上说,_objc_rootRetainCount 可以获取对象的引用计数。但我试了一下,不知道导入哪个头文件才不会报错。可以用另外的方法获取:
id __strong obj = [[NSObject alloc] init];
NSUInteger count = _objc_rootRetainCount(obj);
count = CFGetRetainCount((__bridge CFTypeRef)obj); // CFGetRetainCount
count = [[obj valueForKey:@"retainCount"] intValue]; // KVC
二、 Blocks
block 是带有自动变量(局部变量)的匿名函数。
2.2 block 模式
2.2.1 block 语法
block 型变量可以作为 自动变量、静态变量、全局变量、静态全局变量、函数参数。
- (void)test1 {
// 声明
int (^blk) (int);
// 创建
blk = ^ int (int count) {
return count +1;
};
// 没有返回类型或形参,可以省略
void (^block)(void); // 后面的 void 不能少哦
block = ^ {
NSLog(@"block 2");
};
// 作为参数
[self fun:blk];
// 作为返回值
blk = [self fun2];
}
// 函数参数。名字放在外面。
- (void)fun:(int (^)(int))blk {
blk(3);
}
// 函数返回值。不能有名字。
- (int (^)(int))fun2 {
return ^ int (int count) {
return count +1;
};
}
可以使用 typedef 定义 block:
typedef int (^blc_t)(int);
- (blc_t)fun3 {
// 创建 block
blc_t block = ^int (int count) {
return count + 1;
};
// 也可以使用指针.
// 不加 const 会报错:Pointer to non-const type 'blc_t' (aka 'int (^)(int)') with no explicit ownership
const blc_t *blockPointer = █
return block;
}
2.2.3 截获自动变量值
block 是带有自动变量的匿名函数。带有自动变量值在 block 中表现为截获自动变量值。
- (void)test1 {
int count = 1;
void (^blk)(void) = ^ {
NSLog(@"count = %d", count);
};
count = 2;
blk(); // 输出 count = 1
}
block 捕获了自动变量的值,保存的瞬间值。即使修改了变量的值再执行 block,也没有影响。
2.2.4 __block 说明符
block 不能直接修改自动变量的值,否则会报错:Variable is not assignable (missing __block type specifier)。
用 __block 修饰的变量,block 才可以修改它的值,并且在执行 block 时,拿到的变量的值是最新修改的。
- (void)test__block {
__block int count = 1;
NSMutableArray *array = [NSMutableArray array];
void (^blk)(void) = ^ {
NSLog(@"count = %d", count);
count = 3;
[array addObject:[NSObject new]]; // 不会报错
array = [NSMutableArray array]; // 会报错
};
count = 2;
blk(); // 输出 count = 2
}
2.3 Blocks 的实现
2.3.1 block 的实质
在终端 cd 到文件目录,输入 "clang -rewrite-objc 源代码文件名",可以转换成 cpp 文件。
定义一个继承自 NSObject 的 Test 类,有个 test 方法:
@implementation Test
- (void)test {
void (^blc)(void) = ^ {
printf("哈哈哈");
};
blc();
}
@end
代码转换后,先看 block 语法 ^ { printf("哈哈哈"); }
转换的代码:
// block 的函数,参数是一个 block 指针
static void __Test__test_block_func_0(struct __Test__test_block_impl_0 *__cself) {
printf("哈哈哈");
}
如代码所示,block 匿名函数转换成一个 C语言函数处理。函数名 __Test__test_block_func_0 由类名、所在函数名、在函数出现的顺序和 block_func 拼接成。
函数的参数 __cself 是一个指针,相当于 c++ 的变量 this,或 OC 的变量 self,指向一个 block 的结构体。
函数参数 __cself 的声明:
struct __Test__test_block_impl_0 *__cself
block 转换成结构体 __Test__test_block_impl_0,声明如下:
// 自定义 block 的结构体
struct __Test__test_block_impl_0 {
struct __block_impl impl; // block 的基本定义
struct __Test__test_block_desc_0* Desc; // block 的数据
// 构造函数。
// fp 是 block 的函数的指针,desc 是 block 的数据。
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结构体 _block_impl_0 里面有个构造函数,还有两个结构体变量:
block 的基本定义 __block_impl , block 的数据 _block_desc_0。
先看 __block_impl 的定义:
// block 的基本定义
struct __block_impl {
void *isa; // block 的类
int Flags;
int Reserved;
void *FuncPtr; // 指向 block 的函数
};
__block_impl 和 __Test__test_block_impl_0 有点类似父类和子类,后者会在前者的基础上增加自己的东西。所有自定义的 block 里面都有一个 __block_impl 指针,比如 __Test__test_block_impl_1、__Test__test_block_impl_2。
再看 __Test__test_block_desc_0 的定义:
// block 的数据
static struct __Test__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __Test__test_block_desc_0_DATA = { 0, sizeof(struct __Test__test_block_impl_0)};
// 上面创建的结构体实例 __Test__test_block_desc_0_DATA 会在调用构造函数的时候用到。
其结构为今后版本升级所需的区域和 block 的大小。
总结,自定义一个 block,会生成
一个 block 结构体 _block_impl_0、
一个 block 数据结构体 _block_desc_0、
一个 block 函数 _block_func_0、
所有 block 共用的结构体 __block_impl,
而 _block_impl_0 里面有一个 __block_impl 结构体指针、一个 _block_desc_0 指针、一个构造函数,里面可能还定义有截获的变量。
_block_impl_0 构造函数的实现:
// 构造函数
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
fp 指向 block 要执行的代码所转换的函数 __Test__test_block_func_0,_NSConcreteStackBlock 用于初始化 isa 变量,具体后文会讲解。
Test 类的 test 函数转换后的代码:
static void _I_Test_test(Test * self, SEL _cmd) {
// 创建 block
void (*blc)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA));
// 调用 block
( (void (*)(__block_impl *)) ((__block_impl *)blc) ->FuncPtr) ((__block_impl *)blc);
// 上面的代码去掉类型转换,可以看做下面的代码
{
// 创建 block
struct __Test__test_block_impl_0 tmp = __Test__test_block_impl_0(__Test__test_block_func_0, &__Test__test_block_desc_0_DATA));
struct __Test__test_block_impl_0 *blc = &tmp;
// 调用 block
(*blc->impl.FuncPtr)(*blc);
}
}
先创建一个 __Test__test_block_impl_0 结构体实例 tmp,然后把 tmp 的地址赋给指针 blc。结构体实例 tmp 保存在栈上。
构造函数有两个参数,第一个是 C语言函数指针,是 block 要执行的代码。
第二个是作为静态全局变量初始化的 __Test__test_block_desc_0 结构体实例指针 & __Test__test_block_desc_0_DATA,以下是它的初始化代码:
struct __Test__test_block_desc_0 __Test__test_block_desc_0_DATA = {
0,
sizeof(struct __Test__test_block_impl_0)
};
__Test__test_block_desc_0_DATA 使用 __Test__test_block_impl_0 的大小进行初始化。
构造函数的参数讲完了,看看 block 的初始化过程。假设把 __Test__test_block_impl_0 的 _block_impl 展开:
// 展开 __block_impl
struct __Test__test_block_impl_0 {
void *isa; // block 的类
int Flags;
int Reserved;
void *FuncPtr; // 指向 block 的函数
struct __Test__test_block_desc_0* Desc;
// 构造函数
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, int flags=0) {
// ......
}
};
然后初始化会像下面这样:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __Test__test_block_func_0;
Desc = &__Test__test_block_desc_0_DATA;
接下来看 block 执行部分:
// 原代码
blc();
// 转换后
( (void (*)(__block_impl *)) ((__block_impl *)blc) ->FuncPtr) ((__block_impl *)blc);
// 简化后
(*blc->impl.FuncPtr)(*blc);
blc 其实是个指针,指向 __Test__test_block_impl_0,impl 指向 _block_impl,FuncPtr 指向 __Test__test_block_func_0。
到此已经摸清了 block 的实质,下面解释 block 的 isa 指针。
block 其实是 OC 对象。
打开 objc.h 文件,或者 cmd + shift + o,输入 objc_object 可以找到相关定义。下面的定义都是 OC 1.0 的,2.0 的在这里:https://opensource.apple.com/source/objc4/objc4-750.1/runtime/objc-runtime-new.h.auto.html。
先看对象指针 id 的定义:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
对象结构体 objc_object 的定义:
/// Represents an instance of a class.
struct objc_object {
Class isa;
};
类指针 Class 的定义:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
类结构体 objc_class 的定义:
struct objc_class {
Class isa;
// Objective-C 2.0 已经改了
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char * name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * ivars OBJC2_UNAVAILABLE;
struct objc_method_list * * methodLists OBJC2_UNAVAILABLE;
struct objc_cache * cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
可见在 OC 1.0 中,结构体 objc_object 和 objc_class 是相同的:
指针 | 结构体 | 属性 |
---|---|---|
id | objc_object | Class isa |
Class | objc_class | Class isa |
OC 的对象由类生成,意味着对象结构体实例的 isa 指针指向生成它的类的结构体实例。如下图所示:
各类的结构体就是基于 objc_class 结构体的 class_t 结构体(这是书上原话)。书上说在 objc-runtime-new.h 可以找到 class_t 的定义,但是我没找到。我在转换后的 cpp 文件里面找到 _class_t 的定义。
_class_t 的定义:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
看到这里分不清 objc_object、objc_class、_class_t 的关系,这里只需要知道,block 结构体也有个 isa 指针,block 本质是个 OC 对象。比如 __Test__test_block_impl_0 的 isa = &_NSConcreteStackBlock,_NSConcreteStackBlock 相当于 _class_t 结构体。
举个例子:
@implementation Test
+ (void)load {
[[Test new] test];
}
- (void)test {
// 运行时断点显示为 __NSGlobalBlock__
// 编译的 cpp 文件显示 isa = &_NSConcreteStackBlock
// 原因后文会解释
void (^blc)(void) = ^ {
printf("哈哈哈");
};
blc();
}
@end
控制台断点输出:
(lldb) po [blc class]
__NSGlobalBlock__
(lldb) po [blc superclass]
__NSGlobalBlock
(lldb) po [[blc superclass] superclass]
NSBlock
(lldb) po [[[blc superclass] superclass] superclass]
NSObject
(lldb) po [[[[blc superclass] superclass] superclass] superclass]
nil
可见 block 确实是对象,继承链为 NSGlobalBlock、NSBlock、NSObject。
2.3.2 截获自动变量值
block 通过定义一个相同的变量,来截获自动变量值。
- (void)test {
int count = 2;
void (^blc)(void) = ^ {
printf("哈哈哈 count = %d", count);
};
blc();
}
转换后的代码:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
int count;
__Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
// 。。。
}
};
与之前的代码相比,多了个成员变量 count,而且声明和自动变量是一样的。构造函数也多了个 count 参数。
block 函数:
static void __Test__test_block_func_0(struct __Test__test_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("哈哈哈 count = %d", count);
}
通过指向 block 的指针 __cself 来访问成员变量 count。
2.3.3 __block 说明符
block 通过定义一个相同的变量来截获自动变量值,因此无法在 block 中修改变量的值。
block 通过定义一个指针来截获静态变量的地址,可以访问和修改静态变量的值。
对于全局静态变量和全局变量,不需要指针就能访问和修改它的值,因此 block 不需要额外定义成员变量来截获。
int global_var = 1; // 全局变量
static int static_global_var = 2; // 全局静态变量
- (void)test {
static int static_var = 3; // 局部静态变量
int count = 2;
void (^blc)(void) = ^ {
global_var *= 1;
static_global_var *= 2;
static_var *= 3;
printf("哈哈哈 count = %d", count);
};
blc();
}
转换后的代码:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
int *static_var; // 新增局部静态变量的指针
int count;
};
block 函数:
static void __Test__test_block_func_0(struct __Test__test_block_impl_0 *__cself) {
// 通过指针访问局部静态变量
int *static_var = __cself->static_var;
int count = __cself->count; // bound by copy
global_var *= 1; // 全局变量直接访问
static_global_var *= 2; // 静态全局变量直接访问
(*static_var) *= 3; // 局部静态变量通过指针访问
printf("哈哈哈 count = %d", count);
}
对于自动变量,block 不截获它的指针,因为 block 无法控制自动变量的生命周期,通过指针访问有可能是野指针。
__block
__block 是存储域类说明符,类似于 static、auto 和 register 说明符,用于指定讲变量值设置到哪个存储域中。例如 auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。
用 __block 修饰的自动变量,可以在 block 中修改,是因为自动变量被封装进一个结构体,block 截获了结构体实例指针。
- (void)test {
__block int var = 10;
void (^blc)(void) = ^ {
var *= 10;
};
blc();
}
转换的 block 结构体如下:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
// 新增封装自动变量的结构体的指针
__Block_byref_var_0 *var; // by ref
};
block 新增一个指针,指向封装自动变量的结构体 __Block_byref_var_0。
__Block_byref_var_0 的定义:
struct __Block_byref_var_0 {
void *__isa;
// __forwarding 指向栈上的自己,或指向复制到堆的克隆结构体,后文会解释。
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var; // 这个是自动变量
};
在 test 方法中创建 __block 变量的代码:
// 原代码
__block int var = 10;
// 转换后
__Block_byref_var_0 var = {
0, // __isa
&var, // __forwarding
0, // __flags
sizeof(__Block_byref_var_0), // __size
10 // int var
};
原来创建一个自动变量的代码,变成了创建结构体实例。
block 函数:
static void __Test__test_block_func_0(struct __Test__test_block_impl_0 *__cself) {
__Block_byref_var_0 *var = __cself->var; // bound by ref
(var->__forwarding->var) *= 10;
}
block 函数通过 __cself 获取结构体实例的指针 var,然后通过 var 的 __forwarding 指针获取真正的结构体实例,然后再获取和结构体实例同名的成员变量 var。
__forwarding 指针如下图所示,后文会详细解释:
block 不截获自动变量的指针,是因为无法控制自动变量的生命周期。block 截获结构体实例的指针,为防止野指针,后面会解释 block 如何控制结构体实例的生命周期。(其实就是 block 被复制到堆时,结构体实例也会被复制到堆。)
2.3.4 Block 存储域
block 与 __block 变量的实质:
名称 | 实质 |
---|---|
block | 栈上 block 的结构体实例 |
__block 变量 | 栈上 __block 变量的结构体实例 |
block 有 3 种:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
_NSConcreteStackBlock 类的 block 设置在栈上,
_NSConcreteGlobalBlock 类的 block 设置在数据区域(.data 区),
_NSConcreteMallocBlock 类的 block 设置在由 malloc 分配的内存块(堆)。
全局 block:
void (^globalBlock)(void) = ^(){ printf("global block\n"); };
- (void)test {
globalBlock();
}
此 block 没有使用自动变量,因此不依赖于执行时的状态,所以整个程序中只需一个实例,和全局变量一样设置在数据区域就行。
栈 block:
- (void)test {
// 运行时是 _NSConcreteGlobalBlock
void (^stackBlock)(int) = ^(int count){ printf("stack block\n"); };
stackBlock(99);
}
此 block 不截获任何变量,不依赖于执行状态,只需要一个实例,所以设置在数据区域,也就是说,编译时是 _NSConcreteStackBlock,运行时会是 _NSConcreteGlobalBlock。
截获变量的栈 block:
- (void)test {
int var = 1;
// blc 编译是_NSConcreteStackBlock
// blc 运行时是 _NSConcreteMallocBlock,因为有强引用指向它
void (^blc)(int) = ^(int count) {
printf("var + count = %d", var + count);
};
blc(99);
// 编译是_NSConcreteStackBlock
// 输出 __NSStackBlock,因为没有强引用指向它
NSLog(@"%@", [(^(int count){ printf("var + count = %d", var + count); }) superclass]);
}
对于栈 block:
如果不依赖于执行状态,运行时会是 _NSConcreteGlobalBlock;
如果有强引用指向它,运行时会是 _NSConcreteMallocBlock。
栈上的 block 和 __block 变量,超出作用域就会被废弃。
将它们复制到堆上,超出作用域也可以继续存在。
ARC 有效时,编译器在大多数情形下会自动复制 block 到堆:
- block 作为函数的返回值;
- block 作为 Cocoa 框架带有 usingBlock 的函数的参数时;
- block 作为 GCD 的 API 的参数时;
- block 被强引用指向时;
block 作为函数参数传递时,除了 Cocoa 框架带有 usingBlock 的函数和 GCD 的 API,其他函数是不会被编译器 copy 到栈的,比如下面的情况:
- (NSArray *)getBlockArray {
int var = 10;
// 第一个是 __NSMallocBlock__,后面的是 __NSStackBlock__
NSArray *array = [NSArray arrayWithObjects:
^{NSLog(@"var = %d", var);},
^{NSLog(@"var = %d", var);},
^{NSLog(@"var = %d", var);},
nil];
return array;
}
- (void)test {
// 数组只有第一个没释放,后面的 block 都废弃了
NSArray *array = [self getBlockArray];
void (^blc)(void) = [array firstObject];
blc(); // 第一个可以正常运行,后面的会崩溃
blc = [array lastObject]; // 这里会崩溃,EXC_BAD_ACCESS
blc();
}
打断点可以看到,getBlockArray 函数的数组里 block 的情况。array 的第一个 block 按理应该是 栈 block,可能有强引用数组指针指向它,所以变成了 堆 block,然后后面的 block 都是 栈 block。
问题2341:数组不是会强引用它的元素吗,其他的 block 也应该是 堆 block 才对,求大神指点。
答:数组应该会 retain 它的元素,而 retain 方法对 栈 block 不起任何作用(要改用 copy,后文会解释 ),也就是不会拷贝到堆,所以超出作用域就会废弃。
运行到 test 函数,array 后面的 block 都废弃了,因为它们是 栈 block,已经超出了作用域。只有第一个 堆 block 没有废弃。
问题2342:数组会强引用它的元素,为什么还会被废弃呢?求大神指点。
答:数组会 retain 它的元素,而不是强引用它的元素,具体看问题2341。
对于编译器无法判断的情况,可以手动调用 copy 方法,比如上述代码可以增加调用 copy,array 里的 block 就都是堆 block:
- (NSArray *)getBlockArray {
int var = 10;
// 第一个是 __NSMallocBlock__,后面的是 __NSStackBlock__
NSArray *array = [NSArray arrayWithObjects:
[^{NSLog(@"var = %d", var);} copy],
[^{NSLog(@"var = %d", var);} copy],
[^{NSLog(@"var = %d", var);} copy],
nil];
return array;
}
不同类型的 block,调用 copy 的结果是不同的:
block 的类 | 原来的存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
2.3.5 __block 变量存储域
上一节讲的是 block 存储域。block 复制到堆,__block 变量也会受影响:
__block 变量存储域 | 影响 |
---|---|
栈 | 从栈复制到堆,并被 block 持有 |
堆 | 被 block 持有 |
多个 block 使用同一个 __block 变量时,被复制到堆的 block 会持有变量,并增加变量的引用计数。如果堆上的 block 被废弃,那么它使用的变量也被释放,引用计数 -1。如果变量没有持有者,就会被废弃。
可见 __block 变量和 OC 对象的引用计数式内存管理完全相同。block 复制到堆会持有 __block 变量,如果 block 废弃就会释放 __block 变量。
__block 变量会被复制到堆,所以它的结构体有个 __forwarding 指针,使得不管 __block 变量配置在栈还是堆,都能正确访问该变量(这里可以理解成,访问的都是同一个变量)。
举个栗子:
- (void)test {
__block int var = 0;
int *p1 = &var; // 指向 var 的地址
// __weak 是避免强引用指向 block 导致复制到堆
__weak void (^blk0)(void) = ^{ ++var;};
blk0(); // 没有释放,正常运行
int *p2 = &var; // p2 和 p1 的值是一样的
// block 和 var 复制到堆
void (^blk3)(void) = [^{ ++var;} copy];
int *p3 = &var; // p3 的值和 p1 的不同了
var++;
blk0();
NSLog(@"var = %d", var);
}
++var 用的是堆上的,var-- 用的是栈上的,都可以通过 var.__forwarding->var 来正确访问。复制到堆时,会把堆的地址赋值给栈的 var 的 __forwarding 指针。如图:
2.3.6 截获对象
之前讲过 block 截获自动变量、静态变量、全局变量、__block 变量,下面讲解截获对象。
被 __strong 修饰的变量指向的对象,会被堆 block 持有。
举个栗子:
- (void)test {
__strong void (^blk)(id); // 堆 block,会持有 array,最后输出 count = 3
// __weak void (^blk)(id); // 栈 block, 不持有 array,全部输出 count = 0
{
NSMutableArray *array = [NSMutableArray array];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %li", array.count);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
array 超出作用域就应该被废弃了,可是控制台最后输出 count = 3,说明堆 block 会持有强引用指向的对象。(这个 block 被强引用持有,所以是个 堆 block。)
block 转换的代码如下:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
NSMutableArray *__strong array;
};
block 结构体多了一个 __strong 修饰的成员变量 array。编译器不知道何时废弃 C语言结构体的 strong 变量,但 OC 运行时可以。
为了管理截获的 __strong 修饰的对象,生成了 _block_copy_0 和 _block_dispose_0 函数,结构体 _block_desc_0 也多了两个函数指针成员变量:
// 复制 block 时调用,用于赋值、持有
static void __Test__test_block_copy_0(struct __Test__test_block_impl_0*dst, struct __Test__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
// 废弃 block 时调用,用于释放
static void __Test__test_block_dispose_0(struct __Test__test_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static struct __Test__test_block_desc_0 {
size_t reserved;
size_t Block_size;
// 多了两个函数指针,指向上面的两个函数
void (*copy)(struct __Test__test_block_impl_0*, struct __Test__test_block_impl_0*);
void (*dispose)(struct __Test__test_block_impl_0*);
} __Test__test_block_desc_0_DATA = { 0, sizeof(struct __Test__test_block_impl_0), __Test__test_block_copy_0, __Test__test_block_dispose_0};
__Test__test_block_copy_0 函数在复制 block 到堆时会被调用,
__Test__test_block_dispose_0 函数在堆 block 废弃时被调用。
_Block_object_assign 函数相当于 retain 函数,将栈 block 的变量赋值给堆 block,并让堆 block 持有该变量指向的对象。参数 3 BLOCK_FIELD_IS_OBJECT 是指该变量是个对象。
_Block_object_dispose 函数相当于 release 函数,会释放堆 block 持有的对象。
block 复制到堆的时机:
- 调用 block 的 copy 方法时;
- block 作为函数的返回值时;
- block 赋值给 __strong 修饰的变量时;
- block 作为 Cocoa 框架带有 usingBlock 的函数
或 GCD API 的参数传递时,函数内部会自动复制。
上面的情况都可以归结为:调用 _Block_copy 函数将 block 从栈复制到堆。
在使用 __block 变量时,也会生成类似 __Test__test_block_copy_0 函数和 __Test__test_block_dispose_0 函数:
static void __Test__test_block_copy_0(struct __Test__test_block_impl_0*dst, struct __Test__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->var, (void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __Test__test_block_dispose_0(struct __Test__test_block_impl_0*src) {
_Block_object_dispose((void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);
}
__block 变量和对象的区别是参数类型:
__block 变量 | BLOCK_FIELD_IS_BYREF |
---|---|
对象 | BLOCK_FIELD_IS_OBJECT |
2.3.7 __block 变量和对象
__block 说明符可以修饰任何类型的自动变量,包括对象类型。
- (void)test {
__block id __strong obj = [NSObject new];
void (^blk)(void) = ^ {
NSLog(@"%@", obj);
};
blk();
}
转换的代码:
// __block 变量结构体
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
// 自动变量 obj
__strong id obj;
};
// block 结构体
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
// __block 变量结构体指针
__Block_byref_obj_0 *obj; // by ref
};
// _block_copy_0
static void __Test__test_block_copy_0(struct __Test__test_block_impl_0*dst, struct __Test__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}
// _block_dispose_0
static void __Test__test_block_dispose_0(struct __Test__test_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}
在 block 使用附有 __strong 修饰的对象类型的自动变量的情况下,当 block 从栈复制到堆时,使用 _Block_object_assign 函数持有 Block 截获的对象。当堆上的 Block 被废弃时,使用 _Block_object_dispose 函数释放 Block 截获的对象。
由此可知,只要堆 block 存在,那么变量和对象就会持续处于被持有的状态。
__block 修饰的对象类型的变量和 __strong 修饰的情况类似。
使用 __weak 修饰,或同时使用 __weak 和 __block 修饰,堆 block 不会持有对象。
总结:
堆 block 会持有 __block 和 __strong。
block 复制到堆的时机:
调用 block 的 copy 方法时;
block 作为函数的返回值时;
block 赋值给 __strong 修饰的变量时;
block 作为 Cocoa 框架带有 usingBlock 的函数
或 GCD API 的参数传递时,函数内部会自动复制。
2.3.8 Block 循环引用
block 使用对象或使用对象的属性,都可能持有对象。
@interface Test()
@property (nonatomic, copy) NSString *myName;
@property (nonatomic, copy) void(^blc)(void);
@end
@implementation Test
- (void)test {
Test __weak *weakObj;
{
Test *obj = [Test new];
weakObj = obj;
obj.myName = @"name";
obj.blc = ^{
// Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior.
NSLog(@"%@", _myName); // 不会循环引用
// Capturing 'obj' strongly in this block is likely to lead to a retain cycle.
// NSLog(@"%@", obj.myName); // 会循环引用
// NSLog(@"%@", weakObj.myName); // 不会循环引用
};
}
NSLog(@"weakObj = %@", weakObj);
}
使用 obj.myName 会循环引用,编译器会警告 "lead to a retain cycle"。因为 obj 被 __strong 修饰,block 也被复制到堆,所以 block 会持有 obj,而 obj 也持有 block,造成循环引用。
使用 weakObj.myName 不会循环引用,没有警告。因为堆 block 不会持有 __weak 修饰的变量指向的对象。
使用 _myName 也不会循环引用,编译器只是警告 "implicitly retains self"。按照以前,使用 _myName 也会循环引用才对,书上也是这么说的。可能是 9012 年,苹果优化了吧,这里并不会产生循环引用。
看使用 _myName 的代码转换后,block 确实声明了一个 strong 变量:
struct __Test__test_block_impl_0 {
struct __block_impl impl;
struct __Test__test_block_desc_0* Desc;
Test *const __strong self;
};
求大神指点,使用 _myName 为什么不会循环引用。
为避免循环引用,可以使用 __weak 和 __block 修饰符。
使用 __block 避免循环引用:
- (void)test {
Test __weak *weakObj;
{
Test *obj = [Test new];
weakObj = obj;
__block Test *tmp = obj;
obj.myName = @"name";
obj.blc = ^{
NSLog(@"%@", tmp.myName);
tmp = nil; // 赋值为 nil 才能解决循环引用
};
obj.blc(); // 如果不执行,会导致循环引用
}
NSLog(@"weakObj = %@", weakObj);
}
self 持有 block,block 持有 tmp,tmp 持有 self,形成循环引用。
tmp = nil 后,就只剩 self 持有 block,block 持有 tmp,破坏循环引用。
使用 __block 修饰 tmp,就是为了可以在 block 中修改 tmp = nil。
缺点是,block 一定要执行,否则 tmp 始终持有 self,就会造成内存泄露。
以上说的是 ARC 有效的情况。
在 ARC 无效时,通过使用 __block 来避免循环引用。
在 ARC 无效时,block 从栈复制到堆,不会 retain 有 __block 修饰的对象类型的自动变量。没有 __block 修饰就会被 retain。
2.3.9 copy/release
ARC 无效时:
- block 通过 release 方法释放;
- 堆 block 可以通过 retain 方法持有;
- block 还可以通过 copy 方法持有;
- copy 可以拷贝 block 到堆,并且持有。
注意啦,retain 方法对 栈 block 不起任何作用。通过 retain 只能持有 堆 block。
所以 ARC 无效的情况下,对于 block,推荐使用 copy 持有,而不是 retain。
可以使用 Block_copy、Block_release 函数代替 copy、release 方法。
使用方法以及引用计数的思考方式和 OC 的对象相同。
void (^heapBlock) (void) = Block_copy(stackBlock);
Block_release(heapBlock);
Block_copy、Block_release 其实就是之前出现过的 _Block_copy、_Block_release 方法,即 OC 运行时为 C语言准备的方法。
第三章 Grand Central Dispatch
GCD 的内容不展开讲了,可以看我的另一篇文章:《GCD》。
里面讲了 GCD 的 API,有大量例子。
第三章只记录要点。
3.2 GCD的API
Dispatch Queue 按照追加的顺序(先进先出)执行处理。
种类 | 说明 |
---|---|
Serial dispatch queue | 等待现在执行中处理结束 |
Concurrent dispatch queue | 不等待现在执行中处理结束 |
串行队列要等当前任务完成,才会开始下一个。
并行队列只要开启了前面的任务,不需要等待完成,就可以开始下一个任务。
执行的任务数量和线程数量,由系统决定。并行队列会派发任务到多个线程执行。
通过 dispatch_queue_create 创建队列。
在 ARC 有效时,不需要手动管理内存。
在 MRC 通过 dispatch_retain 和 dispatch_release 管理内存,对 main dispatch queue 和 global dispatch queue 没有作用。
追加到队列的 block 会通过 dispatch_retain 函数持有队列,执行结束通过 dispatch_release 释放队列。
3.2.4 dispatch_set_target_queue
使用 dispatch_set_target_queue 可以设置自己创建的队列的优先级:
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);
修改 object 的优先级与 queue 相同。queue 可以是 dispatch_get_global_queue 获取的全局队列。
假设有多个要串行执行的任务,派发到多个并行执行的串行队列执行,通过 dispatch_set_target_queue 把多个串行队列的目标队列,设置为同一个串行队列,就可以让这些任务串行执行。
如果只想获得某个优先级的队列,可以通过 dispatch_queue_attr_make_with_qos_class 创建队列属性,在创建队列时使用。
3.2.5 dispatch_after
dispatch_after 用于延迟派发 block,派发不等于执行。比如派发 block 到队列是 3 秒后,开始执行 block 的时间可能大于 3 秒。
参数 dispatch_time_t 通过 dispatch_time 函数或 dispatch_walltime 函数创建,前者是相对时间,后者是绝对时间。相对时间是指系统休眠时,计时会暂停,绝对时间就不会暂停,到点就触发。参数以纳秒为单位,比如 3 秒可以写为 3 * NSEC_PER_SEC。当前时间是 DISPATCH_TIME_NOW。
// 3 秒后
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
3.2.6 Dispatch Group
Dispatch Group 可以在所有并行任务执行完后,追加执行一个任务,通过 dispatch_group_notify 追加。
另外,也可以不使用 dispatch_group_notify 追加,可以通过 dispatch_wait 来阻塞当前线程,可以设定超时的时间,不可以取消等待。所有任务完成或超时,dispatch_wait 函数就会返回。
dispatch_group_async 可以异步派发任务。
对于异步函数,可以在函数外部调用 dispatch_group_enter,在函数的回调里调用 dispatch_group_leave。比如网络库 af 的异步请求。
- (void)testDispatchGroup {
// group 会在所有任务完成后释放
dispatch_group_t group = dispatch_group_create();
// queue 会在所有 block 完成后释放
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
// 任务数是指 dispatch_group_t 内部的一个 count 属性。
// 任务数 +1
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"group async 完成");
// block 执行后,任务数 -1
});
dispatch_group_enter(group); // 任务数 +1
// 异步下载图片
[self downloadImageWithComplete:^(UIImage *image) {
NSLog(@"group downloadImage 完成");
dispatch_group_leave(group); // 任务数 -1
}];
// 所有任务完成,即任务数 = 0 时,会提交 block 到 queue。
dispatch_group_notify(group, queue, ^{
NSLog(@"group notify");
});
// 控制台输出
// 2019-06-13 21:06:58.911399+0800 group downloadImage 完成
// 2019-06-13 21:06:59.90973+0800 group async 完成
// 2019-06-13 21:06:59.910374+0800 group notify
}
- (void)downloadImageWithComplete:(void(^)(UIImage *image))block {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
block(nil);
});
}
3.2.7 dispatch_barrier_async
dispatch_barrier_async 可以派发阻塞任务,当阻塞任务前面的任务都完成后,它才会执行,并且等它执行结束,它后面的任务才能开始执行。
注意 dispatch_barrier_async 只能用于自定义的全局队列,不要用于串行队列,或系统创建的全局队列、主队列,否则会当 dispatch_async 处理。(书上没写,源自 API 的注释。)
3.2.8 dispatch_sync
同步派发,会阻塞当前线程,直到派发任务完成。
下面的代码在主线程执行会死锁:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{NSLog(@"Hello?");});
串行队列可能造成死锁,比如在一个任务 A 里面,给自己同步派发一个任务 B。因为是同步派发,所以会阻塞任务 A 的执行,等待任务 B 完成。但串行队列只能开始一个任务,所以任务 B 也在等待任务 A 的完成,于是造成了死锁。
3.2.9 dispatch_apply
同步并发迭代。阻塞当前线程,将 block 分配到多个线程,总共并发执行指定的次数。
并发执行 5 次:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%zu: %@, index, array[index]);
));
NSLog(@"done");
输出为:
4
1
0
2
3
done
3.2.10 dispatch_suspend/dispatch_resume
暂停执行:
dispatch_suspend(queue);
恢复执行:
dispatch_resume(queue);
3.2.11 Dispatch Semaphore
信号量可以控制线程的最大并发数,比如控制最大并发数为 1,就是串行了。
AFNetworking 的代码:
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
终于写完了,欢迎指正错误。