iOS内存管理-基本概念整理

2019-10-15 16:45:31 浏览数 (1)

主要内容: 1.内存区域划分 2.内存管理/引用计数 3.MRC手动管理引用计数 4.ARC自动引用计数 5.内存泄漏问题 6.野指针问题

一、内存区域划分

程序在分配内存时,主要分为:栈区、堆区、静态区、常量区、代码区;

内存区域

具体说明

栈区

存放局部变量的值,系统自动分配和释放;特点:容量小,速度快,有序

堆区

存放通过malloc系列函数或new操作符分配的内存,如对象;一般由程序员分配和释放,如果不释放,则出现内存泄露;特点:容量大,速度慢,无序;

静态区

存放全局变量和静态变量(包括静态局部变量和静态全局变量);当程序结束时,系统回收;

常量区

存放常量的内存区域;程序结束时,系统回收;

代码区

存放二进制代码的区域

从上述分类上看,我们在开发过程中主要涉及的是堆上内存的管理。通常,我们创建一个对象的代码如下:

代码语言:javascript复制
NSObject *obj = [[NSObject alloc] init]; 

上述代码创建了一个NSObject类型的指针obj和一个NSObject类型的对象。obj指针存在栈上,而其指向的对象则是在堆上。这种对象也称之为堆对象

二、内存管理/引用计数

无论是MRC还是ARC环境,Objective-C都采用引用计数来管理内存;每个对象都有一个引用计数器,任何时候指向对象的指针个数和对象的引用计数相等,当一个对象的引用计数为0的时候将会被释放;

OC管理内存涉及到对象的"生成"、"持有"、"释放",MRC需要调用对应的方法来管理引用计数,而ARC则是自动管理引用计数,无需再调用这些内存管理的方法。虽然两者管理内存的形式不同,但是它们都遵循相同的内存管理规律,内容如下:

  1. 自己生成的对象,自己所持有;
  2. 非自己生成的对象,自己也能持有;
  3. 不再需要自己持有对象时释放;
  4. 非自己持有的对象无法释放;
三、MRC手动管理引用计数

MRC,即手动管理引用计数。当我们通过allocretain等方法持有对象后,也必须有相应的release或者autorelease将其释放。总结对象操作与Objective-C内存方法对应关系如下:

对象操作

OC方法

生成并持有对象

以alloc/new/copy/mutableCopy等名称开头方法

持有对象

retain方法

释放对象

release方法

废弃对象

dealloc方法

1.自己生成的对象,自己所持有/非自己生成对象,不持有
代码语言:javascript复制
id obj = [[NSObject alloc] init];  //自己生成并持有对象
id obj1 = [NSMutableArray array];  //取得非自己生成的对象,但不持有对象

OC中使用alloc、new、copy、mutableCopy这些名称开头的方法意味着自己生成对象并持有,否则就是非自己生成的对象不持有。如上源码,使用NSObject类的alloc类方法就能自己生成并持有对象,指向生成并持有对象的指针被赋值给了obj。

通过自定义方法来理解这两种创建对象方法的区别(系统方法也是类似的实现),测试代码如下:

代码语言:javascript复制
//以alloc开头的方法
- (id)allocObject {
    id obj = [[NSObject alloc] init];
    return obj;
}

- (id)object {
    id obj = [[NSObject alloc] init];
    [obj autorelease];  //用该方法,可以使取得的对象存在,但是自己不持有对象
    return obj;
}

autorelease即自动释放,对象已经加入自动释放池,所以获取对象并不持有;涉及到的自动释放池的内容会在后续详细总结。

注意:生成并持有对象的的方法一定是驼峰拼写来命名的方法,如allocallocMyObject等方法;相反allocatemutableCopyed就不属于这类方法;

2.非自己生成的对象,自己也能持有
代码语言:javascript复制
 id obj1 = [NSMutableArray array];  //取得非自己生成的对象,但不持有对象
 [obj retain];                      //通过retain方法,持有了对象

源代码中,NSMutableArray类对象被赋值给变量obj,但是变量obj自己不持有该对象。使用retain方法后可以持有对象。

3.不再需要自己持有对象时释放

自己持有的对象,一旦不需要,持有者有义务释放该对象,释放对象使用release方法。

代码语言:javascript复制
id obj = [[NSObject alloc] init]; //自己生成并持有对象
[obj release];                    //释放自己持有的对象
NSLog(@"%@",obj);                 //已经释放,再次使用会崩溃

虽然指向对象的指针依然保留在变量obj中,看似可以访问,但对象一经释放就绝不可再访问。

4.非自己持有的对象无法释放

在应用程序中释放非自己持有的对象就会造成崩溃,使用代码演示如下:

代码语言:javascript复制
//情况1:释放完不再需要的对象后再次释放,访问了已经废弃的对象而崩溃!
id obj = [[NSObject alloc] init];
[obj release];
[obj release];

//情况2:取得自己并不持有的对象对其释放,释放了非自己持有的对象而崩溃!
id obj = [[NSMutableArray array];
[obj release];
四、ARC自动引用计数

ARC(Automic Reference Counting),即自动引用计数;这是iOS5推出的新特性,iOS4.3也支持ARC,只是不能使用weak。ARC不再需要使用类似retainrelease的操作来持有或者释放对象,从而大大提高了开发效率;

1.ARC使用条件
  1. Xcode4.2或以上版本
  2. 使用LLVM编辑器3.0或以上版本
  3. Xcode编译器选项中设置ARC有效
2.ARC基本原理
  1. ARC下的编译器会在代码编译阶段合适的位置,自动加入retain/release/autorelease的操作;
  2. ARC的规则:只要还有一个强引用指针指向对象,对象就会保存在内存中;
  3. ARC中使用strongweak关键字来修饰对象;strong表示强引用,对应MRC下的retainweak表示弱引用,对应原来的assign,不同的是当对象被释放的时候,对象weak指针自动赋值为nil,从而不会引发野指针错误;
3.ARC所有权修饰符

ARC有效时,OC处理id类型和对象类型必须附加所有权修饰符。所有权修饰符一共有四种:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__strong修饰符: __strong是id类型和对象类型默认的所有权修饰,表示对对象的"强引用";当对象没有任何一个强引用指向它的时候,对象将被释放。

__weak修饰符: 1.__weak__strong修饰符的作用相反,表示弱引用,不会增加引用计数; 2.当对象被释放后,所有指向它的弱引用都会被置为nil,这样避免了野指针问题。 3.__weak修饰符常用于解决循环引用问题; 4.__weak只能用于iOS5以上版本,更早的版本只能使用__unsafe_unretained修饰符。

__unsafe_unretained修饰符: 1.__unsafe_unretained提供弱引用,与__weak作用类似; 2.__unsafe_unretained不能在对象释放后自动置为nil,易产生野指针问题; 3.__unsafe_unretained可用于iOS5之前版本,为兼容ARC弱引用而引入;

__autoreleasing修饰符: 将对象赋值给附有__autoreleasing修饰符的变量, 等同于在MRC下调用对象的autorelease方法,即对象被注册到autoreleasepool

ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块__autoreleasing修饰符;两种环境下的使用情况类比如下图:

对比MRC与ARC的自动释放池使用.png

如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了autoreleasepool;

但事实上,显式使用__autoreleasing修饰符的情况非常少见,这主要是因为ARC的很多情况下,即使是不显式的使用__autoreleasing,也能实现对象被注册到释放池中。这其中就包括以下的几种情况:

  1. 编译器检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到autoreleasepool;
  2. 访问附有__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象;
  3. id的指针或对象的指针在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符;
4.ARC属性修饰符

ARC中的所有权修饰与属性修饰符存在着对应关系,如果不一致还会引起编译错误。总结两者的对应关系如下:

属性修饰符

所有权修饰符

assign

__unsafe_unretained

copy

__strong(但是赋值的是被复制的对象)

retain

__strong

strong

__strong

unsafe_unretained

__unsafe_unretained

weak

__weak

以上各种属性只有copy不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源生成的对象。

5.ARC管理内存的规则

1.不能使用retain/release/retainCount/autorelease内存管理方法; 2.不能使用NSAllocateObject/NSDeallocateObject方法; 3.必须遵守内存管理的方法命名规则; 4.不能显式调用dealloc方法,如[super dealloc]; 5.使用@autoreleasepool块代替NSAutoreleasePool; 6.不能使用区域(NSZone); 7.对象类型变量不能作为C语言结构体(struct/union)的成员; 8.显式转换idvoid *

6.必须遵守内存管理的方法命名规则

MRC下,用于对象生成/持有的方法必须遵守alloc、new、copy、mutableCopy的命名规则。以这些名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC环境下的规则一样。只是ARC下关于init开发的方法规则要更加严格了: 1.必须是实例方法,且返回对象; 2.返回对象应该是id类型或该方法声明类的对象,抑或该类的超类或子类; 3.该返回类型不注册到autoreleasepool上; 4.基本上,init方法只是对alloc方法返回值的对象进行初始化处理并返回对象;

7.显式转换id和void *

这里说到的其实就是Core FoundationFoundation两者之间的转换。 Core Foundation是由C语言实现的,而Foundation由Objective-C实现,两者可以相互转换。

MRC不存在显式转换的问题,因为本来就是手动管理内存。但是为了在ARC也能实现对Core Foundation对象的自动内存管理,我们就必须将其与Objective-C对象的转换。Objective-C中提供了三个关键字__bridge__bridge_retained__bridge_transfer来实现转换。

情况1:__bridge转换

代码语言:javascript复制
/*
MRC代码下,将id变量直接强制转换void*正常,但ARC下报错
id obj = [[NSObject alloc] init];
void *p = obj;
*/
 
//ARC下代码使用__bridge实现单纯的赋值转换
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)(obj);

__bridge可实现Objective-C对象和Core Foundation对象的相互转换;但是其安全性与赋值给__unsafe_unretained修饰符相近,甚至会更低。如果管理时不注意赋值对象的所有者,就容易产生野指针错误导致程序崩溃。

情况2:__bridge_retained转换

代码语言:javascript复制
id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)(obj);
    
/*相当于MRC代码:
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];
*/

__bridge_retained转换可使要转换的变量也持有所赋值的对象。此操作类似于retain。上述代码中变量obj和变量p同时持有对象。 情况3:__bridge_transfer转换

代码语言:javascript复制
id obj = (__bridge_transfer id)p;
    
/*相当于MRC代码:
id obj = id(p)
[obj retain];
[(id)p release];
*/

__bridge_transfer转换提供与__bridge_retained相反的动作,被转换的变量所持有的对象在该变量被赋值给转换的目标后随之释放。此操作与release相似。

五、内存泄漏问题

内存泄露就是本该废弃的对象在超出其生命周期后继续存在。导致系统内存浪费、程序运行速度减慢甚至系统崩溃等严重后果;

总结常见的内存泄露的异常情况如下:

  • AFNetworking循环引用(未使用单例或者没有调用销毁NSURLSession的方法;
  • Block循环引用
  • delegate循环引用
  • NSTimer循环引用
  • 创建的非OC对象内存,在使用完毕后未手动释放;
  • 循环操作创建大量临时对象,导致内存导致内存暴涨;
  • 地图类处理,使用完毕后未及时销毁地图相关组件对象
六、野指针问题

野指针指针就是指向一个已经删除对象或者访问受限内存区域的指针;

注意:野指针不是nil指针,而是指向”垃圾”内存(不可用内存)的指针;

总结ARC下常见的野指针异常情况如下:

0 人点赞