美文网首页IOS
笔记-《Objective-C高级编程 iOS与OS X多线程和

笔记-《Objective-C高级编程 iOS与OS X多线程和

作者: 阿斯兰iOS | 来源:发表于2019-07-19 09:55 被阅读48次

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

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

    相关文章

      网友评论

        本文标题:笔记-《Objective-C高级编程 iOS与OS X多线程和

        本文链接:https://www.haomeiwen.com/subject/zlxoqctx.html