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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Ginhoor/article/details/43736047
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,但因为此前你已经释放了资源,这里就不会遇到任何问题。
     
          

猜你喜欢

转载自blog.csdn.net/Ginhoor/article/details/43736047