深入理解Objective-C的Block

开题之前,说下MRC下的内存管理机制。引用计数式内存管理的思考方式是:
1.自己生成的对象,自己持有。
2.非自己生成的对象,自己也能持有。
3.不再需要自己持有的对象时释放。
4.非自己持有的对象无法释放。

注意:使用NSMutableArray类的array类方法等可以取得谁都不持有的对象,这些方法都是通过autorelease而实现的。

插个小曲,内存泄露处理:释放不及时的 View 或 ViewController

例如,发起网络请求的时候,在网络请求回调的 block 里强引用 ViewController,以便在网络请求回来的时候刷新界面。在网络请求比较慢的情况下,这种做法存在两个问题:

ViewController 被 pop 之后,由于被 block 强引用导致释放不及时
ViewController 被 pop 之后,如果网络请求回来了,不应该继续做刷新界面的事,浪费 CPU
所以,对于这种情况,我们应该在 block 里弱引用 ViewController,而不是强引用。

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;

转载自三石.道的博客http://www.molotang.com/articles/1691.html。其实你可以查阅苹果文档 Blocks Programming Topics或者Objective-C高级编程这本书。

之前的文章中整理过用ObjectiveC开发中常用到的Block代码块,其中也提到了一个和block使用不当的crash例子。接着这个问题,本篇文章将更深一步,对Block的内存使用相关的内容简要整理一下,解释其中的道理和使用Block需要注意的问题。
问题所在

下面给出一段代码:

- (NSArray*) getBlockArray
{
    int num = 916;
    return [[NSArray alloc] initWithObjects:
               ^{ NSLog(@"this is block 0:%i", num); },
               ^{ NSLog(@"this is block 1:%i", num); },
               ^{ NSLog(@"this is block 2:%i", num); },
              nil];
}

- (void) forTest
{
    int a = 10;
    int b = 20;
}

- (void)test
{
    NSArray* obj = [self getBlockArray];
    [self forTest];
    void (^blockObject)(void);
    blockObject = [obj objectAtIndex:2];
    blockObject();
}

如上两个方法实现的代码并不难理解,其中第三个方法我们要去调用。它会调用第一个方法,并返回一个数组,数组中的元素是block代码块。那么在特定的场景下,调用test会发生crash(闪退)。说明这样的调用存在问题,恐怕能看到的应该就是EXC_BAD_ACCESS错误,通常这可以理解为一个“野指针”错误,访问了内存中不该访问的内容。因为 block 获取了局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃。

问题在哪?从“野指针”错误,我们很直接能想到的就是block对象引用到的地址内容已经不是我们想要的了,简单说就是block无效了。可block是对象类型的啊,为什么放在数组对象中回传失效了呢,加入NSArray的对象本身就应该retain过啊。

问题就在这里,下面我们先来看简单下Block与对象的关系。

  1. Block与对象

首先我们先反思几个问题:

block到底是不是对象?
如果是对象,和某个已定义的类的实例对象在使用上是不是一样的?
如果不一样,主要的区别是什么?
对于第一个问题,苹果的Objective-C官方文档中在“Working with Blocks”明确说明:

“Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary. ”

可见,Block是Objective-C语言中的对象。

苹果在block的文档中也提过这么一句:

“As an optimization, block storage starts out on the stack—just like blocks themselves do.”

Clang的文档中也有说明:

“The initial allocation is done on the stack,but the runtime provides a Block_copy function” (Block_copy在下面我会说)

凭这一点,我们就可以回答剩下的两个问题。Block对象与一般的类实例对象有所不同,一个主要的区别就是分配的位置不同,block默认在栈上分配,一般类的实例对象在堆上分配。

而这正是导致本文最初提到的那个问题发生的根本原因。Block对象在栈上分配,block的引用指向栈帧内存,而当方法调用过后,指针指向的内存上写的是什么数据就不确定了。但是到此,retain的疑问还是没有解开。

我们想一想Objective-C引用计数的原理,retain是对一个在堆中分配内存的对象的引用计数做了增加,执行release操作的时候检查计数是否为1,如果是则释放堆中内存。而对于在栈上分配的block对象,这一点显然有所不同,如果方法调用返回,栈帧上的数据自然会作废处理,不像堆上内存,需要单独release,就算NSArray对block对象本身做了retain也无济于事。

Clang文档中提到:

“Block pointers may be converted to type id; block objects are laid out in a way that makes them compatible with Objective-C objects. There is a builtin class that all block objects are considered to be objects of; this class implements retain by adjusting the reference count, not by calling Block_copy.”

那么要是想如本文开头那样,用一个方法对block数组做初始化是否有可行方案呢。答案是肯定的,不过需要真正了解block的使用,至少要会用Block_copy()和Block_release()。

  1. Block的类型和使用

我这里有对某个Block数组的一段Console Log显示,如下:
<__NSArrayI 0x937f240>(
<NSGlobalBlock: 0x126750>,
<NSStackBlock: 0xbfffc788>,
<NSMallocBlock: 0x937f1c0>,
<NSMallocBlock: 0x937f1e0>,
<NSMallocBlock: 0x937f200>,
<NSMallocBlock: 0x937f220>,
<NSGlobalBlock: 0x126818>
)
可以看得出,这些对象都是block,而且还分了3种不同的类型。

其实在Clang的文档中,只定义了两个Block类型: _NSConcreteGlobalBlock 和 _NSConcreteStackBlock 。而在Console中的Log我们看到的3个类型应该是处理过的显示,这些字样在苹果的文档和Clang/LLVM的文档中实难找到。通过字面上来看,可以认为 _NSConcreteGlobalBlock对应于 NSGlobalBlock ,_NSConcreteStackBlock对应于 NSStackBlock ,而NSMallocBlock则是另一种情况。(实际上也正是如此)

NSGlobalBlock,我们只要实现一个没有对周围变量没有引用的Block,就会显示为是它。而如果其中加入了对定义环境变量的引用,就是NSStackBlock。那么NSMallocBlock又是哪来的呢?malloc一词其实大家都熟悉,就是在堆上分配动态内存时。没错,如果你对一个NSStackBlock对象使用了Block_copy()或者发送了copy消息,就会得到NSMallocBlock。这一段中的几项结论可从代码实验得出。

因此,也就得到了下面对block的使用注意点。

对于Global的Block,我们无需多处理,不需retain和copy,因为即使你这样做了,似乎也不会有什么两样。对于Stack的Block,如果不做任何操作,就会向上面所说,随栈帧自生自灭。而如果想让它获得比stack frame更久,那就调用Block_copy(),让它搬家到堆内存上。而对于已经在堆上的block,也不要指望通过copy进行“真正的copy”,因为其引用到的变量仍然会是同一份,在这个意义上看,这里的copy和retain的作用已经非常类似。

“The runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1. The paired function is Block_release, which decreases the reference count by 1 and destroys the object if the count reaches zero and is on the heap.”

在类中,如果有block对象作为property,可以声明为copy。

  1. 其它

如果注释掉其中看似无关的[self forTest]调用,用当前的Xcode版本(我用的是5.1.1)build后,crash是不会发生的,这看起来很有意思。因为forTest方法本身并没有在逻辑上对数组的构建造成什么影响。

实际上这是因为上一个方法调用的栈帧没有被新的数据覆盖,仍然保留原来block数据的原因所致。这样显然是不安全的,是不能保证block数据可用的。

在ARC情况下,我们会发现一个有意思的情况,那就是返回的Block Array,只有元素0是执行过copy的。比如block数组中的第0个block是stack的,那么返回之后在数组index为0处取到的block变成了malloc的。与此同时,其它的block都如同没有执行过copy一样,如上述各段所述。这是一个现象,或者说是一个结论。至于为什么这样,众说纷纭,很多人认为这是编译器的一个bug,欢迎大家多多讨论,给出见解。

在苹果官方的《Transitioning to ARC Release Notes》文档中,写了这样一段话,大家理解一下,尤其是其中的“just work”。

“How do blocks work in ARC?

Blocks ‘just work’ when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more.”

  1. 参考

以上整理了对Block的理解,在开发中注意到这些点足以解决block的特殊性带来的各类问题。要想继续深入,可参看LLVM文档中对block的介绍:

http://clang.llvm.org/docs/Block-ABI-Apple.html

http://clang.llvm.org/docs/AutomaticReferenceCounting.html?highlight=class

补充:如下几篇文章讲解得也很细致,可以参看。

《block没那么难(一)》
https://www.zybuluo.com/MicroCai/note/51116
《block没那么难(二)》
https://www.zybuluo.com/MicroCai/note/57603
《block没那么难(三)》
https://www.zybuluo.com/MicroCai/note/58470

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容