代码改变世界

iOS内存暴增问题追查与使用陷阱

2012-10-12 12:14  三戒1993  阅读(178)  评论(0编辑  收藏  举报

iOS平台的内存使用引用计数的机制,并且引入了半自动释放机制;这种使用上的多样性,导致开发者在内存使用上非常容易出现内存泄漏和内存莫名的增长情况; 本文会介绍iOS平台的内存使用原则与使用陷阱; 深度剖析autorelease机制;低内存报警后的处理流程;并结合自身实例介绍内存暴增的问题追查记录以及相关工具的使用情况;

TAG

内存暴增,内存泄漏,autorelease;内存报警;

1 iOS平台内存管理介绍

iOS平台的内存管理采用引用计数的机制;当创建一个对象时使用alloc或者allWithZone方法时,引用计数就会+1 ;当释放对象使用release方法时,引用计数就是-1 ;这就意味着每一个对象都会跟踪有多少其他对象引用它,一旦引用计数为0,该对象的内存就会被释放掉;另外,iOS也提供了一种延时释放的机制AutoRelease,以这种方式申请的内存,开发者无需手动释放,系统会在某一时机释放该内存; 由于iOS平台的这种内存管理的多样性,导致开发者在内存使用上很容易出现内存泄漏或者程序莫名崩溃的情况,本文会详细介绍iOS平台内存的使用规范与技巧以及如何利用工具避免或者发现问题; 

2 iOS平台内存使用原则

2.1 对象的所有权与销毁

2.1.1 谁创建,谁释放;

如果是以alloc,new或者copy,mutableCopy创建的对象,则必须调用release或者autorelease方法释放内存;

例如: ClassA* obj = [[ClassAalloc ] init];

……

[objrelease ];

obj = nil;  /* 防止野指针*/

2.1.2 谁retain,谁释放;

如果对一个对象发送 retain消息,其引用计数会+1,则使用完必须发送release或者autorelease方法释放内存或恢复引用计数;

例如:ClassA* obj = [[ClassAalloc ] init];/* 引用计数 1*/

ClassA* obj1 = obj;

[obj1retain ];  /*引用计数 2*/

……

[obj1release ]; /* 引用计数 1*/

obj1=nil;

[objrelease ]; /* 引用计数 0*/

obj=nil;

2.1.3 使用完,要释放;

不论使用的是alloc(copy,new)创建的对象,还是通过retain增加了引用计数,在对象使用完后,都要调用release或者autorelease方法;释放内存,确保没有内存泄漏;

2.1.4 没创建且没retain,别释放;

不要释放那些不是自己alloc或者retain的对象,否则程序会crash ;

不要释放autorelease的对象,否则程序会crash

例如:

ClassA* obj = [[[ClassAalloc ] init]autorelease ];/* 自动释放*/

……

[objrelease ]; /* 程序crash */

obj=nil;

NSString* content = [NSString stringWithFormat: @”…”];

/* 系统返回的对象是autorelease 的 */

……

[contentrelease ]; /* 试图释放一个autorelease对象,程序会crash */

content=nil;

2.2 对象的深拷贝与浅拷贝

一般来说,复制一个对象包括创建一个新的实例,并以原始对象中的值初始化这个新的实例。复制非指针型实例变量的值很简单,比如布尔,整数和浮点数。复制指 针型实例变量有两种方法。一种方法称为浅拷贝,即将原始对象的指针值复制到副本中。因此,原始对象和副本共享引用数据。另一种方法称为深拷贝,即复制指针 所引用的数据,并将其赋给副本的实例变量。

2.2.1 深拷贝

实例变量的set方法的实现应该能够反映出您需要使用的复制类型。如果相应的set方法复制了新的值,如下面的方法所示,那么您应该深拷贝这个实例变量:

- (void)setMyVariable:(id)newValue
{
[myVariable autorelease];
myVariable = [newValuecopy ];
}

2.2.2 浅拷贝

如果相应的set方法保留了新的值,如下面的方法所示,那么您应该浅拷贝这个实例变量:

- (void)setMyVariable:(id)newValue
{
[myVariable autorelease];
myVariable = [newValueretain ];
}

2.3 对象的存取方法

2.3.1 属性声明和实现

一般在程序的头文件中,会设置成员变量的属性,obj-C可以自动生成对成员变量的set 和get 函数;

声明:

@property (copy) NSString *str; /* 复制对象,引用计数初始为1*/
@property (readonly) NSString *str1; /* 只读对象,不可更改*/
@property (retain) NSString *str2; /* retain对象,引用计数+1*/
@property (assign) int num; /* 非指针变量,直接赋值 */

实现:

@synthesize str;
@synthesize str1;
@synthesize str2;
@synthesize num;

2.3.2 存取方法的内部实现

属性的声明和实现,实际上是系统帮助开发者自动生成了对成员变量的set和get方法;当然开发者也可以显性的定义set和get函数;下面介绍一下系统自动生成set和get方法的定义,以@property (retain ) NSString *str2; 举例说明:

@interface ClassA : NSObject

-(NSString *) getStr2;

-(void) setStr2:(NSString *) value;

@end

@implementation ClassA

-(NSString *) getStr2{

return str2;

}

- setStr2:(NSString *) value {

if (str2 != value){  /* 如果参数与原来的值不同,则先释放原来的值,然后在赋值并retain */

[str2release ];

str2 = [valueretain ];

}

}

注意: 在属性中声明为retain或者copy的成员变量,在delloc函数中,都要显性的release;

-(void) dealloc{

[str2release ];

……

}

3 iOS平台AutoRelease机制

3.1 自动释放池的概念

自动释放池是一个NSAutoreleasePool实例,其中“包含”已经收到autorelease消息的其他对象;当自动释放池被回收时,它会向其中的每个对象发送一条release消息。一个对象可以被数次放入一个自动释放池中,并且在每次被放入池中的时候都会收到一条release消息。因此,向对象发送autorelease消息(而不是release消息)可以至少将该对象的生命周期延长至自动释放池本身被释放的时候(如果在此期间对象被保留,则它可以存活更久)。

Cocoa总是期望有一个自动释放池可用。如果自动释放池不可用,那么自动释放对象就无法得到释放,您也就泄漏了内存。如果当自动释放池不可用的时候,您发送了autorelease消息,那么Cocoa会记录相应的错误信息。

您可以使用常见的alloc和init消息来创建一个NSAutoreleasePool对象,并使用drain 销毁它。自动释放池应该总是在与它被创建时所处的相同上下文环境(方法或函数的调用,或循环体)中被销毁。

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

……

        [pool drain];

自动释放池被置于一个堆栈中,虽然它们通常被称为被“嵌套”的。当您创建一个新的自动释放池时,它被添加到堆栈的顶部。当自动释放池被回收时,它们从堆栈中被删除。当一个对象收到送autorelease消息时,它被添加到当前线程的目前处于栈顶的自动释放池中。

3.2 自动释放池的作用域与嵌套

我们通常会提及自动释放池是被嵌套的。但是,您也可以认为嵌套自动释放池位于一个堆栈中,其中,“最内层”的自动释放池位于栈顶。如前所述,嵌套自动释放池实际上是这样实现的:程序中的每个线程都维护一个自动释放池的堆栈。当您创建一个自动释放池时,它被压入当前线程的堆栈的栈顶。 当一个对象被自动释放时—也就是说,当一个对象收到一条autorelease消息或者当它作为一个参数被传入addObject:类方法时—它总是被放入堆栈顶部的自动释放池中。

因此,自动释放池的作用域是由它在堆栈中的位置以及它的存在情况定义的 。自动释放对象被添加至栈顶的自动释放池中。如果另一个自动释放 池被创建,则当前位于栈顶的池就超出其作用域,直到新的池被释放为止(此时原来的自动释放池再次成为栈顶的自动释放池)。当自动释放池本身被释放的时候, 它(显然)就永久地超出其作用域。

如果您释放了一个不是位于堆栈顶部的自动释放池,则这会导致堆栈中所有位于它上面的(未释放的)自动释放池,连同它们包含的所有对象一起被释放。 当您用完自动释放池时,如果您一时疏忽,忘记向它发送release消息(不推荐您这样做),那么,当嵌套在它外层的自动释放池中的某个被释放时,它也会被释放。

这种行为对于异常的处理很有意义。如果发生异常,并且线程突然转移出当前的上下文环境,则与该上下文相关联的自动释放池将被释放。但 是,如果被释放的池不是线程堆栈顶部的池,则所有位于该自动释放池之上的自动释放池也会被释放(并在这个过程中释放其中所有的对象)。然后,先前位于被释 放的池下面的自动释放池则成为线程堆栈最顶端的自动释放池。由于这种行为,异常处理程序则不需要释放收到autorelease消息的对象。对于异常处理程序来说,没有必要也不值得向它的自动释放池发送release,除非异常处理程序重新引发该异常。

3.3 自动施放池的手动创建与自动创建

3.3.1 需要手动创建自动释放池

  • 如果你正在编写一个不是基于Application Kit的程序,比如命令行工具,则没有对自动释放池的内置支持;你必须自己创建它们。
  • 如果你生成了一个从属线程,则一旦该线程开始执行,你必须立即创建你自己的自动释放池;否则,你将会泄漏对象。
  • 如果你编写了一个循环,其中创建了许多临时对象,你可以在循环内部创建一个自动释放池,以便在下次迭代之前销毁这些对象。这可以帮助减少应用程序的最大内存占用量。

3.3.2 系统自动创建自动释放池

Application Kit会在一个事件周期(或事件循环迭代)的开端—比如鼠标按下事件—自动创建一个自动释放池,并且在事件周期的结尾释放它.

4 iOS平台内存使用陷阱

4.1 重复释放

在前文已经提到,不要释放不是自己创建的对象;

释放自己的autorelease对象,app会crash;

释放系统的autorelease对象,app会crash;

4.2 循环引用

 
循环引用,容易产生野引用,内存无法回收,最终导致内存泄漏!可以通过弱引用的方式来打破循环引用链;

5 iOS平台内存报警机制

由于iOS平台的内存管理机制,不支持虚拟内存,所以在内存不足的情况,不会去Ram上创建虚拟内存;所以一旦出现内存不足的情况,iOS平台会通知所有已经运行的app,不论是前台app还是后台挂起的app,都会收到 memory warning的notice;一旦app收到memory warning的notice,就应该回收占用内存较大的变量;

5.1 内存报警处理流程

1: app收到系统发过来的memory warning的notice;

2: app释放占用较大的内存;

3: 系统回收此app所创建的autorelease的对象;

4: app返回到已经打开的页面时,系统重新调用viewdidload方法,view重新加载页面数据;重新显示;

5.2 内存报警测试方法

在Simulate上可以模拟低内存报警消息;

iOS模拟器 -> 硬件 -> 模拟内存警告;

开发者可以在模拟器上来模拟手机上的低内存报警情况,可以避免由于低内存报警引出的app的莫名crash问题;

6 iOS平台内存检查工具

6.1 编译和分析工具Analyze

iOS的分析工具可以发现编译中的warning,内存泄漏隐患,甚至还可以检查出logic上的问题;所以在自测阶段一定要解决Analyze发现的问题,可以避免出现严重的bug;

内存泄漏隐患提示 :

Potential Leak of an object allocated on line ……

数据赋值隐患提示 :

The left operand of …… is a garbage value;

对象引用隐患提示 :

Reference-Counted object is used after it is released;

以上提示均比较严重,可能会引起严重问题,需要开发者密切关注!

6.2 内存检测工具Leak

内存检测工具可以通过iOS自带Leak工具检测 是否有内存泄漏;

一般通过Leak工具可以很快的检查出程序哪里有内存泄漏,一般这种问题也比较容易解决,可是有时候即使解决了所有的内存泄漏,但还是发现程序在运行中,内存还是在不断的疯涨,这时候可能就要借助另外一个工具Allocations来检查是那些地方使用的内存比较多而且是持续增长;下面详细介绍一下这两个工具的使用方法: Leak和Allocations;

Leak工具: 
 
通过Leak工具可以很快发现代码中的内存泄漏,通过工具也可以很快找到发生内存泄漏的代码段: 
 
Allocations工具: 
 
此工具会显示出所有申请内存的地方,并统计申请的次数和大小; 从这个列表中可以找出内存申请次数最多且申请内存最大的语句;从而分析出哪些地方使用的内存最多,进而可以优化和改进; 

上图是按照申请内存多少来排序的,可以方便的了解哪些代码申请的内存多;

注意: 
1:iOS的SQLite 最好不要频繁的打开和关闭数据库,这样SQLite在内部会增加内存Cache, 每次会增长51K的内存buffer,如果频繁打开和关闭SQLite的话,内存很快就会涨到几十兆,甚至上百兆!

2:Image的显示,从网络上下载的图片或者头像,Image内部API,每次会增加9K的cache;

7 参考资料

http://www.cocoachina.com/bbs/read.php?tid=15963

http://developer.apple.com/library/IOs/navigation/

by lixin

原文链接: http://www.udpwork.com/redirect/6406