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仍旧监听一些通知事件并进行处理的话就很有可能发生奇葩问题。