YGben

Block(二)


Blocks语法作为C语言的拓展,有很多值得注意的地方。本篇从常用的几点一步步加深对Block的理解。

1. 截获自动变量值

很简单的例子

1
2
3
4
5
6
int val = 100;
void (^blk) (void) = ^{
NSLog(@"val = %d",val);
};
val = 200;
blk();

运行结果:val = 100
Block保存自动变量瞬间值,截获了就在代码块中使用。

2. __block

那么问题来了,我不像让block捕获怎么办?同样上面简单的例子:

1
2
3
4
5
6
__block int val = 100;
void (^blk) (void) = ^{
NSLog(@"val = %d",val);
};
val = 200;
blk();

运行结果:val = 200
唯一的区别是变量val前加了block,不管在block中还是block外,附有block说明符的变量可以赋值且有效,所以val这时候就可以称作 __block变量。

3. 循环引用及解决

注意上面一小段代码blk是局部变量,实际工程中大部分都是全局或者属性。最简单的为例:
typedef void (^blk) (void);
{blk blk; int val} //全局变量 blk和val;

1
2
3
4
blk_ = ^{
NSLog(@"self = %@ \n val = %d",self,val);
};
blk_();

上面这一句代码存在两处循环引用。self和blk 互相引用;val实际上也截获了self,self->val,最终还是self和blk互相引用。通常的解决是使用弱引用__weak。
当然不止两个对象互相持有,复杂的三个或四个对象形成循环引用。

之前不太理解block变量,因为之前看日本人写的《iOS与OS X多线程和内存管理》里面说 使用`blcok变量也可以避免循环引用。之后看到独_奏`博客中weak与block区别,结合写代码发现几个有意思的地方。


  • block 声明前后 blockObj 的内存地址是有所变化的,涉及block 对外部变量的内存管理问题。 如下这段代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MyObject *obj = [[MyObject alloc]init];
    obj.text = @"my-object-1";
    TLog(@"obj",obj);
    __block MyObject *blockObj = obj;
    obj = nil;
    TLog(@"blockObj -1",blockObj);
    void (^testBlock) () = ^(){
    TLog(@"blockObj - block",blockObj);
    };
    TLog(@"blockObj -1",blockObj);

输出结果:

1
2
3
变量内存地址:0x7fff5b3c2a58, 变量值:0x7fc366c1dd90, 指向对象值:my-object-1, --> obj
变量内存地址:0x7fff5b3c2a50, 变量值:0x7fc366c1dd90, 指向对象值:my-object-1, --> blockObj -1
变量内存地址:0x7fc366d2e898, 变量值:0x7fc366c1dd90, 指向对象值:my-object-1, --> blockObj -1


  • __block 本身并不能解决循环引用,只有在block底部把block变量为nil,手动释放方可。(ps 如下第7行代码)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    MyObject *obj = [[MyObject alloc]init];
    obj.text = @"__block test";
    __block MyObject *blockObj = obj;
    void(^testBlock)() = ^(){
    TLog(@"blockObj - block",blockObj);
    blockObj = nil;
    };
    obj = nil;
    testBlock();
    TLog(@"blockObj",blockObj);

输出结果:

1
2
变量内存地址:0x7fcf54625398, 变量值:0x7fcf5461fc30, 指向对象值:__block test, --> blockObj - block
变量内存地址:0x7fcf54625398, 变量值:0x0, 指向对象值:(null), --> blockObj

这样可以处理简单的循环引用,ARC时代手动释放不方便,参考文中段叙2的代码,block变量是可以改变block内部的取值,代码逻辑多了存在隐患。
唐巧大V的一篇博文 中提到解决循环引用两个途径:1.主动断开循环引用;2.使用弱引用。我的理解,循环引用无非是互相持有,主动断开就是一方先“放手”;弱引用,你就不会“持有”我。


  • AFNetworking 中存在一段代码:
    1
    2
    3
    4
    5
    6
    7
    8
    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
    strongSelf.networkReachabilityStatusBlock(status);
    }
    };

为什么这样写?__weak 还不够吗?
若存在异步线程执行,self被之后的代码释放,按照常理weakSelf为nil,block执行就会出错,但因为有__strong 引用,保证block内部self不会被释放,执行完这个局部变量strongSelf也会自动释放。

4. Block的内存管理

面试时候经常问:blcok是在上还是上。block 的属性修饰符为什么写 copy?

说到内存,复习下内存的组成:(详情参考CoderYQC语言-内存管理基础
来自简书CoderYQ

其实在《Objectiv-C高级编程 iOS与OS X多线程和内存管理》中关于Blocks 的实现写的分清楚,(ps C++部分还给老师了),其中有这样一句实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈复制到堆上的代码。自动生成通过objc4运行时runtime/objc-arr.mm_Block_copy函数。
OC代码编译C++代码结构体中存在isa指向内存区域分为:

设置对象的存储域
NSConcreteStackBlock
NSConcreteGlobalBlock 数据区域
NSConcreteMallocBlock

通过po调试代码输出block对象也会有诸如<__NSGlobalBlock__: 0x10d7660e0>的字眼。

block 本身在MRC下可能存在以上三个区域中。ARC下可能在数据区域中,大多数在堆中。为什么非要费死劲弄到上,因为上的Block,如果其所属的变量作用域结束,该Block就被编译器废弃。本来这种事交给程序员(MRC)处理放堆上,苹果的runtime&&ARC替程序员干了。

5. 了解更多:

zfyouxiBlock存储区域
Dreamingwish博客中的Block教程系列
tanqisen正确使用Block避免Cycle Retain和Crash