Objective-C内存管理:Block
栏目: Objective-C · 发布时间: 6年前
内容简介:以下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
内部strong
self
后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内存管理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员成长的烦恼
吴亮、周金桥、李春雷、周礼 / 华中科技大学出版社 / 2011-4 / 28.00元
还在犹豫该不该转行学编程?还在编程的道路上摸爬滚打?在追寻梦想的道路上你并不孤单,《程序员成长的烦恼》中的四位“草根”程序员也曾有过类似的困惑。看看油田焊接技术员出身的周金桥是如何成功转行当上程序员的,做过钳工、当过外贸跟单员的李春雷是如何自学编程的,打小在486计算机上学习编程的吴亮是如何一路坚持下来的,工作中屡屡受挫、频繁跳槽的周礼是如何找到出路的。 《程序员成长的烦恼》记录了他们一步一......一起来看看 《程序员成长的烦恼》 这本书的介绍吧!