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变量自己。

Block原理解析_第1张图片
image

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

Block原理解析_第2张图片
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:可以理解为带有自动变量值的匿名函数。是不是形容的很贴切。

你可能感兴趣的:(Block原理解析)