iOS 内存管理二三事

上篇博文的发表时间是 12 年 7 月,距今已经一年有余… 建博客的时候决定每月至少一篇,汗颜。不过倒也有些原因:7 月的时候脖子不舒服,竟然检查出颈椎间盘突出,折腾了大半年,因祸得福养成了每周跑步的好习惯。12 月后参加了新项目,忙成了狗…… 稍微空下来,写点心得和前面碰到的一些问题,先说说内存管理的二三事。(基础的内存管理可以参考这里或者 Apple 的文档)

你不一定完全明白的 copy

经常会看到一些童鞋在写 property 的时候对于 NSString 的访问属性设置成 copy,问起原因,一般是说:安全啊,重新拷贝一份,免得调用时对象被修改。而事实却是:** 对非易变的 (immutable) 的对象进行 copy 往往只是单纯的返回 retainCount+1 后的相同对象而已 **。原因很简单,对于大部分 immutable 对象而言(尤其是系统对象),其内部的变化对我们是透明的,我们无法直接操作内部对象。这样就带来一个便利:即使只是单纯的 retain 对象仍旧是线程安全的,并且省去了深拷贝带来的性能影响。(顺带吐槽下 C++ 对 const 限定相当恶心,效果又很差,可以轻易被破坏)

[新的更新:其实上面的说法是有问题的,因为虽然属性是 NSString,但是外部可以赋 NSMutableString 进来,这样就触发了另一个问题:如果只是单纯 retain 这个 NSMutableString,相应的值就会随着 NSMutableString 的改变而改变。所以使用 copy 作为 NSString 的访问属性是最安全和合适的:在传入值是 NSMutableString 时返回当前指针的不可变拷贝,而传入 NSString 时只做 retain…… 看山是山,看山不是山,到看山还是山…… 悲剧的轮回啊]

上面的情况是显式调用 copy 实际却只起到 retain 的作用,还有一些系统隐式调用 copy 的情况。第一种就是在 block 里面使用 block,比如在 GCD 的 block 里内嵌另一个 block。因为 GCD 本身会拷贝传入 dispatch_queue 中的 block,内嵌的 block 也会同时被拷贝:When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied. 而另外一个情况就是 NSDictionary 的 key,这些 key 都是默认 copy 的。原因很简单:NSDictionary 是通过 key hash 拿到的值进行查询,如果不对 key 进行拷贝,则在进行查询时会因为 key 的变化而导致查询失败。(所以也不太推荐拿一些复杂易变的对象当作 NSDictionary 的 key)

正确的 setter 写法

看到比较多的 setter 写法是这样的: (以 retain 为例,copy 也是同理)

- (void)setSomeInstance:(SomeClass *)aSomeInstanceValue
{
    [someInstance release]; // <-- original value is released
    someInstance = [aSomeInstanceValue retain];
}

这种写法的最大问题是当原始值和新值指向的是同一块内存区域时,就有可能发生 send retain message to dealloc object 这样的错误。之所以仅仅是有可能是因为传入的参数在大多数情况并不是 retainCount 为 1——- 它很有可能被其他对象说所有,所以这种写法在 90% 多的情况下没有任何问题,但是总归不是一个好的做法,埋下了不大不小的定时炸弹。虽然这其实是个很基础的东西,但是老有人用上面这种方式写 setter 方法,对于有点代码洁癖的我来说真心抓狂。推荐的方法应该是

- (void)setSomeInstance:(SomeClass *)aSomeInstanceValue
{
    if (someInstance != aSomeInstanceValue)
    {
         [someInstance release];
         someInstance = [aSomeInstanceValue retain];
           
    }
}

或者:

- (void)setSomeInstance:(SomeClass *)aSomeInstanceValue
{
    [someInstance autorelease];
    someInstance = [aSomeInstanceValue retain];
}

objc_setAssociatedObject 是个好东西

UIKit 中有两个控件是让我最恶心的:UIAlertView 和 UIActionSheet,原因在于他们是用 delegate 的方式而非 block 实现回调。而实际应用中一个 VC 中通常有出现好几个 UIAlertView 或 UIActionSheet 的情况,这就迫使我们在 delegate 里加入判断相应操作来自于哪个控件的代码。而 setAssociatedObject 可以很好的解决这个问题:通过向 UIAlertView 或者 UIActionSheet 关联相应操作的 block,轻松实现在当前调用的上下文内完成后续操作的处理。而唯一需要注意的是需要在使用完毕后进行清理,防止循环应用引起的内存泄漏。(如果关联对象和当前对象没有任何关系,即使不清理也没有关系,系统自动会在对象 dealloc 时取消关联对象)

Block 的一些注意事项

objc 里面让人最 high 的特性莫过于 GCD+Block 的组合,可以轻松地以同步的方式写出漂亮的异步代码。但是在使用 Block 时仍旧有很多注意事项:

  • 谨防循环引用。其实这个没啥好说的,网上随便一搜就是各种教程:MRC 下注意不要永久地 retain 当前 block 的调用者,ARC 下使用 weak-strong dance 等。

  • 控制好 Block 内对象的生命周期:因为 block 在定义时会 capture 卷入 block 内的对象,这就导致在 block 调用完毕之前这些对象一直处于是无法被释放的状态。一种常见的场景就是:VC 某些方法调用了 GCD 的方法后即使被 pop 出 NavigationController 的堆栈仍旧还没 dealloc,而这时 VC 仍旧监听一些通知事件并进行处理的话就很有可能发生奇葩问题。