Object-C内存管理的理解总结

时间:2022-07-05 19:26:53

今天看到了OC的内存管理这块,觉得很亲切。

自己的习惯是尽量自己掌控程序的空间和时间,有点强迫症的感觉。用C和C++做项目的时候,时时刻刻都在操心这new和delete的配对使用和计数,学习stl和boost的时候看到了智能指针等时候,依然不是很爱使用,还是愿意坚持自己控制new和delete;后来用C#后,一直特别注意Dispose相关的动作,尽早释放对象占有的内存空间,避免无谓的占用一直到程序退出才释放。

OC中系统对每个实例对象地址都记录一个引用次数(当然有特例,见另外一篇随笔),这就是引用计数,在很多面向对象语言中都被采用。

这里总结一下OC中引用计数的显式操作过程:

1. 计数置1:每个实例对象在实际显式alloc创建的时候都会做到引用计数置1。

2. 计数加1: 向对象显式发送retain消息会将引用计数加1。

3. 计数减1: 向对象显式发送release消息会将引用计数减1。

4. 计数为0: 当对象引用计数为0时候,OC系统会自动向对象发送实际释放的消息dealloc。

5. 计数不变:同类型对象的简单赋值不会引起计数改变,结果是两个对象指向相同的内存地址,也就具有相同的计数值,包括计数为0的状态。

下面这段代码很清楚的反应了上面的5个状态。

NSObject * obj = [[NSObject alloc] init]; //obj ref count = 1

NSObject * obj1 = obj; //obj1 ref count = 1

[obj retain]; //obj ref count = 2, obj1 ref count =2

[obj release]; //obj ref count = 1, obj1 ref count = 1

[obj release]; //obj ref count = 0, obj1 ref count = 0

NSObject * obj2 = obj; //obj2 ref count = 0

虽然最后一句看起来我们不会做,但是实际复杂代码中经常会出现不知情的指向了一个引用计数已经为0对象的情况。为什么会出现这种情况,大多数是由于代码中很多隐式引用计数操作引起的。

和上面的类似,我们总结一下引用计数的隐式操作过程,这个也是我们在开发构成中要谨慎细心留意的,特别是在自定义的类型的实现中。

1. 计数置1:所有通过间接调用alloc方法返回对象实例的方法都会将新产生对象的引用计数置1。包括:继承于NSObject的类型(包括Foundation提供的类型和我们自定义的类型)都提供了很多构造alloc和初始化结合的便捷方法,那么它们都会将对象的引用计数置1。比如NSArray的arrayWithObject,NSMutableString的stringWithString等等;对象的深拷贝产生的新对象也是直接或者间接的调用了alloc,那么它的计数也被置1。

2. 计数加1: 所有通过间接调用retain的方法都会将对象的引用计数加1。包括:所有Foundation定义的集合类型(Array, Dictionary, Set等)的添加新元素的方法,比如addObjectAtIndex等都会将添加进入的对象的引用计数加1,实际它们是给该对象发送了retain消息。自己定义的集合类型应该遵守这个约定。

3. 计数减1: 所有通过间接调用release的方法都会将对象的引用计数减1。包括:所有Foundation定义的集合类型的删除对象元素的方法,比如removeObjectAtIndex等都会把对象的引用计数减去1,集合自己release的时候也会给每个元素对象发送release消息以使得元素对象引用减1;此外自动释放池也会在drain的时候给注册到它内部的每个对象发送release消息,以使得对象的引用计数减1。自己定义的集合类型也应该遵守这个约定。

下面描述一下无效对象引用的表现:

计数为0时候OC系统的动作是自动触发的,因为计数为0意味着该对象没有人引用了,那么就可以触发dealloc了,dealloc中必须完成该对象占有资源的释放,然后系统会回收为该对象分配的内存,此时,对象的引用都变成无效的了。需要强调的是,当对象的引用无效后,在不知情的情况下依然通过该引用操作对象的结果是不确定的,因为系统回收了为该对象分配的内存后,不代表该内存被立即被其他数据占用或破坏,所以在某些情况下,会出现对象看起来还能正常工作的现象。好的习惯是,在某个地方release了对象后,将此处引用重新赋值为nil,这样避免继续操作这个对象引用来继续做事。

自动释放池也是个不错的概念,其实可以把方法局部变量看作在一个自动释放池中,这个池的范围是一个方法,在方法出口一定会释放这些局部变量本身占有的内存空间。OC系统的自动释放池扩展了这种方法局部变量的概念,(其他语言中,比如最新的C++和C#标准可以用花括号对{}来表达一个局部变量的生命范围,C#的using调用等,以此达到这种自动释放的效果)。

当然自动释放池和这些也是有所不同的,因为对象如果需要由自动释放池通知release,它必须首先把自己注册到当前的自动释放池中。

首先看注册对象自己到自动释放池的过程:

1. 显式标记自动释放:对对象发送autorelease消息,就会把对象注册到当前自动释放池里。

2. 隐式标记自动释放:在初始化函数实现中调用autorelease消息,这样在对象构造的时候自动将自己注册到当前自动释放池中。

来看段代码:

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

NSObject * obj = [[NSObject alloc] init]; //obj ref count = 1

NSArray * arr = [NSArray arrayWithObject: @"item"]; //arr ref count = 1

[arr retain];//arr ref count = 2

[obj retain];//obj ref count = 2

[arr retain];//arr ref count = 3

[obj retain];//obj ref count = 3

[arr release];//arr ref count = 2

[obj release];//obj ref count = 2

[pool drain];

  drain之后,这里arr和obj的引用次数是多少呢?实际的运行结果是arr ref count = 1, obj ref count = 2。

这个结果证明了,arr在构建的时候已经隐式把自己注册到了当前的自动释放池中了,而obj使用基本的alloc和init并没有包括这个隐式操作,所以pool在drain的时候,发送了release消息给arr,arr的引用计数就减为了1。如果我们在obj实例对象构建好后调用代码 [obj autorelease],那么obj在pool drain之后的引用计数也是1了。

所以,我们在自定义类型的初始化方法实现中应该也遵守这个规律,加入隐式的autorelease调用。

打完收工,娃睡着了,今个还可以做几个练习巩固一下这些EBOOK上的总结,然后再写一个随笔来总结实际开发中类对象的处理规律。