ios底层原理-代码块(block)的本质(二)
问题
1.什么是block,block的本质是什么?
2.block的属性修饰词为什么是copy?使用block有哪些使用注意?
3.block为什么会发生循环引用?
4.block的变量捕获究竟是怎样进行的?
5.block在修改NSMutableArray,需不需要添加__block?
6.__block和__weak的作用是什么?有什么使用注意点?
7.block中访问的对象和变量什么时候会销毁?
上文ios底层原理-代码块(block)的本质(一)中已经解决了前三个问题,本文继续探寻block的本质,补充上面三个问题,以及解答下面的问题。
block对变量的捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
首先我们先了解一下有些什么变量
block的变量捕获
- auto 离开作用域就销毁,局部变量前面自动添加auto关键字。
- static 修饰的变量为指针传递,同样会被block捕获。
- 全局变量 跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。
我们来验证上图是否正确
先新建一个Student类
@interface Student : NSObject
@property(nonatomic,assign)NSInteger age;
@end
@implementation Student
- (void)dealloc
{
NSLog(@"%@ 对象销毁了",self);
}
@end
写下如下代码 :
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int a = 1;
static int b = 2;
Student * studentA = [[Student alloc] init];
void(^block)(void) = ^{
NSLog(@"number, a = %d, b = %d", a,b);
NSLog(@"student : %ld",(long)studentA.age);
};
a = 11;
b = 22;
block();
}
return 0;
}
先转化为c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
Student *__strong studentA;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, Student *__strong _studentA, int flags=0) : a(_a), b(_b), studentA(_studentA) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
Student *__strong studentA = __cself->studentA; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_0, a,(*b));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_1a94a7_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)studentA, sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->studentA, (void*)src->studentA, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->studentA, 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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int a = 1;
static int b = 2;
Student * studentA = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b, studentA, 570425344));
a = 11;
b = 22;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
直接找到__main_block_impl_0
函数中的a,&b,studentA,而__main_block_impl_0
结构体中直接定义了一个新的a把传入的a的值赋给了__main_block_impl_0
结构体中a变量,把&b的地址传给了__main_block_impl_0
结构体中b指针变量,把studentA指针变量赋值给了__main_block_impl_0
结构体中的studentA指针变量。
其中studentA也是auto变量。此时此刻可以看到__main_block_impl_0
中的Student *__strong studentA
是__strong类型的,所以block强引用studentA对象。
那么为什么auto 和 static会有这样的差异呢,auto变量在栈中作用域结束就会被销毁,block在执行的时候有可能自动变量已经被销毁了,因此对于自动变量一定是值传递而不可能是指针传递了,而因为传递的是值得地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会随之改变。归根结底还是为了防止野指针的出现和节约内存开销。
全局变量
typedef void (^Block)(void);
int a = 1;
static int b = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"number, a = %d, b = %d", a,b);
};
a = 11;
b = 22;
block();
}
return 0;
}
转成c++
typedef void (*Block)(void);
int a = 1;
static int b = 2;
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_883251_mi_0, a,b);
}
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(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));
a = 11;
b = 22;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
此时此刻,__main_block_imp_0
并没有添加任何变量,直接调用全局定义的a,b。
block里访问self是否会捕获?
写如下代码
@interface Student : NSObject
@property(nonatomic,assign)NSInteger age;
@end
@interface Student()
{
NSInteger sId;
}
@end
@implementation Student
- (void)catchSelf
{
void(^block)(void) = ^{
NSLog(@"%@",self);
NSLog(@"%ld",(long)self.age);
NSLog(@"%ld",(long)self->_age);
NSLog(@"%ld",self->sId);
};
block();
}
- (void)dealloc
{
NSLog(@"%@ 对象销毁了",self);
}
+ (void)staticFunc
{
NSLog(@"类方法");
}
@end
转化为c++
struct __Student__catchSelf_block_impl_0 {
struct __block_impl impl;
struct __Student__catchSelf_block_desc_0* Desc;
Student *self;
__Student__catchSelf_block_impl_0(void *fp, struct __Student__catchSelf_block_desc_0 *desc, Student *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __Student__catchSelf_block_func_0(struct __Student__catchSelf_block_impl_0 *__cself) {
Student *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_0,self);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_1,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_2,(long)(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$_age)));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_3,(*(NSInteger *)((char *)self + OBJC_IVAR_$_Student$sId)));
}
static void _I_Student_catchSelf(Student * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Student__catchSelf_block_impl_0((void *)__Student__catchSelf_block_func_0, &__Student__catchSelf_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
找到_I_Student_catchSelf
函数也就是我们定义的catchSelf
函数其中默认传了两个参数Student * self
, SEL _cmd
。
static void _C_Student_staticFunc(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_Student_bb73ff_mi_5);
}
可以发现我们定义的类方法也是默认传了这两个参数
此时此刻看到我们的catchSelf方法
- (void)catchSelf
{
void(^block)(void) = ^{
NSLog(@"%@",self);
NSLog(@"%ld",(long)self.age);
NSLog(@"%ld",(long)self->_age);
NSLog(@"%ld",self->sId);
};
block();
}
在block中我使用了sId成员变量
和self->_age实例变量
以及age的get方法
,但在__Student__catchSelf_block_impl_0
中只是捕获了self
(指向调用者)
注意
- self.age调用的是get方法
- self->sId 分类的成员变量 通过地址获取
- self->_age 属性的实例变量 通过地址获取
block中访问对象和变量的销毁
直接导入我们上边写过的Student类 写如下代码
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
NSLog(@"开始");
auto int a = 5;
Student *student = [[Student alloc] init];
student.age = 100;
block = ^{
NSLog(@"------block内%ld",(long)student.age);
};
} // 执行完毕,student没有被释放,a释放
NSLog(@"结束");
} // student 释放
return 0;
}
打印内容
大括号执行完毕之后,student对象
依然不会被释放,变量a
处于作用域之外已经被释放了,上述block为堆block,block里面有一个student指针
,student指针
指向student对象
。只要block还在,student就还在。block强引用了student对象
。
在@autoreleasepool
之后block
被销毁student 对象
被销毁。
查看源代码,也发现block确实如上面说的一样强引用student
__weak作用
对student对象进行__weak操作
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
NSLog(@"开始");
auto int a = 5;
Student *student = [[Student alloc] init];
student.age = 100;
__weak Student *weakStudent = student;
block = ^{
NSLog(@"------block内%ld",(long)weakStudent.age);
};
} // 执行完毕,student被释放,a被释放
NSLog(@"结束");
}
return 0;
}
打印内容
可以发现__weak
添加之后,student
在作用域执行完毕之后就被销毁了。这是为什么呢?让我们把代码转化为c++,看看究竟做了些什么操作。
__weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错。
crun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64-weak.cpp
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Student *__weak weakStudent;//用__weak修饰
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Student *__weak _weakStudent, int flags=0) : weakStudent(_weakStudent) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在__main_block_impl_0
中依然是用的__weak
修饰weakStudent
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结构体__main_block_impl_0
的描述结构体__main_block_desc_0
中多了两个参数copy
和dispose
函数
__main_block_copy_0 和 __main_block_dispose_0
首先我们已经说过__main_block_desc_0
是block的描述,储存着block的字节大小。那两个参数又有什么用呢?
我们就点进__main_block_copy_0
这个函数。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->weakStudent, (void*)src->weakStudent, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
__main_block_copy_0
函数中传入的都是__main_block_impl_0
结构体本身。
copy
本质就是__main_block_copy_0
函数,__main_block_copy_0
函数内部调用_Block_object_assign
函数,_Block_object_assign
中传入的是student对象的地址,student对象,以及3。
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->weakStudent, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
__main_block_dispose_0
函数中传入的都是__main_block_impl_0
结构体本身。
dispose
本质就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部调用_Block_object_dispose
函数,_Block_object_dispose
函数传入的参数是student对象,以及3。
__main_block_copy_0
和__main_block_dispose_0
函数中会根据变量是强弱指针及有没有被__block
修饰做出不同的处理,强指针在block
内部产生强引用,弱指针在block内部产生弱引用。被__block
修饰的变量最后的参数传入的是8,没有被__block
修饰的变量最后的参数传入的是3。
_Block_object_assign函数调用时机及作用
上文已经说过ARC会在赋值给__strong指针时自动进行copy,而此时我们就需要对student对象的引用计数做修改,以确保这个对象在block存在的时候不会被销毁。
当block进行copy操作的时候就会自动调用__main_block_desc_0
内部的__main_block_copy_0
函数,__main_block_copy_0
函数内部会调用_Block_object_assign
函数。
_Block_object_assign
函数会自动根据__main_block_impl_0
结构体内部的student
是什么类型的指针,对student
对象产生强引用或者弱引用。可以理解为_Block_object_assign
函数内部会对student
进行引用计数器的操作,如果__main_block_impl_0
结构体内student指针是__strong
类型,则为强引用,引用计数+1,如果__main_block_impl_0
结构体内student指针是__weak
类型,则为弱引用,引用计数不变。而__main_block_impl_0
结构体内部的student
是什么类型的指针是由外面我们定义的对象的具体类型决定。
_Block_object_dispose函数调用时机及作用
当block从堆中移除时就会自动调用__main_block_desc_0
中的__main_block_dispose_0
函数,__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数。
_Block_object_dispose
会对student
对象做释放操作,类似于release
,也就是断开对student
对象的引用,而student
究竟是否被释放还是取决于student
对象自己的引用计数。
总结
- 栈block
- 如果block是在栈上,将不会对auto变量产生强引用
- 栈上的block随时会被销毁,也没必要去强引用其他对象
- 堆block
如果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)
如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用
__block作用
-
__block可以用于解决block内部无法修改auto变量值的问题
-
__block不能修饰全局变量、静态变量(static)
-
编译器会将__block变量包装成一个对象
-
__block修改变量:a->__forwarding->a
-
__Block_byref_a_0结构体内部地址和外部变量a是同一地址
先来__block
修饰auto变量
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
__block auto int a = 5;
block = ^{
NSLog(@"------block内%ld",(long)a);
};
}
}
return 0;
}
转成c++代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Block block;
{
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_0);
__attribute__((__blocks__(byref))) auto __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 5};
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_2);
}
return 0;
}
可以发现__main_block_impl_0
函数中传的不再是变量a
而是__Block_byref_a_0
类型的变量a
的地址&a
查看__Block_byref_a_0
结构体
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
此时此刻 已经很清晰了变量a包装成了__Block_byref_a_0
结构体,__forwarding
指向的就是这个就是这个结构体自己的地址,__Block_byref_a_0
结构体中a
变量才是存储值的地方
-
__isa
指针 :__Block_byref_a_0
中也有isa指针也就是说__Block_byref_a_0
本质也一个对象。 -
__forwarding
:__forwarding
是__Block_byref_a_0
结构体类型的,并且__forwarding
存储的值为(__Block_byref_a_0 *)&a
,即结构体自己的内存地址。 -
__flags
:0 -
__size
:sizeof(__Block_byref_a_0)
即__Block_byref_a_0
所占用的内存空间。 -
a
:真正存储变量的地方,这里存储局部变量5。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_b48be2_mi_1,(long)(a->__forwarding->a));
}
调用block时,先取出__main_block_impl_0
中的a
,通过a
结构体拿到__forwarding
指针,上面提到过__forwarding
中保存的就是__Block_byref_a_0
结构体本身,这里也就是a(__Block_byref_a_0)
,在通过__forwarding
拿到结构体中的a(5)
变量并修改其值。
__forwarding指针的作用
__forwarding
指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding
指针,在通过__forwarding
指针找到相应的变量。这样设计的目的是为了方便内存管理。block
被复制到堆上时,会将block
中引用的变量也复制到堆中。
此时此刻
__forwarding
函数的操作为了方便堆栈的切换,内存的管理。
__block
修饰对象变量
先写如下代码
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
__block Student *student = [[Student alloc] init];
student.age = 100;
block = ^{
NSLog(@"------block内%ld",(long)student.age);
};
}
}
return 0;
}
查看源码
typedef void (*Block)(void);
struct __Block_byref_student_0 {
void *__isa;
__Block_byref_student_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Student *__strong student;
};
我们对比上面的__Block_byref_a_0
结构体 ,__Block_byref_student_0
中添加了内存管理的两个函数__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
上文提到当block
中捕获对象类型的变量时,block
中的__main_block_desc_0
结构体内部会自动添加copy
和dispose
函数对捕获的变量进行内存管理。
那么同样的当block内部捕获__block
修饰的对象类型的变量时,__Block_byref_person_0
结构体内部也会自动添加__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
对被__block包装成结构体的对象进行内存管理。
当block
内存在栈上时,并不会对__block
变量产生内存管理。当blcok
被copy
到堆上时
会调用block
内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会对__block
变量形成强引用(相当于retain)
当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数,会自动释放引用的__block
变量。
block
内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。
__block
修饰的变量在block
结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。
block在修改NSMutableArray,需不需要添加__block?
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray array];
Block block = ^{
[array addObject: @"c++"];
[array addObject: @"c#"];
NSLog(@"%@",array);
};
block();
}
return 0;
}
command + r
在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block
修饰也可以正确编译。同理add 和remove方法都不需要__block
,同理 NSMutableDictionary
,NSMutableSet
,NSMutableString
和NSMutable.....
都是同一个道理。
归根结底,了解这么多并不只是为了知道怎么防止循环引用,学到的应该是学习进阶的方式,还有设计模式和逻辑结构。
到此block底层篇已经结束了,如有补充或交流请加微信
微信二维码