block
作为iOS开发过程中很常用,我们可以把block
想象成一个封装了一系列方法的函数,可以用于作为参数或者返回值,但是其底层实现及其注意事项,大家可能并不是都能讲的很清楚,下面我简单讲诉下我对其理解,希望对大家有所帮组。
block
的本质
block
的本质其实也是一个对象,为什么这么说呢?因为如果你使用clang
进行转换oc语言中的block
,你会发现block
的结构体也是有isa
指针的,只不过这个结构体还包含了函数的调用以及该函数执行的参数环境,下面我将clang -rewrite-objc
的前后结果附上,大家进行比较也就理解了。
//这是oc代码
void(^blockName)(NSString *arg) = ^(NSString *arg) {
NSLog(@"%@",arg);
};
blockName(@"arg");
//这是重写后c++底层实现
//这是block的定义
void(*blockName)(NSString *arg) = ((void (*)(NSString *))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//这是block的调用
((void (*)(__block_impl *, NSString *))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, (NSString *)&__NSConstantStringImpl__var_folders_lq_g7l9xm350p9fn1h8p3dwts2m0000gn_T_main_9b951d_mi_1);
不难发现,在调用的时候blockName
是被__block_impl *
修饰的,所以可以知道该block的类型是__block_impl *
,所以我们再找到__block_impl *
的定义,
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
可以看到__block_impl
结构体的第一个成员也是isa指针,所以说其实block的本质也是一个对象。
block
的内部结构
通过简单的分析我们知道了block
的本质是一个对象,那它是怎么工作的呢?我们继续在刚刚的例子上进行近一步的分析。
首先我们观察block
的定义部分,发现我们oc代码定义的block
被定义成了__main_block_impl_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;
}
};
发现该block的定义的第一个成员就是刚刚我们找到的__block_impl
,除此之外,我们可以在block
的定义部分发现传人的第一个参数为(void *)__main_block_func_0
,该指针的实现部分是
static void __main_block_func_0(struct __main_block_impl_0 *__cself, NSString *arg) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_lq_g7l9xm350p9fn1h8p3dwts2m0000gn_T_main_9b951d_mi_0,arg);
}
将这些串接起来,我们可以知道block
结构体中FuncPtr
是用来存储block
的实现方法的。
上面的例子中,
block
是不引入外部参数的,但上面例子很容易让我们理解block
本质结下来我们会由浅入深的分别介绍在引入参数的不同情况
//这是oc代码
NSInteger i = 10;
void(^blockName)(void) = ^() {
NSLog(@"wenpq....%ld",i);
};
i = 20;
blockName();
//这是c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger i;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
对比之前不引入参数的情况,可以发现新的block
中增加了一个i
,并且在运行后,我们发现输出的是10,所以可以知道在这种情况下是数值的传递,而并非是地址传递。
下面将i变量定义成static,看在底层block
的结构体有什么变化
//这是oc代码
static NSInteger i = 10;
void(^blockName)(void) = ^() {
NSLog(@"wenpq....%ld",i);
};
i = 20;
blockName();
//这是c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger *i;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
对比之前auto
类型的变量,static类型的变量传递进来的是个指针,所以大家也一定猜到了运行结果,运行结果在这里是20,这个时候有同学就会疑问,为什么有的时候是值传递,有的时候是地址传递,我这里发表下自己的理解,因为作为局部变量,这两个变量的存储空间是不一样的,非静态的局部变量是存在栈中,它随时都有可能会被回收,所以block
只取捕获它的值,而不记录它的地址是比较合理的,而静态变量是一直存在内存中的,所以使用最新的值。
上面的变量无论是静态的还是auto的都是局部变量,那么全局变量是否有什么不同
//这是oc代码
static NSInteger i = 10;
int main(int argc, char * argv[]) {
@autoreleasepool {
void(^blockName)(void) = ^() {
NSLog(@"wenpq....%ld",i);
};
i = 20;
blockName();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//这是c++代码
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
函数中如果访问了全局变量,是不会捕获的,其实这种情况很好理解的,既然都应是全局的变量,任何对象就没必要再去记录该变量,都能直接访问的。
block类型说明
在上面的例子中,我们可以发现block的构造函数的isa指针都指向_NSConcreteStackBlock
所在地址,如果大家感兴趣可以使用[obj class]
打印出来看看block
的真实类型以及其父类是什么类型,这里直接打印出结果
void(^blockName)(void) = ^() {
NSLog(@"wenpq....%ld",i);
};
NSLog(@"%@\n%@\n%@\n%@\n%@",blockName,[blockName class],[[blockName class] superclass],[[[blockName class] superclass] superclass],[[[[blockName class] superclass] superclass] superclass]);
//打印结果
<__NSGlobalBlock__: 0x10f2ab170>
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject
在block
函数中引入参数不同,在运行时可以将其分为全局、栈、堆block,下面分别举例说明这三类block
是如何划分的。
全局block
不访问局部auto变量的block为全局变量,全局block因为不访问auto变量,所以在实际开发中很少使用,因为完全可以用一个方法代替。
static NSInteger i = 10;
NSInteger j = 20;
int main(int argc, char * argv[]) {
@autoreleasepool {
static NSInteger k = 30;
void(^blockName)(void) = ^() {
NSLog(@"wenpq....%ld....%ld....%ld",i,j,k);
};
NSLog(@"wenpq...result...%@",[blockName class]);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//打印结果
wenpq...result...__NSGlobalBlock__
栈block
栈block是之前mrc环境时,block内部引用了auto变量,因为block时存放在栈上数据,所以释放时机是由系统决定的,于是这种类型的block使用起来很不安全,可能造成意想不到的问题。所以要把栈block的内存放到堆内存中,由程序来决定其生命周期。
mrc 环境
NSInteger m = 40;
void(^block2)(void) = ^() {
NSLog(@"wenpq.....%ld",m);
};
NSLog(@"wenpq....result...%@",[block2 class]);
//打印结果
wenpq....result...__NSStackBlock__
堆block
在MRC环境下受用copy操作后,栈block就可以成为堆block
NSInteger m = 40;
void(^block2)(void) = ^() {
NSLog(@"wenpq.....%ld",m);
};
NSLog(@"wenpq....result...%@",[[block2 copy] class]);
//打印结果
wenpq....result...__NSMallocBlock__
以上对栈block和堆block的分析都是在MRC的环境下进行分析,但是现在的工程大都是ARC的形式,所以下面讲诉下ARC环境block的类型,ARC环境在以下情况会自动将栈block进行copy操作,转换成堆block:
- block作为返回值;
- block被强指针指向;
- GCD中的回调block也都是在堆中;
block的强弱引用问题
下面讲诉内容都以ARC环境展开分析
之前举的例子都是用整型作为block
的引入数据,下面我们用对象类型来分析block
的强弱引用
强引用
//oc代码
PQPerson *person = [[PQPerson alloc] init];
person.name = @"zhangsan";
void(^block)(void) = ^(){
NSLog(@"wenpq....%@",person.name);
};
NSLog(@"come_here");
//c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
PQPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, PQPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以发现在这个例子中,block结构体存储的person对象是被强引用的。
弱引用
//oc代码
PQPerson *person = [[PQPerson alloc] init];
person.name = @"zhangsan";
__weak typeof(person) weakPerson = person;
void(^block)(void) = ^(){
NSLog(@"wenpq....%@",weakPerson.name);
};
NSLog(@"come_here");
return 0;
//c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
PQPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, PQPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
后续
关于block
的循环引用问题,会在讲解weak时一同讲解
以上对block都是个人工作中的总结,可能其中有些理解错误或者有偏差,如果大家发现以上讲解有问题或者疑问,欢迎在评论区提出,谢谢阅读!