iOS开发-消息传递方式-Block

说过了target-action

说过了KVO

说过了NotificationCenter

这次我们来说一个一对一的消息传递方式,Block

Block是什么

Block,很多语言中翻译做闭包,用《Objective-C高级编程》中的话说:

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的额匿名函数。

所以,Block就是一个带有自动变量匿名函数

匿名函数

顾名思义,匿名函数就是没有名称的函数,C语言是不允许出现没有函数名的函数的,但是因为实际上调用函数也是调用指向函数的函数指针,但是没有函数名,就没办法获取到函数的指针。那么block具体是怎么来实现的呢?我们先往下看。

自动变量

在栈上声明一个变量如果不是静态变量或全局变量,是不可以在这个栈内声明的匿名函数中使用的,但是在block中却可以。

Block结构

在Xcode里,我们敲入快捷键inlineBlock就会看到有这样一个block的样式提供给我们。


//增加了returnType,可省略

 returnType(^blockName)(parameterTypes) = ^returnType(parameters) {

 statements

 };

其中第二个returnType是我加上去的,为了能看的明显一点。

但是一下子看到一个这样的东西还是有点乱,我们把它拆分成声明部分和实现部分来看就会清楚很多了。

声明Block


returnType(^blockName)(parameterTypes)

声明block中包括了返回类型、^、block名称、参数列表。

实现Block


^returnType(parameters) {

 statements

 };

实现block中包括了返回类型(可省略)、参数列表(可省略)、实现代码。

其中返回值类型可以省略。


^(parameters) {

 statements

 };

参数列表也可以省略。


^{statements};

Block的使用

因为block的传入参数和返回值都可以为空,所以Block的使用可以分为4中模式:

  • 1.无参数、无返回值。

  • 2.有参数、无返回值。

  • 3.无参数、有返回值。

  • 4.有参数、有返回值。

接下来我们就来举例子看看这几种方式的使用。

无参数、无返回值


//无参数、无返回值

- (void)blockWithoutParameterAndWithoutReturn

{

 void(^noParameterNoReturn)(void) = ^(void){

 NSLog(@"无参数、无返回值");

 };

 noParameterNoReturn();

}

有参数、无返回值


//有参数、无返回值

- (void)blockWithParameterAndWithoutReturn

{

 void(^parameterNoReturn)(NSInteger number) = ^(NSInteger number){

 NSLog(@"有参数、无返回值,参数是%lu",number);

 };

 parameterNoReturn(10);

}

无参数、有返回值


//无参数、有返回值

- (void)blockWithoutParameterAndWithReturn

{

 NSInteger(^noParameterReturn)(void) = ^{

 NSInteger number = 20;

 NSLog(@"无参数、有返回值,返回值是%lu",number);

 return number;

 };

 NSInteger number = noParameterReturn();

 NSLog(@"返回值是%lu",number);

}

有参数、有返回值


//有参数、有返回值

- (void)blockWithParameterAndWithReturn

{

 NSInteger(^parameterAndReturn)(NSInteger numberA, NSInteger numberB) = ^(NSInteger numberA, NSInteger numberB){

 NSLog(@"有参数、有返回值,参数是%lu、%lu,返回值是%lu",numberA,numberB,numberA+numberB);

 return numberA+numberB;

 };

 NSInteger numberSum = parameterAndReturn(30,40);

 NSLog(@"返回值是%lu",numberSum);

}

使用typedef定义

除了上边的常规操作之外,block还可以作为OC中的一个参数,这时候可以用到typedef来定义一个block,然后在函数调用时进行参数传递。

比如先定义一个block参数:


//number作为参数,无返回值

typedef void(^typedefBlock)(NSInteger number);

然后声明一个函数中带有此变量


//typedef block

- (void)testTypedefBlockWith:(typedefBlock)testTypedefBlock

{

 NSLog(@"开始使用typedef block");

 testTypedefBlock(12);

 NSLog(@"结束使用typedef block");

}

这时候调用此方法,在回调的方法中就可以获取到传递过来的值。


 [self testTypedefBlockWith:^(NSInteger number) {

 NSLog(@"回调 typedef block number %lu",number);

 }];

Block与外界变量

默认情况

通常情况下,对于block外的变量引用,block默认是将其复制到block的数据结构中实现访问的,也就是说只有block中用到的变量,block才会把他自动截获进来,而且因为截取的是瞬时值,所以之后在外部改变变量的值也不会改变值得大小。因为截获自动变量会存储在block内部,所以会导致block体积变大。

另外需要注意的一点就是block内部只能调用getter方法,不可以调用setter方法,所以是没办法修改外部变量的值的。

iOS开发-消息传递方式-Block_第1张图片
block-capture-1.jpg

比如:


- (void)autoParamterTest

{

 NSInteger number = 100;

 void(^autoParamter)(void) = ^(void){

 NSLog(@"%lu",number); //输出100 

 };

 number = 200;

 autoParamter();         

}

这段代码最后会输出100,因为在定义block时,他已经把number的值复制到block中了,所以再改变他,对block中的值也不会有影响。

另外,在block中对number赋值时,编译器会直接报错。

[图片上传中...(block-setter.png-8afa38-1521083690526-0)]

__block

对于这种情况,OC提供了_block(两个下划线)来修饰外部变量,使用了__block修饰的外部变量,block内部是复制其引用地址来实现访问数据的,所以block内部可以修改block外部的变量值。

iOS开发-消息传递方式-Block_第2张图片
block-capture-2.jpg

- (void)autoBlockParameterTest

{

 __block NSInteger number = 100;

 void(^autoBlockParamter)(void) = ^(void){

 NSLog(@"%lu",number); //输出200

 number = 300;

 NSLog(@"%lu",number); //输出300

 };

 number = 200;

 autoBlockParamter();

}

那为什么在加了 __block修饰符之后就可以访问了呢?后边我们会详细说明,我们先往下看。

Block的循环引用

Block是很好用,但是用不好的时候就容易出现循环引用,比如在某各类将block作为自己的变量,然后又在这个block的方法中使用了这个类自己的东西,这时候两者互相持有就会发生循环引用,引起内存泄漏的问题。比如如下代码:


- (void)blockCircularReference

{

 self.circleBlock = ^(NSInteger number) {

 [self autoParamterTest];

 };

}

但是苹果也给出了相应的解决方案来处理block下的循环引用。

__weak修饰

可以直接用__weak(有两个下划线)来修饰,来打破block中的循环,使用__weak修饰解决循环引用一共有三种实现的方式。

  1. 使用__weak ClassName

- (void)blockCircularReference

{

 __weak MPBlockViewController *weakSelf = self;

 self.circleBlock = ^(NSInteger number) {

 [weakSelf autoParamterTest];

 };

}

  1. 使用__weak typeof(self)

- (void)blockCircularReference

{

 __weak typeof (self) weakSelf = self;

 self.circleBlock = ^(NSInteger number) {

 [weakSelf autoParamterTest];

 };

}

  1. 使用Reactive Cocoa中的@weakify和@strongify

- (void)blockCircularReference

{

 @weakify(self);

 self.circleBlock = ^(NSInteger number) {

 @strongify(self);

 [self autoParamterTest];

 };

}

@weakify, @strongify的具体使用可以看这里

__block

在MRC下,可以直接使用__block进行修饰。

也可以先用__block修饰,然后在block方法中使用完将其设为nil,但是要注意就是block必须要被调用一次。


- (void)blockCircularReference

{

 __block MPBlockViewController *blockSelf = self;

 self.circleBlock = ^(NSInteger number) {

 [blockSelf autoParamterTest];

 blockSelf = nil; //必须设为nil

 };

 self.circleBlock(10);  //必须至少调用一次

}

将self作为参数传递

也可以直接将self作为一个参数传递到block中。


- (void)blockCircularReference

{

 self.circleBlock = ^(MPBlockViewController *vc) {

 [vc autoParamterTest];

 };

}

Block的实现

block实际上是用C语言源码来处理的,含有block的源码首先被转换成C语言编译器能够处理的源码,再作为C进行编译。

Clang

使用LLVM编译器的clang可以将OC的代码翻译成C++的源代码,说是C++的代码,但是实际上也就是C语言的源代码。

使用的方式就是打开Terminal,cd到源代码文件目录,输入:


clang -rewrite-objc 源代码文件名

比如这样一段代码(这里没有引用其他OC的框架,因为引入之后clang出来的cpp文件会巨大,有好几千行):


#include 

int main() {

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

 blk();

 return 0;

}

这段简单的block代码clang之后就会变成如下源码(这里删除了部分代码,只显示了重要的部分):


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;

 }

};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("Block\n");}

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 (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

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

 return 0;

}

乍一看,我天,这都是什么鬼啊 = =。 没事我们一部分一部分来看。

__cself

这里边的参数__cself就相当于C++中指向自身的变量this,在OC中就是self,即参数__cself就是指向block值的变量。

__block_impl

__block_impl是我们要介绍的第一个block中的成员变量,他是一个结构体,其结构如下:


struct __block_impl {

 void *isa;

 int Flags;

 int Reserved;

 void *FuncPtr;

};

  • isa指针,所有对象都有改指针,用于实现对象相关的功能。

  • Flags,用于按bit位表示一些block的附加信息。

  • Reserved,保留变量。

  • FuncPtr,函数指针,指向block要执行的函数,即__main_block_func_0。

__main_block_desc_0

__main_block_desc_0是我们要介绍的第二个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)};

  • reserved,结构体信息保留字段。

  • Block_size,block的大小。

初始化__main_block_func_0

另外一部分就是__main_block_func_0的初始化,用到的就是之前介绍的两个结构体。


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;

 }

};

实现__main_block_func_0

这里主要就是我们在block中要实现的代码。


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("Block\n");

}

实现main函数

另外main函数的源码在这里。


int main() {

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

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

 return 0;

}

截获自动变量

看完了block的基本实现方式,那我们再看看他是如何截获自动变量呢?

我们先定义一个number


#include 

int main() {

 int number = 10;

 void (^ blk)(void) = ^{printf("%d",number);};

 blk();

 return 0;

}

这时候clang之后发现:


struct __main_block_impl_0 {

 struct __block_impl impl;

 struct __main_block_desc_0* Desc;

 int number;  //number被直接加入了__main_block_impl_0结构体中

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

 impl.isa = &_NSConcreteStackBlock;

 impl.Flags = flags;

 impl.FuncPtr = fp;

 Desc = desc;

 }

};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

 int number = __cself->number; // bound by copy

printf("%d",number);}

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 number = 10;

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

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

 return 0;

}

我们看到number被直接加到了__main_block_impl_0结构体中。

__block

这时候我们再用__block来修饰一下number看看:


#include 

int main() {

 __block int number = 10;

 void (^ blk)(void) = ^{printf("%d",number);};

 blk();

 return 0;

}

clang之后发现:


struct __Block_byref_number_0 {

 void *__isa;

__Block_byref_number_0 *__forwarding;

 int __flags;

 int __size;

 int number;

};

struct __main_block_impl_0 {

 struct __block_impl impl;

 struct __main_block_desc_0* Desc;

 __Block_byref_number_0 *number; // by ref

 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__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_number_0 *number = __cself->number; // bound by ref

printf("%d",(number->__forwarding->number));}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 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_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 10};

 void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));

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

 return 0;

}

我们只是加了一个__block,结果代码一下子增加了巨多!

这时候仔细看代码,就能发现多了一个段代码:


__Block_byref_number_0 number = {

(void*)0,(__Block_byref_number_0 *)&number,

 0, 

 sizeof(__Block_byref_number_0),

 10};

找到这个结构体的声明:


struct __Block_byref_number_0 {

 void *__isa;

__Block_byref_number_0 *__forwarding;

 int __flags;

 int __size;

 int number;

};

那如果这时候我们给number赋一个新的值会怎么样呢?


#include 

int main() {

 __block int number = 10;

 void (^ blk)(void) = ^{

 number = 20;

 printf("%d",number);

 };

 blk();

 return 0;

}

clang后发现多了这里变化(就不贴全部代码了):


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

 __Block_byref_number_0 *number = __cself->number; // bound by ref

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

 printf("%d",(number->__forwarding->number));

 }

我们看到在向__block变量赋值时,block的__main_block_impl_0结构体实例持有指向__block变量的__Block_byref_number_0结构体实例的指针。

__Block_byref_number_0结构体的实例的成员变量__forwarding持有指向该实例自身的指针。并通过成员变量__forwarding访问成员变量val。

那么__forwarding又是什么呢?别急,后边我们再说他。

Block的存储域

在__main_block_func_0的初始化时,我们看到了有一行代码是:


 impl.isa = &_NSConcreteStackBlock;

从名字应该可以判断出来,这行代码的意思是这个block是存储在栈上的。

那么除了栈,block还存储在哪些地方呢?

| 类 | 设置对象的存储域 |

| ------| ------ |

| _NSConcreteStackBlock | 栈块 |

| _NSConcreteGlobalBlock | 全局块 |

| _NSConcreteMallocBlock | 堆块 |

iOS开发-消息传递方式-Block_第3张图片
block_Storage_Domain.png

从名字就能看出来:

  • 栈块存在栈内存中,超出作用域后就会马上销毁。

  • 全局块在全局内存中,和全局变量一样。

  • 堆块存在堆内存中,是一个带有引用计数的对象,需要自行管理内存。

那么我们怎么样能够知道block是保存在哪里呢?

全局块

一般情况下,当满足以下情况时,block为_NSConcreteGlobalBlock类对象,也就是放在全局数据区。

  • 1.记录全局变量的地方有block语法时。

  • 2.block语法的表达式中不使用应截获的自动变量时。

栈块

理论上,除了在全局块条件之外的情况下,block都为_NSConcreteStackBlock类对象,也就是设置在栈区。

堆块

那么如果这样说起来,岂不是没有block会在堆上了么?

这就要说到一个问题,就是ARC和MRC下block的不同情况,MRC下访问外界变量的block默认就是存储在栈中了,但是ARC下,block会自动被从栈区拷贝到堆区,然后自动释放。

那为什么ARC下,访问外部变量的block会自动从栈区拷贝到堆区呢?

block中的copy

在栈上的block,如果所在的作用域结束,block和block中的__block变量都会被废弃掉。

iOS开发-消息传递方式-Block_第4张图片
block_copy_1.png

所以,我们需要将Block复制到堆中,延长其生命周期,这样即使是block所在的作用域结束,block还是可以在堆中继续存在。

开启了ARC时,大多数情况下编译器会恰当的判断是否有需要将block从栈复制到堆,如果有,自动生成将block从栈上复制到堆上的代码,block复制执行的是copy实例方法,只要调用了copy方法,栈块就会变成堆块,一般在如下情况时,block会自动copy到堆上。

  • 1.调用Block的copy方法。

  • 2.将Block作为函数返回值时(MRC下需要手动调用copy,否则无效)。

  • 3.将Block赋值给__strong修改变量时(MRC时无效)。

  • 4.向Cocoa框架中含有usingBlock的方法或者GCD的API传递Block参数时。

iOS开发-消息传递方式-Block_第5张图片
block_copy_2.png

 int count = 0;

 blk_t blk = ^(){

 NSLog(@"In Stack:%d", count);

 };

 NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__

 NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__

 NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__

 NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

block的复制操作执行的是copy实例方法,不同类型的block使用copy方法的效果如下:

| block 的类 | 副本源的配置存储域 | 复制效果 |

| ------| ------ | ----- |

| _NSConcreteStackBlock | 栈块 | 从栈复制到堆 |

| _NSConcreteGlobalBlock | 全局块 | 什么也不做 |

| _NSConcreteMallocBlock | 堆块 | 引用计数增加 |

不管block配置在何处,用copy方法复制都不会引起任何问题,在不确定是调用copy即可。


blk = [[[[blk copy] copy] copy] copy];

// 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。

__block变量的存储域

之前只说到了block,那__block变量又会有什么影响呢?使用__block变量的block从栈复制到堆上时,__block变量也会受到影响。

| __block变量的配置存储域 | block从栈复制到堆时的影响 |

| ------| ------ |

| 栈 | 从栈复制到堆并被block持有 |

| 堆 | 被block持有 |

那么栈上的__block变量复制到堆上之后,block是可以同时访问栈上的__block变量和堆上的__block变量,但是具体访问时到底是访问栈上的还是堆上的呢?这时候还记得我们之前说的****__forwarding****变量么?

iOS开发-消息传递方式-Block_第6张图片
block_copy_3.png

通过__forwarding, 无论是在block中还是 block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

Block的实践

说了这么多,我们来看看block在实际开发中比较常见的使用方法吧。

一般情况下,block会用来作为方法回调的功能,和代理的方法比较相似,处理一些比较耗时的操作比如网络数据的下载,在下载好之后直接调用block回调,返回正确或错误的信息。

block会使得代码结构紧凑,逻辑清晰,接下来我们就看一个简单的:

首先我们先声明一个typedef block


typedef void(^MPBlockDownloadHandler)(NSData * receiveData, NSError * error);

然后在下载函数中传入block作为参数,并在下载结束后调用block。


- (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (MPBlockDownloadHandler)handler

{

 NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];

 NSURLSession * session = [NSURLSession sharedSession];

 //执行请求任务

 NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

 if (handler) {

 dispatch_async(dispatch_get_main_queue(), ^{

 handler(data,error);

 });

 }

 }];

 [task resume];

}

最后调用这个函数:


 [self downloadWithURL:@"https://www.vactualpapers.com/web/wallpapers/sights-and-scenes-of-beautiful-singapore-hd-wallpaper-29/thumbnail/lg.jpg" parameters:nil handler:^(NSData *receiveData, NSError *error) {

 if (error) {

 NSLog(@"下载失败:%@",error);

 }else {

 NSLog(@"下载成功,%@",receiveData);

 }

 }];

这样,一个简单的利用block实现网络加载回调的功能就做好了。

最后

好了,这就是block的全部内容了。说是写消息传递,好像越来越跑偏了。。。

另外以上内容仅供个人学习使用,大部分内容来自《Objective-C高级编程 iOS与OS X多线程和内存管理》。如果有什么地方不对,还请大佬们多多指教。

参考文档

Objective-C 高级编程

iOS开发-由浅至深学习block

《Objective-C 高级编程》干货三部曲(二):Blocks篇

谈Objective-C block的实现

译 Block 小测验

iOS Block用法和实现原理

iOS Block 详解

iOS开发-消息传递方式-Block_第7张图片
block_copy_2.png

你可能感兴趣的:(iOS开发-消息传递方式-Block)