关于Blocks,总得知道点什么

Blocks是iOS4之后引入的新特性,Blocks顾名思义为块,引申为代码块,使用Blocks可以很轻松的实现匿名函数以及函数的传递。

目录

  • Blocks语法
  • Block变量
  • 截获自动变量
  • Blocks实现
    • 自动变量
    • __block变量
  • Block生命周期
  • __block生命周期

Blocks语法

Blocks语法还是挺简单的,在C语言中,一个完整的函数需要包含返回值+函数名+参数列表+函数体,每一项都不可以省略,而在Blocks中,大部分都可以省略,除了Blocks的标志符^与方法体,一个完整的Blocks是这样的:

//完整Blocks
^int (int a){return a;}
//省略返回值的Blocks,返回值默认与return类型一致
^(int a){return a;}
//省略返回值与参数列表的Blocks
^{//爱干嘛干嘛}

Block变量

Blocks与函数相比,不仅书写上简便,运用上也更加灵活。Blocks可以作为变量使用,如下所示:

//名为blk的block变量
int (^blk)(int);
//给它赋值
blk=^int (int a){return a;}
//使用它
blk(1);

如果需要在函数返回值中返回Block,则可以写成如下形式

//func函数被"包裹"起来了
int (^func())(int){
    return ^(int a){return a;};
}

这样写阅读起来会比较麻烦,通用的做法是使用typedef定义一个类型。

typedef int (^blk_t)(int);

blk_t func(){
    return ^(int a){return a;};
}

截获自动变量

Blocks有两个十分重要的特性:

  • 截获自动变量
  • 截获变量不可修改

例如:

int a=1;
void (^blk)(void)=^{printf("a=%d\n",a);};
a=2;
blk();

打出的LOG是a=1,详细原理下文再详细解读,简单来说就是当block被创建的时候会自动截获它的函数体中所包含的变量。

截获的变量不可以修改,例如这样:

void (^blk)(void)=^{
            a=2;
            printf("a=%d\n",a);};

编译器会报错:

Variable is not assignable (missing __block type specifier)

自动变量不可修改,那对象是否能够修改呢,例如这样:

NSMutableArray arr=[[NSMutableArray alloc]init];
void (^blk)(void)=^{
            
            arr=[[NSMutableArray alloc]init];
            };

这样也是不行的,那我们折中一下,换成修改对象结构呢?

NSMutableArray arr=[[NSMutableArray alloc]init];
void (^blk)(void)=^{
            id obj=[[NSObject alloc]init];
            [arr addObject:obj];
            };

这样是被系统允许的,究其原因是因为第二种修改方式虽然修改了arr的结构,但是它指向的地址却没有被修改,所以即使我们调用addObject修改了arr的结构,在系统看来也只是"使用"arr而已。

如果非要在Block中修改变量,我们可以使用__block修饰符修饰变量。就像这样

__block int a=1;
void (^blk)(void)=^{
a=3;
printf("a=%d\n",a);};
a=2;
blk();

打出的LOG为a=3。

Block实现

自动变量

Block语法虽然看起来有些复杂,但是被编译的时候还是作为C语言源代码来编译的。
让我们用clang编译器编译一下,虽然与LLVM编译器有点差异,但是差异不是很大,看看在clang下的代码,对理解Blocks有很大好处。我们编译这段最简单的代码,在xcode新建一个OC文件

int a=1;
void (^blk)(void)=^{
printf("a=%d\n",a);};
blk();

我们找到目录下的main.m文件,在命令行输入

cd main.m文件所在目录
clang -rewrite-objc main.m

我们会编译出一个main.cpp文件。打开文件,会出现一大堆代码,但是这都不是我们需要关注的,我们要关注的只是一小段代码。拖到底部,这段才是我们需要关注的代码。

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("a=%d\n",a);}

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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a=1;
        void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;

    }

}

很明显

void (^blk)(void)=^{printf("a=%d\n",a);};

被编译成了

void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

我们把类型转换的语法删除,其实剩下的就是

*blk=__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);

我们接着看,__main_block_impl_0实际上是一个结构体,调用的是它的构造方法。

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

C++中,构造函数后面紧跟着冒号加初始化列表,各初始化变量之间以逗号(,)隔开。因此,当我们初始化block的时候,变量值已经被存储到结构体的变量中了。在block后改变自动变量的值就会“失效”。

那轮到我们调用block的时候又是怎样来调用的?

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

是不是看得一头雾水,一步一步先把前面的格式去掉,式子就还剩

((__block_impl *)blk)->FuncPtr)((__block_impl *)blk)

到这里我们大致可以明白,blk先被强制类型转换成__block_impl,然后再调用其中的FuncPtr变量。在我们去看__block_impl定义之前,先关注一下blk的类型,blk是一个void指针类型,但是实际上它指向的是__main_block_impl_0这个结构体。这个结构体的空间结构如下:


根据结构体指针的强制类型转换规则(详情点击这里),这个blk指针被强制转换成了__block_impl指针,然后再取出它里面的FuncPtr变量,其定义如下:

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

既然调用了FuncPtr变量,那我们就从头开始寻找它到底什么时候被赋值,结果发现在blk初始化的时候传入的__main_block_func_0最终被赋到了FuncPtr上,__main_block_func_0就是我们的匿名函数:

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

            printf("a=%d\n",a);}

到这里就可以解释前面两个重要的特性了:

  • 截获自动变量

截获的变量在初始化的时候就被存储起来,使用的时候再拿出来

  • 截获变量不可修改

这主要是因为截获的变量为自动变量,再次使用的时候超出了自动变量的作用域,如果换成其他全局或者静态变量则可以被修改。这里可能还有人要较真,如果是静态局部变量呢?不是也和自动变量一样超出作用域了吗?这里偷懒就不贴代码了,当变量为静态局部变量时,block中保存的是静态局部变量的指针而不是静态局部变量的值,最后修改的时候会通过保存下来的指针找到静态局部变量再进行修改,因此可以在block中修改静态局部变量。似乎自动变量也可以通过保存指针的方式进行存储,其实是行不通的。究其原因是因为自动变量的生命周期会随作用域的结束而结束,即使保存了指针也无法访问到自动变量,而静态变量的生命周期是整个程序的生命周期。

这是简单的block实现图,画得丑大概就这样了


__block变量

上文说到自动变量在block中是不可修改的,如果非要修改可以加上__block修饰符,那我们编译一下,看看block又是怎么处理__block修饰符的。

源代码:

int __block a=1;
int __block b=2;
void (^blk)(void)=^{
   a=3;
   b=4;
   printf("a=%d\n,b=a=%d\n",a,b);};
blk();

编译后:

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __Block_byref_b_1 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__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
  __Block_byref_b_1 *b = __cself->b; // bound by ref

            (a->__forwarding->a)=3;
            (b->__forwarding->b)=4;
            printf("a=%d\n,b=a=%d\n",(a->__forwarding->a),(b->__forwarding->b));}
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*/);_Block_object_assign((void*)&dst->b, (void*)src->b, 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*/);_Block_object_dispose((void*)src->b, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
        __Block_byref_b_1 b = {(void*)0,(__Block_byref_b_1 *)&b, 0, sizeof(__Block_byref_b_1), 2};
        void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, (__Block_byref_b_1 *)&b, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;

    }

}

编译后的代码略长,跟上面分析一样的流程就不说了。看点不一样的。很显然,我们加过__block修饰符的变量已经成为一个结构体了。

这是它的构造函数以及初始化过程:

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};

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

这个结构体不仅存储了我们赋予的值(1),还存储了自身结构体的指针(forwarding),这主要是为了保证数据的一致性,等到__block变量生命周期时再解释:

这是block的构造函数:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) 

我们传入的a以指针的形式被存储了下来。最后是修改__block部分

  __Block_byref_a_0 *a = __cself->a; // bound by ref
  __Block_byref_b_1 *b = __cself->b; // bound by ref

            (a->__forwarding->a)=3;
            (b->__forwarding->b)=4;

Block先拿到自身保存的a(结构体)的指针,再从指针中拿到指向a(结构体)的指针,最后从该指针从拿到保存的a(变量)的值进行修改。这么说可能有点绕,但是过程就是这样,__block将变量包裹起来,然后把指针传递给Block保存,需要修改的时候再从Block->__block->变量拿出来。

Block生命周期

Block在oc中被当做对象来处理,其成员变量中的isa可以佐证这一点。
isa有三种参数

  • NSConcreteStackBlock
  • NSConcreteGlobalBlock
  • NSConcreteMallocBlock

这三个参数分别对应着Block的存储域(栈,堆,data区)。
我们之前的代码中,Block作为一个自动变量出现,因此参数为NSConcreteStackBlock,如果把Block做成一个全局变量或者静态变量,那么参数就会变成NSConcreteGlobalBlock。不过在ARC中,NSConcreteStackBlock大部分在编译时就被转换成了NSConcreteMallocBlock或者NSConcreteGlobalBlock。这种转换是有好处的,可以使Block超出作用域存在。

一般来说,大部分的Block都生成在栈上,栈上Block的生命周期与所属变量的作用域同步。

void (^blk)(void)=^{printf("a=%d\n",a);};

当blk的作用域结束时,Block也被废弃了。看上去合情合理,接着看下面的例子:

typedef int (^blk_t)(int);
{
    NSArray *arr=[self getArr];
    blk_t blk=(blk_t)[arr objectAtIndex:0];
    blk();
}
-(NSArray*)getArr{
    int a=10;
    //Block被做成了自动变量并作为参数传入方法
    return [[NSArray alloc]initWithObjects:^{NSLog(@"1000000000%d",a);},^{NSLog(@"20000000");}, nil];
}

这段代码在ARC是能够运行的,但是在MRC中却有可能CRASH掉,错误为EXC_BAD_ACCESS,原因在于我们试图访问一个已经被释放的对象。
当我们运行到blk()之前是这样的:



当调用blk()之后就变成了这样:



我们的Block已经被释放掉了,所以会报错。之前为什么说可能CRASH掉,是因为Block释放需要一些时间(具体多少不太清楚),不是超出作用域就释放,在释放之前调用blk()还是有可能访问到未被释放的Block的。

如果在ARC中又会怎样呢?如图所示:


Block从栈上被复制到了堆里,堆里的Block作为副本,并不会随着栈上Block的释放而释放,因此可以调用Block(ps:顺带解释一下数组第二个Block为什么会成为NSGlobalBlock,通过clang转换以后的代码与xcode本身转换会有一些差异,当Block不使用应截获的变量时(a),它会被系统做成NSGlobalBlock而不是NSStackBlock)。虽然这样写不会CRASH,但是这是一个不好的习惯,Block并不是每一次都会被复制到堆里,数组只有第一个Block会自动从栈上复制到堆里。我们改成这样,即使是ARC也会被CRASH掉:

return [[NSArray alloc]initWithObjects:^{NSLog(@"1000000000%d",a);},^{NSLog(@"20000000%d",a);}, nil];

正确的写法应该是这样,手动的把Block复制到堆里,虽然写法有点奇怪

return [[NSArray alloc]initWithObjects:[^{NSLog(@"1000000000%d",a);} copy],[^{NSLog(@"20000000%d",a);} copy], nil];

总结一下,Block的生命周期和它被做成的对象有很大关系,做成NSStackBlock时常常在自动变量作用域结束时被释放,如果想要延长Block的生命周期,可以将Block copy到堆上(ARC中系统会自动帮忙做,当Block被作为方法参数时例外),堆上的Block不随栈上Block的释放而释放,堆上的Block遵循对象的引用计数方式管理生命周期。

__block变量的生命周期

__block变量的生命周期和Block差不多,也是一言不合就释放的主,上文说到Block会被复制到堆中,Block中的__block自然也是"嫁鸡随鸡,嫁狗随狗"了,那么问题来了,既然是复制,那么同一时间,__block有可能会存在两个(栈与堆),这可不行,因为我们要保证数据的一致性,万一栈上的__block修改了变量,而堆上的没有修改,那就会导致数据不一致。为了防止这种情况的发生,__block采用了__forwarding。__block被复制的时候,__forwarding指向堆上的block,而堆上的__forwarding仍然指向自身。如图所示:

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

推荐阅读更多精彩内容