探寻Block的本质(6)—— __block的深入分析

__block的使用场景

大家应该都知道,如果想在block内部修改从外部捕获的auto变量的值,可以在该auto变量定义的时候,加上关键字__block。代码案例如下

#import <Foundation/Foundation.h>

typedef void(^CLBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int a = 10;
        int b = 30;
        CLBlock myblock = ^{
            a = 20;
            NSLog(@"%d",b);
        };
        
        myblock();
        
        NSLog(@"myblock执行完之后,a = %d",a);
    }
    
    return 0;
}

*********************运行结果*********************
2019-09-04 19:41:51.709406+0800 Block学习[29867:3904669] 30
2019-09-04 19:41:51.709706+0800 Block学习[29867:3904669] myblock执行完之后,a = 20

__block只可以用来作用于auto变量,它的目的就是为了能够让auto变量能够在block内部内修改。而全局变量和static变量本来就可以从block内部进行修改,因此__block对它们来说没有意义,所以__block被规定只能用于修饰auto变量,这一点应该不难理解。

__block的本质

老套路,我们先通过终端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp来看一下__block以及block在底层张什么样子。首先看看block的底层结构

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp,
                      struct __main_block_desc_0 *desc,
                      int _b,
                      __Block_byref_a_0 *_a,
                      int flags=0) : b(_b), a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

为了比较,我特意加了一个int b作为对比,顺便回顾一下,基本类型的auto变量被block捕获的时候,就是通过值拷贝的形式把值赋给block内部相对应的基本类型变量。而案例里面的__block int a = 10,我们可以看到在底层,系统是把int a包装到了一个叫__Block_byref_a_0的对象里面。这个对象的结构如下

struct __Block_byref_a_0 {
  void *__isa;//有isa,是一个对象
__Block_byref_a_0 *__forwarding;//指向自身类型对象的指针
 int __flags;//不用关心
 int __size;//自己所占大小
 int a;//被封装的 基本数据类型变量
};

看看在main函数中__Block_byref_a_0被赋了什么值

//__block int a = 10;
__Block_byref_a_0 a = {(void*)0,
                             &a,
                              0,
      sizeof(__Block_byref_a_0),
                             10
                        };

image

图中可以看出来,10被存储到了block内部__Block_byref_a_0对象的成员变量int a上。__Block_byref_a_0对象里面的成员变量__forwarding实际上指向了__Block_byref_a_0对象自身。
我们来看block内的代码对于a的赋值是如何操作的

image

为什么用a->__forwarding->a,而不是a->a直接拿到int a,通过__forwarding转一圈有什么用意?这个等会解答。

这样__block的底层实现就说完了。

__block的细节

上面,我们知道了通过 __block int a = 10定义之后,这个a底层是一个__Block_byref_a_0对象,数值10存放在这个对象内部的成员变量int a上面。但是我们在写代码的时候,可以直接通过__Block_byref_a_0对象a来赋值,那么在block定义初始化结束,完成变量捕获之后,oc代码中再次通过a访问到的到底是什么呢?例如下面

image

我们先来看一份代码案例

**********************testVC.m**********************
#import "testVC.h"

@implementation testVC

typedef void(^CLBlock)(void);

struct __Block_byref_a_0 {
    void *__isa;
    struct __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_a_0 *a; // by ref
    
};




 struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
     void *copy;
     void *dispose;
 };



- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    [self blockTest];
    
}

- (void)blockTest {
    __block int a = 10;
    
    CLBlock myblock = ^{
        a = 20;
        NSLog(@"此时在在myblock内部的oc代码里直接通过a访问的内存空间是:%p",&a);
    };
    
    struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)myblock;
    NSLog(@"myBlock经过初始化,完成变量捕获之后,其内部的[__Block_byref_a_0 *] a = %p",tmpBlock->a);
    NSLog(@"此时在在myblock外部的oc代码里直接通过a访问的内存空间是:%p",&a);
    
    myblock();
}

@end

*************************运行结果************************
2019-09-04 21:28:07.189104+0800 BT[30733:3968805] myBlock捕获完变量之后,[__Block_byref_a_0 *] a = 0x7ffeede8f840
2019-09-04 21:28:07.189221+0800 BT[30733:3968805] 此时在在block外部的oc代码里直接通过a访问的内存空间是:0x7ffeede8f858
2019-09-04 21:28:07.189296+0800 BT[30733:3968805] 此时在在block内部的oc代码里直接通过a访问的内存空间是:0x7ffeede8f858

从打印我们看到,myblock内部的[__Block_byref_a_0 *] a指向的地址是0x7ffeede8f840,之后我们在任意地方通过a访问的内存地址是0x7ffeede8f858,十六进制下它们地址相差了0x18,也就是十进制下的24个字节。

image

从示意图可以看出,通过[__Block_byref_a_0 *] a的地址往高地址走24个字节,正好是它内部封装的那个int a。也就是说我们在oc代码里面完成了myblock的初始化以及 __block变量的捕获之后,只能通过a访问到被封装在 __ Block_byref_a_0 * 内部的这个int a的内存空间。

苹果这么做的意图我猜测是想向开发者隐藏__ Block_byref_a_0 *的存在,希望开发者把__block int a就当成一个普通的int a来看待。苹果吗,总是这么小家子气,可以理解。(此处纯属自我发挥,还待大牛给出正解:)

__block的内存管理

我们知道,如果block捕获一个基础类型的auto变量,是不用考虑内存管理的。但是__block的本质作用,是将所修饰的对象包装成一个__ Block_byref_xx_x *,然后进行捕获,而__ Block_byref_xx_x *本质上也是一个对象,因此肯定需要处理它的内存管理问题。

我们已经知道,如果一个block位于栈空间上,那么是不需要考虑被它所捕获的对象类型的auto变量的内存管理问题的。所谓的内存管理,是针对创建在堆空间上的oc对象而言的,因为我们作为开发者,只能够管理堆上的空间。栈空间的内存是由系统管理的,不用我们操心。

关于内存管理问题这里,我们所讨论的问题需要考虑三个关键因素:__block__weak对象变量基本类型变量,他们合法的组合有如下几种:

  1. 基本类型变量
  2. 对象变量
  3. __weak + 对象变量
  4. __block + 基本类型变量
  5. __block + 对象变量
  6. __block + __weak + 对象变量

我在【对象类型的auto变量捕获】这一篇里面详细分析了一个对象类型的auto变量被block捕获时的内存管理过程,上面的1、2、3这三种场景已经得到了说明。下面我们来分析一下4、5、6这三种场景。

4 --- __block + 基本类型变量

首先代码上一份

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            __block int a = 10;
            myblock = ^ {
                a = 20;
            };
        }
        
        myblock();
    }
    return 0;
}

编译之后block相关的底层结构如下


struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref 
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
        {
        _Block_object_assign(
                             (void*)&dst->a, 
                             (void*)src->a, 
                             8/*BLOCK_FIELD_IS_BYREF*/♥️♥️♥️♥️♥️♥️
                            );
        }

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
        {
        _Block_object_dispose(
                                (void*)src->a, 
                                8/*BLOCK_FIELD_IS_BYREF*/♥️♥️♥️♥️♥️♥️
                             );
        }

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

我们知道__block int a = 10;这句代码的作用,是将int a包装在struct __Block_byref_a_0内部,这样block实际上捕获的是这个struct __Block_byref_a_0,它可以被当作一个对象来看待,所以内存管理上面,最终仍然是通过_Block_object_assign_Block_object_dispose这两个函数来处理,但是可以看到这两个函数的最后一个参数是8(对于对象类型的捕获,传递的参数是3),这个参数表明了即将要处理的是一个struct __Block_byref_a_0,因为它是没有__weak__strong标记的,所以处理方式很简单,就是copy到堆上的时候,同时需要进行retain,dispose的时候同时需要进行release




5 --- __block + 对象变量

上代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            CLPerson *person = [[CLPerson alloc] init];
            __block CLPerson *blockPerson = person;
            myblock = ^ {
                blockPerson.age = 10;
            };
        }
        
        myblock();
    }
    return 0;
}

编译之后, __block CLPerson *blockPerson的底层结构如下

struct __Block_byref_blockPerson_0 {
  void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 CLPerson *__strong blockPerson;
};

从这个结构可以看出两点变化:

  • 相比较基本类型变量,对象类型的变量被__block修饰后,底层所生成的__Block_byref_xxx_x结构体里面多了两个函数指针,__Block_byref_id_object_copy__Block_byref_id_object_dispose
  • 对象类型的变量被封装到__Block_byref_xxx_x内部以后,默认是被__strong修饰的。

上面发现的两个新函数指针__Block_byref_id_object_copy__Block_byref_id_object_dispose就是当__Block_byref_xxx_x被拷贝到堆空间的时候,以及将要被系统释放的时候调用的。我们可以在main函数里面找到它们的最终赋值,分别是__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131,它们的定义如下

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_object_assign_Block_object_assign这两个函数,二从参数可以看出,它们所要处理的对象就是__Block_byref_id_object_dispose内部所封装的对象类型变量,也就是我们代码中的CLPerson *blockPerson,因为默认blockPerson是被__strong修饰的,所以接下来对于blockPerson的内存管理方式就和我们之前所分析过的是一样的。




6 --- __block + __weak + 对象变量

上代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLBlock myblock;
        {
            CLPerson *person = [[CLPerson alloc] init];
            __block __weak CLPerson *weakBlockPerson = person;
            myblock = ^ {
                weakBlockPerson.age = 10;
            };
        }
        
        myblock();
    }
    return 0;
}

这里就直接给出编译之后的底层结构struct __Block_byref_xxx_x来进行对比

struct __Block_byref_weakBlockPerson_0 {
  void *__isa;
__Block_byref_weakBlockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 CLPerson *__weak weakBlockPerson;
};

因为我们显式地给对象变量加上了__weak,因此struct __Block_byref_xxx_x内部封装的就是一个指向对象的弱指针CLPerson *__weak weakBlockPerson。根据上面的分析,最后同样进入到_Block_object_assign_Block_object_assign这两个函数进行处理,处理方式不再赘述。

最后在通过图例在梳理一下

__block + 基本类型变量
__block + 对象类型变量
__block + __weak + 对象类型变量

最后再来解决那个我们中篇遗留的问题:__forwarding的作用
从上图可以很清晰的看出,当__Block_byref_xxx_x(假设为A)从栈空间被拷贝到堆空间(假设堆上的那一份为B)的时候,栈上A__forwarding指针会被指向堆空间上的B,而B本身的__forwarding仍然指向B自己,因为在底层访问__Block_byref_xxx_x所封装的目标变量,是通过__Block_byref_xxx_x->__forwarding->目标变量,这样,无论我们访问入口对象__Block_byref_xxx_x是在栈上还是在堆上,都能保证最终访问到的目标变量是堆空间上的那一份。这样的设计就正好契合了堆空间上的__Block_byref_xxx_x对象存在的目的。

到此,关于__block的底层实现就分析到这里。

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

推荐阅读更多精彩内容