Objective-C内存管理
内存管理基础
对象的创建
首先分配内存空间,然后初始化
Class *a = [[Class alloc] init];
对象的销毁
调用release引用计数(retainCount)减1
[a release];
引用计数为0时,系统调用dealloc方法销毁对象
对象的赋值
使用对象赋值时,retainCount不会增加,需要手动retain
[a retain];
Class *b = a;
自动释放对象池
在创建对象时调用autorelease
Class *a = [[[Class alloc] init] autorelease];
这样就无需调用release,系统会自动释放对象
[a release];
自动释放对象池原理剖析
autorelease pool不是原生的,需要手动创建,,但是xcode在新建
项目时已经帮你写好了一个
autorelease的对象是NSAutoreleasePool
NSAutoreleasePool内部包含一个可变数组(NSMutableArray),
用来保存声明为autorelease的所有对象,
如果一个对象声明为autorelease,系统就会把这个对象加入到数组中
Class *a = [[[Class alloc] init] autorelease];
NSAutorelease自身在销毁的时候,会遍历一遍这个数组,
release数组中每个成员,如果此时数组中成员retainCount为1,那么
release之后,retainCount为0,对象完全被销毁,如果此时数组成员的
retainCount大于1,那么release之后,retainCount大于0,此对象依然
没有被销毁,内存泄露
内存管理规则
您只能释放或自动释放您所拥有的对象
如果您使用名字以“alloc”或“new”开头或名字中包含“copy”的方法(例如
alloc,newObject或mutableCopy)创建了一个对象,则您会获得该对象的所有权
;或者如果您向一个对象发送了一条retain消息,则您也会获得该对象的所有权。
您可以使用释放release或自动释放autorelease来释放一个对象的所有权。自动
释放autorelease的意思是“将会发送释放release消息”
局部对象和成员对象的释放
如果在方法中alloc了一个局部对象,那你必须在方法之前调用对象的
release或者使用autorelease。
如果你在类中alloc了一个成员对象,且没有调用autorelease,那么你
必须在类的dealloc方法中调用成员对象的release
retain与release
谁retain的对象谁就调用release。只要你调用了retain,无论这个对象
是怎么生成的,就都要调用release
release一个对象后立即把指针清空
注意:指针为空release也不会报错
[a release];
a = nil;
在子类的dealloc应调用父类的dealloc方法
- (void) delloc() {
[super dealloc];
}
创建返回对象的方法
在方法中创建返回的对象一定要是autorelease的,因为很多人都是这么
做的,不会去手动release方法返回的对象
存取对象的属性
如果您需要将接收到的对象存储为某个实例变量的属性,您必须保留或复制该
对象,通常您应该将这部分工作交给getter方法和stter方法
Property的内存管理
@property可以生成getter和stter方法,它还可以做内存管理
@property(nonatomic, retain) UITextField *userName; //1
@property(nonatomic, retain,readwrite) UITextField *userName; //2
@property(atomic, retain) UITextField *userName; //3
@property(retain) UITextField *userName; //4
@property(atomic,assign) int i; //5
@property(atomic) int i; //6
@property int i; //7
上面的代码,1和2是等价的,3和4是等价的,5,6,7是等价的,
也就是说atomic是默认行为,assign是默认行为,readwriter是默认
行为,但是如果你写上:
@property(nonatomic)NSString *name;
这里将会报一个错,因为property获取时retainCount会加1,如果
不设置为retain就无法增加计数
此错误只会在对象属性中出现如果是int float等基础类型就不会出现,
这里是我个人的理解。
@property内存管理其实就是生成指定行为的getter和setter
如果自己实现了getter和setter但是没有实现行为,编译器
就会根据其行为生成getter和setter
@synthesize生成getter与setter
如果property指定了atomic则会生成线程安全的代码,保证多线程访问时
getter与setter不会被同时访问
如果property指定了nonatomic则不会生成线程安全的代码,不过这样会
比atomic的访问速度要快
atomic = 原子
nonatomic 非原子
以下是官方文档用来参考
对象所有权策略
任何对象都可能拥有一个或多个所有者。只要一个对象至少还拥有一个所有者,它就会继续存在。如果一个对象没有所有者,则运行时系统会自动销毁它(参考“回收对象”)。为了确保您清楚自己何时拥有一个对象而何时不拥有对象,Cocoa设置了以下策略:
- 任何您自己创建的对象都归您所有。
您可以使用名字以“alloc”或“new”开头或名字中包含“copy”的方法(例如alloc,newObject,或mutableCopy)来“创建”一个对象。
- 您可以使用retain来获得一个对象的所有权。
请记住,一个对象的所有者可能不止一个。拥有一个对象的所有权就表示您需要保持该对象存在。(“存取方法”更详细地讨论了这部分内容。)
- 当您不再使用您所拥有的对象时,您必须释放对这些对象的所有权。
您可以通过向一个对象发送release消息或autorelease消息(在“自动释放”这一部分更详细地讨论了autorelease)来释放您对它的所有权。因此,用Cocoa的术语来说,释放对象的所有权通常被称为“释放”(releasing)对象。
- 您不能释放非您所有的对象的所有权。
这主要是前面的策略规则的一个隐含的推论,在这里将其明确地提出。
幕后:保留计数
所有权策略是在调用retain方法后通过引用计数—通常被称为“保留计数”—实现的。每个对象都有一个保留计数。
- 当您创建一个对象时,该对象的保留计数为1。
- 当您向一个对象发送
retain
消息时,该对象的保留计数加1。 - 当您向一个对象发送release消息时,该对象的保留计数减1。
当您向一个对象发送autorelease消息时,该对象的保留计数会在将来的某个阶段减1。
- 如果一个对象的保留计数被减为0,该对象就会被回收(请参考“回收对象”)。
重要:通常您不必显式地查询对象的保留计数是多少(参考retainCount)。其结果往往容易对人产生误导,因为您可能不知道您感兴趣的对象由何种框架对象保留。在调试内存管理的问题上,您只需要确保您的代码遵守所有权规则。
自动释放
NSObject
对象定义的autorelease
方法为后续的释放标记了接收者。通过向对象发送autorelease
消息,您亦是声明:在您发送消息的作用域之外,您不想再保留该对象。这个作用域的范围是由当前的自动释放池定义的,可参考“自动释放池”。
您可以这样实现上面提到的sprockets
方法:
– (NSArray *)sprockets { |
|
NSArray *array = [[NSArray alloc] initWithObjects:mainSprocket, |
auxiliarySprocket, nil]; |
return [array autorelease]; |
} |
使用alloc
创建数组;因此您将拥有该数组,并负责在使用后释放所有权。而释放所有权就是使用autorelease
完成的。
当另一个方法得到Sprocket对象的数组时,这个方法可以假设:当不在需要该数组时,它会被销毁,但仍可以在其作用域内的任何地方被安全地使用(请参考“共享对象的有效性”)。该方法甚至可以将数组返回给它的调用者,因为应用程序对象为您的代码定义好了调用堆栈的底部。
autorelease
方法可以使您很轻松地从一个方法返回一个对象,并且仍然遵循所有权策略。为了说明这一点,来看两个sprockets
方法的错误实现:
- 这种做法是错误的。根据所有权策略,这将会导致内存泄漏。
- 对象只能在
sprockets
方法的内部引用新的数组对象。在该方法返回后,对象将失去对新对象的引用,导致其无法释放所有权。这本身是没有问题的。但是,按照先前提出的命名约定,调用者并没有得到任何提示,不知道它已经获得了返回的对象。因此调用者将不会释放返回的对象的所有权,最终导致内存泄漏。 - 这种做法也是错误的。虽然对象正确地释放了新数组的所有权,但是在
release
消息发送之后,新数组将不再具有所有者,所以它会被系统立即销毁。因此,该方法返回了一个无效(已释放)的对象:
– (NSArray *)sprockets { |
|
NSArray *array = [[NSArray alloc] initWithObjects:mainSprocket, |
auxiliarySprocket, nil]; |
return array; |
} |
– (NSArray *)sprockets { |
|
NSArray *array = [[NSArray alloc] initWithObjects:mainSprocket, |
auxiliarySprocket, nil]; |
[array release]; |
return array; // array is invalid here |
} |
最后,您还可以像这样正确地实现sprockets
方法:
– (NSArray *)sprockets { |
|
NSArray *array = [NSArray arrayWithObjects:mainSprocket, |
auxiliarySprocket, nil]; |
return array; |
} |
您并没有拥有arrayWithObjects:
返回的数组,因此您不用负责释放所有权。不过,您可以通过sprockets
方法安全地返回该数组。
重要:要理解这一点,很容易令人联想到arrayWithObjects:
方法本身正是使用autorelease
实现的。虽然在这种情况下是正确的,但严格来讲它属于实现细节。正如您不必关心一个对象的实际保留计数一样,您同样不必关心返回给您的对象是否会自动释放。您唯一需要关心的是,您是否拥有这个对象。
共享对象的有效性
Cocoa的所有权策略规定,被接收的对象通常应该在整个调用方法的作用域内保持有效。此外,还可以返回从当前作用域接收到的对象,而不必担心它被释放。对象的getter方法返回一个缓存的实例变量或者一个计算值,这对您的应用程序来说无关紧要。重要的是,对象会在您需要它的这段期间保持有效。
这一规则偶尔也有一些例外情况,主要可以总结为以下两类。
- 当对象从一个基本的集合类中被删除的时候。
- 当对象从一个基本的集合类中被删除时,它会收到一条
release
(不是autorelease
)消息。如果该集合是这个被删除对象的唯一所有者,则被删除的对象(例子中的heisenObject
)将被立即回收。 - 当一个“父对象”被回收的时候。
- 在某些情况下,您通过另外一个对象得到某个对象,然后直接或间接地释放父对象。如果释放父对象会使其被回收,而且父对象是子对象的唯一所有者,那么子对象(例子中的
heisenObject
)将同时被回收(假设它在父对象的dealloc
方法中收到一条release
而非autorelease
消息)。
heisenObject = [array objectAtIndex:n]; |
[array removeObjectAtIndex:n]; |
// heisenObject could now be invalid. |
id parent = <#create a parent object#>; |
// ... |
heisenObject = [parent child] ; |
[parent release]; // Or, for example: self.parent = nil; |
// heisenObject could now be invalid. |
为了防止这些情况发生,您要在接收heisenObject
后保留该对象,并在使用完该对象后对其进行释放,例如:
heisenObject = [[array objectAtIndex:n] retain]; |
[array removeObjectAtIndex:n]; |
// use heisenObject. |
[heisenObject release]; |
存取方法
如果您的类中有一个实例变量本身是一个对象,那么您必须保证任何为该实例变量赋值的对象在您使用它的过程中不会被释放。因此,您必须在对象赋值时要求获取它的所有权。您还必须保证在将来会释放任何您当前持有的值的所有权。
例如,如果您的对象允许设置它的main Sprocket,您可以这样实现setMainSprocket:
方法:
– (void)setMainSprocket:(Sprocket *)newSprocket { |
[mainSprocket autorelease]; |
mainSprocket = [newSprocket retain]; /* Claim the new Sprocket. */ |
return; |
} |
现在,setMainSprocket:
可能在被调用时带一个Sprocket对象的参数,而调用者想要保留该Sprocket对象,这意味着您的对象将与其他对象共享Sprocket。如果有其他对象修改了Sprocket,您的对象的main Sprocket也会发生变化。但如果您的Thingamajig需要有属于它自己的Sprocket,您可能会认为该方法应该复制一份私有的副本(您应该记得复制也会得到所有权):
– (void)setMainSprocket:(Sprocket *)newSprocket { |
[mainSprocket autorelease]; |
mainSprocket = [newSprocket copy]; /* Make a private copy. */ |
return; |
} |
以上几种实现方法都会自动释放原来的main sprocket。如果newSprocket
和mainSprocket
是同一个的对象,并且Thingamajig对象是它的唯一所有者的话,这样做可以避免一个可能出现的问题:在这种情况下,当sprocket被释放时,它会被立即回收,这样一旦它被保留或复制,就会导致错误。下面的实现也解决了这个问题:
– (void)setMainSprocket:(Sprocket *)newSprocket { |
if (mainSprocket != newSprocket) { |
[mainSprocket release]; |
mainSprocket = [newSprocket retain]; /* Or copy, if appropriate. */ |
} |
} |
在所有这些情况中,看起来好像最终为您的对象设置的mainSprocket泄漏了,因为您不用释放对它的所有权。这些由dealloc
方法负责,在“回收对象”部分进行了介绍。“存取方法”部分更详细地描述了存取方法及其实现。
回收对象
当一个对象的保留计数减少至0时,它的内存将被收回—在Cocoa术语中,这被称为“释放”(freed)或“回收”(deallocated)。当一个对象被回收时,它的dealloc方法被自动调用。dealloc
方法的作用是释放对象占用的内存,释放其持有的所有资源,包括所有实例变量对象的所有权。
如果在您的类中有实例变量对象,您必须实现一个dealloc
方法来释放它们,然后调用超类的dealloc
实现。例如,如果Thingamajig类含有mainSprocket
和auxiliarySprocket
实例变量,您应该这样实现该类的dealloc
方法:
- (void)dealloc { |
[mainSprocket release]; |
[auxiliarySprocket release]; |
[super dealloc]; |
} |
重要:决不要直接调用另一个对象的dealloc
方法。
您不应该让系统资源的管理依赖于对象的生命周期;参考“资源管理”。
当应用程序终止时,对象有可能没有收到dealloc
消息。由于进程的内存在退出时被自动清空,因此与调用一切内存管理方法相比,简单地让操作系统清理资源效率更高。
通过引用返回的对象
Cocoa的一些方法可以指定一个通过引用(即ClassName **
或 id *
)返回的对象。下面有几个使用NSError
对象的例子,该对象包含有错误出现时的信息,例如:
- initWithContentsOfURL:options:error: (
NSData
) - initWithContentsOfFile:encoding:error: (
NSString
) - executeFetchRequest:error: (
NSManagedObjectContext
)
在这些情况下,前面描述的规则同样适用。当您调用这些方法中的任何一种时,由于您没有创建NSError
对象,因此您不会拥有该对象—同样也无需释放它。
NSString *fileName = <#Get a file name#>; |
NSError *error = nil; |
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName |
encoding:NSUTF8StringEncoding error:&error]; |
if (string == nil) { |
// deal with error ... |
} |
// ... |
[string release]; |
如果因为任何原因,返回的对象的所有权不能遵守基本规则,这将在方法的文档中明确地阐明(例如,参考dataFromPropertyList:format:errorDescription:)。
保留循环
在某些情况下,两个对象之间可能会出现循环引用的情况,也就是说,每一个对象都包含一个实例变量引用对方对象。例如,考虑一个文本程序,程序中对象间的关系如图1所示。“文档(Document)”对象为文档中的每个页面创建一个“页(Page)”对象。每个Page对象具有一个实例变量,用来跟踪该页所在的文档。如果Document对象保留了Page对象, 同时Page对象也保留Document对象,则这两个对象都永远不会被释放。只有Page对象被释放,Document的引用计数才能变为0,而只有Document对象被回收,Page对象才能被释放。
图 1 保留循环示意图
针对保留循环问题的解决方案是“父”对象应保留其“子”对象,但子对象不应该保留其父对象。因此,在图1中,document对象要保留page对象,但page对象不保留document对象。子对象对其父对象的引用是一个弱引用的例子,这部分内容在“对象的弱引用”有更充分的描述。
对象的弱引用
保留一个对象创建了一个对该对象的“强”引用。一个对象只有在它的所有强引用都被释放后才能被回收。因此,一个对象的生命周期取决于其强引用的所有者。在某些情况下,这种行为可能并不理想。您可能想要引用一个对象而不妨碍对象本身的回收。对于这种情况,您可以获取一个“弱”引用。弱引用是通过存储一个指向对象的指针创建的,而不是保留对象。
弱引用在可能会出现循环引用的情况下是必不可少的。例如,如果对象A和对象B互相通信,两者都需要引用对方。如果每个对象都保留对方对象,则这两个对象只有在它们之间的连接中断后才能被回收,但是它们之间的连接又只能在有对象被回收后才能中断。为了打破这种循环,其中一个对象需要扮演从属角色,得到另一个对象的一个弱引用。举个具体的例子,在视图层次中,父视图拥有其子视图,也因此能够保留子视图,但父视图并不归子视图所有;然而子视图仍需要知道谁是它的父视图,因此它保持一个对其父视图的弱引用。
Cocoa中弱引用的其他适用情况包括:表格数据源,大纲视图项,通知观察者以及其余项目标和委托,但不仅限于上述情况。
重要:在Cocoa中,引用表格数据源,大纲视图项,通知观察者和委托都被看作是弱引用(例如,NSTableView
对象不保留其数据源,NSApplication
对象不保留它的委托)。本文档仅仅介绍了这一公约的例外情况。
在向您弱引用的对象发送消息时,您需要小心谨慎。如果您在一个对象被回收之后向它发送消息,您的应用程序将会崩溃。您必须为对象何时有效制定有明确界定的条件。在大多数情况下,被弱引用的对象知道其他对象对它的弱引用,这和循环引用的情况是一样的,并且它还能够在自己被回收时通知其他对象。例如,当您向通知中心注册一个对象的时候,通知中心会存储一个对该对象的弱引用,并且在适当的消息发布时,还会向该对象发送消息。当对象被回收时,您需要向通知中心解注册该对象,以防通知中心向这个已经不存在的对象继续发送消息。同样,当一个委托对象被回收时,您需要通过向其他对象发送一条带nil
参数的setDelegate:
消息来删除委托链接。这些消息通常由对象的dealloc
方法发出。
资源管理
通常,不应该由您来管理一些稀缺资源,比如文件描述符,网络连接和dealloc
方法中的缓冲区/高速缓存。特别地,您设计的类不应该让您错误地认为dealloc
会在您觉得该调用的时候被调用。dealloc
的调用可能会因为bug或应用程序销毁而被延误或回避。
相反,如果您有一个类,由它的实例管理稀缺资源,则您在设计应用程序时应该让自己知道何时不再需要这些资源,并且可以在那个时刻通知实例“清理”这些资源。通常,接下来您应该释放该实例,然后调用dealloc
,但如果您不这样做也不会有问题。
如果您试图让dealloc
肩负起资源管理的责任,会出现的一些问题:
- 对象图销毁的顺序依赖性。
对象图销毁机制内在是无序的。虽然通常您可能期望甚至得到一个特定的顺序,但是这样做的同时您也引入了脆弱性。如果一个对象意外落入自动释放池,则销毁顺序会改变,这可能会导致意想不到的后果。
- 稀缺资源的未回收。
内存泄露当然是应该被修复的bug,但它们一般来说不会是直接致命的错误。然而,如果稀缺资源在您希望它们被释放时没有被释放,这会导致非常非常严重的问题。例如,如果您的应用程序耗尽了文件描述符,那么用户可能无法保存数据。
- 清除在错误的线程上被执行的逻辑。
如果一个对象在意外的时刻落入自动释放池,那么无论它进入哪个线程池,它都将被回收。这对于那些本应该只由一个线程访问的资源来说很容易产生致命的错误。