Block
原理Block
变量捕获Block
类型copy
操作和Block
内部访问对象类型的变量__block
修改变量及其本质__block
内存管理Block
循环引用问题
Block
是一种可以在C
、C++
以及Objective-C
代码中使用,类似于“闭包(closure
)”的代码块,借助Block
机制,开发者可以将代码像对象一样在不同的上下文环境中进行传递。
(这里说的不同上下文环境,我举个例子:比如在A
函数中定义了一个变量,它是一个局部变量,那么我要在B
函数中去访问,这里就属于两个不同的上下文环境)
一、Block
原理
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int,int) = ^(int a, int b){
NSLog(@"a = %d, b = %d, age = %d",a,b,age);
};
block(3,5);
}
return 0;
}
将上面main.m
编译生成C++
代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
main()
函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
}
return 0;
}
__main_block_impl_0
结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int 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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __maib_block_desc_0 {
size_t reserved;
size_t Block_size;
};
我们定义block
变量,其实下面这句代码:
void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
就是调用结构体__main_block_impl_0
内部的构造函数初始化一个结构体出来,然后取结构体地址&__main_block_impl_0
赋给block
指针,所以block
底层是下面结构体;调用构造函数传了三个参数:
(void *)__main_block_func_0
、&__main_block_desc_0_DATA
、age
其中(void *)__main_block_func_0
是下面函数的地址,这个函数就是封装了block
执行逻辑的函数,通过上面的构造函数传给__block_impl
结构体的FuncPtr
:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
}
同样,第二个参数类型&__main_block_desc_0_DATA
是下面结构体地址,最终通过构造函数赋给了Desc
,其中Block_size
表示block
结构体的大小;
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)};
在main()
函数中调用block(3, 5)
最终转化为下面代码,通过将block
强制转换为__block_impl
(这里__block_impl
类型是__main_block_impl_0
结构体第一个成员,所以可以转) ,最终直接找到impl
中的FuncPtr
进行调用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
二、Block
变量捕获
Block
变量捕获是指在Block
内部访问外部变量时,如果外部变量是局部变量,则Block
内部会将其捕获,具体捕获形式看外部的这个局部变量是auto
类型还是static
类型:
如果是auto
类型,直接将变量的值传递给Block
内部,Block
结构体内部会生成一个变量来存储传进来的值,所以在Block
外边改变age=20
,调用block()
时内部打印的结果依然是age=10
,因为此时进行的是值传递;
如果是static
类型,会将变量的地址传递给Block
内部,block
结构体内部会生成一个指针变量来存储传进来的地址值,所以在block
外边改变height=20
,调用block()
时内部打印的结果是height=20
,因为此时进行的是指针传递;
下面进行验证:
- 局部变量两种情况:
// 局部变量两种情况
int main(int argc, const char * argv[]) {
@autoreleasepool {
// case-1: auto变量,离开作用域就销毁
auto int age = 10; //等价于 int age = 10;
// case-2: static变量
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d",age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
打印结果:
age is 10, height is 20
编译成C++
:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *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;
}
};
从编译生成的结构体看出,age
是值传递,height
是指针传递;定义完block
就将10
和&height
捕获到block
内部,后边调用block
时访问的结构体内部age
是捕获到的值10,height
是捕获到的地址&height
;
- 全局变量:因为是在全局区,所以任何函数内部可以直接访问
总结一下:
Block
的本质:Block
本质上也是一个OC
对象,它内部也有个isa
指针,但它是一个封装了函数调用以及函数调用环境的OC
对象;
三、Block
类型
Block
有三种类型,可以通过调用class
方法或者isa
指针查看具体类型,最终都是继承自NSBlock
类型:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello World!");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
}
return 0;
}
打印结果:
05-block类型[46881:69217078] __NSGlobalBlock__
05-block类型[46881:69217078] __NSGlobalBlock
05-block类型[46881:69217078] NSBlock
05-block类型[46881:69217078] NSObject
三种类型:
-
__NSGlobalBlock__
(_NSConcreteGlobalBlock
) -
__NSStackBlock__
(_NSConcreteStackBlock
) -
__NSMallocBlock__
(_NSConcreteMallocBlock
)
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^{
NSLog(@"Hello");
};
int age = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", age);
};
NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
NSLog(@"%d",age);
} class]);
}
return 0;
}
打印结果:
05-block类型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
那不同类型Block
分别对应什么情况呢?
static int height = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 没有访问外部变量
void (^block1)(void) = ^{
NSLog(@"--------------");
};
// 访问 auto 变量
int age = 10;
void (^block2)(void) = ^{
NSLog(@"--------------%d", age);
};
// 访问 static 变量
void (^block3)(void) = ^{
NSLog(@"=-------------%d", height);
};
NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
}
return 0;
}
打印结果:
05-block类型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__
可以看出,在没有访问外部变量的情况下,block1
是一个__NSGlobalBlock__
类型,存放在数据区,此时的block1
就相当于我们定一个了一个函数,函数中的代码没有访问另外一个函数(此处为main()
)中的变量;同理,block3
虽然访问外部变量,但static
变量是全局的,同样相当于单独拿出去定义一个和main()
函数上下文无关的函数;
由于block2
访问了auto
变量,相当于在block2
封装的函数中访问了另外一个函数内部的变量(main()
函数中的局部变量age
),此时block2
变为__NSStackBlock__
,因为它需要保存这个局部变量,由于是在ARC
环境,会自动对__NSStackBlock__
类型进行copy
操作,所以 block2
打印类型是一个 __NSMallocBlock__
类型;
关闭ARC
在MRC
环境下打印:
打印结果:
05-block类型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__
可以看出block2
确实是一个__NSStackBlock__
类型;
四、copy
操作和Block
内部访问对象类型的变量
copy
操作分MRC
和ARC
两种情况:
-
MRC
环境:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
// 情况一:没有访问外部 auto 变量
// 它是一个 NSGlobalBlock
// 内存是放在数据段
void (^block)(void) = ^{
NSLog(@"---------");
};
// 情况二:访问外部 auto 变量 age
// 它是一个 NSStackBlock 随时会被回收
// 内存是在栈上
// 通过 copy 操作转变为 NSMallocBlock 把它放到堆上保活
void (^block2)(void) = [^{
NSLog(@"---------%d",age);
} copy];
// 因为在 MRC 环境 不用时要进行 release 操作
[block2 release];
}
return 0;
}
-
ARC
环境:
在ARC
环境下,编译器会根据情况自动将栈上的Block
拷贝到堆上,即自动进行一次copy
操作,比如以下情况:
- 情况一:
Block
作为函数返回值
// 定义一个block类型
typedef void (^DJTBlock)(void);
// block作为函数返回值
DJTBlock myblock()
{
// case1: 这里没有访问auto变量 是一个NSGlobalBlock
return ^{
NSLog(@"------------");
};
// 相当于下面这样写
// DJTBlock block = ^{
// NSLog(@"------------");
// };
// return block;
//-----------------------------------------------------------------
// case2: 这里访问了auto 是一个NSSackBlock 作为函数返回值ARC下自动copy成NSMallocBlock
// int age = 10;
// return ^{
// NSLog(@"------------%d",age);
// };
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// ARC环境下 调用myblock()函数
// DJTBlock作为myblock函数的返回值 编译器自动进行一次 copy 操作
// 所以 block变量指向的 DTJBlock 此时已经在堆上
DJTBlock block = myblock();
block();
// 打印 block 类型
NSLog(@"%@",[block class]);
}
return 0;
}
打印结果:
05-block--copy[64907:9167520] ------------
05-block--copy[64907:9167520] __NSGlobalBlock__
打印结果是一个NSGlobalBlock
类型,这是因为在函数my block()
内部没有访问auto
变量(上面block
类型有阐述),而对NSGlobalBlock
类型的Block
执行copy
操作生成的Block
还是NSGlobalBlock
,所以如果将返回改为myblock()
函数内注释部分,就会打印__NSMallocBlock__
。
- 情况二:将
Block
赋值给__strong
强指针时
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
// Block被强指针指着
DJTBlock block = ^{
NSLog(@"------------%d",age);
};
block();
// 打印 block 类型
NSLog(@"%@",[block class]);
}
return 0;
}
打印结果:
05-block--copy[69520:9293376] ------------10
05-block--copy[69520:9293376] __NSMallocBlock__
- 情况三:
Block
作为Cocoa API
中方法各含有usingBlock
的方法参数时:
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
这个参数Block
也是一个堆上的block
;
- 情况四:
Block
作为GCD API
的方法参数时:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
Block
内部访问对象类型的变量
先看一个有趣的现象:
// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end
// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
}
NSLog(@"-----------------");// 打断点
}
return 0;
}
在上面NSLog(@"-----------------");
处打断点,运行程序发现控制台打印:
05-block访问对象类型的auto变量[77563:9561984] DJTPerson----dealloc
(lldb)
说明在断点前的中括号结束,person
变量就已经释放,接着我们定义一个block
,在内部访问person
的age
属性:
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"-----------%d",person.age);
};
}
NSLog(@"-----------------");// 打断点
}
return 0;
}
通用在NSLog(@"-----------------");
处打断点,运行程序发现控制台无打印,说明person
没被回收。
为什么被第二种情况下person
没有被回收呢?为了验证我们将代码简化并编译成C++
来进行底层原理分析:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
DJTBlock block = ^{
NSLog(@"-----------%d",person.age);
};
}
return 0;
}
日常操作命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
在编译生成的C++
文件中查看生成的block
结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
由于person
是DJTPerson *
类型,所以捕获到block
内部也是DJTPerson *
类型,即struct __main_block_impl_0
结构体内部可以看到有一个DJTPerson *
类型变量person
;下面先从一个角度理解为什么person
没有被释放:
在上面代码中,我们定义的Block
是被一个DJTBlock
类型的变量block
强引用的,即这句代码:
DJTBlock block = ^{
NSLog(@"-----------%d",person.age);
};
在ARC
环境下,被强引用的这个Block
(访问了auto
变量)会自动拷贝到堆上,而这个Block
内部(编译成C++
即为struct __main_block_impl_0
结构体)又有一个DJTPerson*
类型的指针指向外面这个person
对象,所以只要这个Block
在,那么这个强指针就在,所以外边的person
对象不会被释放;
换成MRC
环境:
// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end
// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
[super dealloc];
NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"-----------%d",person.age);
};
[person release];
}
NSLog(@"---------------");// 打断点
}
return 0;
}
依然在NSLog(@"---------------");
打断点,发现控制台打印结果:
05-block访问对象类型的auto变量[83896:9803518] DJTPerson----dealloc
发现person
被释放,这是因为即使block
内部访问了person
对象,MRC
环境下,block
内部访问了auto
变量,它是一个栈上block
,但并不会自动拷贝到堆上,由于它是一个NSStackBlock
,内部并不会对外部person
强引用(这里说强引用并不准确,在MRC
环境没有强引用说法,应该描述为没有对外边person
进行retain
操作,但为了好理解 so...),所以在执行完[person release]
以后,虽然Block
还没有离开其作用域(Block
作用域到return 0;
前到大括号),但person
就被释放;可以通过[block copy]
将其复制到堆上,这样内部就会对外边的person
强引用(其实是retain操作
)从而保活person
,当然在Block
销毁的时候,内部对person
还会进行一次release
操作,这样一加一减,就保持了平衡;
要点:栈空间的Block
(NSStackBlock
)是不会对外边auto
对象进行保活(ARC
环境表现为不会强引用,MRC
下表现为不会进行retain
操作),只有拷贝到堆上(NSMallocBlock
)才会对其自动保活。
回到ARC
环境:
看一下__weak
作用:
int main(int argc, const char * argv[]) {
@autoreleasepool {
DJTBlock block;
{
DJTPerson *person = [[DJTPerson alloc] init];
person.age = 10;
// 这里使用 __weak 修饰
__weak DJTPerson *weakPerson = person;
block = ^{
NSLog(@"-----------%d",weakPerson.age);
};
}
NSLog(@"---------------"); // 打断点
}
return 0;
}
依然在NSLog(@"---------------");
处打断点,打印结果为:
05-block访问对象类型的auto变量[87323:9930285] DJTPerson----dealloc
这说明,即使在ARC
环境,Block
被拷贝到堆上,由于我们用__weak
类型的__weakPerson
访问了外部auto
变量,它也不会对外部person
进行强引用。
同样我们把上述代码编译成C++
,由于弱引用需要运行时机制来支持,所以我们不能进行静态编译,还需要运行时调用,指定运行时系统版本,所以编译命令如下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
在生成的C++
代码中找到__main_block_impl_0
结构体,发现是一个弱引用:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
总结一下:
-
Block
在栈上,无论ARC
、MRC
环境,block
内部都不会对外部对象类型的auto
变量产生强引用,就算Block
内部生成强指针,也不会对外部person
产生强引用,因为Block
自己就在栈上,随时可能被销毁; -
Block
在堆上:
在ARC
环境下,访问外部对象类型的auto
变量,编译后:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
DJTPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在 __main_block_desc_0
结构体中,多了两个函数:__main_block_copy_0
和__main_block_dispose_0
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};
分别看它们实现:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
当对Block
进行copy
操作时,会调用这个__main_block_copy_0
函数,在它内部调用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/)
,会对外部person
对象产生强引用或者弱引用,这取决于block
内部使用__strong
指针还是__weak
指针访问。
当Block
从堆上移除,会调用__main_block_dispose_0
函数,它内部调用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
,会对外部person
对象进行一次release
操作。
在MRC
环境下,也是由这两个函数决定是否进行retain
和release
操作。
五、__block
修改变量及其本质
我们先看下面一段代码:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
DJTBlock block = ^{
age = 20; // 报错:Variable is not assignable (missing __block type specifier)
NSLog(@"-----------%d",age);
};
}
return 0;
}
ARC
下直接在Block
内部修改age
会报错,这就相当于在block
生成的结构体中FuncPtr
指向的函数中去修改main
函数中的局部变量(如果这里age
是static
或者全局变量,可以修改,因为这两种变量一直在内存中),上下文环境发生了改变,所以不能直接访问age
;我们使用__block
修饰age
变量,然后编译成C++
:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
DJTBlock block = ^{
age = 20;
NSLog(@"-----------%d",age);
};
}
return 0;
}
会生成下面结构体:
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;
}
};
可以看到在Block
结构体中多了__Block_byref_age_0 *age;
,看一下 __Block_byref_age_0
发现它也是一个结构体:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
这里看出编译器将 __block
修饰的变量(这里是age
)包装成一个对象__Block_byref_age_0
(因为它内部有isa
指针,所以可以认为它是个对象),Block
内部(__main_block_impl_0
结构体中)并不会直接拥有这个变量age
,而是拥有__Block_byref_age_0
这个结构体,然后__Block_byref_age_0
结构体中有一个int age
变量,我们在Block内部改变age = 20
,实际上就是赋值给__Block_byref_age_0
结构体中的age
变量。
我们对__block int age = 10
转化成的C++
代码进行简化:
// __block int age = 10;对应下面c++代码:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
// 进行简化
__Block_byref_age_0 age = {
0,
&age,
0,
sizeof(__Block_byref_age_0),
10
};
对应到下面结构体初始化:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
__forwarding
指针传入&age
指向__Block_byref_age_0 age
结构体自己(这里&age
是结构体地址,不要混淆),10赋值给了__Block_byref_age_0
结构体内部的age
变量;我们再看下修改age
为20的代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
// 这里是 age = 20;
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
}
可以发现先通过 __cself->age
找到__Block_byref_age_0
结构体,然后(age->__forwarding->age) = 20;
通过__forwarding
指针修改结构体内部的age
变量,__forwarding
指向结构体自己,那为什么要多此一举通过__forwarding
指针去修改内部age
,而不通过结构体指针直接去修改呢?这是为了保证Block
被copy
到堆上时,不管访问栈上还是堆上Block
,通过forwarding
指针都是找到堆上。
这里如果__block
修饰的是一个对象类型,比如下面代码:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
__block NSObject *obj = [[NSObject alloc] init];
DJTBlock block = ^{
obj = nil;
age = 20;
};
}
return 0;
}
转换为C++
同样会多生成一个对应的结构体,只不过内部会多出两个方法copy``和dispose
方法来负责相应的内存管理:
// __block age 对应的结构体
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
// __block NSObject 对应的结构体
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
下面看一个例子:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mutarray = [NSMutableArray array];
DJTBlock block = ^{
[mutarray addObject:@(12)];
[mutarray addObject:@(13)];
};
}
return 0;
}
这里不会报错,是因为我们并没有修改mutarray
指针,而是在使用mutarray
指针,除非我们修改mutarray
指针的值,比如 mutarray = nil;
才需要__block
来修饰;
六、__block
的内存管理
我们知道,当Block
内部访问外部对象类型的变量时,如下面简单代码:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%p", object);
};
block();
}
return 0;
}
block
编译成C++
后的结构:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object; //内部强引用外部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在block
结构体内部会生成一个强指针指向外边的object
对象,并且在block
被拷贝到堆上时,调用__main_block_desc_0
中的copy
函数,对这个指针指向的对象进行一次retain
操作,即引用计数+1
,当然如果用__weak
修饰object
会生NSObject *__weak object;
此时不会强引用;
那当我们用__block
修饰变量时,比如分别修饰基础数据类型age
,和对象类型obj1
,如下代码:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
__block NSObject *obj1 = [[NSObject alloc] init];
NSObject *object = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%d %p %p", age,obj1, object);
};
block();
}
return 0;
}
编译成C++
:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_obj1_1 {
void *__isa;
__Block_byref_obj1_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj1;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object;
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : object(_object), age(_age->__forwarding), obj1(_obj1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
经__block
修饰的变量age
和obj1
分别生成构体__Block_byref_age_0
和 __Block_byref_obj1_1
,它们的本质就是OC
对象,所以在block
对应的结构体内部生成两个结构体指针指向这两个对象,即
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
它们其实和object
一样,因为__block
修饰的变量也是转换成结构体,而且内部有isa
指针,其实就是OC
对象,所以也会在__main_block_desc_0
中生成两个函数:copy
和dispose
,来管理对象的内存,可以看下结构:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->object, (void*)src->object, 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->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->object, 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
内存管理可以总结一下:
-
当
Block
在栈上时,内部并不会对__block
修饰的外部变量产生强引用 -
当
Block
被copy
到堆上时,会调用Block
内部的copy
函数,而copy
函数内部会调用_Block_object_assign
函数,它内部会对__block
变量形成强引用(retain
)。
-
当
Block
从堆上移除时,会调用Block
内部的dispose函数,dispose
函数内部会调用_Block_object_dispose
函数,在_Block_object_dispose
函数中会对引用的__block
变量进行引用计数-1
(release
)
下面我们对比下Block
内部访问外部变量几种情况:
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//直接将20存放在Block生成的结构体中
int num = 20;
//Block结构体内部生成一个强指针 强引用object对象
NSObject *object = [[NSObject alloc] init];
// Block内部生成一个弱指针 弱引用object对象
__weak NSObject *weakObject = object;
// Block内部生成一个结构体指针,指针指向的结构体内部存储着变量age
__block int age = 10;
//Block内部生成一个结构体指针,指针指向的结构体内部存储着变量obj1
__block NSObject *obj1 = [[NSObject alloc] init];
DJTBlock block = ^{
NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
};
block();
}
return 0;
}
编译成C++
看看block
结构体,和上边注释的一致:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
NSObject *__strong object;
NSObject *__weak weakObject;
__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里主要对比一下 对象类型的auto
变量和__block
修饰的变量内存管理的区别:
- 相同点:
- 当
Block
在栈上时,对它们都不会产生强引用 - 当
Block
拷贝到堆上时,都会通过copy
函数来处理它们:
(1)__block
变量(假设变量名叫做a
)
(2)对象类型的_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
auto
变量(假设变量名叫做p
)_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 当
Block
从堆上移除时,都会通过dispose
函数来释放它们
(1)__block
变量(假设变量名叫做a
)
(2)对象类型的_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
auto
变量(假设变量名叫做p
)_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 当
- 不同点(主要是在引用的问题上)
(1)对象类型的auto
变量,根据传进来时是__strong
还是__weak
类型决定调用copy
函数时Block
内部对传进来的变量进行强还是弱引用。
(2)如果时__block
类型的变量,比如__block int age = 20;
,它被封装成一个OC
对象,调用copy
函数时Block
内部直接对它产生强引用,对它的内存进行管理,不存在__weak
修饰int age
这种操作,所以没有弱引用这一说。(这里强引用的是age
转换成的结构体对象,真正的age
变量的值存储在结构体里边);
但是如果是下面代码
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
__block __weak NSObject *weakObject = object;
DJTBlock block = ^{
NSLog(@"%p %p", object, weakObject);
};
block();
}
return 0;
}
编译成C++
:
struct __Block_byref_weakObject_0 {
void *__isa;
__Block_byref_weakObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__weak weakObject;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__strong object;
__Block_byref_weakObject_0 *weakObject; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到在__block
修饰变量生成的结构体 __Block_byref_weakObject_0
内部,通过__weak
弱引用变量weakObject
,即Block
结构体内部是一个强指针指向__block
生成的结构体,即这句代码
__Block_byref_weakObject_0 *weakObject;
(注意虽然名字中有`weak`但这是一个强指针)
而在结构体__Block_byref_weakObject_0
内部:
NSObject *__weak weakObject;
这才是一个弱指针,指向外部传入的弱引用对象weakObject
,它表达了外部传入变量的类型是__weak
还是__strong
注意:这里在MRC
下有个特殊情况,在__block
生成的结构体内部,始终都是弱引用,不会对外边对象进行强引用。
在
MRC
环境下验证, 下面代码在block();
调用前person
就已经挂了,说明确实内部没有强引用:
#import "DJTPerson.h"
typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block DJTPerson *person = [[DJTPerson alloc] init];
DJTBlock block = [^{
NSLog(@" %p", person);
} copy];
[person release];
block();
}
return 0;
}
七、Block
相关问题
-
Block
的原理是怎样的?本质是什么? -
__block
的作用是什么?有什么使用注意点? -
Block
的属性修饰词为什么是copy
?使用Block
有哪些使用注意? -
Block
在修改NSMutableArray
,需不需要添加__block
?
理解上边原理再回答这些问题应该不难吧。
网友评论