《iOS高级内存管理编程指南》学习笔记

Object-C 一共有3种内存管理方式:

1. MRR (Manual Retain-Release)手动持有-释放。采用了引用计数模型,由基础类NSObject和运行时(Runtime Eviroment)共同提供。

2. ARC (Automatic Retain-Count)自动引用计数。此方式采用与MRR相同的引用计数系统,但是在编译时(Compile-time)插入了内存管理的方法。

3. 垃圾回收方式。系统自动跟踪对象与对象之间的引用关系。对于没有引用的对象,自动回收。这种模式与MRR和ARC都不同,且只能用于Mac OS

 

错误的内存管理方式一般有2种:

1. 释放或者覆盖了正在使用中的数据。造成内存异常,程序崩溃。

2. 不用的数据却没有释放,导致内存泄露。

 

所有权策略通过引用计数来实现

     通常称之为“retain count”。每个对象都有一个retain count。

     当建立一个对象时,它的retain count 为1。

     对象调用 retain方法时,它的retain count +1。

     对象调用 release方法时,它的retain count -1。

     对象调用 autorelease方法时,它在retain count将在未来某事 -1。

     当对象retain count 为0,就会被dealloc

正常使用下,这个计数并不需要关心,只需要遵守所有权使用规则就行。

 

基础使用规则:

1. 用retain 实现对一个对象的持有

2. 不在需要使用一个对象时,必须用release放弃持有

3. 正在使用的对象不能使用release

4. retain与release成对出现

5.autorelease 延迟release,当没有被持有时会自动释放。常用于返回值

6.通过 “&” 返回的对象是没有所有权的。例如 NSError

7.alloc、new、copy、mutableCopy等方法会返回对象的所有权,而类方法创建的则基本为autorelease

 

 

Core Fundation 对象的使用规则:

set和get方法

     get:直接返回指针即可

     set:由于有新值导入,为了保证新值在被使用前不会被释放,需要先给新值做retain操作。然后在对旧值做release,最后将新值赋值给指针

     初始化方法和dealloc不要使用 get和set方法,而应该直接初始化指针。(可以将初始化放入get方法中)

 

循环引用规则:

     retain就是对一个对象的强引用,在引用被释放前,该对象是无法被dealloc的,这就可能出现一个循环引用的问题:两个对象相互强引用。

     解决方法就是将其中一者的引用变成弱引用。 Cocoa的规则是:父对象建立对子对象的强引用,而子对象只对父对象弱引用。

     在Cocoa种,弱引用的例子有:Table的datasource、delegate,Outline试图项目的Outline view item、观察者(Notification Observer)和其他的target以及delegate。

     当你向一个Notification Center注册一个对象时,Notification Center对这个对象是弱引用的,并且在有消息需要通知这个对象时,就发送消息给这个对象,当这个对象dealloc的时候,你必须向Notification Center取消这个对象的注册。这样,这个Notification Center就不会再发送消息到这个不存在的对象了。同样,当一个delegate对象被dealloc的时候,必须向其他对象发送一个setDelegate:消息,并传递nil参数,从而将代理关系撤销。这些消息通常在对象的dealloc方法中发出。

 

Cocoa的所有权策略:

     返回的对象,在调用者的调用方法中,始终保持有效。所以在方法内部,不必担心你收到的返回对象会被dealloc。

但也有例外:

     1.当一个对象从NSarray(Dictionary、Set也一样)中删除的时候

          a = [array objectAtIndex:n];

          [array removeObjectAtIndex:n];

          当一个对象从array中被删除,系统会立即调用对象的release方法(而不是autorelease)。如果这个时候,这个array是该对象的唯一属主,那么这个对象a会立即被dealloc。

    

     2.当父对象被dealloc的时候

          id A = [[A alloc] init];

          B = [A child];

          [A release];//或者 self.A = nil;

          有时候,通过父对象A获取子对象B,然后间接或直接地release了父对象A。如果父对象A的release造成了它被dealloc,且该父对象A恰好是子对象B的唯一属主,那么子对象B就会同时被dealloc(假定父对象的dealloc方法中对子对象发送的是release消息,而不是autorelease)。

 

 

NSArray、NSDictionary、NSSet容器拥有其包容的对象的所有权

     如果一个对象放入了容器中,容器就会获得该对象的所有权。当容器自己release的时候,或者该对象从容器中删除时,容器会放弃该对象的所有权。

 

 

自动释放池(Autorelease Pool)

     Autorelease Pool 的机制,为你提供了一个“延时”release 对象的机制。当你既想放弃对象所有权,但又不想立即发生放弃行为的时候(比如作为返回值return)。

    

     Autorelease Pool 是NSAutorelease类的一个实例,它是得到了autorelease消息的对象的容器。在autorelease pool被dealloc的时候,它会自动给池中的对象发送release消息。一个对象可以被多次放入到同一个autorelease pool,每一次放入(调用 autorelease方法)都会造成将来收到一次release。

    

     多个autorelease pool之间的关系,通常描述是“嵌套关系”,实际上是按照栈的方式工作的(后进先出)。当一个新的autorelease pool创建后,它就位于这个栈顶。当pool被dealloc的时候,就从栈中删除。当对象调用autorelease方法时,实际上它会被放到“这个线程”“当时”位于栈顶的那个pool中(由此可以推定,每个线程都有一个私有的autorelease pool的栈)。

    

     Cocoa希望程序中长期存在一个 autorelease pool。如果 pool不存在,autorelease的对象就无从 release了,从而造成内存泄露。当程序中没有autorelease pool,你的程序还在 调用对象的autorelease方法,就会发出一个错误日志。AppKit和UIkit框架自动在每个消息循环的开始都创建一个池(比如鼠标按下时间、触摸事件)并在结尾处销毁这个pool。正因此如此,你实际上不需要创建autorelease pool,甚至不用知道如果创建 autorelease pool。

    

上面说的是通常情况,下面是例外的情况:

     1.如果你写的程序,不是基于UI Framework。例如你写的是一个基于命令行的程序。

     2.如果你程序中的一个循环,在循环体重创建了大量的临时对象。

          你可以在循环体内部新建一个autorelease pool ,并在一次循环结束时销毁这些临时对象。这样可以减少程序对内存的占用峰值。

     3.如果你发起了一个secondary线程(main线程以外的线程)。这时你“必须”在线程的最初执行代码中创建autorelease pool,否则你的程序就内存泄露了。 

     

     通常我们用alloc和init来新建一个NSAutoreleasePool对象,用drain消息来销毁这个pool。如果你调用 pool对象的autorelease 或者retain方法,会出现程序异常。Autorelease Pool 必须在其所“诞生”的上下文环境(对方法或函数的调用出,或者循环体的内部)中 进行drain。

     Autorelease Pool 必须用 inline(作为局部变量) 的方法使用,你永远不需要把一个这样的pool作为成员变量来处理。

 

使用本地Autorelease Pool来减少内存占用峰值

     许多程序所用的临时对象都是autorelease的,因此这些对象在pool被销毁钱是占用内存的。想要减少对内存的占用峰值,就应该使用本地的autorelease pool。当pool被销毁时,那些临时对象都会被release。

     Demo:

     NSArray *urls = <# 一个包含url的数组 #> ;

     for(NSURL *url in urls ) {

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

          NSError *error = nil;

          NSString *fileContents = [[[NSString alloc] initWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]autorelease];

          // 使用这些string

          [loopPool drain];

     }

     

     这个 for 循环每次处理一个文件。从循环体的开始,创建一个池,在循环结束时将这个pool销毁掉,在pool中的所有autorelease对象都会被release。

     在 autorelease pool已经dealloc之后,pool中的对象被视为失效,而不要再使用他们,或者把他们作为返回值返回。如果你必须在autorelease之后还要使用某个临时对象,你可以调用该对象的retain方法,然后等这个pool已经调用了drain后,再次调用autorelease。

Demo:

- (id)findMatchingObject:(id)anObject {

     id match = nil;

     while (match == nil) {

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

          //创建大量临时数据

          match = [self expensiveSearchForObject:anObject];

          if (match != nil) {

               [match retain];

          }         

          [subPool drain];

     }

     return [match autorelease];

}    

     在subPool有效的时候,我们调用 match对象的retain方法。在subPool被drain之后,我们又调用了match的autorelease方法。通过这几步后,match没有进入subPool,而是进入了比subPool更早的一个autorelease pool。这样实际上是延长了match 对象的生命周期,使得 match 可以在循环体之外也能被使用,还是得match可以作为返回值返回给findMatchingObject的调用者。

     

Autorelease Pool 和线程

     Cocoa程序的每一个线程都维护着一个自己的NSAutorelease对象的栈。当线程结束的时候,这些Pool对象就会被release。如果你写的程序仅仅是一个基于Fondation的程序,又或者你detach一个线程(关于detached thread,请参考 Threading Programming Guide),你需要新建一个你自己的autorelease pool。

     如果你的程序是一个长期运行的程序,可能会产生大量的临时数据,这是你必须周期性地销毁、新建 autorelease pool (Kit 在主线程中就是这么做的),否则 autorelease 对象就会积累并吃掉大量内存。如果你detached线程不调用Cocoa,你就不必新建autorelease pool。

     注意:除非是Cocoa 运行于多线程模式,否则如果你使用POSIX线程 API 来启动一个secondary线程,而不是使用NSThread,你是不能使用Cocoa的,当然也就不能使用NSAutorelease。Cocoa只有在detach了它的第一个NSThread对象之后,才能进入多线程模式。为了在secondary POSIX 线程中使用Cocoa,你的程序首先要做的是 detach 至少1个NSThread,然后立即结束这个线程。你可以用NSThread的isMultiThreaded 方法来检测Cocoa 是否处于线程模式、

 

Autorelease Pool的作用域(Scope)

     一个autorelease pool的作用域实际是由它在栈中的位置所决定的。最顶上的pool,就是当下存放autorelease对象的pool。如果这时新建了一个pool,原有的pool就离开了scope,直到这个new pool 被drain后,原有的pool 才会再次回到最顶端,进入scope。drain后的pool,就永远不再Scope了。

    

     如果你drain 一个pool,但这个pool并不是栈顶,那么栈内位于它上面的所有pool都drain了(这意味着所有他们容纳的对象都将调用release消息)。如果你不小心忘记了drain一个pool,那从嵌套结构上来看,当更外一层pool drain的时候,会摧毁这个pool。

     

     这种特性,对于出现程序运行异常的情形是有用的。当异常出现,系统立即从当前执行的代码中跳出,当前现场有效的pool被 drain。然而一旦这个pool不是栈顶的那个pool,那么它上面的pool都会被 drain。最终,比这个pool更早的pool会成为栈顶。这种行为机制,使得Exception Handler 不必处理异常发生所在现场autorelease对象的release工作。对于Exception Handler而言,既不希望也不必要为autorelease pool 中的对象的release方法,除非 handler is re-raising the exception(原文没翻译,我也没看懂,记录下)

 

内存垃圾回收

     内存垃圾回收系统(Garbage Collection Programming Guide 中 讲述)并不使用 autorelease pool。

     如果你开发的是一个混合框架(既用到了内存垃圾回收,还用到了引用计数器),那么autorelease pool 为垃圾 内存回收者提供了线索。当autorelease pool 进行release 的时候,恰恰是提示垃圾内存回收者现在需要回收内存。

     在垃圾回收的环境中,release 实际上什么都不做。正因为如此,NSAutoreleasePool 才提供了一个drain 方法。这个方法在引用计数环境下等同于release,但在垃圾回收环境下,触发了内存回收的行为(前提是此时内存新分配的数量,超过了阀值)。所以如果摧毁autorelease pool时候应该用drain,而不是release。

 

关于dealloc

1. 永远不需要手动调用dealloc

2. 结尾必须调用super的dealloc

3. 不可将系统的资源与对象的生命周期绑定。(比如不能将关键系统资源在对象被dealloc的时候才释放)

4. 进程的内存会在退出时自动回收。当应用退出,对象可能收不到dealloc消息。和调用所有对象的内存管理方法比起来,系统的回收效率更高。

5. 不要用dealloc管理关键系统资源(文件句柄、网络连接、缓存等)。因为你无法设计出一个类,想让系统什么时候调用dealloc就什么时候调用(你能做的只是release,至于release是否会导致系统一定调用dealloc,还需要看对象是否有其他属主)。由于系统性能的下降、自身的Bug,有可能推迟或搁置dealloc的调用。

     1.对象图的拆除顺序问题。

          实际上,对象图的拆除是没有任何顺序保证的。也许你认为、希望有一个具体明确的顺序,实际上是无法预计的。

     2.系统稀缺资源不能回收。

          例如内存泄露问题,文件句柄被用光。

     3.释放资源的操作由其他线程来做。

          如果一个对象在一个不确定的时刻被放入了autorelease池中。它将被线程池中的线程来dealloc。这对于只能提供单一线程访问的资源而言,是致命的错误。

    

    正确的做法:如果对象管理了稀缺资源,它必须知道它什么时候不再需要这些资源,并在此时立即释放资源。通常情况下,此时,你会调用release来delloc,但因为此前你已经释放了资源,这里就不会遇到任何问题。

     

          

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值