Block原理解析

Block语法简介

Block:可以理解为带有自动变量值的匿名函数。

Blocks提供了类似由C++和Objective-C类生成实例变量或对象来保持变量值的方法。

Block语法定义
^返回值 类型参数列表 表达式

^void (int event) { NSLog(@"。。。");}

Block类型变量定义
int (^blk)(int); 可以认为是匿名函数的地址,但是实际上它是是被看成对象来操作的,有自己的isa指针。

简单的Block原理分析

我们来分析最简单的block:我们定了一个变量名称为blk的Block变量,在定义部分省略了返回值和类型参数列表,然后在下面调用它,打出一串”Block”;


void (^blk)(void) = ^{printf("Block\n");};
blk();

源码通过clang,去掉一些类型转换我们可以得到以下代码


struct _block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//Block定义的结构体
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;
}
};
//我们的Block里面的函数
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}
//存储block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __mainBlock_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
/*以下是我们的代码部分*/
//赋值部分,
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//调用部分
(*blk->impl.FuncPtr)(blk);
/*以下是我们的代码部分*/

看起来好像很麻烦?居然两句代码变出了这么多代码。慢慢分析起来其实也不难理解

C++中,struct 约等于 class,唯一差别是struct中的默认成员属性是public的。class中的默认成员属性是private的。所以struct也可以拥有变量和函数。

首先系统自动给我们生成了三个结构体。


//block的结构体定义
struct __main_block_impl_0 {
struct _block_impl impl;//Block isa ,函数地址等定义
struct __main_block_desc_0 *Desc;//Block size等信息定义
};
struct _block_impl {
void *isa;//所属的类
int Flags;
int Reserved;
void *FuncPtr;//函数地址
};
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} 

生成了两个函数


//Block信息初始化的函数
__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;
}
//我们的Block里面的函数,_cself就是调用这个函数的调用者的指针
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}

我们定义Block的代码如下


void (^blk)(void) = ^{printf("Block\n");};

转化成


/*
初始化
__main_block_func_0:函数地址,
__mainBlock_desc_0_DATA:block的size信息
*/
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;

定义了一个main_block_impl_0的block,初始化函数为main_block_impl_0,传入函数指针和block的大小等信息


//Block信息初始化的函数
__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;
}

我们看到这个block的class类型为_NSConcreteStackBlock,这个下面会详细解释。函数地址为FuncPtr。所以iOS里的Block是被当成一个类来看待的,有自己的存储空间。可以理解为带有自动变量值的匿名函数

我们调用block的代码如下


//调用部分,
blk();

转化成


//调用部分
(*blk->impl.FuncPtr)(blk);

拿到上面定义的Block变量blk,找到函数地址,调用函数,并把调用者也就是blk传递进去。

Block会截获自动变量


int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt,val);};
val = 2;
fmt = "these values were changed. val = %d\n";
blk();

输出为输出

val = 10

而不是

these values were changed. val = 2

说明自动变量截获只能保存执行block语法瞬间的值

但我们知道加上__block,是可以在Block内部对变量进行修改的。详细讲__block(__block storage-class-specifier)为存储类型说明符,

c语言有以下说明符:

  • tydedef
  • extern
  • static:表示静态变量存储在数据区
  • auto:表示自动变量存储在栈
  • register:应将其保存在CPU的寄存器中(而不是栈或堆)

__block类似于后三种,表示将变量值设置到哪个存储区
如果我们加上__block


__block int val = 10;
void (^blk)(void) = ^{val=1;};

进行编译后,并剔除和以上通过clang一样的部分,我们看到以下不同


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

我们发现val变量居然变成了结构体实例__Block_byref_val_0,既在栈上生成了__Block_byref_val_0结构体实例,且初始化为10

^{val=1;}赋值过程变成什么样子了呢


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
}

找到Block下面的val变量,拿出val变量的__forwarding指向的val变量,拿出val变量下的val值赋值。如果Block此时在栈区,那么__forwarding指向val变量自己。

image

copy到堆后:__forwarding指向堆区的val变量

image

Block的类型

  • _NSConcreteStackBlock: 存储在栈区,需要截取变量
  • _NSConcreteGlobalBlock: 1.存储在程序数据区域,2.不需要截取变量
  • _NSConcreteMallocBlock: 存储在堆区

根据之前的分析,我们看到block的isa指针为_NSConcreteStackBlock,里面有个Stack,可以猜到,这个为存储在栈区的Block。

我们在记述全局变量的地方使用Block语法时候,生成的Block为_NSConcreteGlobalBlock,因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量的截获。


void (^blk)(void) = ^{printf("global Block");};
int main(){
}

那如何使得栈上的Block到堆上呢?

ARC 条件下编译器会适当判断,自动生成将block从栈上复制到堆上的代码。


//比如
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate * count;};
}

该代码返回设置在栈上的Block函数。但函数作用域结束,栈上的Block被废弃。但编译器自动会加上copy

什么情况下编译器不能进行判断要不要加copy,而需要手动执行copy?

  1. 向方法或者函数参数中传递block;
  2. 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的实例方法和 dispatch_async函数前,不用手动copy。

在用如NSArray的 initWithObjects 前,需要手动copy。


typedef void (^blk_t)(void);
NSArray *blocks = [self getBlockArray];
blk_t blk = (blk_t)[blocks objectAtIndex:0];
blk();
- (NSArray *)getBlockArray {
int val = 0;
return [NSArray arrayWithObjects: ^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk0:%d",val);}, nil];
}
//会发生崩溃。因为NSArray 的initWithObjects因为系统不确定加入的是不是block,不会自动执行copy操作,如果我们也不执行,在作用域外调用就会发生崩溃。

也许你会想,那么任何时候都用copy就好啦。但是从栈上的block copy到堆上很耗CPU。所以最好自己判断需不需要把Blockcopy到栈上

综上:我们要想把栈上的Block复制到堆上,只有执行copy方法,有些情况下,系统会自动帮我们执行,但也有些情况我们需要手动执行copy。

栈上的Block被复制到堆的情况

  • 手动调用Block的copy实例方法
  • Block作为函数返回值返回
  • 将block赋值给附有__strong修饰符id类型的类或Block类型的成员变量。
  • 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

注: __weak, __strong 用来修饰变量,此外还有 __unsafe_unretained, __autoreleasing 都是用来修饰变量的。
__strong 是缺省的关键词。
__weak 声明了一个可以自动 nil 化的弱引用。
__unsafe_unretained 声明一个弱应用,但是不会自动nil化,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针了。
__autoreleasing 用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。

各种类型的Block调用copy后

Block类型 存储区域 赋值效果
_NSConcreteStackBlock 栈-》堆
_NSConcreteGlobalBlock 程序数据区域 什么也不做
_NSConcreteMallocBlock 引用计数+1

所以不管任何时候copy方法复制都不会出错。但是多次调用copy会不会引起内存释放问题呢?


//多次调用copy
blk = [[[[blk copy] copy] copy] copy];
//代码解释
{
/*
将配置在栈上的Block赋值给blk变量。
*/
blk_t temp = [blk copy];
/*
将配置在堆上的block赋值给tmp变量,temp强持有Block
*/
blk = temp;
/*
将变量tmp的Block赋值为变量blk,blk强持有Block
此时block的持有者为变量temp和blk;
*/
}
/*
由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
由于Block的此时还被blk持有,所以没有废弃。
*/
{
/*
配置在堆上的Block被赋值给blk;同时变量blk持有强制引用的Block
*/
blk_t temp = [blk copy];
/*
将配置在堆上的block赋值给tmp变量,temp强持有Block
*/
blk = temp;
/*
将变量tmp的Block赋值为变量blk,blk强持有Block
此时block的持有者为变量temp和blk;
*/
}
/*
由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
由于Block的此时还被blk持有,所以没有废弃。
*/
/*下面重复*/

答案是 :多次调用copy完全不会有任何问题

一个含有__block变量的block被copy

__block变量的配置存储域 Block从栈赋值到堆时候的影响
从栈赋值到堆并被Block持有
被Block持有

我们看看以下代码,一个在栈上的Block

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

同样的如果Block在堆上两个输出也一样:

说明

无论在Block语法中,Block语法外使用__block变量,还是__block变量配置在栈上或者堆上,都可以顺利访问同一个__block

说到Block不得不谈循环引用问题,但是比较简单,网上一大堆,这里也不分析了。

小结

本文探索了Block的底层实现机制,我们发现Block在iOS中是作为对象来管理的。现在再看看这句话
Block:可以理解为带有自动变量值的匿名函数。是不是形容的很贴切。

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

推荐阅读更多精彩内容

  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 899评论 1 3
  • Block基础回顾 1.什么是Block? 带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言...
    Bugfix阅读 6,730评论 5 61
  • Block实际上是Objective-C对闭包的实现。 关于闭包的概念: In programming langu...
    chushen61阅读 338评论 0 0
  • Block是什么? Block实际上是Objective-C对闭包的实现。 关于闭包的概念:In programm...
    Gekkko阅读 1,416评论 0 12
  • 原创文章转载请注明出处,谢谢 这段时间重新回顾了一下Block的知识,由于只要讲原理方面的知识,所以关于Block...
    北辰明阅读 3,439评论 2 9