深入Objective-C block 捕获变量

block.c:

struct Block_layout {
    void *isa;//指向所属类的指针,也就是block的类型 NSStackBlock NSGlobalBlock  NSMallocBlock   
    int flags;//标志变量,在实现block的内部操作时会用到
    int reserved;//保留变量
    void (*invoke)(void *, ...);//执行时调用的函数指针,block内部的执行代码都在这个函数中
    struct Block_descriptor *descriptor;//block的详细描述,包含 copy/dispose 函数,处理block引用外部变量时使用
    /* Imported variables. */
//variables: block范围外的变量,如果block没有调用任何外部变量,该变量就不存在
};

struct Block_descriptor {
    unsigned long int reserved;//保留变量
    unsigned long int size;//block的内存大小
    void (*copy)(void *dst, void *src);// 拷贝block中被 __block 修饰的外部变量
    void (*dispose)(void *);//和 copy 方法配置应用,用来释放资源
};

block类型

NSStackBlock 存储于栈区
NSGlobalBlock 存储于程序数据区
NSMallocBlock 存储于堆区

NSGlobalBlock

block 内部没有引用外部变量的 Block 类型都是 NSGlobalBlock 类型,存储于全局数据区,由系统管理其内存,retain、copy、release操作都无效。引用static也为globalBlock

NSStackBlock

block 内部引用外部变量,retain、release 操作无效,存储于栈区,变量作用域结束时,其被系统自动释放销毁。MRC 环境下,[[mutableAarry addObject: blockA],(在arc中不用担心此问题,因为arc中会默认将实例化的block拷贝到堆上)在其所在作用域结束也就是函数出栈后,从mutableAarry中取到的blockA已经被回收,变成了野指针。正确的做法是先将blockA copy到堆上,然后加入数组。支持copy,copy之后生成新的NSMallocBlock类型对象。

NSMallocBlock

如上例中的_block,[blockA copy]操作后变量类型变为 NSMallocBlock,支持retain、release,虽然 retainCount 始终是 1,但内存管理器中仍然会增加、减少计数,当引用计数为零的时候释放(可多次retain、release 操作验证)。copy之后不会生成新的对象,只是增加了一次引用,类似retain,尽量不要对Block使用retain操作。

在ARC环境下,Block也是存在__NSStackBlock的时候的,平时见到最多的是_NSMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 NSStackBlock 类型的 block 转换为 NSMallocBlock 类型

clang前后

原代码

-(void)blockDemo{
    void (^block)(void) = ^{   
};
}

clang后:

struct __block_impl {  
void *isa;//指向所属类的指针,也就是block的类型
int Flags;  //标志变量,在实现block的内部操作时会用到
int Reserved;  //保留变量
void *FuncPtr;//block执行时调用的函数指针
};
//block内部实现 func0
static void __ViewController__blockDemo_block_func_0(struct __ViewController__blockDemo_block_impl_0 *__cself) {
 }

static struct __ViewController__blockDemo_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}
 __ViewController__blockDemo_block_desc_0_DATA = { 0, sizeof(struct __ViewController__blockDemo_block_impl_0)};


static void _I_ViewController_blockDemo(ViewController * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__ViewController__blockDemo_block_impl_0((void *)__ViewController__blockDemo_block_func_0, &__ViewController__blockDemo_block_desc_0_DATA));
}

捕获变量 __block

-(void)blockDemo{
    
    __block int a = 100;
    void (^block)(void) = ^{
        a++;
    };
    
    block();
}

clang后

struct __Block_byref_a_0 {
  void *__isa; //指向所属类的指针,被初始化为 (void*)0
__Block_byref_a_0 *__forwarding;//指向对象在堆中的拷贝
 int __flags;//标志变量,在实现block的内部操作时会用到
 int __size;//对象的内存大小
 int a;//原始类型的变量
};
static void __ViewController__blockDemo_block_func_0(struct __ViewController__blockDemo_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref


        (a->__forwarding->a)++;

    }

可以看到 多了一个结构体 被__block修饰的变量被封装成了一个对象,类型为__Block_byref_a_0,然后把&a作为参数传给了block。
其中,isa、__flags 和 __size 的含义和之前类似,而 __forwarding 是用来指向对象在堆中的拷贝,runtime.c 里有源码说明:

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    ...
    struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
    copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
    // 堆中拷贝的forwarding指向它自己
    copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
    // 栈中的forwarding指向堆中的拷贝
    src->forwarding = copy;  // patch stack to point to heap copy
    copy->size = src->size;
    ...
}

这样做是为了保证在 block内 或 block 变量后面对变量a的访问,都是直接访问堆内的对象,而不上栈上的变量。同时,在 block 拷贝到堆内时,它所捕获的由 __block 修饰的局部基本类型也会被拷贝到堆内(拷贝的是封装后的对象),从而会有 copy 和 dispose处理函数。

block 用copy修饰

首先, block是一个对象, 所以block理论上是可以retain/release的. 但是block在创建的时候它的内存是默认是分配在栈(stack)上, 而不是堆(heap)上的. 所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当你在该作用域外调用该block时, 程序就会崩溃.
1.一般情况下你不需要自行调用copy或者retain一个block. 只有当你需要在block定义域以外的地方使用时才需要copy. Copy将block从内存栈区移到堆区.
2.其实block使用copy是MRC留下来的也算是一个传统吧, 在MRC下, 如上述, 在方法中的block创建在栈区, 使用copy就能把他放到堆区, 这样在作用域外调用该block程序就不会崩溃.
3.但在ARC下, 使用copy与strong其实都一样, 因为block的retain就是用copy来实现的.

你可能感兴趣的:(深入Objective-C block 捕获变量)