Block原理,为什么block能捕获变量 -- 原理篇

主要参考了这些文章 , 有删改 : 

深入研究Block捕获外部变量和__block实现原理 - 简书

iOS底层原理总结 - 探寻block的本质(一) - 简书  

iOS底层原理总结 - 探寻block的本质(二) - 简书                      

Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。block本质上也是一个oc对象或者说是一个结构体,内部也有一个isa指针。block是封装了函数调用(函数指针)以及函数调用环境(捕获到的参数)的OC对象。

Block在OC中的实现如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

从结构图中很容易看到isa,所以OC处理Block是按照对象来处理的。

block的类型

在iOS中,isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种. 

我们通过代码用class方法或者isa指针查看具体类型。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
        void (^block)(void) = ^{
            NSLog(@"Hello");
        };
        
        NSLog(@"%@", [block class]);
        NSLog(@"%@", [[block class] superclass]);
        NSLog(@"%@", [[[block class] superclass] superclass]);
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

以上介绍是Block的简要实现,接下来我们来仔细研究一下Block的捕获外部变量的特性以及__block的实现原理。

研究工具:clang
为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。该命令是

clang -rewrite-objc main.m

目录

  • 1.Block捕获外部变量实质

  • 2.Block的copy和release

一.Block捕获外部变量实质

拿起我们的Block一起来捕捉外部变量吧。

说到外部变量,我们要先说一下C语言中变量有哪几种。一般可以分为一下5种:

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。

我们先根据这4种类型

  • 自动变量
  • 静态变量
  • 静态全局变量
  • 全局变量

写出Block测试代码。

这里很快就出现了一个错误,提示说自动变量没有加__block,由于__block有点复杂,我们先实验静态变量,静态全局变量,全局变量这3类。测试代码如下:

#import <Foundation/Foundation.h>

int global_i = 1;

static int static_global_j = 2;

int main(int argc, const char * argv[]) {

    static int static_k = 3;
    int val = 4;

    void (^myBlock)(void) = ^{
        global_i ++;
        static_global_j ++;
        static_k ++;
        NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    };

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);

    myBlock();

    return 0;
}

运行结果

Block 外  global_i = 2,static_global_j = 3,static_k = 4,val = 5
Block 中  global_i = 3,static_global_j = 4,static_k = 5,val = 4

这里就有2点需要弄清楚了
1.为什么在Block里面不加__bolck不允许更改变量?
2.为什么自动变量的值没有增加,而其他几个变量的值是增加的?自动变量是什么状态下被block捕获进去的?

为了弄清楚这2点,我们用clang转换一下源码出来分析分析。

(main.m代码行37行,文件大小832bype, 经过clang转换成main.cpp以后,代码行数飙升至104810行,文件大小也变成了3.1MB)

源码如下

int global_i = 1;

static int static_global_j = 2;

// 这个结构体最后就赋值给了myBlock,所以说block的本质是结构体
struct __main_block_impl_0 {
  struct __block_impl impl; // 封装了函数实现的结构体
  struct __main_block_desc_0* Desc; // 里面有内存管理函数,Block_size表示block的大小
  int *static_k; // 捕获到的局部静态变量
  int val; // 捕获到的普通局部变量
  // 如果还有其他捕获的变量,会继续在下面列出来

    // 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述
    // 返回一个__main_block_impl_0类型的结构体,赋值给了block
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr; // 函数指针,指向block的实现__main_block_func_0 
};
// block中的代码被封装成一个函数,最后给到了__main_block_impl_0的impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

static struct __main_block_desc_0 {
  size_t reserved; // 预留参数
  size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};


int main(int argc, const char * argv[]) {

    static int static_k = 3;
    int val = 4;
    // 调用了block的初始化函数,返回了一个结构体给myBlock
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);

    // 找到了这个block的函数指针,调用
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

首先全局变量global_i和静态全局变量static_global_j的值增加,它们没有被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

接下来仔细看看自动变量和静态变量的问题。
在__main_block_impl_0中,可以看到静态变量static_k和自动变量val,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量了。

接着看block的构造函数,

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)

这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中。

main里面的myBlock闭包中的__main_block_impl_0结构体,初始化如下

void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));


impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_impl_0; 
Desc = &__main_block_desc_0_DATA;
*_static_k = 4;
val = 4;

到此,__main_block_impl_0结构体就是这样把自动变量捕获进来的。也就是说,在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。

这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。

Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

再研究一下源码,我们注意到__main_block_func_0这个函数的实现

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量val虽然被捕获进来了,但是是用 __cself->val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量val的值,依旧没法去改变Block外面自动变量val的值。

OC可能是基于这一点,在编译的层面就防止开发者可能犯的错误,因为自动变量没法在Block中改变外部变量的值,所以编译过程中就报编译错误。

调用block执行内部代码

// 执行block内部的代码, 都是类型转换,不用细看
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
// 去掉强制类型转换后变成这样, 
block->FuncPtr(block, 3, 5);

通过上述代码可以发现调用block是通过block找到FunPtr直接调用,通过上面分析我们知道block指向的是__main_block_impl_0类型结构体,但是我们发现__main_block_impl_0结构体中并不直接就可以找到FunPtr,而FunPtr是存储在__block_impl中的,为什么block可以直接调用__block_impl中的FunPtr呢?

重新查看上述源代码可以发现,(__block_impl *)block将block强制转化为__block_impl类型的,因为__block_impl是__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。

上面我们知道,FunPtr中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0函数,可以发现第一个参数就是__main_block_impl_0类型的指针。也就是说将block传入__main_block_func_0函数中,便于从中取出block捕获的值。
 

最后看下转换的结果:

小结一下:
到此为止,上面提出的第二个问题就解开答案了。

1.为什么在Block里面不加__bolck不允许更改变量?

Block只捕获Block中会用到的变量。自动变量是以值传递方式传递到Block的构造函数里面去的。由于只捕获了自动变量的值,并非内存地址, block内外是2个完全不同的变量, 只是恰好那个时刻的值一样,修改block内部的值不能影响到block外部的值, 编译器提前告诉开发者, 采用了编译报错的方式.   所以Block内部不能改变自动变量的值。

  2.为什么自动变量的值没有增加,而其他几个变量的值是增加的?自动变量是什么状态下被block捕获进去的?

自动变量的值为什么没有增加? block内外的自动变量是2个完全不同的变量(各自的地址不一样), 只是恰好那个时刻的值一样,修改block内部的值不能影响到block外部的值.

为什么其他几个变量的值是增加的? Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。对于静态变量采用的指针传递, 全局变量采用的直接访问, 所以可以修改.

自动变量是什么状态下被block捕获进去的? 自动变量采用值传递的方式捕获进去, 比如外界是变量 a, 只不过是在block内也有一个叫变量a的值, 而且block内变量a的初始值和外界的a变量值是一致的. 

由问题一又引申出的新问题, 为什么加了__block就能修改变量的值了?

回到上面的例子上面来,4种变量里面只有静态变量,静态全局变量,全局变量这3种是可以在Block里面被改变值的。仔细观看源码,我们能看出这3个变量可以改变值的原因。

静态全局变量,全局变量由于作用域的原因,于是可以直接在Block里面被改变。他们也都存储在全局区。

静态变量传递给Block是内存地址值,所以能在Block里面直接改变值。

根据官方文档我们可以了解到,苹果要求我们在自动变量前加入 __block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。

总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)。

先来实验一下第一种方式,传递内存地址到Block中,改变变量的值。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {

  NSMutableString * str = [[NSMutableString alloc]initWithString:@"Hello,"];

        void (^myBlock)(void) = ^{
            [str appendString:@"World!"];
            NSLog(@"Block中 str = %@",str);
        };

    NSLog(@"Block外 str = %@",str);

    myBlock();

    return 0;
}

控制台输出:
Block 外  str = Hello,
Block 中  str = Hello,World!

看结果是成功改变了变量的值了,转换一下源码。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableString *str;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_str, int flags=0) : str(_str) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableString *str = __cself->str; // bound by copy

            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), 
(NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
      _Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

static struct __main_block_desc_0 {
  size_t reserved;
  size_t 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
   };

int main(int argc, const char * argv[]) {
    NSMutableString * str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)
objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"),
 sel_registerName("alloc")), 
sel_registerName("initWithString:"), 
(NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0);

        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

在__main_block_func_0里面可以看到传递的是指针。所以成功改变了变量的值。

改变外部变量值的第二种方式是加 __block这个放在实战篇里面讨论,接下来我们先讨论一下Block的copy的问题,因为这个问题会关系到 __block存储域的问题。

二.Block的copy和dispose

OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

先来说明一下3者的区别。

1.从捕获外部变量的角度上来看

  • _NSConcreteStackBlock:
    1 . 用到了外部局部变量 。
    2.  MRC下就会生成一个_NSConcreteStackBlock

  • _NSConcreteMallocBlock:

      1. 用到了外部局部变量。
      2. 调用了copy方法 , block会被复制一份到堆中成为MallocBlock ;

在ARC下 , block 作为返回值时或者赋值给一个strong/copy修饰的对象会自动调用copy ,   所以ARC下大多数是_NSConcreteMallocBlock ; 
MRC下手动调用copy , 也会到达堆区 . MRC下赋值不会触发copy操作 , 所以MRC下不手动调copy就是_NSConcreteStackBlock.

ARC下一般对Block都有赋值操作,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 __NSStackBlock__ 类型的 block 转换为 __NSMallocBlock__ 类型。

  ARC环境下不赋值就是__NSStackBlock__ , 比如:

#import <Foundation/Foundation.h>
 
int main(int argc, const char * argv[]) {
 
    __block int temp = 10;
 
    NSLog(@"%@",^{
                    NSLog(@"*******%d %p",temp ++,&temp);
                  });
 
    return 0;
}

输出

<__NSStackBlock__: 0x7fff5fbff768>
这种情况就是ARC环境下Block是__NSStackBlock的类型。

block声明写法

通过上面对MRC及ARC环境下block的不同类型的分析,总结出不同环境下block属性建议写法。
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

  • _NSConcreteGlobalBlock:

       不论是ARC还是MRC,没有用到外界变量或只用到全局变量、静态变量(结构体中没有捕获变量)的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

没有用到外部变量肯定是_NSConcreteGlobalBlock,这点很好理解。不过只用到全局变量、静态变量的block也是_NSConcreteGlobalBlock , 所有block的类型是根据是否捕获变量来判定的。举例如下:

#import <Foundation/Foundation.h>

int global_i = 1;
static int static_global_j = 2;

int main(int argc, const char * argv[]) {

    static int static_k = 3;

    void (^myBlock)(void) = ^{
            NSLog(@"Block中 变量 = %d %d %d",static_global_j ,static_k, global_i);
        };

    NSLog(@"%@",myBlock);

    myBlock();

    return 0;
}

输出:

<__NSGlobalBlock__: 0x100001050>
Block中 变量 = 2 3 1

可见,只用到全局变量、静态全局变量的block也可以是_NSConcreteGlobalBlock。

2.从持有对象的角度上来看:

  • _NSConcreteStackBlock是不持有对象的。
//以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

    void (^myBlock)(void) = ^{
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };

    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

输出:

1.Block外 obj = 1
2.Block外 obj = 1
Block中 obj = 1
  • _NSConcreteMallocBlock是持有对象的。
//以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

    void (^myBlock)(void) = [^{
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    }copy];

    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

    [myBlock release];

    NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

输出:

1.Block外 obj = 1
2.Block外 obj = 2
Block中 obj = 2
3.Block外 obj = 1
  • _NSConcreteGlobalBlock也不持有对象
//以下是在MRC下执行的
    void (^myBlock)(void) = ^{

        NSObject * obj = [[NSObject alloc]init];
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };

    myBlock();

输出:

Block 中 obj = 1

由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上。

1.手动调用copy
2.Block是函数的返回值
3.Block被强引用,Block被赋值给__strong或者id类型
4.调用系统API入参中含有usingBlock的方法

以上4种情况,系统都会默认调用copy方法把Block赋复制堆上

但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。

copy函数把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int);

上面是源码中2个常用的宏定义和4个常用的方法,一会我们就会看到这4个方法。

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

    // 1
    if (!arg) return NULL;

    // 2
    aBlock = (struct Block_layout *)arg;

    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }

    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;

    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;

    // 8
    result->isa = _NSConcreteMallocBlock;

    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }

    return result;
}

上面这一段是Block_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程。对应有9个步骤。

void _Block_release(void *arg) {
    // 1
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;

    // 2
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;

    // 3
    if (newCount > 0) return;

    // 4
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        _Block_deallocator(aBlock);
    }

    // 5
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }

    // 6
    else {
        printf("Block_release called upon a stack Block: %p, ignored\\\\n", (void *)aBlock);
    }
}

上面这一段是Block_release的一个实现,实现了怎么释放一个Block。对应有6个步骤。

上述2个方法的详细解析可以看这篇文章

为什么要有copy和dispose

因为在C语言的结构体中,编译器没法很好的进行初始化和销毁操作。这样对内存管理来说是很不方便的。所以就在 __main_block_desc_0结构体中间增加成员变量 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime进行内存管理。

相应的增加了2个方法。

static struct __main_block_desc_0 {
  size_t reserved;
  size_t 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,  // 赋值给了copy的函数指针
     __main_block_dispose_0 // 赋值给了dispose的函数指针
   };

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

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

这里的_Block_object_assign和_Block_object_dispose就对应着retain和release方法。堆空间的block自己销毁之后也会对持有的对象进行release操作。

copydispose函数中传入的都是__main_block_impl_0结构体本身。

copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象,以及8。

dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是person对象,以及8。

_Block_object_assign函数调用时机及作用

当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。

_Block_object_assign函数会自动根据__main_block_impl_0结构体的捕获对象是什么类型的指针,对捕获对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对捕获的对象进行引用计数器的操作,如果__main_block_impl_0结构体内捕获的对象指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内对象指针是__weak类型,则为弱引用,引用计数不变。

_Block_object_dispose函数调用时机及作用

当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对捕获对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

总结

  1. 一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copydispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retain操作,因此一旦block捕获的变量是对象类型就会会自动生成copydispose来对内部引用的对象进行内存管理。

  2. 当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用。

  3. 如果block被拷贝到堆上。copy函数会调用_Block_object_assign函数,根据auto变量的修饰符(__strong,__weak,unsafe_unretained)做出相应的操作,形成强引用或者弱引用

  4. 如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量 , 也会调用对象类型的dispose , 对象的引用计数减1。

原理篇到此结束了 , 下面请看看实战篇 , __block , 编译器会做怎样的处理.

Block原理,为什么block能捕获变量 -- 实战篇_想名真难的博客-CSDN博客

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值