Block深入分析

虽然网上Block已经被写烂了,自己还是觉得,无论如何自己抱着求真的态度自己看一遍源码。之前介绍过Block的基本使用Block。这一片将深入介绍一下Block的具体实现。

真没想到自己一直认为自己做了这么久的iOS不应该再去纠结Block这样的知识点,回头一看,知识还是得温故而知新。

测试代码

分析思路:使用clang -rewrite-objc将含有Block的.m文件翻译为.cpp。对照着翻译之后的源码进行分析。

现在定义如下几个block。

//没有捕获变量
void blockFunc0()
{
    void (^block)(void) = ^{
        NSLog(@"num ");
    };
    block();
}

//普通局部变量
void blockFunc1()
{
    int num = 100;
    void (^block)(void) = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}
//普通__block局部变量
void blockFunc2()
{
    __block int num = 100;
    void (^block)(void) = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}

//全局变量
int num = 100;
void blockFunc3()
{
    void (^block)(void) = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}

//静态变量
void blockFunc4()
{
    static int num = 100;
    void (^block)(void) = ^{
        NSLog(@"num equal %d", num);
    };
    num = 200;
    block();
}

上面是准备的测试代码

普通局部变量(blockFunc1)

blockFunc1和blockFunc0翻译之后的差距就只是__blockFunc0_block_impl_0和__blockFunc1_block_impl_0结构体中多了num这个字段,其余的都一样。所以这几直接从blockFunc1讲起。

执行完clang -rewrite-objc命令之后会得到不少的警告,最终翻译出来的文件比较大。因为翻译之后方法的名字不会变,所以可以通过搜索相关的方法名就能快速找到翻译之后方法对应的位置。

blockFunc1翻译为(注意对比翻译前后的结果

void blockFunc1()
{
    int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

根据上面的结果可以提取出如下几个重要的数据结构

数据结构

__block_impl

定义block的结构体,根据定义可以知道Block实际上就是对象,保存了一个ISA指针。那么如果是对象的话就很顺利成长的把block的内存,生命周期管理同对象关联起来。目前block_impl的isa指针有_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock

struct __block_impl {
  void *isa;//表明Block实际上是个对象
  int Flags;
  int Reserved;
  void *FuncPtr;
};

字段对应的含义

字段名 含义
isa isa 指向实例对象,表明 block 也是一个 Objective-C 对象。block 有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,isa 对应有三种值。因为block是对像,则就有对象的内存管理及生命周期管理。恰恰三个对应的值也就是这个作用。
Flags 按位表示一些block的附加信息
Reserved 保留变量
FuncPtr 函数指针,指向具体的block实现的函数调用地址

__blockFunc1_block_desc_0

保存对block的一些描述信息,比如保留字段大小,以及block结构大小。并且这里定义__blockFunc1_block_desc_0_DATA,并且还初始化了。可以看到初始化的情况下reserved为0,Block_size就是__blockFunc1_block_impl_0(包含了两种结构体的结构体,下面会介绍)大小。

static struct __blockFunc1_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockFunc1_block_desc_0_DATA = { 0, sizeof(struct __blockFunc1_block_impl_0)};

字段名 含义
reserved 保留字段的大小
Block_size block结构大小

__blockFunc1_block_impl_0

保存了block相关的信息,是前面两种结构体的结合体,包含了捕获的外部变量。比如这里的num,初始化的时候会初始化block_imp内部变量,如函数指针,block_imp的对象类型。

struct __blockFunc1_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc1_block_desc_0* Desc;
  int num;//定义的变量
  //初始化传入参数有函数指针,block的秒速
  __blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

字段对应的含义

字段名 含义
impl block_imp结构体信息
Desc 对block的附加描述
*** 对外部捕获的变量

方法翻译

Block内容

Block里面的具体内容被翻译为了__blockFunc1_block_func_0函数,注意int num = __cself->num; // bound by copy表明了num的值是值接拷贝过去的。这一点将会随着捕获外部变量的作用域不同而不同,后面会总结。

static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) {
  int num = __cself->num; // bound by copy
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_1, num);
    }

Block调用

这就是OC中的blockFunc1最终被翻译的结果。

void blockFunc1()
{
    int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

注意void在C++中的一些不同,比如void*表示“任意类型的指针”或表示“该指针与一地址值相关,但是不清楚在此地址上的对象的类型”。,可以简单的理解为泛型的表示。 最终block被翻译为了函数指针。

因为在__blockFunc1_block_impl_0创建的时候设置了最后一个参数flags=0的默认值,所以上面在创建__blockFunc1_block_impl_0没有传入参数flags。

这里还需要注意的就是使用了C++中的结构体强转类型。对应的代码的就是__blockFunc1_block_func_0转为__block_impl类型。因为C++中,只要高地址的数据类型相同(也就是首地址相同)就可以实现强转。因为在__blockFunc1_block_func_0结构体中。第一个数据类型就是__block_impl所以可以实现强转。

__block局部变量(blockFunc2)

上面通过了第一个例子分析了整个过程。后面加与不加block其实实现内容都一样。下面列举几个不同点。

如果使用__block修饰变量,则在生成为__blockFunc2_block_impl_0的时候对外部变量的修饰符不一样。具体来讲对应到下面的__Block_byref_num_0 *num; // by ref。同时在初始化__blockFunc2_block_impl_0的时候num会初始化为_num->__forwarding

struct __blockFunc2_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc2_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

数据结构

_block_imp的结构同上面一样。

__Block_byref_num_0

和上面不同,但是多了一个__Block_byref_num_0。他的定义如下。

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

其中各个字段含义如下:

字段名 含义
__isa 对象指针
__forwarding 指向自己的指针
__flags 标志位
__size 结构体大小
num 外部变量

__blockFunc2_block_impl_0

相比之前多了一个__Block_byref_num_0字段。该字段在初始化的时候就已经赋值了。

struct __blockFunc2_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc2_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__blockFunc2_block_desc_0

static struct __blockFunc2_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __blockFunc2_block_impl_0*, struct __blockFunc2_block_impl_0*);
  void (*dispose)(struct __blockFunc2_block_impl_0*);
} __blockFunc2_block_desc_0_DATA = { 0, sizeof(struct __blockFunc2_block_impl_0), __blockFunc2_block_copy_0, __blockFunc2_block_dispose_0};

相对于blockFunc1的翻译结果多了一个copy的方法以及一个dispose方法,并且在初始化的时候就确定了这两个方法。

后面会知道这两个方法起的作用就是内存管理的作用。

__blockFunc2_block_copy_0
static void __blockFunc2_block_copy_0(struct __blockFunc2_block_impl_0*dst, struct __blockFunc2_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}

找到了_Block_object_assign的函数声明为void _Block_object_assign(void *, const void *, const int);。当block field是指针类型的时候就会发生拷贝。可以很明确的看到进行拷贝的其实是__blockFunc2_block_impl_0结构体中的__Block_byref_num_0。

__blockFunc2_block_dispose_0
static void __blockFunc2_block_dispose_0(struct __blockFunc2_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}

同样最终释放的也是__blockFunc2_block_impl_0中的__Block_byref_num_0。所以__Block_byref_num_0这个对象非常重要。

方法翻译

Block内容

追忆这里直接传递的是__Block_byref_num_0结构体指针。在这个结构体指针里面保存了之前的外部变量。使用到外部变量的时候直接从结构体里面获取。

static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
  __Block_byref_num_0 *num = __cself->num; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_2, (num->__forwarding->num));
    }

block调用

void blockFunc2()
{
    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
    void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    (num.__forwarding->num) = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

首先创建了一个__Block_byref_num_0结构体。里面记录了自己的指针以及结构体大小和外部变量的值100。在给外部变量赋值的时候使用了(num.__forwarding->num) = 200;,也通过指针的方式复制。这样就达到改变外部变量值得目的。

情况分析

根据上面的内容,当在使用__block修饰变量之后,之前直接出现外部变量(这里的num)的地方都被__Block_byref_num_0这个结构体所替换换。并且在此基础上还多了copy以及dispose方法。最终也是通过__Block_byref_num_0实现的内存管理。

copy和dispose的作用如下:

  • 当blockFunc2从栈上被copy到堆上时,会调用__blockFunc2_block_copy_0将blockFunc2类型的成员变量num(具体来讲应该是__Block_byref_num_0)从栈上复制到堆上,而这个时候__forwarding指针就会指向堆上的结构体;

  • 当blockFunc2被释放时,相应地会调用__blockFunc2_block_dispose_0来释放blockFunc2类型的成员变量num(具体来讲应该是__Block_byref_num_0)。同样__forwarding指针指向堆上的结构体也就被释放。

  • 为什么要这么做呢?其实也是为了保证block能够访问有效的正确内存区域。

因为blockFunc2函数中的局部变量num和函数__blockFunc2_block_impl_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__blockFunc2_block_impl_0时,blockFunc2函数栈还没展开完成,变量num还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了已经被销毁了,再用指针访问就会报常见的坏内存访问。

因为block可以作为属性,并且也经常作为参数传递,而Block最终展开的是一个函数,展开的函数里面的变量作用域和被block被调用的函数作用域是不同的。通常在情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了已经被销毁了,再用指针访问就会报常见的坏内存访问。因此block有copy方法将其在堆上,所以在block被copy的同时,将局部变量间接也copy放在堆上就能够保证局部变量可以被block正常访问到。具体来讲可以看看下面这张图,最终是通过__forwarding指针来实现这个目的:

全局变量(blockFunc3)

这种情况下转换的结果block和不捕获任何变量的block结果是一样的。

struct __blockFunc0_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc0_block_desc_0* Desc;
  __blockFunc0_block_impl_0(void *fp, struct __blockFunc0_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int num = 100;//注意,这里已经声明了一个num为全局变量

struct __blockFunc3_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc3_block_desc_0* Desc;
  __blockFunc3_block_impl_0(void *fp, struct __blockFunc3_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

因为是全局变量,所以任何地方都可以修改。跟block没什么关系(因为block最终也是转换为函数来调用)。

static局部变量(blockFunc4)

这种情况下重点看一下如下几点:

__blockFunc4_block_impl_0中保持的是int *num;也即是指针。

struct __blockFunc4_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc4_block_desc_0* Desc;
  int *num;
  __blockFunc4_block_impl_0(void *fp, struct __blockFunc4_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block调用被转换为:

void blockFunc4()
{
    static int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

注意这里声明了static int num = 100;所以传递的是指针,和blockFunc1中最大的不同就是int num 变成了int *num 由整型变成了整型指针类型,注意:这个不再是传值,而是传地址,这说明对应 static 修饰的自动变量值在被 block 截获之后仍可以与外部自动变量保持同步,因为它们的地址是同一个。

Block内存管理

Block一共有三种类型,三面介绍的几种block全是_NSConcreteGlobalBlock。


_NSConcreteGlobalBlock

经过测试只有当 block 字面量写在全局作用域时,即为 global block。而且仅此一种,网上有人当 block 字面量不获取任何外部变量时也是global block,但是经过自己测试还是_NSConcreteStackBlock类型。

globalblock如下形式:

_NSConcreteStackBlock

这种类型block是最多的一种,处于内存的栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。


block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,就是 _NSConcreteMallocBlock。

_NSConcreteMallocBlock

当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block

堆上的 block 类型为 _NSConcreteMallocBlock,所以会将 _NSConcreteMallocBlock 写入 isa。对应到代码上就是impl.isa = &_NSConcreteMallocBlock

ARC 下的 block

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上。
block 作为函数的参数传递时,编译器不会自动调用 copy 方法

如下这几种情况都不用手动拷贝

  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 拷贝;

比如自动拷贝的情况:

/************ ARC下编译器自动拷贝block ************/
typedef int (^blk_t)(int);
blk_t func(int rate)
{
    return ^(int count){return rate * count;};
}

如果没有自动拷贝,因为外部传入的参数放到的是栈上,如果后面去调用这个返回的block肯定会发生异常,但是在ARC下面不会出问题,于是翻译一下。可以查到如下代码

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); 
}

由于 block 字面量是创建在栈内存,通过 objc_retainBlock() 函数拷贝到堆内存,让 tmp 重新指向堆上的 block,然后将 tmp 所指的堆上的 block 作为一个 Objective-C 对象放入 autoreleasepool 里面,从而保证了返回后的 block 仍然可以正确执行。

需要手动执行copy的block:

/************ ARC下编译器手动拷贝block ************/
id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects: 
                            ^{NSLog(@"blk0:%d", val);}, 
                            ^{NSLog(@"blk1:%d", val);}, nil];
}

这里block最为了函数参数,编译器不会自动拷贝。所以在调用这个方法的时候会出现异常。

总结

Block原理总算是讲完了。其难点就是对于外部变量的捕获场景比较多。通过分析源码,比较麻烦的就是加了__block的情况。通过__forwarding指针达到栈和堆上面的切换。这里没有列举对象类型的例子原因是对象类型其实就是一个指针。简单来讲就是把上面的num换成指针类型而已,其余的规则都是一样的。

__block的作用可以简单总结为,处理两个函数作用域访问访问变量的时候,防止坏内存访问。

后面讲了Block的几种类型以及相关内存管理,同堆栈一样。最后将了ARC环境下Block的注意事项。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258