Block

将我认为的比较易懂的关于block的文章整理到一起:

文章链接:

你真的理解__block修饰符的原理么?

__block 与 __weak的区别理解

在本文的开头,提出两个简单的问题,如果你不能从根本上弄懂这两个问题,那么希望你阅读完本文后能有所收获。

为什么block中不能修改普通变量的值?

__block的作用就是让变量的值在block中可以修改么?

如果有的读者认为,问题太简单了,而且你的答案是:

因为编译器会有警告,各种教程也都说了不能修改。

应该是的吧。

那么我也建议你,抽出宝贵的几分钟时间阅读完本文吧。在开始揭开__block的神秘面纱之前,很不幸的是我们需要重新思考一下block的本质和它的实现。

block是什么?

很多教程、资料上都称Block是“带有自动变量值的匿名函数”。这样的解释显然是正确的,但也是不利于初学者理解的。我们首先通过一个例子看一看block到底是什么?

typedef void (^Block)(void);

Block block;

{   

         int val = 0;  

        block = ^(){         

               NSLog(@"val = %d",val); 

        };

}

block();

抛开block略有怪异的语法不谈,其实对于一个block来说:

它更像是一个微型的程序。

为什么这么说呢,我们知道程序就是数据加上算法,显然,block有着自己的数据和算法。可以看到,在这个简单的例子中,block的数据就是int类型变量val,它的算法就是一个简单的NSLog方法。对于一般的block来说,它的数据就是传入的参数和在定义这个block时截获的变量。而它的算法,就是我们往里面写的那些方法、函数调用等。

我认为block像是一个微型程序的另一个主要原因是一个block对象可以由程序员选择在什么时候调用。比如,如果我喜欢,我可以设置一个定时器,在10s后执行这个block,或者在另一个类里执行这个block。

当然,我们还注意到在上面的demo中,通过typedef,block非常类似于一个OC的对象。限于篇幅和主题,这里不加证明的给出一个结论:Block其实就是一个Objective-C的对象。有兴趣的读者可以结合runtime中类和对象的定义进一步思考。

block是怎么实现的?

刚刚我们已经意识到,block的定义和调用是分离的。通过clang编译器,可以看到block和其他Objective-C对象一样,都是被编译为C语言里的普通的struct结构体来实现的。我们来看一个最简单的block会被编译成什么样:

//这个是源代码

int main(){

    void (^blk)(void) = ^{

            printf("Block\n");

    };

    block();

    return 0;

}

编译后的代码如下:

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;

        __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;

        }

};

struct void__main_block_func_0(struct__main_block_impl_0  *__cself) {

        printf("Block\n");

}

static struct__main_block_desc_0{

        unsigned long reserved;

        unsigned long Block_size;

}__main_block_desc_0_DATA  =  {

        0,

        sizeof(struct  __main_block_impl_0)

};

代码非常长,但是并不复杂,一共是四个结构体,显然一个block对象被编译为了一个__main_block_impl_0类型的结构体。这个结构体由两个成员结构体和一个构造函数组成。两个结构体分别是__block_impl和__main_block_desc_0类型的。其中__block_impl结构体中有一个函数指针,指针将指向__main_block_func_0类型的结构体。总结了一副关系图:


block在定义的时候:

//调用__main_block_impl_0结构体的构造函数

struct  __main_block_impl_0  tmp  =  __main_block_impl_0(__main_block_func_0,  &__main_block_desc_0_DATA);

struct  __main_block_impl_0  *blk  =  &tmp;

block在调用的时候:

(*blk->impl.FuncPtr)(blk);

之前我们说到,block有自己的数据和算法。显然算法(也就是代码)是放在__main_block_func_0结构体里的。那么数据在哪里呢,这个问题比较复杂,我们来看一看文章最初的demo会编译成什么样,为了简化代码,这里只贴出需要修改的部分。

struct  __main_block_impl_0 {

        struct  __block_impl impl;

        struct  __main_block_desc_0  *Desc;

        int  val;

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

                impl.isa  =  &_NSConcreteStackBlock;

                impl.Flags  =  flags;

                impl.FuncPtr  =  fp;

                Desc  =  desc;

        }

};

struct  void  __main_block_func_0(struct  __main_block_impl_0  *__cself){

        int  val  =  __cself->val;

        printf("val = %d",val);

}

可以看到,当block需要截获自动变量的时候,首先会在__main_block_impl_0结构体中增加一个成员变量并且在结构体的构造函数中对变量赋值。以上这些对应着block对象的定义。

在block被执行的时候,把__main_block_impl_0结构体,也就是block对象作为参数传入__main_block_func_0结构体中,取出其中的val的值,进行接下来的操作。

为什么__block中不能修改变量值?

如果你耐心地看完了上面非常啰嗦繁琐的block介绍,那么你很快就明白为什么block中不能修改普通的变量的值了。

通过把block拆成这四个结构体,系统“完美”的实现了一个block,使得它可以截获自动变量,也可以像一个微型程序一样,在任意时刻都可以被调用。但是,block还存在这一个致命的不足:

注意到之前的__main_block_func_0结构体,里面有printf方法,用到了变量val,但是这个block,和最初block截获的block,除了数值一样,再也没有一样的地方了。参见这句代码:

int val = __cself->val;

当然这并没有什么影响,甚至还有好处,因为int val变量定义在栈上,在block调用时其实已经被销毁,但是我们还可以正常访问这个变量。但是试想一下,如果我希望在block中修改变量的值,那么受到影响的是int val而非__cself->val,事实上即使是__cself->val,也只是截获的自动变量的副本,要想修改在block定义之外的自动变量,是不可能的事情。这就是为什么我把demo略作修改,增加一行代码,但是输出结果依然是”val = 0”。

//修改后的demo

typedef  void  (^Block)(void);

Block block;

{

        int val = 0;

        block = ^(){

                NSLog(@"val = %d",val);

        };

        val = 1;

}

block();

既然无法实现修改截获的自动变量,那么编译器干脆就禁止程序员这么做了。

__block修饰符是如何做到修改变量值的

如果把val变量加上__block修饰符,编译器会怎么做呢?

//int val = 0; 原代码

__block int val = 0;//修改后的代码

编译后的代码:

struct __Block_byref_val_0 {

        void  *__isa;

        __Block_byref_val_0  *forwarding;

        int  __flags;

        int  __size;

        int  val;

};

struct   __main_block_impl_0 {

        struct   __block_impl impl;

        struct   __main_block_desc_0  *Desc;

        __Block_byref_val_0  *val;

        __main_block_impl_0(void  *fp,  struct  __main_block_desc_0 *desc, __Block_byref_val_0  *_val,  int  flags = 0) : val(_val->__forwrding){

                impl.isa  =  &_NSConcreteStackBlock;

                impl.Flags  =  flags;

                impl.FuncPtr  =  fp;

                Desc  =  desc;

        }

};

struct  void  __main_block_func_0(struct __main_block_impl_0  *__cself){

        __Block_byref_val_0  *val  =  __cself->val;

        printf("val = %d",val->__forwarding->val);

}

改动并不大,简单来说,只是把val封装在了一个结构体中而已。可以用下面这个图来表示五个结构体之间的关系。


但是关键在于__main_block_impl_0结构体中的这一行:

__Block_byref_val_0 *val;

由于__main_block_impl_0结构体中现在保存了一个指针变量,所以任何对这个指针的操作,是可以影响到原来的变量的。

进一步,我们考虑截获的自动变量是Objective-C的对象的情况。在开启ARC的情况下,将会强引用这个对象一次。这也保证了原对象不被销毁,但与此同时,也会导致循环引用问题。

需要注意的是,在未开启ARC的情况下,如果变量附有__block修饰符,将不会被retain,因此反而可以避免循环引用的问题。

回到开篇的两个问题,答案应该很明显了。

1.由于无法直接获得原变量,技术上无法实现修改,所以编译器直接禁止了。

2.都可以用来让变量在block中可以修改,但是在非ARC模式下,__block修饰符会避免循环引用。注意:block的循环引用并非__block修饰符引起,而是由其本身的特性引起的。


其他文章补充:

1.

Blocks可以访问局部变量,但是不能修改

如果修改局部变量,需要加__block。

声明block的时候实际上是把当时的临时变量又复制了一份,在block里即使修改了这些复制的变量,也不影响外面的原始变量。即所谓的闭包。但是当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。所以,在block里面对指针指向内容做的修改,在block外面也一样生效。

2.

在ARC的环境里面,默认情况下当你在block里面引用一个Objective-C对象的时候,该对象会被retain。当你简单的引用了一个对象的实例变量时,它同样被retain。但是被__block存储类型修饰符标记的对象变量不会被retain。

而在非ARC下,则不会retain这个对象,也不会导致循环引用。

3.

在垃圾回收机制里面,如果你同时使用__weak和__block来标识一个变量,那么该block将不会保证它是一直是有效的。 

4.__block和__weak修饰符的区别其实是挺明显的:

//使用了__weak修饰符的对象,作用等同于定义为weak的property。当原对象没有任何强引用的时候,弱引用指针也会被设置为nil。

//__block对象在block中是可以被修改、重新赋值的。

1)__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。

2)__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。

3)__block对象可以在block中被重新赋值,__weak不可以。

4)__block对象在ARC下可能会导致循环引用,非ARC下会避免循环引用,__weak只在ARC下使用,可以避免循环引用。

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

推荐阅读更多精彩内容