这一篇主要是对Blocks的总结。
既然说是对Blocks的总结,那什么是Blocks?
Blocks是对C语言的扩充功能,一句话概括就是带有自动变量的匿名函数。顾名思义,匿名函数就是没有名称的函数。
好啦,接下来带着疑惑来看看Block的语法吧~
如它的定义所示,它没有函数名,它可以有自动变量也就是形参,还可以有返回值,另外Blocks带有插入记号“^”,该记号便于查找到Block。
一般的形式为:^ 返回值类型 (参数列表){ 表达式 };
^int (int a){
return a;
};
二般情况下,Blocks没有参数,参数列表也是可以省略的。
^int{
return 66666;
};
参数都可以省略,返回值类型当然也是可以省略的啦~
^(int a){
NSLog(@"%d",a);
};
/*这个block返回void类型*/
^(int a){
return a;
};
/*
这个block省略了返回值类型,但是有返回语句,故该block返回int类型
*/
※※※在这里需要注意的是,当省略返回值类型时,如果表达式有return语句就使用该返回值的类型,如果没有就使用void类型。如果表达式有多个return语句,那么所有return语句的返回值类型必须相同。
既然两个都可以省略,那可以一起省略不???当然可以啦~~~
^{
NSLog(@"test");
};
像这样,省略了返回值类型和参数列表的block形式应该最熟悉不过了吧~
Block语法就是这么简单啦~从记述方法来看,除了没有名称以及带有^记号外,其他的和C语言函数定义也大同小异啦~
在C语言中,可以把函数的地址赋给函数指针类型变量。同样的,在Block语法下,也可以将Block语法赋给声明为Block的类型变量。好的,那接下来就看看Block类型变量呐~(∩_∩)
int (^blk)(int);
像这样,
Block类型变量的声明方式为:返回值类型 (^ 变量名)(参数列表);
看过它的声明方式,下面就看看如何将Block语法赋给它吧、
int (^blk)(int) = ^int (int count){return count+1;};
Block类型变量也可以向其他的Block类型变量赋值。
int (^blk1)(int );
blk1 = blk;
Block类型变量也
可以作为自动变量、函数参数、静态变量、全局变量来使用。
这里以作为函数参数来举例。
int test (int (^blk)(int a))
{
...
}
大家是不是觉得Block类型变量作为函数参数使用有点长呢?恩,其实我也是这么觉得~
这时候呢,就可以像使用函数指针类型时那样,用typedef来解决这个问题啦~
typedef int (^block)(int );
int test(block blk){
...
}
这样是不是看着就舒服多啦~哈哈~
在使用Block时,其实就像调用函数那样,直接调用就可以啦~
int a = blk(1);
NSLog(@"%d",a);//输出结果为2
int b = test(blk);
既然说,Block是带有自动变量的匿名函数,那它是如何带有自动变量的呢?这就是接下来要说的啦~
{
int a = 5;
const char *s = "value is";
void (^blk)(void) = ^{
NSLog(@"%s:a = %d",s,a);
};
a = 10;
s = "value changed";
blk();
}
通过运行这段代码,发现输出的结果为 : value is:a = 5;
恩?a的值居然没变?exo me???
真相只有一个!在Block中带有自动变量值表现为截获自动变量值,也就是说,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。所以呢,在执行了Block语法后,即使改写自动变量的值也不会影响Block执行时所使用的值。
小明说,在外面改不行,那我在Block里面改总行了吧!哼哼~
like this?编译器会报错的,那该怎么办?不用担心,使用__block说明符就好啦~
int __block b = 0;
void (^blk)(void) = ^{
b = 1;
};
像这样的,使用
__block说明符
的变量可以在Block中赋值,把这种变量称为
__block变量
刚刚说到在Block中向截获的自动变量赋值会报错,那截获OC对象呢?也会出错么?
从这个栗子可以看出,使用截获的OC对象没有任何问题,但是向其赋值会产生编译错误。这种情况,也是需要给截获的自动变量添加__block说明符才能对其赋值。
还有一个需要注意的,在使用C语言数组时需要小心使用指针。
诶?这次怎么只是使用它也报错?因为在现在的Block中,截获自动变量的方法并没有实现对于C语言数组的截获。但是可以使用指针来解决这个问题。
const char *str = "hello";
void (^blk)(void) = ^{
NSLog(@"%c",str[2]);
};
对于这一块的问题总结如下:
1.为什么Block中不能修改自动变量值?
答:自动变量以值传递的方式将自动变量的值保存在Block的结构体实例中,因为并没有传递自动变量的地址,故不能修改值。
2.为什么对OC对象增删元素可以但是对其赋值会报错?
答:修改OC对象可以,因为修改对象并没有改变对象指针。
3.为什么全局变量、静态全局变量、静态变量可以在Block中修改?
答:全局变量、静态全局变量的地址不变,作用域广,不会对Block的结构体产生影响,所以可以在Block中修改值。对于静态变量,虽然地址是唯一的,虽然Block超出了它的作用域,所以讲静态变量的指针传给了Block的结构体,故在Block中也可以修改值。
4.为什么使用__block说明符的自动变量可以在Block中修改值?
答:将 int __block a = 0; 通过clang命名查看源码如下(截取片段):
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int a;
};
通过此段源码,可以看到__block修饰的变量生成了自己的结构体实例,__forwarding是指向自身的指针,a相当于原自动变量。
也就是说,这个结构体实例并不属于某个Block,因为这要保证多个Block可以使用一个__block 变量。
好啦。这几个问题就说到这,有不同见解的期待评论。
既然说到源码了,那我们也来看看Block的源码吧~
int main(){
void (^blk)(void) = ^{
};
blk();
return 0;
}
就这么几行源码,通过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;
}
};
//__cself是指向Block值得变量。Block转换过来的函数,根据所属函数名以及出现的顺序命名
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
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;
}
是不是有点惊讶,怎么这么多啊?我们重点看看最后一个函数,也就是main函数。
第一句转换部分有点难划分,做如下处理:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
struct __main_block_impl_0 *blk = &tmp;
这一句就是将__main_block_impl_0结构体类型的自动变量赋值给__main_block_impl_0结构体指针类型的变量blk.
第二句除去转换部分可变化为:
(*blk->impl.FuncPtr)(blk);
这一句就是简单的函数指针调用函数,在__main_block_impl_0结构体构造函数中,由Block转换的__main_block_func_0函数的指针被赋值给成员变量FuncPtr,另外__main_block_func_0函数的参数__cself指向Block值。所以说在函数调用中Block是作为参数进行传递。
在这里,还需要说明的一点是isa指针。在OC中,每一个对象都一个isa指针,指向对象的类。每个类也有一个isa指针,指向它的元类。元类也有一个isa指针,指向根元类。根元类的isa指针指向自己。
至此,应该已经说明了Block的实质,就是Objective-C对象。
__main_block_imp_0结构体就相当于OC对象的结构体。
将Block作为OC的对象处理,关于该类的信息放在_NSConcreteStackBlock中,对isa指针初始化。
OK~说到_NSConcreteStackBlock了,那就说说Block存储域吧
block根据在内存的位置可以分为三种类型:
__NSConcreteGlobalBlock : 一般是不使用自动变量或者使用全局变量的Block,存储在程序的数据域。不依赖与执行时的状态,所以整个程序中只有一个实例。不持有对象
_NSConcreteStackBlock : 使用自动变量的Block,存储在栈中。不持有对象
_NSConcreteMallocStack : 复制栈上的Block到堆中。不持有对象
提问 : 为什么要复制栈上的Block到堆上呢?
回答 : 在栈上的Block超出作用域就会被废弃,当然位于栈上的__block变量也一样。如果将Block和__block变量从栈上赋值到堆上,不就解决啦,这样即使block语法记述的变量作用域结束,堆上的Block仍然存在。在前面说过,__block实例结构体中的__forwarding指针是指向自己的,这样的话无论该变量位于栈上还是堆上都可以正确访问 。
从栈上复制到堆上的情况总结:
1.调用copy实例方法
2.将Block作为函数返回值返回
3.Block被赋值给__strong修饰符修饰的变量时
4.方法名中含有usingBlock的Cocoa框架方法或在GCD的API中传递Block时;
当Block从栈上复制到堆上时会持有对象,若此时对象也持有Block那就会造成循环引用的问题,举个栗子
typedef void (^block)(void);
@interface Object : NSObject
{
block blk;
}
@end
@implementation Object
- (id)init{
self = [super init];
blk = ^{NSLog(@"self : %@",self);};
return self;
}
@end
{
id o = [[Object alloc]init];
NSLog(@"o : %@",o);
return 0;
}
在此例中,Block持有self,self持有Block。这正是循环引用。可以使用__weak 或 __unretained_unsafe来解决。
1.使用__weak修饰符
- (id)init{
self = [super init];
id __weak tmp = self ;
blk = ^{NSLog(@"self : %@",tmp);};
return self;
}
此处,不用判断tmp是否为nil,因为Block存在时,self一定存在。
2.使用__unsafe_unretained
- (id)init{
self = [super init];
id __unsafe_unretained tmp = self ;
blk = ^{NSLog(@"self : %@",tmp);};
return self;
}
使用__unsafe_unretained不用担心垂悬指针的问题。
最后一个问题了。
当ARC无效时,一般需要手动将Block从栈复制待堆上,另外还需要手动是否复制到堆上的Block。这是,通过copy方法从栈上复制到堆上,通过release方法来释放。如果Block有一次复制并配置在堆上就可以通过retain实例方法持有,但是对于配置在栈上的Block使用retain方法没有作用。
当ARC无效时,__block说明符被用了避免Block中的循环引用、因为当Block从栈上复制到堆上时,如果Block使用的变量为富有__block修饰符修饰的变量,该变量不会被Block持有。还是拿上一个循环引用的栗子来解决~
- (id)init{
self = [super init];
__block id tmp = self ;
blk = ^{NSLog(@"self : %@",tmp);};
return self;
}
就是这样子的啦、
说了这么多,来总结一下吧、
1. Blocks是带有自动变量的匿名函数。所谓匿名函数,就是没有函数名称,但是有插入记号“^”。
2.Block类型变量,可以作为函数参数、函数返回值等使用。
3.Blocks截获自动变量值,Block中自动变量值不可修改,但是使用__block说明符的自动变量值可以被修改。截获OC对象,可以使用但是不能赋值。不能截获C语言数组,但是可以使用指针。
4.Block的实质就是Objective-C对象。
5.Block的存储域,分为三种:栈、堆、数据区。
6.Block的循环引用和解决办法。
7.ARC无效时Block的使用。
就这么多啦~有不妥当的地方,小伙伴们踊跃提出来呢~
如果对您有所帮助,点个赞哦、双击666~谢谢~