前言
本文不用于商业用途,只是对个人知识的一个梳理和总结,其中借鉴引用了其他博客里面的内容,文末会给出本文的参考文章,如果侵犯到原著者的权益请在评论区留言,我会马上删除对应文段。
Block是什么
Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。
通常来说,block都是一些简短代码片段的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调。
block是什么?在回答这个问题之前,先介绍一下什么是闭包。在 wikipedia 上,闭包的定义) 是:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻译过来,闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。简而言之,所谓闭包就是能够读取其它函数内部变量的函数。
block 实际上就是 Objective-C 语言对于闭包的实现。这个解释用到block来也很恰当:一个函数里定义了个block,这个block可以访问该函数的内部变量。
Block是对象吗?
block是不是对象?答案显而易见:是的。
下图是block的数据结构定义,显而易见,在Block_layout里,我们看到了isa指针,为什么说block是对象呢,原因就在于isa指针,在objective-c语言的内部,每一个对象都有一个isa指针,指向该指针的类。
因为所有对象的都有isa 指针,用于实现对象相关的功能。
block 的数据结构定义如下(图片来自 这里):
对应的结构体定义如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:
isa 指针,所有对象都有该指针,用于实现对象相关的功能。
flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
reserved,保留变量。
#invoke,函数指针,指向具体的 block 实现的函数调用地址。
descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
通过对 block内部结构的分析,我们知道了一个 block 实际是一个对象,它主要由一个 isa 和 一个 invoke(函数指针,指向具体的 block 实现的函数调用地址) 和 一个 descriptor 组成 。
Block的分类
在 Objective-C 语言中,一共有 3 种类型的 block:
内存和复制
- _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
简单地讲,如果一个block中没有引用外部变量并且没有被其他对象持有,就是NSConcreteGlobalBlock。NSConcreteGlobalBlock是全局的block,在编译期间就已经决定了,如同宏一样。
- _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
NSConcreteStackBlock就是引用了外部变量的block,但是只是简单的引用,不会持有外部对象。
- _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。
NSConcreteMallocBlock其实就是一个block被copy时,将生成NSConcreteMallocBlock,不过值得注意的是NSConcreteMallocBlock会持有外部对象。只要这个NSConcreteMallocBlock存在,内部对象的引用计数就会+1
Block的声明属性时的关键字
block方法常用声明:@property (copy) void(^MyBlock)(void); 如果超出当前作用域之后仍然继续使用block,那么最好使用copy关键字,拷贝到堆区,防止栈区变量销毁。
由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。
并且在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。并且因为block是一段代码,即不可变。所以对于block 使用copy 还是strong 效果是一样的。亲测是这样的,网上有些解释说不能使用 strong 是错误的。
Block对于局部变量的修改问题
为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。
该命令是 : clang -rewrite-objc block.c
block.c 是一个文件名称。
命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现的。
//我们使用clang来分析 使用__block和不使用 __block时,block内部的实现机制。
#下面是未使用__block的情况
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;
}
};
# 我们可以看到 main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,
# 被复制到 main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,
# 不会影响外部的实际变量 a。
# 下面这个是 使用 __block的情况
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
# main_block_impl_0 中引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
__Block_byref_i_0 结构体中带有 isa,说明它也是一个对象
对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示(图片来自 这里):
对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示(图片来自 这里):
担心循环引用?
只要看持有block的对象是不是也被block持有,如果没有持有,就不用担心循环引用问题了。
__weak typeof(self)weakSelf = self; 也可以打断循环引用的引用链
typeof()的作用:gcc的一个扩展。可以看成一个一元运算符。 typeof和sizeof用法非常类似! sizeof(exp.)返回的是exp.的数据类型大小; typeof(exp.)返回的就是exp.的数据类型。exp.可以是任意类型,所以返回的也是和exp.对应的任意类型。 通俗的说就是:可以根据typeof()括号里面的变量,自动识别变量类型并返回该类型。
__weak UIViewController* weakSelf = self;
__weak typeof(self)weakSelf = self;
作用是等同的。
block对于以参数形式传进来的对象,会不会强引用?
其实block与函数和方法一样,对于传进来的参数,并不会持有
我们对截获的变量可以进行操作,而不能直接进行赋值,如果在Block内部修改局部变量的值需要用到 _block 修饰才行。
# 对截获的变量可以进行操作进
NSMutableArray *array = [[NSMutableArray alloc]init];
void (^blo)() = ^{
[array addObject:@"Obj"];
};
# _block 修饰才可以修改局部变量
__block int b = 0;
void (^blo)() = ^{
b = 3;
};
为什么需要Block变量名称?我们可以这样理解,我们通过这个Block变量名称来获取Block的指针,然后通过这个指针就可以来使用Block函数。我们先来看一下如何声明一个Block变量
# 反编译 block
clang -rewrite-objc main.m
# 可以理解为block的基类
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
Block的使用
Block不但可以作为独立的函数使用(有参数和返回值),也能够当作函数参数,首先我们声明一个Block类型变量 ,并加上typedef修饰符即可。
typedef void(^Blo)(NSString *s1,UIColor *c);
逆向传值
前面我们已经知道Blcok是一个匿名函数,同时也是一个指针,那么使用Block就可以弥补在iOS中函数传递的功能。通常是这么用的:
页面B的.h文件中定义了这样一个Block执政,然后声明了一个变量,像这样:
typedef void(^Blo)(NSString *s1,UIColor *c);
@property (nonatomic, copy) Blo block;
然后我们在页面A当中有这么一段代码:
ViewController *b = [[ViewController alloc]init];
__weak ViewController *wself = self;
b.block = ^(NSString *s1,UIColor *c){
NSLog(@"%@",s1);
wself.view.backgroundColor = c;
};
[self.navigationController pushViewController:b animated:true];
然后在页面B的任意地方我们调用block变量,像这样:
self.block(@"str",[UIColor redColor]);
# 就会在A页面中调用B页面传过来的参数,在A页面进行操作,对控制器A进行改变,这样的做法通常用做 控制器 反向传值。
Block的使用中很容易出现的问题
(1)一个类中有一个Block性质的属性,并且在代码里面有用到,如果在对象初始化的时候,不做处理是会崩溃的,这也是block不方便的地方,不像代理可以实现也可以不实现。
iOS block中 EXC_BAD_ACCESS(code=1,address= 0x10)
[self.navigationController pushViewController:[[XSDCSearchViewController alloc]init] animated:NO];
XSDCSearchViewController
self.seachParameter(dic);
报错 EXC_BAD_ACCESS(code=1,address= 0x10)
有两处的跳转VC都需要实现block性质的属性,只设置了一处,忘记了这处设置,造成了崩溃。
(2)在block中 alloc init一个变量 并且 push到这个对象中时是会 崩溃的。
block 中引用一个对象。
XMGPerson *p = [[XMGPerson alloc] init];
__weak XMGPerson *weakP = p;
如果直接在block中 alloc init一个变量 并且 push到这个对象中时是会 崩溃的,
崩溃发生在这个VC的视图刚刚出现没有多久后。
对于Block我们需要认识到
- 是C++中的Struct(本文未提到)。
- 用来弥补iOS中函数传递的功能。
- 他是一段代码块的内存的指针。
- 和delegate一样的功能,但是显的更加简洁。
- block的代码是内联的,效率高于函数调用
- block对于外部变量默认是只读属性
- block被Objective-C看成是对象处理
小结
后续会持续更新
本文参考文章
深入浅出-iOS Block原理和内存中位置
唐巧-谈Objective-C block的实现
深究Block的实现
Objective-C中的Block
网友评论