Objective-C的block特性(2)
转载请注明出处:http://www.cnblogs.com/idalink/articles/4427375.html
已经说过,block具有捕获变量(包括全局变量、成员变量、局部变量)的特性,这个特别使其完全区别于c语言函数,而block的魅力正在于此。
1、从block捕获变量谈起
既然block能够捕获变量,如果block对象没有被销毁,而局部变量被销毁,这算不算逻辑权限呢?很自然的一点,那就是block访问局部变量,这个变量毫无疑问会被调用栈回收掉,而这个时候block访问的又是哪位呢?假设被访问的对象不在栈上,而是堆区,这个对象依然存在被过早释放的可能,那block是不是要持有这个对象,并增加引用计数呢?
2、block的内存分配
被捕获的变量受block的生命周期影响,那block的内存分配是个怎样的机制?我们创建一个伪本地block,并观察。
- (void)viewDidLoad {
[super viewDidLoad];
MyBlock localBlock1 = ^(){ };
NSLog(@"地址--%@", localBlock1);
int var = 1001;
MyBlock localBlock2 = ^(){
NSLog(@"var = %d", var);
};
NSLog(@"地址--%@", localBlock2);
}
会得到如下输出:
2015-04-14 23:54:13.201 test_block[787:42086] 地址--<__NSGlobalBlock__: 0xc4048> 2015-04-14 23:54:13.202 test_block[787:42086] 地址--<__NSMallocBlock__: 0x7965a8c0>
这个block从代码上看似是个局部变量,其实是个(静态)全局变量。因为在ARC模式下,block总是分配在静态存储区。非ARC下还没测验。可以参考:http://blog.csdn.net/hherima/article/details/38586101
在此上传一个前人大牛的很有意义的论果:
图片来自:http://blog.csdn.net/hherima/article/details/38586101
既然可以得知,ARC模式下,block总是被分配在静态存储区,自然不会面对stack到static的转变,那么block的生命周期也就大大简化。在ARC模式下,block被copy并不会生成新的block对象,只是使block的引用计数自增一。
3、block对局部变量的影响
局部变量总是随着栈的销毁而销毁,block必然无法访问栈的这块内存。所以在无__block修饰下,block只会把这个变量当成常量使用,并在block内部创建同名变量,并且无法修改这个变量。编译级别,block无法修改访问对象。既然创建了一个新的变量,则block外部对该变量的修改也不会涉及到block的内部同名变量。这是已经是完全两个变量了。
//代码
//外围修改变量不会设计block内部
- (void)viewDidLoad { [super viewDidLoad]; int var = 1001; MyBlock myblock = ^{ NSLog(@"block内部输出var %d", var); }; var ++; myblock(); }
输出:
2015-04-15 00:06:35.028 test_block[867:46317] block内部输出var 1001
要使block具备修改外部对象的能力,必须要对对象添加__block修饰。这是不是会与堆栈调用冲突呢?这个逻辑怎么处理呢?
- (void)viewDidLoad { [super viewDidLoad]; __block int var = 1001; NSLog(@"新对象的地址 %p", &var); MyBlock myblock = ^{ NSLog(@"block内部输出地址 %p", &var); }; NSLog(@"被block访问后的地址 %p", &var); myblock(); }
输出:
2015-04-15 00:11:22.360 test_block[907:48126] 新对象的地址 0xbff2df08 2015-04-15 00:11:22.361 test_block[907:48126] 被block访问后的地址 0x7bf29460 2015-04-15 00:11:22.361 test_block[907:48126] block内部输出地址 0x7bf29460
发生了一个神奇一幕,局部变量被block捕获之后,地址竟然完全变了。不得不怀疑,block把变量从栈复制到静态存储区了。
4、block对全局变量的影响
全局变量的生命周期不会受到stack的约束,自然而然只需要能对对象的持续引用即可保持对象的持久性。可以推测,block 访问全局变量,不会拷贝对象,而只会调整引用计数。
@interface TSObject : NSObject @end @implementation TSObject @end @interface ViewController () @end typedef void (^MyBlock)(); @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; TSObject *tsObject1 = [[TSObject alloc] init]; NSLog(@"引用计数1-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); TSObject *tsObject2 = tsObject1; NSLog(@"引用计数2-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); MyBlock localBlock1 = ^(){ NSLog(@"输出地址2 %@", tsObject1); }; NSLog(@"输出地址1 %@", tsObject1); NSLog(@"引用计数3-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); localBlock1(); } @end
输出:
2015-04-15 00:21:42.214 test_block[1077:53288] 引用计数1-->1 2015-04-15 00:21:42.215 test_block[1077:53288] 引用计数2-->2 2015-04-15 00:21:42.215 test_block[1077:53288] 输出地址1 <TSObject: 0x7af25ef0> 2015-04-15 00:21:42.215 test_block[1077:53288] 引用计数3-->4 2015-04-15 00:21:42.215 test_block[1077:53288] 输出地址2 <TSObject: 0x7af25ef0>
可以看出,对象地址并为发生改变,而饮用技术却增加了。为什么会增加2呢,而不是1呢?因为block最开始位于常量区,后来移动到了全局堆区,发生了copy,自然而然所持有的对象自增2。还有疑问,这里先用一个新的指针tsObject2赋值对象,这是为了比较__block指针对引用计数的影响。稍后分析。
如果把指针加上__block修饰符会如何呢?
- (void)viewDidLoad { [super viewDidLoad]; __block TSObject *tsObject1 = [[TSObject alloc] init]; NSLog(@"引用计数1-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); NSLog(@"输出地址1 %@", tsObject1); TSObject *tsObject2 = tsObject1; NSLog(@"引用计数2-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); MyBlock localBlock1 = ^(){ NSLog(@"输出地址3 %@", tsObject1); }; NSLog(@"输出地址2 %@", tsObject1); NSLog(@"引用计数3-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); localBlock1(); }
输出:
2015-04-15 00:29:18.048 test_block[1149:55775] 引用计数1-->1 2015-04-15 00:29:18.048 test_block[1149:55775] 输出地址1 <TSObject: 0x7b0b35a0> 2015-04-15 00:29:18.049 test_block[1149:55775] 引用计数2-->2 2015-04-15 00:29:18.049 test_block[1149:55775] 输出地址2 <TSObject: 0x7b0b35a0> 2015-04-15 00:29:18.049 test_block[1149:55775] 引用计数3-->2 2015-04-15 00:29:18.049 test_block[1149:55775] 输出地址3 <TSObject: 0x7b0b35a0>
这是为什么,block没有改变引用计数,对象不会被销毁吗?继续修改代码测试:
- (void)viewDidLoad { [super viewDidLoad]; __block TSObject *tsObject1 = [[TSObject alloc] init]; NSLog(@"引用计数1-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); NSLog(@"输出地址1 %@", tsObject1); TSObject *tsObject2 = tsObject1; NSLog(@"引用计数2-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); MyBlock localBlock1 = ^(){ NSLog(@"输出地址3 %@", tsObject2); }; NSLog(@"输出地址2 %@", tsObject1); NSLog(@"引用计数3-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); localBlock1(); }
输出:
2015-04-15 00:31:03.878 test_block[1178:56611] 引用计数1-->1 2015-04-15 00:31:03.878 test_block[1178:56611] 输出地址1 <TSObject: 0x7bf6b810> 2015-04-15 00:31:03.879 test_block[1178:56611] 引用计数2-->2 2015-04-15 00:31:03.879 test_block[1178:56611] 输出地址2 <TSObject: 0x7bf6b810> 2015-04-15 00:31:03.879 test_block[1178:56611] 引用计数3-->4 2015-04-15 00:31:03.879 test_block[1178:56611] 输出地址3 <TSObject: 0x7bf6b810>
block的引用计数又“复活”了?这该如何解释?__block到底做了什么?编译器难道在做非逻辑吧必须性优化?
5、block对成员变量的影响
现在创建两个成员变量,并且其中一个指针由_block修饰。看看引用计数变化和地址的变化。
typedef void (^MyBlock)(); @interface TSObject :NSObject @end @implementation TSObject @end @interface ViewController () { TSObject *tsObject1; __block TSObject *tsObject2; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; tsObject1 = [[TSObject alloc] init]; NSLog(@"输出地址1 %@", tsObject1); NSLog(@"引用计数1-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); MyBlock localBlock1 = ^(){ NSLog(@"输出地址2 %@", tsObject1); NSLog(@"引用计数3-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); }; NSLog(@"引用计数2-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject1)); localBlock1(); tsObject2 = [[TSObject alloc] init]; NSLog(@"成员变量输出地址1 %@", tsObject2); NSLog(@"成员变量引用计数1-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); MyBlock localBlock2 = ^(){ NSLog(@"成员变量输出地址2 %@", tsObject2); NSLog(@"成员变量引用计数3-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); }; NSLog(@"成员变量引用计数2-->%ld", CFGetRetainCount((__bridge CFTypeRef)tsObject2)); localBlock2(); } @end
输出:
2015-04-15 12:23:50.854 test_block[7249:1675955] 输出地址1 <TSObject: 0x17400f410> 2015-04-15 12:23:50.855 test_block[7249:1675955] 引用计数1-->1 2015-04-15 12:23:50.855 test_block[7249:1675955] 引用计数2-->1 2015-04-15 12:23:50.855 test_block[7249:1675955] 输出地址2 <TSObject: 0x17400f410> 2015-04-15 12:23:50.855 test_block[7249:1675955] 引用计数3-->1 2015-04-15 12:23:50.856 test_block[7249:1675955] 成员变量输出地址1 <TSObject: 0x170017fb0> 2015-04-15 12:23:50.856 test_block[7249:1675955] 成员变量引用计数1-->1 2015-04-15 12:23:50.856 test_block[7249:1675955] 成员变量引用计数2-->1 2015-04-15 12:23:50.856 test_block[7249:1675955] 成员变量输出地址2 <TSObject: 0x170017fb0> 2015-04-15 12:23:50.856 test_block[7249:1675955] 成员变量引用计数3-->1
可以看出,对象地址不变,引用计数没有影响。
6、block的捕获变量的总结
(1)无法修改局部变量,除非使用__block修饰。
(2)被__block修饰的对象,如果对象在栈上,则将改对象移动到堆上。
(3)被捕获的变量通过增加引用计数持有对象。至于被__block修饰的指针,引用计数并不变。