Block存储

Block存储区域

在之前我们看到了block的isa指向的为_NSConcreteStackBlock 其实 除此之外还还有其他两种:

  • _NSConcreteStackBlock:该类对象被设置到栈上
  • _NSConcreteGlobalBlock:与全局变量类似,被设置在城区的数据区域.data区
  • _NSConcreteMallocBlock:设置在由malloc函数分配的内存块(堆)中

block为_NSConcreteGlobalBlock的情况:

  1. 记述全局变量的地方有Block语法时
    block写在全局变量区,此时转换源代码可以看到impl.isa=&_NSConcreteGloabalBlock。因为在全局变量区中不能使用自动变量,所以不存在对自动变量的截获,因此整个程序只需要一个实例即可,故将Block用结构体实例设置在全局变量相同的数据区

  2. Block语法的表示式中不使用截获的自动变量时

    typedef int(^blk_t)(int)
    for(int rate=0; rate<10; ++rate) {
    blk_t blk = ^(int count){
    return count
    }
    }

    即使在函数内部,不在全局变量区域使用Block语法,只要Block不截获自动变量,就可以将Block用结构体实例设置在数据区域

Block在超出变量作用域还能存在的原因

对于配置在全局数据区的block 超出变量作用区可以安全使用。
设置在栈上的block和__block变量,是在栈上,当作用域结束,会被废弃,可以通过将block__block变量复制到堆上解决此问题

当将block复制到堆上后,其isa=&_NSConceteMallocBlock

在ARC情况下,编译器大多会自动判断,生成将block从栈到堆上的代码

typedef int(^blk_t)(int)
blk_t func(int rate) {
    return ^(int count) {
        return rate*count
    }
}

//转换为源代码
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) //等效于 tmp = Block_Copy(tmp)
    return objc_autoreleaseReturnValue(tmp)
}

tmp = objc_retainBlock(tmp)其实就是执行了Block_Copy函数

虽然大多时候编译器会自动判断,但是特殊情况,我们需要手动copy,将其从栈复制到堆上

  1. 当向方法或者函数的参数中传递Block时,需要手动copy
    但是如果在方法或者函数中对复制了传递过来的参数,就不需要在调用函数之前手动复制了
    以下为不需要手动复制情况:

    • Cocoa框架中的方法,并且方法名字中含有usingBlock
    • GCD的API中
    //例如
    - (id)getBlockArray {
    int val = 10
    return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d", val);} copy], nil];
    }
    //作为参数传递的block需要copy 否则block在出了该函数作用区域就被废弃

不论block'在何处,对其执行copy方法都不会有任何问题

  • 对栈上的block copy 将其从栈上复制到堆上
  • 对数据区的block copy 什么也不做
  • 对堆上的block copy 引用计数增加

注意

对block多次调用 copy 也是不会有任何问题的 [[[[blk copy] copy] copy] copy]

__block变量存储区域

当Block从栈复制到堆上时,其使用的__block变量也会复制到堆上,此时Block会持有__block变量

当多个block使用__block变量时,当第一个block从栈复制到堆上时,__block变量就复制到堆上,并被改block持有,而其他block再从栈复制到堆上时,只是对该__block的引用计数增加了而已

注意

在栈上的block只是对__block变量使用 而并不会持有

block使用forwarding原因

这样设计的主要原因是: 为了不论__block变量是在栈还是堆上都能够正确访问

因为在将__block从栈复制到堆上之后,我们就有两个可访问:栈上的__block变量和堆上的__block变量
此时访问该变量的方法均为++(val->__forwarding->val) 将变量从栈复制到堆上时,就将成员变量__forwarding的值替换为了指向了堆上的__block变量结构体实例的地址

在将block从栈上复制到堆上后,此时会同时存在栈上和堆上的Block和__block变量,栈上的block保持不变,而堆上的block的isa设置为&_NSConcreteMallocBlock。栈上的__block变量的结构体指针__forwarding会被修改指向堆上的__block变量,而堆上的__block变量的__forwarding指针不变, 依然指向堆上的__block变量

截获对象

当block内部使用的是 id类型或者对象类型变量时,

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

此时array在变量作用域结束或 依旧存在

注意

必须在block后执行copy方法,因为只有调用了_Block_Copy方法才能持有截获的__strong类型对象。否则不调用_Block_Copy函数的话,即使截获了对象,也会随着变量作用域的结束而被废弃

//转换源码
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    id __strong array; //id __strong修饰符的成员变量
    //结构体初始化函数
    __main_block_impl_0(void *fp, strct __main_block_desc_0, id __strong _array, int flags=0): array(_array) {
        impl.isa = & NSConcreatStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
}

可以看到 array被截获成为block结构体的成员变量

内存管理中说C语言的结构体中不能含有附有__strong修饰符的成员变量。因为编译器不知道何时能进行C语言结构体的初始化和废弃操作,不能很好的管理内存。但是Object-C的运行时 能帮助准确掌握Block从栈复制到堆上以及堆上的block被废弃的时机,因此在Block的结构体中即使有__strong或者__weak的修饰符的变量,也可以恰当的进行初始化和废弃。为此需要在_main_block_des_0中增加成员变量copy和dispose,以及作为指针赋值给该成员变量的_main_block_copy_0_main_block_dispose_0函数

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct _main_block_impl_0 *src) {
    _Block_object_assign(&det->array, src->array, BLOCK_FIFLD_IS_OBJECT)
}

static void __main_block_dispose_0(struct _main_block_impl_0 *src) {
    _Block_object_dispose(src->array, BLOCK_FIFLD_IS_OBJECT)
}

static struct __main_block_desc_0 {
    unsigned long reserved; 
    unsigned long Block_size; 
    void (*copy)(struct __main_block_impl_0 *)
    void (*dispose)(struct __main_block_impl_0 *)
}

使用_Block_object_assign将对象类型赋值给block结构体的成员变量并持有该对象。相当于调用了retain方法
使用_Block_object_dispose来释放在block中的结构体变量中的成员变量,相当于release实例方法
当block复制到堆上时调用copy函数,当堆上的block被废弃时调用dispose函数

Block复制到堆上的时机?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型的成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时

根据block存储区域的学习可知,其实就是当__Block_copy方法被调用时,Block从栈复制到堆上
当释放block谁都不持有时,调用dispose函数,相当于调用dealloc实例方法

__block <==> 对象

我们在使用__block变量时 与 截获对象类型相似

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct _main_block_impl_0 *src) {
    _Block_object_assign(&det->val, src->val, BLOCK_FIFLD_IS_BYREF)
}

static void __main_block_dispose_0(struct _main_block_impl_0 *src) {
    _Block_object_dispose(src->val, BLOCK_FIFLD_IS_BYREF)
}

通过BLOCK_FIFLD_IS_BYREF/BLOCK_FIFLD_IS_OBJECT区分是__block变量还是对象类型。
与对象相同 copy函数持有__block变量 dispose释放持有的__block变量,因为都被block持有,因此与block类似可以在超出其变量作用域而存在

__block对象变量

__block id obj = [[NSObject alloc] init]


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

源代码转换如下:

struct __Block_byref_obj_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    void (* __Block_byref_id_object_copy)(void *, void*)
    void (* __Object_object_dispose)(void *)
    __strong id obj;
}

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

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

__block声明

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

与之前Block截获__strong或者id类型的过程相同,当__block变量从栈复制到堆上的时候,使用_Object_object_assign函数,持有赋值给__block变量的对象,当__block变量被废弃时,使用_Object_object_dispose函数,释放赋值给__block变量的对象。因此,只要__block变量在堆上继续存在,那么该变量就会处于被持有状态

对于__weak修饰符,即使附加了__block修饰符 当变量被释放时,nil也会被附加到__weak修饰符的变量中

注意

因为不存在__autorelasing修饰符和__block修饰符同时存在的必要,因此 同时指定会引起编译错误

Block循环引用

如果在block中使用__strong类型修饰符的变量,因为会随着从栈复制到堆上,该对象被block持有而引起循环引用

typedef blk_t blk;
@interface MyObject: NSObject 
{
    blk_t blk_;
    id obj_;
}
@end

@implementation MyObject 
- (id)init {
    self = [super init]
    blk_ = ^{
        NSLog(@"%@", obj_);
    };
    return self;
}
@end

实际上
blk_=^{NSLog(@"obj_=%@", self->obj_);}block内部捕获了self

对于该实例 将不会dealloc,因为 blk_是该实例成员成员变量,而且blk_因为赋值给成员变量也会从栈复制到堆,blk_持有其附有的__strong修饰符的id类型变量self

使用__weak避免循环引用

- (id)init {
    self = [super init]
    id __weak obj = obj_;
    blk_ = ^{
        NSLog(@"%@", self);
    };
    return self;
}

注意

因为此时此时Block存在 持有其值的对象必定存在 因此也不需要进行判断是否为nil

使用__block避免循环引用

- (id)init {
    self = [super init]
    __block id tmp = self
    blk_ = {
        NSLog(@"%@", tmp)
        tmp = nil;
    };
}

...

int main() {
    id o = [[NSObject alloc] init]
    [o execBlock];
}

此时代码并没有引起循环引用,但是如果不调用execBlock即执行成员变量blk_就会循环引用,造成内存泄漏

当执行了该block后,__Block变量不再持有该对象,而block虽然持有__block变量但是也就不再造成循环引用了

注意

使用__block避免循环引用必须执行block

copy/release