OC底层学习-Block
1.Block的本质
1.1 block的底层结构
- OC中block代码转换成C++代码:
//OC代码:
void(^block)(void) = ^(){
NSLog(@"this a block");
};
block();
//转换后的C++代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//定义block变量
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//执行block内部的代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
//上述main函数对应的是OC代码中的两句
//block的底层代码结构:
//__main_block_impl_0 这个结构体的地址就是__block_impl impl的地址
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//C++中的结构体的构造函数 返回一个结构体对象(类似于oc的init方法)
//第一个参数是封装执行代码函数的地址,把这个地址复制给了FuncPtr属性
__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;
}
};
//struct __block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;//指向了将来要执行的代码地址,block会将要执行的代码放到一个函数中去,函数地址就赋值给这个指针,将来用过这个地址去调用这个函数,就可以执行里面的代码
};
//struct __main_block_desc_0* block中的描述信息
static struct __main_block_desc_0 {
size_t reserved;//保留
size_t Block_size;//block大概占多少内存
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//sizeof是计算这个结构体占多大的内存
由此可见Block底层是一个结构体,Block会把需要执行的代码封装成一个函数,然后把这个函数的地址复制给FuncPtr
属性,将来使用这个地址来调用这个函数,就可以执行里面的代码。
上述是没有参数的Block,那如果是带有参数的Block,那么底层的代码会有什么变化吗
void(^block)(int, int) = ^(int a, int b){
NSLog(@"this a block====%d ====%d",a,b);
};
block(10,20);
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//首先block的调用方式是不一样了
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
}
return 0;
}
//封装的调用函数多了两个参数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_f57476_mi_0,a,b);
}
1.2 block变量捕获-auto
- Block内部访问外部变量:
//C语言: 有个默认关键字auto ==》 auto int age = 10; 默认可以省略(自动变量:离开作用域就销毁)
int age = 10;
void(^block)(void) = ^{
NSLog(@"block 访问外部变量===%d",age);
};
age = 20;
block();
//执行代码 打印结果 age = 10;
//下面我们看看底层的C++代码结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//底层结构体中多了一个成员变量age
int age;
// age(_age) : 意思是传递进来的age参数将来赋值给age成员
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
//在执行构造函数的时候传递的参数age是10 ,所以Block的内部的成员age 是10 值传递
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//修改外部的age变量值,但是block内部的成员age却没有发生变化 值还是 10,因为一开始的时候我们已经将10这个值赋值给age了
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//当调用函数的时候,取出blcok内部的age成员的值
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_a407cb_mi_0,age);
}
由此可知,当我们创建block对象的时候,age的值10 已经别储存在block的内部了(age的值被block捕获进来),所以当你修改age的值时仅仅是修改了外部的age的值,但是block的值却没有发生改变,所以打印结果是10
1.3 Block变量捕获(capture)-static
int age = 10;
static int height = 10;
void(^block)(void) = ^{
NSLog(@"block 访问外部变量===%d height=====%d",age,height);
};
age = 20;
height = 20;
block();
//打印结果: block 访问外部变量===10 height=====20
//C++底层代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
static int height = 10;
//定义block的时候,height传递进去的是height的地址值
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height; //缓存height的地址值
//height的地址值传递进来赋值给block内部的成员height
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block调用代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
//取出的是height的地址值
int *height = __cself->height; // bound by copy
//参数运算的是 *height 也就是放问成员height地址值的实际值,也就是外部变量的height的值,所以是20
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_44654d_mi_0,age,(*height));
}
由此可见,当block访问外部的static修饰的局部变量时,内部会产生一个成员来储存这个外部变量的地址,所以你在外部修改static修饰变量的值,那么blcok被调用的时候,内部对于static修饰的变量是根据地址值
去访问,所以最后访问到值是你在外部修改了值,也是height打印20的原因
1.4 block变量捕获-全局变量
block访问外部的全局变量:
int age_ = 10;
static int height_ = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"block 访问外部变量===%d height=====%d",age_,height_);
};
block();
}
return 0;
}
//C++的底层代码:
int age_ = 10;
static int height_ = 10;
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;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_bcbbee_mi_0,age_,height_);//直接访问外部的变量
}
由此可见如果是全局变量,block不会把变量捕获到block的内部(不需要捕获,因为是全局变量,哪个函数都可以访问,而局部变量需要跨函数访问所以需要捕获),block执行的时候是 直接访问外部的变量
1.5 Block访问self或类中的属性
@interface GYPerson : NSObject
/** <#descrption#> */
@property (nonatomic, strong) NSString *name;
- (void)test;
@end
@implementation GYPerson
- (void)test {
void(^block)(void) = ^{
NSLog(@"block=======%@",self);
};
block();
}
@end
//C++代码
struct __GYPerson__test_block_impl_0 {
struct __block_impl impl;
struct __GYPerson__test_block_desc_0* Desc;
//由此block的底层结构代码可知 block对self进行了捕获
GYPerson *self;
__GYPerson__test_block_impl_0(void *fp, struct __GYPerson__test_block_desc_0 *desc, GYPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __GYPerson__test_block_func_0(struct __GYPerson__test_block_impl_0 *__cself) {
GYPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_GYPerson_2066b6_mi_0,self);
}
//如果block内部是访问_name成员变量,我们来看看底层代码结构
struct __GYPerson__test_block_impl_0 {
struct __block_impl impl;
struct __GYPerson__test_block_desc_0* Desc;
GYPerson *self;
__GYPerson__test_block_impl_0(void *fp, struct __GYPerson__test_block_desc_0 *desc, GYPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __GYPerson__test_block_func_0(struct __GYPerson__test_block_impl_0 *__cself) {
GYPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_GYPerson_74dab0_mi_0,(*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_GYPerson$_name)));//调用的时候确实调用了name
}
由此可见,Block访问当前self或是方位类中的成员变量,都是直接捕获当前self,我们在OC中定义的方法,其实前面两个参数是 self(当前对象) 和 _cmd(当前方法对象),所有其实self是一个形参,也就是局部变量,所以你访问self的时候肯定会先把self对象给捕获到block中
访问成员变量的时候,blcok内部不会直接捕获成员变量,也是首先是把整个self对象捕获到block中,然后在通过self去访问成员变量
1.6 Block变量捕获(capture)总结
- 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
- block内部会专门新增一个成员来储存外边变量的值,我们称之为捕获
- auto和static为什么会有这个差异?
- auto因为是自动变量,可能会销毁的,变量的内存可能会消失,所以blcok内部执行的时候不可能去访问变量的内存
- static修饰的变量是一直都保留在内存中的,所以执行block调用的时候还可以根据
地址值
去访问这个变量在内存中的值
- 结论: 只要是局部变量,block访问局部变量,就会把这个变量捕获进去
- block的内存布局图:
1.7 blcok的类型
- block实质是一个OC对象,其有以下特征:
- block内部有一个isa指针
- block可以调用class方法,产看其类型
代码验证block的继承关系
void(^block)(void) = ^(){
NSLog(@"this a block");
};
block();
NSLog(@"type=====%@",[block class]);
NSLog(@"type=====%@",[[block class] superclass]);
NSLog(@"type=====%@",[[[block class] superclass] superclass]);
NSLog(@"type=====%@",[[[[block class] superclass] superclass] superclass]);
//打印结果:
/**
2020-08-13 09:41:06.592599+0800 Block[3809:51707] this a block
2020-08-13 09:41:06.593054+0800 Block[3809:51707] type=====__NSGlobalBlock__
2020-08-13 09:41:06.593114+0800 Block[3809:51707] type=====__NSGlobalBlock
2020-08-13 09:41:06.593143+0800 Block[3809:51707] type=====NSBlock
2020-08-13 09:41:06.593170+0800 Block[3809:51707] type=====NSObject
*/
由此可知block确实是一个OC对象,继承体系:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
最终是继承自NSObject
-
block的类型:
- NSGlobalBlock (_NSConcreteGlobalBlcok):没有访问auto变量的block
- NSMallocBlock (_NSConcreteMallocBlcok):__NSStackBlock__调用copy方法
- NSStackBlock (_NSConcreteStackBlcok)访问了auto变量
- 图解:
-
三种block的在内存放的地方图解:
首先我们关闭ARC环境,使用MRC环境来执行下面代码
示例代码:
void(^block)(void) = ^(){
NSLog(@"this a block");
};
int age = 10;
void(^block2)(void) = ^{
NSLog(@"block type====%d",age);
};
NSLog(@"%@ %@ %@",[block class],[block2 class],[^{
NSLog(@"block======%d",age);
} class]);
//打印结果: __NSGlobalBlock__ __NSStackBlock__ __NSStackBlock__
- __NSStackBlock__访问变量容易出现的问题:
void(^block)(void);
void test() {
int age = 20;
block = ^{
NSLog(@"block=====%d",age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
//执行结果: block=====-272632552 没有打出我们预料的20 的值
因为block访问了auto变量,我们称之为__NSStackBlock__,它存放在栈区
上,当test()被调用完成之后,这个栈区上的内存可能被销毁,这块内存里面的数据可能变成了垃圾,直接影响了block内部捕获的age的值(因为age是存在栈上的,block也是分布在栈上的,函数调用完毕,栈内存的数据可能是个垃圾数据了),而我们在此去调用block去访问,可能访问到的是一个垃圾值,栈上的特点是自动销毁
-
如何解决上述block存在栈区上,访问值出现问题的情况?
- 想方设法把block的内存放到
堆区
上,就是把__NSStackBlock__ 类型变成__NSMallocBlock__类型(只要当前block调用copy
方法) - 关于三种block在内存中的存放区域和复制效果图:
- 想方设法把block的内存放到
-
在ARC环境下,属性声明block时,使用copy关键字修饰?
- 是为了保存bloc,__NSStackBlock__的内存存在去栈区,随时可能销毁,为了保证block不销毁,需要把block从栈区复制到堆区去,使用copy修饰。
- 在ARC环境下,系统同会自动为block调用copy方法,这样__NSStackBlock__类型的block会复制到堆区,变成了__NSMallocBlock__类型的blcok,如果是在MRC环境下block使用copy之后,如果在不用之后,需要调用release方法释放,不然会有内存泄漏,ARC环境下却不需要
1.8 答疑
- block底层结构中FuncPtr指向的方法(block将来需要执行的代码)放在代码区
- 函数调用栈就是在栈区开辟了一块区域,当调用函数了时候,会指定一块栈区的内存给这个函数用,单是函数一旦调用完毕,这个快栈区的内存就会回收,将来变成垃圾数据,后面可能给别的用,会被别的覆盖
- 函数外面定义的变量称之为全局变量,函数里面定义的是局部变量
1.9 block-copy(ARC环境下自block对象copy到堆上的情况)
- 在ARC环境下,编译器会根据情况自动将栈上的block赋值到堆上,比如以下情况:
- block作为函数参数返回值时
- 将block赋值给__strong指针(强指针)时,也会自动进行一次copy操作
- block作为CocoaAPI中方法含有usingBlock的方法参数时
- block作为GCD API的方法参数时
//block作为函数返回值
typedef void(^block)(void);
block test(){
int age = 10;
return ^{
NSLog(@"block=========%d",age);
};
}
//测试代码
block block2 = test();
block2();
NSLog(@"block type=======%@",[block2 class]);
//打印结果:block type=======__NSMallocBlock__ 由此可见block作为返回值时,ARC下系统回族东copy到堆上
//block有强引用时
void(^block1)(void);
int age = 10;
block1 = ^{
NSLog(@"--------%d",age);
};
NSLog(@"type=====%@",[block1 class]);
//打印结果:type=====__NSMallocBlock__ 可见当block有强引用指向它时,ARC下系统会自动copy到堆上
1.10 block访问对象类型的auto变量
-
示例代码:
- 首先我们需要把OC转换成C++代码
- 在使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m转换代码的时候,可能会遇到以下问题
cannot create __weak refrence in file using manual refrence
- 解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0 main.m
-
在ARC环境下的代码转换和实现:
typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
GYBlock block;
{
GYPerson *person = [[GYPerson alloc] init];
person.name = @"zhangsan";
//__weak GYPerson *weakPorson = person;
block = ^{
NSLog(@"=========%@",person.name);
};
}
}
return 0;
}
//转换之后的C++代码
//block直接访问person对象时,对person有强引用
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
GYPerson *__strong person; //对person的强引用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//如果使用weakPorson来访问person,是不会对这个对象产生强引用的
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
GYPerson *__weak weakPorson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__weak _weakPorson, int flags=0) : weakPorson(_weakPorson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//在ARC环境下block内部捕获引用对象的变化
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPorson, (void*)src->weakPorson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPorson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
- 当处在栈上的Block内访问外部对象时,不会影响外部对象的生命周期(ARC和MRC环境)
- 当处在堆空间的Block访问外部对象时:
- 当Block内访问的person对象(强引用修饰person)时,只要Block对象没有销毁,person对象是不会销毁的
- 当Block内部访问的perosn对象(弱引用修饰person)时,只要出了person对象的作用域,person对象就会销毁,不受Block的影响
-
当block内部访问了对象内类型的auto变量时:
- 如果block是在栈上,将不会对auto变量产生强引用
- 如果block被拷贝到堆上
- copy函数内部会调用
_Block_object_assign
函数(copy函数调用时机:栈上的Block赋值到堆时) _Block_object_assign
函数会根据auto变量的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,类似于retain(形成强引用,和弱引用)
- copy函数内部会调用
- 如果block从堆上移除
- 会调用block内部的
dispose
函数(dispose
调用时机:堆上的Block被废弃时) dispose
函数内部会调用_Block_object_dispose
函数_Block_object_dispose
函数会自动释放引用的auto变量(断开Block对auto的引用),类似于release
- 会调用block内部的
-
Block内部只有访问
引用对象
的时候会出现copy
和dispose
函数来管理auto对象的内存。
1.11 block修改auto变量的值、__block修饰符
-
怎么在Block内部修改auto变量的值:
- 使用static来修饰这个auto变量
- 把这个auto变量修改成为全局变量
- 使用__blcok来修饰这个auto变量,但不会修改变量的性质
-
__blcok可以用于解决block内部无法修改auto变量值的问题
-
__block不能修饰全局变量、静态变量(static)
-
编译器会将__block变量包装成一个对象
typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
GYBlock block = ^{
age = 20;
NSLog(@"age is=====%d",age);
};
block();
}
return 0;
}
//使用__block修饰的auto变量在block内部的底层结构
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;//这个指针指向它自己
int __flags;
int __size;
int age;//储存外部变量的值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref //block内部这个指针指向 __Block_byref_age_0这个结构体
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
GYBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
//那么底层是如何修改变量值的
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
//通过指针拿到封装age值对象(age->__forwarding->age),然后在把这个对象内部的age值修改成20 ,在打印出来
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8s_wljbbn3d6bd0mt_kk8cxxdlc0000gn_T_main_59e0d5_mi_0,(age->__forwarding->age));
}
- 使用__blcok修改变量的本质:
- 当使用__block修饰变量的时候,首先会把变量包装一个对象(struct __Block_byref_age_0),
__Block_byref_age_0
对象中有一个__forwarding
成员指向它自己,还有一个isa
指针,然后对象中还有一个成员就是__block修饰的对象(这个变量就是当初需要修改的变量),现在存储在这个结构体对象中,而这个整体现在又被我们的block的对象拥有着。 - 修改过程:通过blcok内的包装对象的指针,找到包装对象,然后修改包装对象中变量的值,这样就修改完成了
__main_block_impl_0->__Block_byref_age_0->__forwarding-auto变量
- 当使用__block修饰变量的时候,首先会把变量包装一个对象(struct __Block_byref_age_0),
- 结构图
1.12 __block的内存管理
1.12.1block的调用过程
-
当block在栈上时,并不会对__block变量产生引用
-
当block被copy到堆时,会调用block内部的
copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会对__block变量形成强引用(retain) -
过程图:
-
block从堆中移除时,会调用block内部的
dispose
函数,dispose
函数内部会调用_Block_object_dispose
函数,而_Block_object_dispose
函数会自动释放引用的__block
变量(release) -
过程图:
-
对象类型的auto变量、__block变量的区别
- 相同点:
- 当block在栈上时,对外部的变量都不会产生强引用
- 当block拷贝到堆上时,都会内部copy函数来处理他们,当block从堆上移出时,会通过dispose函数来释放他们
- 不同点:
- 当内部调用copy函数的时候,如果是对象类型的auto变量,内部对auto变量的引用,会根据当前变量的修饰符来决定是强引用,还是弱引用,而对于__block类型的变量,都是强引用
- 修饰符:
- 相同点:
block一开始被申明时,__block和block都是在栈上的,由于ARC环境下对block的对象的强引用,会自动把block和__block包装的对象都拷贝(copy)到堆上,对__block包装对象是强引用
1.12.2 __block中的__forwarding指针
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;//这个就是__block包装对象中的__forwarding指针
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 为什么需要__forwarding指针了?
- 刚开始__blcok对象在栈上,__forwarding指针是指向自己,当blcok被复制到堆上的时候,那么__block对象也会随之赋值到堆上, 此时栈上__blcok包装对象内部的 __forwarding指针是指向复制到堆上的__block变量包装的对象,所以就保证了你无论是访问栈上还是堆上__block,只要是通过__forwarding指针访问就可以访问到堆上的__blcok对象
- 示意图:
1.12.3__blcok修饰的对象类型
OC代码:
typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block GYPerson *person = [[GYPerson alloc] init];
GYBlock block = ^{
NSLog(@"%p=====",person);
};
block();
}
return 0;
}
转换之后的block的C++代码:
typedef void(*GYBlock)(void);
struct __Block_byref_person_0 {
void *__isa;
__Block_byref_person_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
GYPerson *person;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_person_0 *person; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 如果person对象前面加上一个__weak来修饰看转换的C++代码:
typedef void(^GYBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
GYPerson *person = [[GYPerson alloc] init];
__block __weak GYPerson *weakPerson = person;
GYBlock block = ^{
NSLog(@"%p=====",weakPerson);
};
block();
}
return 0;
}
typedef void(*GYBlock)(void);
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
GYPerson *__weak weakPerson;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_weakPerson_0 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 由此可见 block内部有一个
__Block_byref_person_0 *person
指针是指向__block包装的结构体对象struct __Block_byref_person_0
,而该对象中有一个GYPerson *person
指针或则是GYPerson *__weak weakPerson
指正指向真正的person对象(该指针的强弱和该访问外界person对象的强、弱指针有关,如果外界是强引用,这里也是强引用,反之则是弱引用(注意:这里仅限于ARC时会retain,MRC是不会reatin)也就是说在MRC的环境下struct __Block_byref_person_0
对象在copy到堆上的时候,对象中的person指针不会对Person对象产生一个强引用,而是弱引用,在MRC的环境下,这个指针都是弱指针,不会因为访问对象时的强弱引用,而产生强弱之分)。
1.13 block的循环引用问题
- 测试代码:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^testBlock)(void);
@interface GYPerson : NSObject
/** <#descrption#> */
@property (nonatomic, assign) int age;
/** <#descrpition#> */
@property (nonatomic, copy) testBlock block;
@end
NS_ASSUME_NONNULL_END
#import "GYPerson.h"
@implementation GYPerson
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
//测试代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
GYPerson *person = [[GYPerson alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"ags is a === %d",person.age);
};
}
NSLog(@"111111111111111");
return 0;
}
当我们执行完成程序之后, person对象并没有销毁,接下我们看看底层结构代码
//block的底层结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//block内部有一个强引用引用这person对象
GYPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
由此可以产生循环引用的本质: 两个对象互相强引用对方
1.13.1 ARC环境下如何解决循环引用
在ARC环境下可以使用__weak
、__unsafe_unretained
解决循环引用的问题
GYPerson *person = [[GYPerson alloc] init];
person.age = 10;
//__unsafe_unretained typeof(person) weakPerson = person;
__weak typeof(person) weakPerson = person;
person.blcok = ^{
NSLog(@"age is a===%d",weakPerson.age);
};
//__unsafe_unretained 修饰
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
GYPerson *__unsafe_unretained weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__unsafe_unretained _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//__weak修饰
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
GYPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, GYPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
代码运行结果:
由上述代码我们可以看出,当我们使用__weak
、__unsafe_unretained
来修饰的person变量的时候, 我们可以发现block内部对person对象是一个弱引用,这样就解决了循环引用的问题
1.13.2 __weak
、__unsafe_unretained
的区别
__weak
:不会产生强引用,当__weak
指向的对象销毁了,系统把把__weak
的指针储存的地址值设置成为nil,不会产生野指针的问题(__weak自己有一套管理机制)__unsafe_unretained
不会产生强引用,不安全,当指向的对象销毁时,系统不会把指针指向的地址值设置为nil,如果这个时候还访问该指针指向的对象,会发生野指针现象
1.13.3 使用__block来解决循环引用(必须调用block)
- 使用__block来解决循环引用,代码分析:
int main(int argc, const char * argv[]) {
@autoreleasepool {
//使用__block来解决循环引用
GYPerson *person = [[GYPerson alloc] init];
__block GYPerson* weakPerson = person;
person.age = 10;
person.block = ^{
NSLog(@"ags is a === %d",weakPerson.age);
weakPerson = nil;
};
person.block();
}
NSLog(@"111111111111111");
return 0;
}
上述代码执行结果:
但是 如果我们不写weakPerson = nil;
和person.block();
这两句代码,执行结果表示person对象并没有被释放
- 由此可知使用__block来解决循环引用 是必须要执行block代码块的,这也意味着如果你的block可能永远不执行,那么程序就会产生内存泄漏的问题(这也是该方法的缺点,但是这个方法确实可以解决循环引用)
下面我们来分析下这段代码的底层结构代码:
//__block包装的对象
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
//person对象
GYPerson *__strong weakPerson;
};
//person中的blcok对象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_weakPerson_0 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
-
总结:上述代码实际产生了三个对象,__block、person、block,person对象持有block对象,而block对象又持有__block对象,而__block包装的对象内部又持有person对象,这样就形成了一个三角形的循环引用关系,而我们上面添加的
weakPerson = nil;
和person.block();
这两句代码实际上是断开__block包装对象内部对person对象的强引用(上面讲过,使用__blcok修饰的变量,就是__block
包装对象内部的那个对象),导致三角循环引用关系断开,从而解决循环引用的问题 -
关系示意图:
1.13.4 MRC下的循环引用关系
- 在MRC下面我们可以使用
__unsafe_unretained
来解决循环引用(注意:MRC不支持__weak)
GYPerson *person = [[GYPerson alloc] init];
__unsafe_unretained GYPerson* weakPerson = person;
person.age = 10;
person.block = [^{
NSLog(@"ags is a === %d",weakPerson.age);
} copy];
[person release];
-
使用
__unsafe_unretained
来修饰(不会产生强引用),就是block内部不会对person强引用,那么person的引用计数器一直到是1,当调用release
方法,person就会销毁 -
MRC环境也可以使用__block来解决循环引用,( 因为我们上面讲过,在MRC环境下__blcok的包装对象是不会对person对象进行强引用的(在ARC环境下会,系统会自动执行类似reatin操作,进行强用用,尽管block是在堆上,也不会产生强引用),那么也就是说__blcok强引用person 是不存在的,导致没有构成三角循环引用的关系,所以当person当调用
release
方法,person就会销毁
2. 面试题
2.1 block的原理是怎么样的?本质是什么?
- block的本质上也是一个OC对象,它的内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
2.2 _block的作用是什么?有什么使用注意点?
- 会把修饰的变量包装成一个对象,可以解决blcok内部无法修改auto变量值的问题
- 注意点:会对修饰的变量进行一个相应的内存管理,而且在MRC的环境下是不会对修饰的oc对象产生强引用
2.3 block的属性修饰词为什么是copy?使用block有哪些注意点?
- block一旦没有进行copy操作,就不会再堆上,方便我们对block的声明周期进行管理
- 使用注意:循环引用的问题
2.4 block在修改NSMutableArray.需不需要添加__blcok?
- 不需要加上__block
3. 答疑
3.1block使用strong和copy修饰的区别
- 在ARC环境下是没有区别的,系统都把block对象copy到堆上,但是在MRC下是有区别,MRC下copy是会把block对象copy到堆上,但是strong是不会copy的