天天看点

iOS内存管理IOS内存管理

IOS内存管理

  • IOS内存管理
    • 管理原则
        • 1. 非ARC模式,MRR(manual retain-release)
        • 2. ARC模式
    • 循环引用问题
    • 内存管理注意事项
    • 常见的内存管理问题

IOS内存管理

iOS需要开发者管理内存,很高高级语言都有垃圾回收机制,但iOS开发却没有垃圾回收机制。虽然iOS没有垃圾回收机制,但是mac OS有垃圾回收机制。iOS将内存管理的任务交给了开发者。

管理原则

OC中的内存管理的黄金法则:

  • 如果对一个对象使用了alloc、[Mutable]copy,retain,那么你必须使用相应的realease或者autorelease。

1. 非ARC模式,MRR(manual retain-release)

MRR内存管理里的一个核心原则,“只负责你拥有的对象的生命周期”,也就是说,如果你对一个对象有所有权,那么你就要负责其回收的工作,否则,你不需要,也不能取回收你不拥有的对象。

1:所有使用alloc, new, copy或mutabelCopy,以及这些关键词开头的函数返回的对象,你都是拥有所有权的,也就是要负责这些对象的内存回收工作。这是iOS开发中的一种约定,所以,当你编写自己的alloc, new或copy类型的函数时,也请遵循这样的命名规范。

2:retain返回的对象,拥有所有权。例如显示调用retain函数返回的结果,或者synthesize 的retain类型的成员变量。

3:所有使用其他函数返回的对象,没有所有权。

4:返回的对象的引用,没有所有权。

5:autorelease返回的对象没有所有权。


           
  1. cocoa中的内存管理机制——引用计数
  • 引用计数(reference counting)又称为保留计数(retain counting),引用计数的数值表示有几个其它对象在使用它。
  • 每一个对象都拥有一个引用计数
  • 当对象被创建的时候,引用计数的值为1 当发送retain消息的时候,该对象的引用计数加1,该对象的引用计数为2
  • 当这个对象发送release消息的时候,该对象的引用计数减1
  • 当一个对象的引用计数为0时,系统自动调用dealloc方法,销毁该对象。
引用计数是实例对象的内存回收唯一参考
引用计数(retainCount)是Objective-C管理对象引用的唯一依据。调用实例的release方法后,此属性减一,减到为零时对象的dealloc方法被自动调用,进行内存回收操作,也就是说我们永不该手动调用对象的dealloc方法。
关于dealloc方法

       它的作用是,当对象的引用计数为0时,系统会自动调用dealloc方法,回收内存,它的一般写法为:

-(void)dealloc
{      
[super  dealloc];                                                         [person release];                                                                                 
}                                                                                                                                                                      
在这里为什么要调用父类的dealloc方法呢?
           子类的某些实例是继承自父类的,因此,我们需要调用父类的dealloc方法,来释放父类拥有的这些对象。
一般来说调用的顺序是,当子类的对象释放完时,然后再释放父类的所拥有的实例,这一点与调用初始化方法,正好相反。

           
  1. 对象所有权

    当一个所有者(可以是任何一个OC对象)做了以下某个动作的时候,它就拥有了对一个对象的所有权。

  • 1,在OC中,对象不断的被其它创建、使用和销毁,为了保证程序不产生额外的内存开销,当对象不再被需要以后,应当被立即销毁。
  • 2,拥有对一个对象的使用权,我们称为是”拥有”这个对象。对象的拥有者个数至少为1,对象才得以存在,否则它应当被立即销毁。
  • 3,为了确定你(一个对象)何时具有对另外一个对象的所有权,以及明确对象所有者的权利和义务,Cocoa设立了一套标准。只有当你对一个对象做了alloc,copy和retain等操作之后,你才拥有它的所有权。当你不再需要这个对象的时候,你应该释放你对它的所有权。千万不要对你没有所有权的对象进行释放。
//(1)如果创建或者复制某个对象时,则拥有了该对象的所有权,即包含下列关键词时:
               alloc,allocWithZone:,copy,copyWithZone:,mutableCopy,mutableCopyWithZone:

  //    (2)如果没有创建或复制对象,而是保留引用,同样拥有该对象的使用权
              retain
  //    (3)当拥有了某个对象的所有权,在不需要某一个对象时,需要释放他们,用
             release,autoRelease
           
  1. 自动释放池的相关用法

    (1)cocoa中的自动释放池(Autorelease pool),是能够自动释放赤忠的对象的。NSObject类提供了一个autorelease消息,当我们想一个对象发送autorelease消息的时候,这个对象就会随着释放池的销毁而释放。如果要向使用使用自动释放池释放对象,我们首先要有一个入池操作:

//入池对象5.0之后的写法
//创建自动释放池
@autoreleasepool {
    //入池对象5.0之后的写法
 }
 
//入池对象5.0之前写法
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
[pool release];


           

自动释放池是以栈的形式实现的,当某个对象调用了autorelease方法时,该对象会被加入自动释放池的栈顶。对于发送了autorelease消息的对象,当自动释放池销毁时,自动释放池会对这些对象发送一条release消息,来释放他们。

2)向自动释放池发送release及drain消息的区别

当我们向自动释放池pool发送release消息时,它会向池中的每一个发送了autorelease消息的对象发送一条release消息,并且自身也会销毁。当向它发送drain消息时,只会释放里面吧的对象,而不会销毁自己。

2. ARC模式

当你在编译程序的时候提供自动管理内存的功能,它会自动加入内存的控制代码,控制对象的生命周期,大大简化了内存管理的步骤,ARC管理内容的原理就是,编译器会在适当的地方自动插入retain、release和autorelease消息

循环引用问题

内存管理注意事项

  • 1:避免循环引用,如果两个对象互相为对方的成员变量,那么这两个对象一定不能同时为retain,否则,两个对象的dealloc函数形成死锁,两个对象都无法释放。
  • 2:不要滥用autorelease,如果一个对象的生命周期很清晰,那最好在结束使用后马上调用release,过多的等待autorelease对象会给内存造成不必要的负担。
  • 3:编码过程中,建议将内存相关的函数,如init, dealloc, viewdidload, viewdidunload等函数放在最前,这样比较显眼,忘记处理的概率也会降低。
  • 4:AutoReleasePool对象在释放的过程中,在IOS下,release和drain没有区别。但为了一致性,还是推荐使用drain。
  • 5:在delloc方法中只释放引用并解除监听

1.1 在delloc方法中释放对象所拥有的引用并解除监听

(1)ARC会通过自动生成的.cxx_destruct方法在delloc中添加释放代码。对象所拥有的其他非Objective-C对象也要释放。比如CoreFoundation对象。

(2)在delloc要把原来配置过的观测行为都清理。如果用NSNotification给此对象订阅过某种通知,那么一般应该在注销,这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。

1.2开销较大或系统稀缺资源应该在delloc前释放:比如文件描述符、套接字、大块内存

原因如下:

(1)不能指望dealloc方法必定在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。如果非要等到系统调用dealloc方法时才释放,那么保留这些稀缺资源的时间有些长了。

(2)系统并保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。

通常的做法是,实现另外一些清理的方法,当应用程序用完资源对象后,就调用此方法。比如,如果某对象管理着连接服务器所用的套接字,此对象可能要通过套接字连接数据库。对于对象所属的类,其接口:

@interface EOCServerConnection : NSObject
- (void)open:(NSString*)address;
- (void)close;
@end

在dealloc中也要调用“清理方法”,以防开发者忘了清理这些资源。
- (void)close {
    _close = YES;
}

- (void)dealloc {
    if (!_close) {
        [self close];
    }
}
           

1.3不要在dealloc方法里随便调用其他方法。

无论在这里调用什么方法都不太应该,因为对象此时已经接近尾声了。如果在这里所调用的方法又要异步执行某些任务,当这些任务执行完后,要回调此对象,告诉对象任务已完成,而此时如果对象已摧毁,那么回调就会出错。

在dealloc里也不要调用属性的存取方法,因为这些方法会覆写,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于“键值观测”(KVO)机制的监控之下,该属性的观察者可能会在属性值改变时“保留”或使用这个即将回收的对象。

  • 6:编写“异常安全代码”时留意内存管理问题

    异常安全:在发生异常时,不会导致内存泄露。

2.1手动管理引用计数时异常安全

@try {
        EOCSomeClass *object = [[EOCSomeClass alloc] init];
        [object soSomethingThatMayThrow];
        [object release];
    }
    @catch (NSException *exception) {
        NSLog(@"Whoops, there was an error. Oh well...");
    }
           

如果soSomethingThatMayThrow抛出异常,异常会令过程终止并跳转至catch块,因而其后的那行release代码不会运行。解决办法释放使用@finally,无论是否抛出异常,其中代码都保证会运行,且只运行一次。

EOCSomeClass *object
    @try {
        object = [[EOCSomeClass alloc] init];
        [object soSomethingThatMayThrow];
    }
    @catch (NSException *exception) {
        NSLog(@"Whoops, there was an error. Oh well...");
    }
    @finally {
        [object release];
    }
           

2.2ARC环境下异常安全

由于不能调用release,所以无法像手动管理引用计数时那样把释放操作移到@finally块中。若使用ARC且必须捕获异常,则需要打开编译器的-fobjc-arc-exceptions标志。ARC默认情况下是没有实现异常安全的,其原因如下:

1.ARC实现异常安全需要加入大量样板代码,以便跟踪清理的对象,从而在在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便不抛出异常也是如此。而且,添加进来的额外代码还会明显增加应用程序的大小。

2.Objective-C代码中,只有当应用程序因异常状况而终止时才抛出异常。

  • 7:.以“自动释放池块”降低内存峰值

    释放对象有两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中,自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。

    一般情况下,无须担心自动释放池的创建问题。系统会自动为每个线程创建一个默认的自动释放池,每次执行“事件循环”时,就将其清空。

创建自动释放池的语法如下:

@autoreleasepool {

//…

}

自动释放池于左花括号处创建,并于右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。自动释放池机制就像“栈”一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

//将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值。
    NSArray *databaseRecords = /* ... */
    NSMutableArray *people = [NSMutableArray new];
    for (NSDictionary *record in databaseRecords) {
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
           

EOCPerson的初始化函数也许会再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。

NSArray *databaseRecords = /* ... */
    NSMutableArray *people = [NSMutableArray new];
    for (NSDictionary *record in databaseRecords) {
        @autoreleasepool {
            EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
            [people addObject:person];

        }
    }
           

如果把循环内代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。新增的自动释放池可以减少内存峰值,因为系统会在块的末尾把某些对象回收掉。

是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没有完成这一步,那就别着急优化。尽管自动释放池的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。

  • 8:“用僵尸对象”调试内存管理

    向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。

//Cocoa提供了“僵尸对象”来调试内存管理问题。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会正在回收它们。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。
设置NSZombieEnbled后:
void PrintClassInfo(id obj) {
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===", class_getName(cls), class_getName(superCls));
}

int main(int argc, char *argc) {
    EOCClass *obj = [[EOCClass alloc] init];
    NSLog(@"Before release");
    PrintClassInfo(obj);       // === EOCClass : NSObject ===
   
    [obj release];
    NSLog(@"After release");
    PrintClassInfo(obj);       // === _NSZombile_EOCClass : nil ===
}
           

对象所属的类已由EOCClass变为_NSZombie_EOCClass。_NSZombie_EOCClass实际上是在运行期生成的,当首次碰到EOCClass类的对象要变成僵尸对象时,就会创建这个类。创建过程如下:

//获取回收对象所属的类名
    const char *clsName = object_getClassName(self);
   
    //生成僵尸对象名
    const char *zombieClsName = "_ZSZombie_" + clsName;
   
    //判断这个僵尸对象是否已经存在
    Class zombieCls = object_lookUpClass(zombieClsName);
   
    //如果不存在就创建
    if (!zombieCls) {
        //获得僵尸对象_NSZombie_模板
        Class baseZobieCls = objc_lookUpClass("_NSZombie_");
       
        zombieCls = object_duplicateClass(baseZobieCls,zombieClsName);
    }
   
    //回收对象
    objc_destructInstance(self);
   
    object_setClass(self,zombieCls);
           
这个过程是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把dealloc方法“调配”成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变成_NSZombie_OriginalClass了。
僵尸类是从名为_NSZombie_的模板类里复制出来的。创建新类的工作由运行期函数objc_duplicationClass( )来完成,它会把整个_NSZombie_类结构拷贝一份,并赋予其新的名字。副本的超类、实例变量以及方法都和复制前相同。
_NSZombie_类是类似于NSObject的根类,并未实现任何方法,该类只有一个实例变量,叫做isa。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。在完整的消息转发机制中,___forwarding___是核心,它首先要做的事就包括检查接收消息的对象所属的类名。若名称前缀为_NSZombie_,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就中止了。

虽说内存泄露了,但这只是调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄露无关要紧。 
           

常见的内存管理问题

错误的内存管理往往包括两类:

  • 1,释放(free)或者覆盖(over-write)正在使用中的数据:造成内存异常,导致应用程序 崩溃,甚至导致数据损坏。
  • 2,不用的数据却不释放,从而导致内存泄露。

    内存泄露,就是有内存分配但是不释放它,哪怕这块内存已经不用了。泄露,导致你的应用程序占用越来越多的内存,并导致整体性能的下降,或者在iOS平台上导致应用终止。

  • 3, 如果你总是考虑内存管理的实现细节,而不是你实际的管理目标,那么你会感觉到从“引用计数”的角度理解内存管理实际是极其困难的。所以,你真正应该考虑的是对象的“所有权”(Ownership)以及对象图(Object Graph)。
  • 4, 当一个方法所返回的对象,其所有权属于你的时候, Cocoa 用一种非常直接的命名规范来告诉你。请参看内存管理策略;尽管最基础的策略也是最直接的,有一些有效的做法可以让内存管理更加容易,从而帮助你实现程序的稳定性和健壮性,从而使其占用更少的资源。请参看内存管理实战;自动释放池(Autorelease Pool)使得你可以用一种不同的方式来发送release消息。当你想放弃对一个对象的所有权,但又不想让这个所有权的释放立即生效(比如,你在方法中要返回这个对象),这种机制就很有用了。有几种情况你应该需要使用自动释放池。请参看使用Autorelease池。

继续阅读