笔记-《Objective-C高级编程 iOS与OS X多线程和内存管理》

第一章自动引用计数,第二章 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:

  1. 调用 alloc、new、copy、mutableCopy 等开头驼峰命名的方法创建的对象,用完后要调用 release;
  2. 调用非 alloc/new/copy/mutableCopy 方法群获得的对象,使用前先 retain,用完后调用 release;
  3. 实现非 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-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 是怎么找到变量地址的???)。

废弃对象的步骤:

  1. objc_release
  2. 因为引用计数为 0 所以执行 dealloc
  3. _objc_rootDealloc
  4. object_dispose
  5. objc_destructInstance
  6. objc_clear_deallocating

对象被废弃时,调用的 objc_clear_deallocating 的动作如下:

  1. 以废弃对象的地址为 key,从 weak 表获取记录。
  2. 将记录中所有 __weak 变量赋值为 nil。
  3. 从 weak 表中删除该记录。
  4. 从引用计数表中,删除以废除对象地址为 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 = &block;
    
    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 指针指向生成它的类的结构体实例。如下图所示:

OC 类与对象的实质

各类的结构体就是基于 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 如何控制结构体实例的生命周期。(其实就是 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 的类和存储域

全局 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 变量,超出作用域就会被废弃。
将它们复制到堆上,超出作用域也可以继续存在。

复制到堆的 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 的废弃和 __block 变量的释放

可见 __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 指针。如图:

__block 变量的 __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;
}


终于写完了,欢迎指正错误。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容