本章讲解block的用法和底层数据结构,以及使用过程中需要注意的点。
block本质
前几篇文章讲过了,class
是对象,元类也是对象,本质是结构体,那么block是否也是如此呢?block
具有这几个特点:/
- block本质上也是一个OC对象,它内部也有isa指针
- block是封装了函数调用以及函数调用环境的oc对象
先简单来看一下block
编译之后的样子
1 | int main(int argc, const char * argv[]) { |
命令行执行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.mm -o main.cpp
,来到main.cpp
内部,已经去除多余的转化函数,剩余骨架,可以看得更清晰。
1 | struct __main_block_impl_0 { |
最终block
转化成__main_block_impl_0
结构体,赋值给变量block
,传入参数是__main_block_func_0
和__main_block_desc_0_DATA
来执行__main_block_impl_0
的构造函数,__main_block_desc_0_DATA
函数赋值给__main_block_impl_0->FuncPtr
,执行函数是block->FuncPtr(block)
,删除冗余代码之前是((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
,那么为什么block
可以直接强制转化成__block_impl
呢?因为__main_block_impl_0
结构体的第一行变量是__block_impl
,相当于__main_block_impl_0
的内存地址和__block_impl
的内存地址一样,强制转化也不会有问题。
变量捕获
变量捕获分为3种:
变量类型 | 是否会捕获到block内部 | 访问方式 | 内部变量假定是a |
---|---|---|---|
局部变量 auto | 会 | 值传递 | a |
局部变量 static | 会 | 指针传递 | *a |
全局变量 | 不会 | 直接访问 | 空 |
auto变量捕获
auto
变量,一般auto
是省略不写的,访问方式是值传递,关于值传递不懂的话可以看这篇博客,
看下这个例子
1 | int age = 10; |
有没有疑问呢?在block
执行之前age =20
,为什么输出是10呢?
将这段代码转化成c/c++
,如下所示:
1 | struct __main_block_impl_0 { |
结构体__main_block_impl_0
多了一个变量age
,在block
转化成c
函数的时候__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)
直接将age的值存储在__main_block_impl_0.age
中,此时__main_block_impl_0.age
是存储在堆上的,之前的age
是存储在数据段的,执行block
访问的变量是堆上的__main_block_impl_0.age
,所以最终输出来
age is 10`。
static变量捕获
我们通过一个例子来讲解static和auto区别:
1 | void(^block)(void); |
转化成源码:
1 | void(*block)(void); |
当执行完test()
函数,age
变量已经被收回,但是age
的值存储在block
结构体中,level
的地址存储在__test_block_impl_0.level
,可以看到level
类型是指针类型,读取值的时候也是*level
,则不管什么时间改动level
的值,读level
的值都是最新的,因为它是从地址直接读的。所以结果是age is 10,level is 13
。
全局变量
全局不用捕获的,访问的时候直接访问。我们来测试下
1 | int age = 10; |
转化成c/c++
1 | int age = 10; |
可以看出来编译之后仅仅是多了两行int age = 10;
static int level = 12;
,结构体__main_block_impl_0
内部和构造函数并没有专门来存储值或者指针,原因是当执行__main_block_func_0
,可以直接访问变量age
和 level
,因为全局变量有效区域是全局,不会出了main
函数就消失。
基本概括来讲就是超出执行区域与可能消失的会捕获,一定不会消失的不会捕获。
我们再看下更复杂的情况,对象类型的引用是如何处理的?
1 | @interface FYPerson : NSObject |
block
和block2
都是结构体__FYPerson__test_block_impl_1
内部引用了一个FYPerson
对象指针,FYPerson
对象属于局部变量,需要捕获。第2个block
访问_name
捕捉的也是FYPerson
对象,访问_name
,需要先访问FYPerson
对象,然后再访问_name
,本质上是访问person.name
,所以捕捉的是FYPerson
对象。
验证block是对象类型:
1 | //ARC环境下 |
可以了解到block
是继承与基类的,所以block
也是OC对象。
block的分类
block
有3种类型,如下所示,可以通过调用class
方法或者isa
指针查看具体类型,最终都是继承来自NSBlock
类型。
- NSGlobalBLock(_NSConcreteGLobalBlock)
- NSStackBlock(_NSConcreteStackBlock)
- NSMallocBLock(_NSConcreteMallocBlock)
在应用程序中内存分配是这样子的:
1 | --------------- |
block类型 | 环境 |
---|---|
NSGlobalBLock | 没有访问auto变量 |
NSStackBlock | 访问auto变量 |
NSMallocBLock | NSStackBlock 调用copy |
验证需要设置成MRC,找到工程文件,设置project->Object-C Automatic Reference Counting=
为NO
1 | int age = 10; |
没有访问auto
变量的block
属于__NSGlobalBlock__
,访问了auto变量的是__NSStackBlock__
,手动调用了copy
的block
属于__NSMallocBlock__
。__NSMallocBlock__
是在堆上,需要程序员手动释放[block3 release];
,不释放会造成内存泄露。
每一种类型的block
调用copy
后的结果如下
block类型 | 副本源的配置存储域 | 复制效果 |
---|---|---|
NSGlobalBLock | 堆 | 从栈复制到堆 |
NSStackBlock | 程序的数据区域 | 什么也不做 |
NSMallocBLock | 堆 | 引用计数+1 |
在ARC环境下,编译器会根据自身情况自动将栈上的block复制到堆上,比如下列情况
- block作为函数返回值时
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时
在ARC环境下测试:
1 | typedef void (^FYBlock)(void); |
arc
环境下,没访问变量的block
是__NSGlobalBlock__
,访问了局部变量是__NSMallocBlock__
,有强指针引用的是__NSMallocBlock__
,强指针系统自动执行了copy操作,由栈区复制到堆区,由系统管理改为开发者手动管理。
所以有以下建议:
MRC下block属性的建议写法
- @property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法
- @property (strong, nonatomic) void (^block)(void);
- @property (copy, nonatomic) void (^block)(void);
对象类型数据和block交互
平时我们使用block
,对象类型来传递数据的比较多,对象类型读取到block
中用__block
修饰符,会把对象地址直接读取到block
结构体内,__weak
修饰的对象是弱引用,默认是强引用,我们看下这段代码
1 | //FYPerson.h |
使用下面该命令转化成cpp
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp |
摘取关键结构体代码:
1 | struct __main_block_impl_0 { |
FYPerson *__weak __weakPerson
是__weak
修饰的对象
当block内部换成block = ^{
NSLog(@" %d",person.age);
};
,转换源码之后是
1 | struct __main_block_impl_0 { |
person
默认是使用__storng
来修饰的,arc
中,block
引用外界变量,系统执行了copy
操作,将block
copy
到堆上,由开发者自己管理,转c/c++
中结构体描述为
1 | static struct __main_block_desc_0 { |
有对象的使用,则有内存管理,既然是arc,则是系统帮开发者管理内存,函数void (*copy)
和void (*dispose)
就是对block的引用计数的+1
和-1
。
如果block被拷贝到堆上
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会根据auto变量的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
如果block从堆上移除
- 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
- _Block_object_dispose函数会自动释放引用的auto变量(release,引用计数-1,若为0,则销毁)
函数 | 调用时机 |
---|---|
copy函数 | 栈上的Block复制到堆时 |
dispose函数 | 堆上的Block被废弃时 |
题目
person什么时间释放?
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |
3s后释放,dispatch
对block
强引用,block
强引用person
,在block
释放的时候,person
没其他的引用,就释放掉了。
变换1:person
什么时间释放
1 | FYPerson *person = [[FYPerson alloc]init]; |
__weak
没有对perosn
进行强引用,咋执行完dispatch_block则立马释放,答案是立即释放。
变换2:person
什么时间释放
1 | FYPerson *person = [[FYPerson alloc]init]; |
person
被内部block
强引用,则block
销毁之前person
不会释放,__weakPerson
执行完person
不会销毁,NSLog(@"---%d",person.age)
执行完毕之后,person
销毁。答案是4秒之后NSLog(@"---%d",person.age)
执行完毕之后,person
销毁。
变换3:person
什么时间释放
1 | FYPerson *person = [[FYPerson alloc]init]; |
person
被强引用于第一层block
,第二层弱引用person
,仅仅当第一层block执行完毕的时候,person
释放。
修改block外部变量
想要修改变量,首先要变量的有效区域,或者block持有变量的地址。
例子1:
1 | int age = 10; |
报错的原因是age
是值传递,想要不报错只需要将int age = 10
改成static int age = 10
,就由值传递变成地址传递,有了age
的地址,在block
的内部就可以更改age
的值了。或者将int age = 10
改成全局变量,全局变量在block
中不用捕获,block
本质会编译成c
函数,c
函数访问全局变量在任意地方都可以直接访问。
__block本质
__block
本质上是修饰的对象或基本类型,编译之后会生成一个结构体__Block_byref_age_0
,结构体中*__forwarding
指向结构体自己,通过(age->__forwarding->age) = 20
来修改变量的值。
1 | struct __Block_byref_age_0 { |
age
在block
外部有一个,在block
内部有一个,他们是同一个吗?我们来探究一下:
1 | typedef void (^FYBlock)(void); |
经过__block
修饰之后,之后访问的age
和结构体__Block_byref_age_0
中的age
地址是一样的,可以判定age
被系统copy
了一份。
例子:
1 | __block int age = 10; |
使用命令编译
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m |
摘录主要函数:
1 | struct __Block_byref_age_0 { |
static void main_block_copy_0(struct main_block_impl_0dst, struct __main_block_impl_0src) {
_Block_object_assign((void)&dst->age, (void)src->age, 8/BLOCK_FIELD_IS_BYREF/);
_Block_object_assign((void)&dst->obj, (void)src->obj, 3/BLOCK_FIELD_IS_OBJECT/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
1 |
|
typedef void (^FYBlock)(void);
@interface FYPerson : NSObject
@property (nonatomic,copy) FYBlock blcok;
@end
@implementation FYPerson
- (void)dealloc{
NSLog(@”%s”,func);
}
@end
int main(int argc, const char argv[]) {
@autoreleasepool {
NSLog(@” age1:%p”,&age);
FYPerson obj=[[FYPerson alloc]init];
[obj setBlcok:^{
NSLog(@”%p”,&obj);
}];
NSLog(@”————–”);
}
return 0;
}1
2
输出是:
age1:0x7ffeefbff4e8
block 执行完毕————–1
2
3
`obj`通过`copy`操作强引用`block`,`block`通过默认`__strong`强制引用`obj`,这就是`A<---->B`,相互引用导致执行结束应该释放的时候无法释放。
将`main`改成
FYPerson obj=[[FYPerson alloc]init];
weak typeof(obj) weakObj = obj;
[obj setBlcok:^{
NSLog(@”%p”,&weakObj);
}];1
2
结果是
age1:0x7ffeefbff4e8
block 执行完毕————–
-[FYPerson dealloc]1
2
3
4
5
使用`__weak`或`__unsafe__unretain`弱引用`obj`,在`block`执行完毕的时候,`obj`释放,`block`释放,无相互强引用,正常释放。
#### `__weak`和`__unsafe__unretain`
`__weak`和`__unsafe__unretain`都是弱引用`obj`,都是不影响`obj`正常释放,区别是`__weak`在释放之后会将值为nil,`__unsafe__unretain`不对该内存处理。
下面我们来具体验证一下该结论:
typedef void (^FYBlock)(void);
@interface FYPerson : NSObject
@property (nonatomic,assign) int age ;
@end
@implementation FYPerson
-(void)dealloc{
NSLog(@”%s”,func__);
}
@end
struct __Block_byref_age_0 {
void isa;
struct Block_byref_age_0 forwarding;
int flags;
int size;
int age;
};
struct block_impl {
void isa;
int Flags;
int Reserved;
void FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (copy)(void);
void (dispose)(void);
};
struct main_block_impl_0 {
struct block_impl impl;
struct __main_block_desc_0 Desc;
FYPerson *unsafe_unretained unsafe_obj;
};
int main(int argc, const char argv[]) {
@autoreleasepool {
// insert code here…
FYBlock block;
{
FYPerson obj=[[FYPerson alloc]init];
obj.age = 5;
weak typeof(obj) unsafe_obj = obj;
block = ^{
NSLog(@"obj->age is %d obj:%p",__unsafe_obj.age,&__unsafe_obj);
};
struct __main_block_impl_0 *suct = (__bridge struct __main_block_desc_0 *)block;
NSLog(@"inside struct->obj:%p",suct->__unsafe_obj);//断点1
}
struct __main_block_impl_0 *suct = (__bridge struct __main_block_desc_0 *)block;
NSLog(@"outside struct->obj:%p",suct->__unsafe_obj);//断点2
block();
NSLog(@"----end------");
}
return 0;
}1
2
根据文中提示断点1处使用`lldb`打印`obj`命令
(lldb) p suct->__unsafe_obj->_age
(int) $0 = 5 //年龄5还是存储在这里的
inside struct->obj:0x102929d80
1 |
|
-[FYPerson dealloc]
outside struct->obj:0x0
p suct->unsafe_obj->_age
error: Couldn’t apply expression side effects : Couldn’t dematerialize a result variable: couldn’t read its memory1
2
3
已经超出了`obj`的有效范围,`obj`已经重置为nil,也就是`0x0000000000000000`。
上文代码`__weak`改为`__unsafe_unretained`再次在`obj`断点1查看地址:
(lldb) p suct->unsafe_obj->_age
(int) $0 = 5
inside struct->obj:0x10078c0c0
1 |
|
-[FYPerson dealloc]
outside struct->obj:0x10078c0c0
(lldb) p suct->unsafe_obj->_age
(int) $1 = 51
2
3
4
5
6
`__unsafe_unretained`在`obj`销毁之后内存并没有及时重置为空。
当我们离开某个页面需要再执行的操作,那么我们改怎么办?
实际应用A:
-(void)test{
weak typeof(self) weakself = self;
[self setBlcok:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@”perosn :%p”,weakself);
});
}];
self.blcok();
}
int main(int argc, const char argv[]) {
@autoreleasepool {
{
FYPerson obj=[[FYPerson alloc]init];
[obj test];
NSLog(@”block 执行完毕————–”);
}
NSLog(@”person 死了”);
}
return 0;
}
输出:
block 执行完毕————–
-[FYPerson dealloc]
person 死了1
2
3
猛的一看,哪里都对!使用`__weak`对`self`进行弱引用,不会导致死循环,在`self`死的时候,`block`也会死,就会导致一个问题,`self`和`block`共存亡,但是这个需要3秒后再执行,3秒后,`self`已经死了,`block`也死了,显然不符合我们的业务需求。
那么我们剥离`block`和`self`的关系,让`block`强引用`self`,`self`不持有`block`就能满足业务了。如下所示:
block typeof(self) weakSelf = self;//block或者没有修饰符
dispatch_async(dispatch_get_main_queue(), ^{
sleep(2);
NSLog(@”obj:%@”,weakSelf->_obj);
});
//perosn :0x0`
当self
不持用block
的时候,block
可以强引用self
,block
执行完毕自己释放,也会释放self
,当self
持有block
,block
必须弱引用self
,则释放self
,block
也会释放,否则会循环引用。
总结
block
本质是一个封装了函数调用以及调用环境的结构体
对象__block
修饰的变量会被封装成结构体
对象,之前在数据段的会被复制到堆上,之前在堆上的则不受影响,解决auto
对象在block
内部无法修改的问题,在MRC
环境下,__block
不会对变量产生强引用.block
不使用copy
则不会从全局或者栈区域移动到堆上,使用copy
之后有由发者管理- 使用
block
要注意不能产生循环引用,引用不能变成一个环,主动使其中一个引用成弱引用,则不会产生循环引用。 __weak
修饰的对象,block
不会对对象强引用,在执行block
的时候有可能会值已经被系统置为nil
,__unsafe_unretained
修饰的销毁之后内存不会及时重置为空。
我们看的cpp
是编译之后的代码,runtime
是否和我们看到的一致呢?请听下回分解。
资料下载
- 学习资料下载
- demo code
runtime可运行的源码
本文章之所以图片比较少,我觉得还是跟着代码敲一遍,印象比较深刻。
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间