Objective-C内存管理:Block
栏目: Objective-C · 发布时间: 7年前
内容简介:以下Block在ARC环境下能正常运行吗?若能分别打印什么值?Objective-C中的Block中文名闭包,是C语言的扩充功能,是一个匿名函数并且可以截获(保存)局部变量。通过三个小节来解释这个概念。因为Block是在模仿C语言函数指针的写法:
-
Block作为属性声明时为什么都声明为Copy?
-
Block为什么能保存外部变量?
-
Block中
__block关键字为何能同步Block外部和内部的值? -
Block有几种类型?
-
什么时候栈上的Block会复制到堆?
-
Block的循环引用应该如何处理?
-
Block外部
__weak typeof(self) weakSelf = self;Block 内部typeof(weakSelf) strongSelf = weakSelf;,为什么需要这样操作?
Block测试:
以下Block在ARC环境下能正常运行吗?若能分别打印什么值?
void exampleA_addBlockToArray(NSMutableArray*array) {
char b = 'A';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleA() {
NSLog(@"---------- exampleA ---------- \n");
NSMutableArray *array = [NSMutableArray array];
exampleA_addBlockToArray(array);
void(^block)(void) = [array objectAtIndex:0];
block();
}
复制代码
void exampleB_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("B\n");
}];
}
void exampleB() {
NSLog(@"---------- exampleB ---------- \n");
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void(^block)(void) = [array objectAtIndex:0];
block();
}
复制代码
typedef void(^cBlock)(void);
cBlock exampleC_getBlock() {
char d = 'C';
return^{
printf("%c\n", d);
};
}
void exampleC() {
NSLog(@"---------- exampleC ---------- \n");
cBlock blk_c = exampleC_getBlock();
blk_c();
}
复制代码
NSArray* exampleD_getBlockArray() {
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk1:%d",val);}, ^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk0:%d",val);}, nil];
}
void exampleD() {
NSLog(@"---------- exampleD ---------- \n");
typedef void (^blk_t)(void);
NSArray *array = exampleD_getBlockArray();
NSLog(@"array count = %ld", [array count]);
blk_t blk = (blk_t)[array objectAtIndex:1];
blk();
}
复制代码
NSArray* exampleE_getBlockArray() {
int val = 10;
NSMutableArray *mutableArray = [NSMutableArray new];
[mutableArray addObject:^{NSLog(@"blk0:%d",val);}];
[mutableArray addObject:^{NSLog(@"blk1:%d",val);}];
[mutableArray addObject:^{NSLog(@"blk2:%d",val);}];
return mutableArray;
}
void exampleE() {
NSLog(@"---------- exampleE ---------- \n");
typedef void (^blk_t)(void);
NSArray *array = exampleE_getBlockArray();
NSLog(@"array count = %ld", [array count]);
blk_t blk = (blk_t)[array objectAtIndex:1];
blk();
}
复制代码
void exampleF() {
NSLog(@"---------- exampleF ---------- \n");
typedef void (^blk_f)(id obj);
__unsafe_unretained blk_f blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
复制代码
void exampleG() {
NSLog(@"---------- exampleG ---------- \n");
typedef void (^blk_f)(id obj);
blk_f blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
复制代码
void exampleH() {
NSLog(@"---------- exampleH ---------- \n");
typedef void (^blk_f)(id obj);
blk_f blk;
{
id array = [[NSMutableArray alloc] init];
id __weak weakArray = array;
blk = ^(id obj) {
[weakArray addObject:obj];
NSLog(@"array count = %ld", [weakArray count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
复制代码
void exampleI() {
NSLog(@"---------- exampleI ---------- \n");
typedef void (^blk_g)(id obj);
blk_g blk;
{
id array = [[NSMutableArray alloc] init];
__block id __weak blockWeakArray = array;
blk = [^(id obj) {
[blockWeakArray addObject:obj];
NSLog(@"array count = %ld", [blockWeakArray count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
复制代码
什么是Block
Objective-C中的Block中文名闭包,是 C语言 的扩充功能,是一个匿名函数并且可以截获(保存)局部变量。通过三个小节来解释这个概念。
其他语言中的Block概念
| 程序语言 | Block的名称 |
|---|---|
| Swift | Closures |
| Smalltalk | Block |
| Ruby | Block |
| LISP | Lambda |
| Python | Lambda |
| Javascript | Anonymous function |
为什么Block的写法很别扭?
因为Block是在模仿C语言函数指针的写法:
int func(int count) {
return count + 1;
}
// int (^tmpBlock)(int i) = ...
int (*funcptr)(int) = &func;
复制代码
但是Block的写法依旧非常难记,国外的朋友更是专门写了一个叫fuckingblock网页提供Block的各种写法。
截获局部变量(或叫自动变量)
// 演示截取局部变量
int tmpVal = 10;
void (^blk)(void) = ^{
printf("val = %d", tmpVal); // val = 10
};
tmpVal = 2;
blk();
复制代码
这里依旧显示 val = 10 ,Block会截取当前状态下 val 的值。至于为什么能截获局部变量的值,我们下一节中讨论。
Block实现原理
Block结构
通过 clang -rewrite-objc main.m 将上面的示例代码翻译成C,关键代码如下:
// Block基础结构
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
复制代码
Block如何截取局部变量
// 根据示例中blk的实现,生成不同的 __main_block_impl_0 结构体。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int tmpVal;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int flags=0) : tmpVal(_tmpVal) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
根据上面的代码能解决我们3个疑惑:
-
__block_impl中有isa指针,那么Block也是一个对象。 - 生成不同的
__main_block_impl_0,这里结构里面包含int tmpVal就是我们局部变量,而__main_block_impl_0的构造函数中是值传递。所以block内部截获的变量不受外部影响。 -
__main_block_impl_0构造函数中有个void *fp函数指针指向的就是block实现。
我们向上面示例代码再添加多一些变量类型:
static int outTmpVal = 30; // 静态全局变量
int main(int argc, char * argv[]) {
int tmpVal = 10; // 局部变量
static int localTmpVal = 20; // 局部静态变量
NSMutableArray *localMutArray = [NSMutableArray new]; // 局部OC对象
void (^blk)(void) = ^{
printf("val = %d\n", tmpVal); // val = 10
printf("localTmpVal = %d\n", localTmpVal); // localTmpVal = 21
printf("outTmpVal = %d\n", outTmpVal); // outTmpVal = 31
[localMutArray addObject:@"newObj"];
printf("localMutArray.count = %d\n", (int)localMutArray.count); // localMutArray.count = 2
};
tmpVal = 2;
localTmpVal = 21;
outTmpVal = 31;
[localMutArray addObject:@"startObj"];
blk();
}
复制代码
对应输出结果为:
val = 10
localTmpVal = 21
outTmpVal = 31
localMutArray.count = 2
clang -rewrite-objc main.m 后关键代码如下:
static int outTmpVal = 30;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int tmpVal;
int *localTmpVal;
NSMutableArray *localMutArray;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int *_localTmpVal, NSMutableArray *_localMutArray, int flags=0) : tmpVal(_tmpVal), localTmpVal(_localTmpVal), localMutArray(_localMutArray) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
因为涉及到OC对象,这里还会有2个新的方法,这2个方法会放到后面讲:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src){
_Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码
-
static int outTmpVal = 30;储存在内存中的.data段,static限制了作用域,该文件作用域内可修改。 -
static int localTmpVal = 20;在int main(int argc, char * argv[]) { }作用域可修改,注意__main_block_impl_0构造函数中是传递的*_localTmpVal指针,所以外部修改Block内部同样有效,因为是static所以,Block内部也可以修改localTmpVal的值。 -
NSMutableArray *localMutArray向__main_block_impl_0传递的是指向的地址,所以localMutArray内部操作对于block内同样有效。
- 静态变量的这种方式同样也可以作用到局部变量上,传递一个指针到block内,通过指针来读取指向的值,通知也可以修改。但是这种方式在block离开局部变量所在作用域后再调用就会出现问题,因为局部变量已经被释放。
-
static int localTmpVal = 20;能通过指针的方式修改值,NSMutableArray *localMutArray修改指向的值为什么不可以? 这是clang对于Block内修改指针的一个保护措施。
总结下:
-
静态变量、静态全局变量、全局变量都可以访问,修改,保持同一份值。 - OC对象,可以进行内部操作。但不能修改OC对象的值(指向的内存地址)。
__block关键字如何实现?
同样的方式,我们先看 __block 用C是怎么实现的,下面是一段使用 __block 的代码:
int main(int argc, char * argv[]) {
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
printf("val = %d", val);
};
blk();
}
复制代码
翻译成C,只保留关键代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
复制代码
这就是 __block 对应C中的新结构体:
-
*__forwarding是一个与自己同类型的指针。 -
int val;这个变量就是为了保存原本__block int val = 10;的值。 - 并且
__block int val = 10;对应的结构体__Block_byref_val_0也是和之前一样创建在栈上的。
接下来继续看, blk 的结构:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0.... // 和之前的__block_impl构造方式一致
};
复制代码
blk 结构内部新增了 __Block_byref_val_0 *val 作为成员变量,和之前原理一致。
blk 的实现 val = 1; :
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
printf("val = %d", (val->__forwarding->val));
}
复制代码
(val->__forwarding->val) = 1; 这句非常重要,不是直接通过 val->val 进行赋值操作,而是经过 __forwarding 指针进行赋值,这带来非常大的灵活性,现在是 blk 和 __block int val 都是在栈上, __forwarding 也都指向了栈上的 __Block_byref_val_0 。以上代码解决了在Block内修改外部局部变量的值。
__block 新增了2个方法: __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(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码
通过方法命名和参数,可以大致猜出是对 Block 的拷贝和释放。
Block和__block的储存区域
通过以上 clange 的编译,Block和__block都是有isa指针的,两者都应该是Objective-C的对象。isa指向的就是它的类对象。在ARC下大致有以下几种,根据名字可以知道对应储存空间:
- _NSConcreteStackBlock 栈上
- _NSConcreteGlobalBlock 全局 对应的是.data段
- _NSConcreteMallocBlock 堆上
clang转出的结果和运行代码时 Block 实际显示的isa类型是不一样的,在实际的编译过程中已经不会经过clang翻译成C再编译。
_NSConcreteGloalBlock 有两种情况下可以生成:
- 声明的是全局变量Block。
- 在作用域内但是不截获外部变量。
_NSConcreteStackBlock 因为在栈上,在函数作用域内声明的Block。
_NSConcreteMallocBlock 正因为 _NSConcreteStackBlock 的作用域在栈上,超出作用域后想要继续使用Block,这就得复制到堆上。那些情况会触发这种复制:
- ARC下大多数情况会自动复制。比如,栈上
block赋值给Strong修饰的属性时。Block作为一个返回值时(超出作用域还能使用,autorelease处理对象生命周期)。 - 需要手动copy。 向方法或函数的参数中传递Block时 ,编译器无法判断是什么样的情况,因为从Block从栈上复制到堆上很消耗cpu。所以编译器并没有帮忙
copy。 - Cocoa框架的方法且方法名中含有
usingBlock等时,不用外部copy。内部已经进行copy。 -
GCD的Api,也不用外部copy。
这里有个比较经典的例子(摘自《Objective-C高级编程》):
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);}, nil];
}
{
id obj = [self getBlockArray];
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
// crash
复制代码
在ARC情况下,NSArray 数组类会有2个元素,第一个在堆上,第二个栈上。在超出getBlockArray作用域后,第二栈上的block会变成野指针。在所有作用域结束时,Array会释放数组内所有元素。野指针对象执行销毁时会触发崩溃。 正常情况下 NSArray 应该持有数组内所有元素。但使用 initWithObjects: 方法时,发现只有第一个元素进行了持有操作,第二个 Block 依旧在栈上。当我使用 NSMutableArray 的 addObject: 方法时,每个Block都会进行持有赋值到堆上。我怀疑应该是 initWithObjects: 方法中多参形式比较特殊。
反复提到Block就是OC的对象,对于对象Copy会带来哪些变化:
| Block类 | 原来储存域 | 复制产生的影响 |
|---|---|---|
| _NSConcreteStackBlock | 栈 | 从栈复制到堆 |
| _NSConcreteGlobalBlock | .data | 无变化 |
| _NSConcreteMallocBlock | 堆 | 引用计数增加 |
__block的储存区域
Block是一个OC对象,所以涉及到从栈到堆,引用计数的变更等,常见OC对象内存管理的问题。同时Block在堆上时又会对 __block 进行持有,那么对于 __block 同样也是OC对象,内存管理有什么区别呢?
Block从栈复制到堆时对__block变量产生的影响:
| __block 存储域 | Block 从栈复制到堆时对__block的影响 |
|---|---|
| 栈 | 从栈复制到堆并被Block持有 |
| 堆 | 被Block持有 |
-
__block从栈上复制到堆上后,原本栈上的__block依旧会存在,被复制到堆上的__block会被Block持有__block的引用计数会增加,栈上的__block会因为作用域结束而释放,堆上的__block会在引用计数归零后释放。 - 堆上的
__block的内存管理就是OC对象的引用计数管理方式,没有被其他Block持有时引用计数归0后释放。
上面提到当 __block 从栈上复制到堆上,会有两个 __block 产生,一个栈上的一个堆上的。这两个不同储存区域的 __block 是如何实现数据同步的?
这就利用 __block关键字如何实现? 中提到的指向自己的 *__forwarding ,当持有 __block 的Block没有从栈上拷贝到堆上时: *__forwarding 指向栈上的 __block , 当持有 __block 的Block拷贝到堆上时后,栈上的 __block -> __forwarding ->堆上的 __block ,堆上的 __block -> __forwarding ->堆上的 __block 。读起来有点绕,借用《Objective-C高级编程》中的插图:
__block 和 OC对象从栈上复制到堆上?
上面讲了 Block 和 __block 在从栈上复制到堆上时的一些变化。为了解决 __block 和 OC对象 在 Block结构体 内的生命周期问题,新增了一下几个方法:
- 在
__main_block_desc_0中新加2个成员方法:copy和dispose,这是两个函数指针,指向的分别就是__main_block_copy_0和__main_block_dispose_0。 - 在
Block中使用OC对象和__block关键字时新增的2个方法:__main_block_copy_0和__main_block_dispose_0,这两个方法用于在Block被copy到堆上时,管理__block和OC对象的生命周期。
Block:
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*);
}
复制代码
OC对象:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码
__block:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码
捕获 OC对象 和使用 __block 变量时在参数上会不同:
| OC对象 | BLOCK_FIELD_IS_OBJECT |
|---|---|
| __block | BLOCK_FIELD_IS_BYREF |
_Block_object_assign 就相当于 retain 方法, _Block_object_dispose 就相当于 release 方法,但是我们在clang翻译的C语言中并没有发现 __main_block_copy_0 和 __main_block_dispose_0 的调用。只有在以下时机 copy 和 dispose 方法才会调用:
| copy函数 | 栈上的Block复制到堆时 |
|---|---|
| dispose函数 | 堆上的Block被废弃时(引用计数为0) |
什么时候栈上的Block会复制到堆?
- 调用
Block的copy实例方法。 -
Block作为函数返回值返回时。(autorelease对象延长生命周期) - 将
Block赋值给附有__strong修饰符的id类型的类或Block类型成员变量(赋值给Strong修饰的Block类型属性时,编译器会帮忙复制到堆)。 - 在方法名中含有
usingBlock的Cocoa框架方法或GCD的api中传递Block时。
Block tips
一、哪些情况下Block内self为nil时会引起崩溃?这个时候需要使用Weak-Strong-Dance。
-
使用
self.blockxxx()时,使用clang转换成C时,可以看到Bblock的调用实际是调用Block`内的函数指针与OC对象调用发消息的形式不一样。 -
其他业务场景,比如使用
self的成员变量做NSAarry或NSDictionary做增加操作时。不要无脑使用,更加清晰的理解
Weak-Strong-Dance,Block内部strongself后Block会继续持有self,有些场景并不需要。
解答
- 声明成
Strong与Copy效果都一样。在ARC环境下编译会自动将作为属性的Block从栈Copy到堆,这里Apple建议继续使用Copy防止 程序员 忘记编译器有Copy动作。 - Block内部能截获外部变量。
Block结构体中会有创建一个成员变量与截获的变量类型一直,这个值与截获时的值一致,这是一个值传递,保存的是一个瞬时值。 -
__block关键字的实现是一个结构体,结构体中有个自己同类型的*_farwarding指针,当Block在栈上,__block也是在栈上时:*_farwarding指向栈上的自己。当Block拷贝到堆,堆中创建的__block的*_farwarding指向自己,同时将栈上的*_farwarding指向堆中__block。 - 三种。栈上,堆上,全局。
- 1 手动
copy。2 作为返回值返回。3 将Block赋值给__strong修饰的id类型或Block类型成员变量。4 方面名中含有usingBlock的cocoa框架方法或GCD。 - 使用
__weak弱引用,或者手动断开强引用。 -
Block内的weakSelf可能会出现nil的情况,nil可能会造成奔溃或是其他意外结果。所以在Block内作用域内声明一个Strong类型的局部变量,在作用域结束后会自动释放不会造成循环引用。
编程题目答案,请参考Github上的repo: TestBlock 。
参考
- 《Objective-C高级编程》
- 浅谈 block - 截获变量方式
- Blocks Programming Topics
- Working with Blocks
- fuckingblock
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Go 语言内存管理(二):Go 内存管理
- Objective-C的内存管理(1)——内存管理概述
- [译] 图解 Go 内存管理与内存清理
- 图解 Go 内存管理器的内存分配策略
- Go:内存管理分配
- Redis内存管理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出Tapestry
董黎伟 / 电子工业出版社 / 2007-3 / 49.0
本书以循序渐进的方式,从Tapestry框架技术的基本概念入手,讲解Tapestry框架在J2EE Web应用程序中的整体架构实现。使读者在学习如何使用Tapestry框架技术的同时,还能够获得在J2EE Web应用程序中应用Tapestry框架的先进经验。 本书详细介绍了Hivemind框架的原理与应用,使读者不但可以通过Hivemind来重构Tapestry的官方实现,还可以使用Hive......一起来看看 《深入浅出Tapestry》 这本书的介绍吧!