block再学习
- 什么是block
- block是带有自动变量的匿名函数
- block语法
- block的实现
- block的实质
- 截获自动变量
- __blcok说明符
- Block存储域
- __block变量存储域
- 使用__block变量用结构体成员变量__forwarding的原因
- 截获对象
什么是block
Block时c语言的扩充功能,它允许开发者定义一段可重用的代码,并在需要时像变量一样使用这段代码。
对于block最重要的几个特点,总结如下:
- Block本质上是一个OC对象,具有自己的isa指针。
- 它可以看作是带有自动捕获变量能力的匿名函数。
- Block可以捕获和存储它所在的环境中的变量和常量。
首先我们先从它可以看作是带有自动捕获变量能力的匿名函来了解和分析block ;
block是带有自动变量的匿名函数
首先我们先来了解一下自动变量的概念 ;
在 Objective-C(OC)中,自动变量(Automatic Variables)通常指的是在函数或方法内部定义的局部变量,这些变量在函数或方法被调用时自动在栈(stack)上分配内存,并在函数或方法执行完毕后自动释放内存。这些变量的作用域仅限于定义它们的函数或方法内部。
其实在这个地方我们可以把自动变量理解为局部变量 ;
在Objective-C中,Block可以捕获其定义范围内可见的局部变量,但是它们捕获这些变量的方式取决于这些变量的存储类型修饰符。修饰符之后仔讲 ;
匿名函数如它的名称一样,是一种没有名称的函数。
那我们为什么要用block,而不直接用函数呢,明明普通函数能实现,
以下是使用Block的一些主要原因:
-
闭包(Closure)特性:
Block可以捕获其定义范围内的变量和常量,包括外部函数的局部变量和全局变量。这使得Block可以访问和操作这些变量,就像它们是Block自己的局部变量一样。这种闭包特性使得Block能够封装和保存函数的状态,从而实现更复杂的逻辑。 -
匿名性:
Block没有名称,因此它们可以作为参数传递给其他函数或方法,或者作为属性存储在对象中。这使得Block可以方便地在代码之间传递和使用,而无需担心命名冲突或额外的命名空间管理。 -
类型安全:
虽然Block在语法上类似于C函数,但它们是Objective-C的类型,并且支持类型检查。这意味着可以在编译时捕获与Block类型相关的错误,从而提高代码的质量和可维护性。 -
简洁性: Block可以内联定义在需要使用它们的代码块中,无需单独声明和定义函数。这使得代码更加简洁和易读,同时减少了函数调用的开销。
-
异步编程:
在Objective-C中,异步编程通常涉及到回调函数的使用。使用Block作为回调函数可以简化代码结构,并减少嵌套回调的复杂性。例如,使用Grand
Central Dispatch (GCD)进行异步任务时,可以使用Block作为任务完成后的回调处理程序。 -
响应式编程:
Block可以与响应式编程模式结合使用,以处理异步事件和流数据。通过使用Block来处理事件和数据流,可以创建更加灵活和可响应的应用程序。 -
与Objective-C对象的集成:
Block可以与Objective-C对象无缝集成,并且可以轻松地在Block内部访问和操作对象属性和方法。这使得Block成为处理Objective-C对象和集合类的强大工具。 -
内存管理:
在Objective-C中,Block的内存管理可以通过__block修饰符和ARC(自动引用计数)来管理。这减少了手动管理内存的需要,并降低了内存泄漏和野指针的风险。简单的说,block提供了更多的灵活性和便利性,特别是在与Objective-C对象交互的上下文中。
顺便说一下:带有自动变量的匿名函数这一概念并不指blocks,它还存在于许多其他程序语言中;
block语法
Block 声明和定义:
returnType (^blockName)(parameters) = ^(parameters) {
// Block 的实现代码
};
- returnType:Block 返回的类型,如果 Block 没有返回值,则为 void。
- blockName:Block 的名称,可选,通常省略以创建匿名 Block。
- parameters:Block 接收的参数列表,如果没有参数则为空。
- ^:这是 Block 的字面量语法,用于开始定义 Block。
如:
int (^addBlock)(int, int) = ^(int a, int b) {
return a + b;
};
使用 Block:
int sum = addBlock(3, 4); // sum 现在为 7
Block 类型声明:
当不使用 typedef 来声明 Block 变量类型时:
// 声明一个返回 int 类型,接受两个 int 类型参数的 Block 变量
int (^addBlock)(int, int) = ^(int a, int b) {
return a + b;
};
// 调用 Block
int result = addBlock(3, 4);
NSLog(@"The result is: %d", result); // 输出 "The result is: 7"
使用 typedef 来声明 Block 变量类型时:
// 使用 typedef 声明 Block 类型别名
typedef int (^IntToIntBlock)(int, int);
// 使用类型别名声明 Block 变量
IntToIntBlock multiplyBlock = ^(int a, int b) {
return a * b;
};
// 调用 Block
int product = multiplyBlock(3, 4);
NSLog(@"The product is: %d", product); // 输出 "The product is: 12"
第二种声明很简单,主要注意一下第一种声明,或者两者间的区别 ;
如:
int (^ func ()) (int) {
}
上面这个形式看起来就挺怪的,但实际上这是一个返回值为block类型的函数func;
block的实现
block的实质
block实际上是作为普通的c语言代码来处理的 ;
原代码:
通过clang转换后的可读源代码:
通过block使用的匿名函数实际上被作为简单的语言函数来处理;
_cself为指向block得变量 ;
从上面转换过来的源码可以看出block在c语言中的结构就是一个结构体 ;
结构体的声明如下:
这事一个嵌套的结构体,这里就顺便给出其中的两个结构体的声明 ;
isa指针在对象,类,元类中就了解了,这也说明了block是一种对象;flag是一种标志,具体不太清楚;Reserved是版本升级所需要的区域;FuncPtr是指向函数的指针,实际上这个函数也是block中的具体实现 ;
Block——size是Block的大小,也是结构体的大小 ;
这里的构造函数初始化了Block结构体的成员变量,Blcok中自动变量的捕获也在这里完成 ;
这里会想下面这样初始化在:
isa指向了这个对象的类;FuncPtr和Desc分别指向了它们的构造函数,完成成员变量的初始化 ;
主函数中的
转换后:
这段代码将在栈上生成的__main_block_impl_0结构体实例的指针赋值给变量blk ;
对应了:
而:
则对应着blk();
截获自动变量
先给出转换后的源代码:
对比一下前面的源代码,这里只有结构体和使用时调用的函数不一样 ;
先看结构体:
其实从这个结构我们也可以猜到Block对自动变量的捕捉是通过成员变量赋值实现的 ;
这里也解答了,为什么Block无法捕获c语言中的数组 ,因为在c语言中数组是无法直接赋值的,但可以通过指针实现 ;
顺便注意一下,Blcok语法表达式中没有使用的自动变量是不会被捕获的,甚至在Block结构体中都不会声明它的成员变量 ;
这里相当于setting方法;
这个相当于getting方法 ;
总的来说,“截获自动变量”意味着在执行Block语法时,Block表达式所使用的自动变量被保存到Block结构体中去了 ;
__blcok说明符
一般来说,Blcok会截获Block语法中使用过的自动变量,即使在Block外对这些自动变量进行重写,也不会改变Block中已经捕获的值;如果尝试在Block内部进行重写 ,编译器在编译时会自动检测并抛出错误;
这也意味着不便,有什么办法能在Block中直接改写Block中的自动变量呢 ;
这里有两种方法:
1.c语言中有部分变量是允许Block改写值的;
- 静态变量
- 全局变量
- 静态全局变量
具体可以看看两端代码转换前后的对比:
这里发现全局变量的使用与c语言中无二,甚至不会有Block的捕获机制;
但静态变量在结构体中是以指针捕获的;至于为什么用指针,这是超出作用域使用变量的最简单方法 ;
那为什么普通的自动变量为什么不采用指针捕获;这是因为静态变量与其他变量之前生命周期的差别 ;
2.使用__block说明符:
和它类似的还有
- static
- auto
- regist
其中auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中 ;
只有__block得存储域待会讲;
先来看看使用__Block的自动变量在Block中的具体实现:
对比之前的,我们发现多了一个__block_byref_val_0结构体,它和block的结构体很像,但多了一个forwarding指针指向自身 ;
看起来就是把val变量转换为一个结构体变量来捕获 ;
这里结构体的结构可以参考下图:
当使用Block时:
这里看起来有点复杂;那为什么有成员变量__forwarding?虽然后面讲,但我猜是通过它来延长自动变量的生命周期之类的 ;
另外要注意一点,__block变量的____block_byref_val_0结构体对象并不在__main_block_impl_0结构体中,__main_block_impl_0结构体中的是它的指针,而对象在主函数中,这样做就可以在多个Block中使用__blcok变量 ;
Block存储域
Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量,所谓结构体类型的自动变量,即栈上生成的该结构体的实例 ;
但我们知道,除了栈以外,存储域还有数据区域和堆 ;
所以Block对象的存储域也不仅仅在栈 ;
下面给出Block对象的类和存储域间的关系:
平时用到的Block对象大多在栈,当
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时
时,Block为——NSConcreteGlobalBlock类对象;
#import "ViewController.h"
#import <objc/runtime.h>
typedef int(^blk_t)(int);
blk_t glbblk = ^(int count){return count ;} ;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count){return count ;} ;
Class blockClass = object_getClass((__bridge id _Nullable)((__bridge void *)(glbblk)));
NSLog(@"%@",NSStringFromClass(blockClass)) ;
printf("%d\n",blk(1)) ;
}
}
#import "ViewController.h"
#import <objc/runtime.h>
typedef int(^blk_t)(int);
blk_t glbblk = ^(int count){return count ;} ;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count){return count ;} ;
Class blockClass = object_getClass((__bridge id _Nullable)((__bridge void *)(blk)));
NSLog(@"%@",NSStringFromClass(blockClass)) ;
printf("%d\n",blk(1)) ;
}
}
配置在全局变量上的Block,从变量作用域外也可以通过指针安全使用,但设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;同上,配置在栈上的__block变量也是如此 ;
那么我们在使用中,Block时如何超出变量作用域存在的:
Blocks提供了讲Block和__block变量从栈上复制到堆上的方法 ;这样就能保证Block不被废弃 ;
当Block被复制到堆上时,isa指针也就是类会被赋为_NSConcrete MallocBlock,__block变量也是如此,但它的成员变量forwarding可以同时访问栈上和堆上的__block变量 ;
这个所谓的复制方法,其实就是oc中经常使用的copy,当我们对一个Block对象使用copy时有三种情况:
不过在提到使用copy方法前,我们要知道,但ARC有效时,大多数情形下编译器都会恰当地进行判断,自动生成讲Block从栈上复制到堆上的代码 ;
除此之外的情形就必须手动复制了,比如像方法或函数的参数中传递Block时;但如果在方法或函数中适当地复制了传递过来的参数,那也不必手动复制了,如:
- Cocoa框架的方法切方法名中含有usingBlock等时
- GCD的API
书上讲的必须手动复制的例子:
NSArray* array = [self getBlockArray] ;
typedef void(^blk01_t)(void);
blk01_t blk = [array objectAtIndex:0] ;
blk () ;
- (id) getBlockArray {
int val = 10 ;
return [[NSArray alloc] initWithObjects:^{NSLog(@"%d",val);},^{NSLog(@"%d",val);}, nil];
}
但不太清楚的一点是我在实际运行的时候程序不会异常,而是会正常执行;
再回到调用copy方法,我们会发现不管Block配置在何处,用copy方法都不会引起任何问题,在不确定时调用copy方法即可 ;不过将Block从栈上复制到堆上是相当消耗cpu的;
如下:
blk = [[[[blk copy] copy] copy] copy] ;
多次调用copy方法进行复制在ARC下是没有任何问题的 ;
__block变量存储域
当把使用了__block变量的Block从栈上复制到堆上时,__block
变量也会产生影响:
当把栈上的Block从栈上复制到堆上时,Block中使用的__block变量也会一并复制到堆上;当该Block已经复制到堆上时,复制Block对__block变量没有任何影响 ;
当多个Block中使用相同的__block变量时,任何一个Block从栈复制到堆,都会把__block变量也复制到堆上,即使其他Block再复制到堆上,也只会增加__blockde1引用计数;这和oc的内存管理是一样的 ;
同理,废弃Block也是一样的:
使用__block变量用结构体成员变量__forwarding的原因
不管__block变量配置在栈上还是在堆上,都能正确地访问变量
即通过Block的复制,__block变量也从栈复制到堆。此时可同时访问栈上的和堆上的__block变量。对于这句话,看
__block int val = 0 ;
void (^blk) (void) = [^{ ++ val ;} copy] ;
++val ;
blk () ;
NSLog(@"%d",val) ;
其中Block中的val为复制到堆上的__block变量用结构体实例 ;
而外面的val为栈上的结构体实例 ;
当复制到堆上时,成员变量的forwarding的值替换为目标堆上的__block变量用结构体实例的地址;
所以其实void (^blk) (void) = [^{ ++ val ;} copy] ;
++val ;
这两句都可以转换为:
++ (val.__forwarding->val) ;
无论是在Block语法中,Block语法外使用__block变量,还是__block变量配置在栈上还是堆上,都可以顺利的访问同一个__block变量 ;
截获对象
书上的例子大概都是基于MRC实现的,不过我在手动管理时也没有成功,所以这部分和内存管理息息相关 ;
typedef void(^blk_t)(id);
blk_t blk ;
{
id array = [[NSMutableArray alloc] init] ;
blk = [^(id obj) {
[array addObject:obj] ;
NSLog(@"%ld",[array count] ) ;
} copy] ;
}
blk([[NSObject alloc] init]) ;
blk([[NSObject alloc] init]) ;
blk([[NSObject alloc] init]) ;
这里的array,当在调用blk时,已经超出了他的作用域,按理来说应该被废弃,但这时发生了截获对象,可能类似于前面截获自动变量的值,但也有些不一样 ;与前者相比,最明显的区别在于:
因为涉及到了对象,简单的c语言结构体中不能含有__strong修饰的变量 ;但是oc可以通过引用计数进行内存管理 ;
所以在__main_block_desc_0结构体中增加的成员变量copy和dispose以及作为指针赋值给该成员变量的__main_block_copy_0函数(copy函数)和__main_block_dispose_0函数 (dispose函数);
这个时候代入内存管理来看 ;
copy函数使用_Blokc_object-assign函数(相当于retain)将对象类型对象赋值给Block用结构体成员变量array中并持有该对象 ;
dispose函数使用_Block_object_dispose函数(相当于release),释放赋值在Block用结构体成员变量array中的对像 ;
那么这些函数什么时候调用呢:在Block从栈复制到堆时,以及堆上的Block被废弃时会调用 ;
什么时候栈上的Block会复制到堆:
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时 ;
上面的这些情况下,栈上的Block被复制到堆上,但其实可以总结为_Block_copy函数被调用时Block从栈复制到堆上 ;
相对的,在释放复制到堆上的Block后,谁都不持有Block,而使其被废弃时调用dispose函数,这相当于对象的dealloc实例方法 ;
通过这种构造,通过使用附有__strong修饰符的自动变量,Block截获的对象能够超出变量作用域存在 ;也就是引用计数管理 ;
因此,Block中使用对象类型成员变量时,除了一下情形外,推介调用Block的copy实例方法 ;
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时