Objc高级编程之Block-读书笔记

Block存储域

Block也是Objc对象,其类型有以下几种:

  • _NSConcreteStatckBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

目前ARC已经不再创建_NSConcreteStatckBlock类型的Block对象,进一步确保了block在使用时候的有效性,不过本书的内容我还是如实记录。_NSConcreteStatckBlock类的对象存储在栈(Stack)上,_NSConcreteGlobalBlock类的对象存储在数据区(.data)上,_NSConcreteMallocBlock类的对象存储在堆(Heap)上。

因为_NSConcreteGlobalBlock类型的block内部不依赖执行时的状态,所以能够用结构体存储在与全局变量相同的数据区域中。在整个程序周期内都可以安全地使用。但是在栈上_NSConcreteStatckBlock类的block,如果其所在作用域结束后就会被销毁,__block变量也一样。

Apple提供了将block和__block变量从栈复制到堆的方法解决了这个问题,这样即使block的作用域结束,我们还是可以继续安全地访问堆上的block。

通过block结构体成员变量__forwarding的帮助,__block变量无论在栈上还是在堆上都能够被正确地访问(后面详细说明)。

typedef int (^blk_t)(int);
blk_t func(int rate) {
    return ^(int count) {return rate * count;};
}
//等价于
typedef int (^blk_t)(int);
blk_t func(int rate) {
    blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp);
}
//而objc_retainBlock实际上就是_Block_copy

大多数情况下编译器会适当地判断block的内存管理,不过少数情况还是需要开发者手动管理。比如向方法的参数传递block时,编译器就不能判断是否对block进行copy。这主要以来方法内部的实现是否自己对作为参数的block进行copy,对于copy过的方法,使用时就不用再对block手动copy。相反,就需要考虑手动复制(当然,目前已经没有_NSConcreteStatckBlock类的block存在也不存在这样的问题了)。

- (id)getBlockArray {
    int val = 10;
    return @[[^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy]];
}

不管block之前存储在哪,用copy复制都不会引起任何问题,如果不确定时最好copy。

__block变量存储域

当block从栈copy到堆时,其使用的所有__block变量也全部从栈复制到堆。如果block已经在堆上,对block再次copy也不会对__block变量有任何影响。

多个block使用同一个__block变量时,任何一个block先从栈copy到堆后,__block变量也被copy到堆上并被持有,当其他block再copy到堆上时,仅仅增加__block变量的引用计数。

block从堆中被销毁时,其使用的__block变量也会被销毁。block的内存管理和objc对象的内存管理完全相同。

不管__block变量存储在栈上还是堆上,都能够正确地访问对应的变量。所有栈中的__block变量,将其架构体内部的forwarding值从自己替换为堆上的__block变量,这样无论栈上__block还是堆上__block都是forwarding指向同一个堆上结构体。

截获对象

blk_t blk;
{
   id array = [[]NSMutableArray alloc] init];
   blk = [^(id obj) {
       [array addObject:obj];
       NSLog(@"array count = %ld", [array count]);
   } copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

作用域结束后block调用正常,意味着变量array其超出作用域而存在并没有被销毁。

//编译器模拟代码
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    void (*dispose)(struct __main_block_impl_0*);
}__main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    id __strong array;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
    id __strong array = __cself->array;
    [array addObject:obj];
    NSLog(@"array count = %ld", [array count]);
}

blk_t blk;
{
    id __strong array = [[NSMutableArray alloc] init];
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
    blk = [blk copy];
}
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);

作用域的局部自动变量array赋值给了block结构体中的成员变量,按照之前的描述,C语言结构体不能含有strong修饰符变量,因为编译器不知道应该何时对C语言结构体进行初始化和销毁从而管理内存,但是objc的runtime能够准确把握block从栈copy到堆以及从堆中销毁的时机,因此能够自动管理block结构体的初始化和销毁时机。

具体表现如下:

  • 调用block的copy方法时
  • block作为方法的返回值返回时
  • 将block赋值给类的__strong修饰符id类型成员变量
  • 方法名中含有usingBlock的Cocoa框架方法和GCD方法中传递block作为参数时

实现该功能需要在block描述结构体(XX_block_desc_X)中添加copy和dispose方法。在main_block_copy_0方法里,**_Block_object_assign相当于retain,持有对象并赋值给block实现结构体(XX_block_impl_X)中的成员变量。在main_block_dispose_0方法里,_Block_object_dispose**相当于release,释放赋值在block实现结构体中的成员变量。其中,参数BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF用语区分对象类型和__block类型。

__block变量和对象

__block id obj = [[NSObject alloc] init];

//编译器模拟代码
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*);
    __strong id obj;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

__Block_byref_obj_0 obj = {
    0,
    &obj,
    0x2000000,
    sizeof(__Block_byref_obj_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    [[NSObject alloc] init]
};

由此看出,__block变量虽然是个结构体,但实际上也是一个objc对象,并有持有和释放方法,其中使用_Block_object_assign持有__block对象,使用_Block_object_dispose释放__block对象。__block对象可以为__strong、__weak和__unsafe_unretained,不过使用__unsafe_unretained时注意出现野指针访问无效对象的情况,而__autoreleasing与__block编译时不通过。

copy/release

由于block时C语言的扩展,所以在C语言中也可以使用block语法。此时,使用Block_copy和Block_release代替objc的copy和release方法即可。另外,ARC无效时,__block修饰符被用来避免block中的循环引用。因为block从栈copy到堆时,__block成员变量如果是对象类型,则block不会retain它,反之则retain它。这样来避免block的循环引用。由于ARC和MRC时,__block修饰符的用途有很大区别,因为在开发时,必须要知道编译环境是ARC还是MRC,这一点需要特别注意。


Objc高级编程之Block-读书笔记
https://hllovesgithub.github.io/2015/12/12/2015-12-12-Objc高级编程之Block-读书笔记/
作者
Hu Liang
发布于
2015年12月12日
许可协议