block 那点儿破事

在日常开发中,我们常常会定义block将一段代码保存起来等待合适的时机调用来完成一系列的操作(hehe...出bug了吧)。我们知道block中无法修改引用的外部变量除非使用 __block 修饰外部变量,使用block时需要格外注意循环引用。在文章中我们将会详细探讨究竟是什么原因导致了上述问题。

从hello word开始

编写hello word是一位程序员必备的编程技能,为了提升我们的逼格与社会接轨,我们也来写一段hello word。

int main() {
    void(^block)(void) = ^{
        printf("hello word");
    };
    block();
    return 0;
}

很简单是不是? 然而事情并没有那么简单,此hello word非彼hello word。
接下来进入烧脑环节 ^_^ ,有条件的读者可以先喝罐一二三四五六七八个核桃...
接下来我们借助clang编译器,将这段block转换为C++代码。在控制台输入 clang -rewrite-objc 文件路径,我们会得到一个.cpp文件,打开文件找到 main 函数。

int main() {
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

Excuse me? 这一坨是什么鬼?懵逼吗?懵逼就对了因为代码没贴全😊😊😊。

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
    printf("hello word");
}

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

好了,贴全了,各位是不是有种恍然大悟茅塞顿开的感觉。
路人甲乙丙丁戊己庚辛壬癸:你TM在逗我...
哎哎哎,别急我话还没说完呢麻烦各位刀先收一收,大家都是文明人。
那什么... 二营长,你他娘的意大利炮能不能先抬回去!
#@#¥¥#...

咳咳... 好了我们先切入正题。
接下来我们逐行分析。

void(^block)(void) = ^{
    printf("hello word");
};
//对应C++代码
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

void(*block)(void):定义一个名为block的函数指针指向类型为 void(*)(void) /无返回值无参 的函数。而从代码中可以看出实际上block指向的并非一个函数而是一个结构体对象。
__main_block_impl_0:block结构体 __main_block_impl_0表示这个block是名为mian的函数中定义的第0个block。(第0个?emm... 有毛病?没毛病!)

struct __main_block_impl_0 {
//可以理解为block的基类,所有block结构体都包含__block_impl中定义的成员变量
struct __block_impl impl;
//block描述信息
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    //block存储区域 _NSConcreteStackBlock栈  _NSConcretGlobalBlock全局 _NSConcretMallocBlock堆
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    //函数指针 指向代码块所对应的函数
    impl.FuncPtr = fp;
    //block描述信息
    Desc = desc;
}

__main_block_func_0:block对应的函数,函数的实现由block中包含的代码转换而来。这个函数接收__main_block_impl_0类型的结构体对象做为入参。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //__cself的作用在此我们先不做解释后文中讲解__block时在详细说明。
    //我们的block中输出hello word的代码
    printf("hello word");
}

__main_block_desc_0_DATA:包含block的描述信息。

static struct __main_block_desc_0 {
    size_t reserved;
    //结构体大小
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

block();
//对应C++代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

上文中我们提到block指针指向的并非是一个函数,而是一个block结构体对象,所以在这里我们通过block指针找到结构体对象中的FuncPtr指针,FuncPtr指针指向的才是我们需要调用的函数,并将block结构体对象做为入参。(千万不要看着一堆圆括号就感觉头大,不妨将圆括号中的内容拆解一下可能会比较好理解)

(
    //类型转换 将FuncPtr强转为void (*)(__block_impl *)类型的函数指针
    (void (*)(__block_impl *))
    //取FuncPtr
    ((__block_impl *)block)->FuncPtr
)
//将block转为__block_impl * 类型做为入参 调用FuncPtr
((__block_impl *)block);

到这里大家是不是对block有了一些不一样的理解呢?
如果没有请把上面的内容再认真阅读一遍。

我们知道在block中是不能修改引用的外部变量的,如果想要修改那么我们需要使用__block来修饰外部变量,接下来我们就来看一看__block到底对我们的代码做了什么。

咦? 二营长人呢?
路人甲:他去看上面的内容了。
emm...


__block? 放开那行代码,让我来!

在讲__block之前我们不妨先看看下面的代码

int main () {
    int a = 10;
    void(^block)(void) = ^{
        printf("%d",a);
    };
    block();
    return 0;
}    

打开生成的C++文件我们可以发现__main_block_impl_0的成员变量中多了一个 int a;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0结构体的构造函数中多了一个int类型的参数_a,_a最终会被赋值给结构体中的成员变量a。而结构体中的成员变量a、函数参数_a、外部变量a他们的内存地址都不相同。当执行过block的语法后,即使外部对a重新赋值也不会改变block结构体中a的值(修改的不是同一块内存)。
讲道理我们是可以修改结构体中成员变量a的值的,但是当我们修改a的值时编译器会无情的给我们一个大大的报错。

xCode:兄dei,这里的a和外面的a不是同一个a,你改这里的a外面的a不会变的,哈哈哈...
程序员0:哦,__block。
xCode:咳咳... 老夫纵横江湖这么多年,见过的程序员连起来能绕地球两圈,你是他们中最优秀的,没有之一!
程序员0:哦。


为了能够在block内部修改引用的外部变量的值,我们需要用__block修饰外部变量。

int main () {
    __block int a = 10;
    void(^block)(void) = ^{
        a = 20;
        printf("%d",a);
    };
    block();
    return 0;
}

同样转为C++代码

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
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            (a->__forwarding->a) = 20;
            printf("%d",(a->__forwarding->a));
}
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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main () {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
}

说实话,确实有点辣眼睛... 不过别担心,同样的我们逐行分析。

__block int a = 10;
//您的__block请查收
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

我们的int a变成了__Block_byref_a_0 a,what? __Block_byref_a_0又是什么鬼。

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

__Block_byref_a_0结构体中包含一个本身类型的指针 __forwarding 和一个 int 类型的变量a 。当我们使用__block修饰变量a时变量被转换成了上述结构体对象a,而这个结构体中的__forwarding会被赋值为本身的地址,__forwarding的作用会在后续讲解block在内存中的存储区域时讲到,这里我们先不管他。我们对a赋值会转换为对结构体中的成员变量a赋值。

void(^block)(void) = ^{
    a = 20;
    printf("%d",a);
};
//C++
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

block:函数指针,指向的实际是一个block结构体。
__main_block_impl_0:我们的block被转换为结构体

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

__main_block_func_0:其中包含了block中的代码。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    //通过__forwarding来修改结构体中a的值确保修改的正确性
    (a->__forwarding->a) = 20;
    printf("%d",(a->__forwarding->a));
}

前面我们提到过__forwarding指向的是自己的地址,为什么这里不直接对成员变量a赋值而是通过__forwarding找到自己然后在对自己的成员变量a赋值呢?可能大家会感觉这样做是多此一举,但是现实很残酷。我们后面会详细介绍。

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

至于这一行我们就不讲了,不懂请翻前面的内容。
好吧实际上到这里为止好像我们只知道__block把我们的 int a 转换成了 __Block_byref_a_0 a ,还挖了一堆坑。
其实__block只做了一件事情,那就是把我们的变量转换为结构体,至于为什么这样做我们还需要先了解一下block的存储区域。


block存储区域
_NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁

_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量

_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁

当我们把block作为全局变量使用时,生成的block将被存储在全局区,对应的impl.isa会被设置为 &_NSConcreteGlobalBlock。除此之外block创建时内存分配在栈上impl.isa会被设置为 &_NSConcreteStackBlock,作用域结束block会在出栈时被销毁。那么问题来了,如果我们需要block脱离当前作用域的限制该怎么办呢?这时候_NSConcreteMallocBlock就粉墨登场了。
将栈上的block复制到堆上,这样当栈上的block超过他的作用域时,堆上的block还可以继续存在,被复制到堆上的block结构体成员变量isa将变为 &_NSConcreteMallocBlock。

impl.isa = &_NSConcreteMallocBlock;

__forwarding

当我们将一个栈上的block复制到堆上时,与其相对应的__block变量同样会被复制到堆上,此时我们拥有两份block以及__block变量,并且他们的内存地址不同,此时问题就出现了。
栈上的block结构体存储的是栈上的__block变量而堆上的block结构体存储的是堆上的__block变量,当我们在其中一个block中修改__block变量时,另一个block结构体中的__block变量如何进行同步?
此时我们的__forwarding终于派上了大用处,当我们将栈上的__block变量拷贝到堆上时,将栈上的变量的__forwarding指向堆上的变量,堆上的变量的__forwarding指向自己,这样无论是在栈上还是堆上__forwarding始终指向堆上的变量,当我们通过__forwarding修改变量时修改的都是堆上的变量。

copy

因此就有了我们上面见到的代码

(a->__forwarding->a) = 20;

为什么block属性需要使用copy修饰

在ARC下,使用strong和copy都是一样的,因为在访问/修改外部变量的时候,block都是在堆区,苹果官方建议使用copy

在MRC下,单纯的Block是存放在全局/常量区的,如果Block访问/修改外部变量后,block存放在了栈区,在栈区是不可以全局共享的,只有堆区的对象,变量才会被全局共享,所以使用copy拷贝一份Block到堆区中,这样Block才会全局共享


关于循环引用

程序员1:呦,哥们,撸代码呢。
程序员0:嗯(self.xxx = xxx)。
程序员1:哎哎哎,哥们,你这里循环引用了,block里面怎么能直接用self,快weak weak。
程序员0:滚犊子!

block中使用self就一定会循环引用吗? 呵呵...

造成循环引用的条件是出现引用环,而解决的方案也很简单,打破引用环。
我们先看看下面的代码

@implementation RetainCircle {
    void(^_block)(void);
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        void(^block)(void) = ^{
            NSLog(@"%@",self);
        };
        block();
        _block = ^{
            NSLog(@"%@",self);
        };
        _block();
    }
    return self;
}

- (void)dealloc {
    NSLog(@"GG思密达");
}

@end

问:有循环引用吗?
这两个block都引用了self,block和self的关系无非就是被self持有和不被self持有,上面的代码所有的组合都占了,那必须有循环引用啊,这还用问吗?
程序员嘛,就这一点好,逻辑推理能力6的不要不要的,来来来,双击666,小礼物走一波~

咳咳...其实我想问的是哪一个block有循环引用。

上面两个block中都持有了self,而_block被self持有,这样就造成了_block持有self,self持有_block。

_block:哥,要不你先挂?
self:凭什么我先挂,我是你哥,要挂也轮不到我。
_block:hehe...
block:两位哥哥,小弟先走一步,勿念...

所以当我们在block中使用self时并不一定会造成循环引用。
如果造成了循环引用也不要担心,我们是有解决方案的,先平复一下心情,下面我们看一看解决方案。

解决方案:

略。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

啊,不好意思各位,翻错页码了,见谅见谅。

MRC环境中:
新建一个__block的局部变量,并把self赋值给它,而在block内部则使用这个局部变量来进行取值。因为__block标记的变量是不会被自动retain的。

__block typeof(self) mSelf = self;
_block = ^{
    NSLog(@"%@",mSelf);
};

ARC环境下:
__block要换成__weak,因为ARC环境下自动释放池会自动做引用计数的增减。此处我们需要一个弱引用对象controller指向self对象,这样即便在block中使用了controller,由于它是一个弱引用,可以使用self的地址空间但是并不会造成引用计数加1

__weak typeof(self) weakSelf = self;
_block = ^{
    NSLog(@"%@",weakSelf);
};

补充 2018-5-18

关于解决block的循环引用还需要了解的一件事情

如果我们的block中引用了成员变量该怎么解决循环引用呢?


image.png

当然是weak啦


image.png

信度娘,得永生

Excuse me?

image.png

那你让我strong可别怪我不客气了。


image.png

哎呦喂!好了。开心,撸斤小龙虾先。

为什么在block里面需要使用strong
是为了保证block执行完毕之前self不会被释放,执行完毕的时候再释放。这时候会发现为什么在block外边使用了__weak修饰self,里面使用__strong修饰weakSelf的时候不会发生循环引用?!
__strong修饰的self只是为了保证在block内部执行的时候不会释放,但存在执行前self就已经被释放的情况,导致self=nil。注意判空处理。

不会引起循环引用的原因
因为block截获self之后self属于block结构体中的一个由__strong修饰的属性,会强引用self, 所以需要使用__weak修饰的weakSelf防止循环引用。
block使用的__strong修饰的self是为了在block生命周期中self不会提前释放。self实质是一个局部变量(在block这个“函数”里面的局部变量),当block执行完毕就会释放自动变量self,不会对self进行一直进行强引用。

参考

iOS内存管理---block机制详解
iOS 关于 __block 底层实现机制的疑问?
blocksruntime
iOS Block底层实现原理详解
你真的理解__block修饰符的原理么?

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

推荐阅读更多精彩内容