内存管理理解与分析

iOS 内存管理

iOS 中的程序内存结构


在 iOS 程序中,内存可以粗略的分为五个区域:

Name Descroption
由操作系统自动分配和释放,常用来存放函数的参数值、局部变量的值等。优点是快速高效,缺点是有限制,数据
一般由程序员分配和释放,常用来存储对象
全局区 用来存储已经初始化的全局变量和静态变量,程序结束时才会被释放回收
常量区 用来存储常量的区域,程序结束时才会被释放回收
代码段 用来存放程序的执行代码,直到程序结束才会释放回首

在 iOS程序中,只有堆区中存放的数据是需要手动释放回收的,其它区域存储的数据的释放和回收都由系统进行管理。当一个 iOS 程序启动后,它的全局区、常量区和代码区就已经确定了。

  • 栈区 (stack) 是由编译器自动分配和释放,用来存放函数的参数值、局部变量等。栈是系统数据结构,对应进程/线程是唯一的。优点是快速高效,缺点是有限制,数据不灵活。

  • 堆区 (heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能由操作系统回收。优点是灵活方便,数据适应面广泛,但是效率有一定降低。

  • 全局区/静态区 (static) 存放全局变量和静态变量,初始话的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在另一块区域,程序结束后由系统自动释放。

  • 文字常量区,用来存储常量字符串,程序结束后由系统释放。

  • 代码区,存放函数的二进制代码

iOS 中的内存管理


因为 iOS 程序的内存分配中,只有堆区是有程序员进行管理的,所以 iOS 的内存管理大致上就是可以认为是堆区内存的管理。

在 Objective-C 中,使用引用计数来确定一个对象所占有的内存空间是否应该被回收。它的工作原理可以描述为:

Objective-C 中的每一个对象都有一个类型为 unsigned long 的 retainCount 的属性,这个属性由拥有它的对象进行维护。当我们新创建出这个对象的一个实例时,这个对象实例的 retainCount 值为1,每当一个新的引用指向对象,对象的 retainCount 值就会增加1,每当这个对象实例的引用减少一个,retainCount 的值就减少1。当着对象实例的 retainCount 的值为0时,代表这个对象实例没有被引用,系统会自动将这个对象实例的内存空间回收并同时调用这个实例对象的 dealloc 方法。

需要注意的几个问题:

  • 常量是没有引用计数的
  • 使用对象实例的属性值进行赋值,不会引用这个对象
  • 释放对象实例时会调用 dealloc 方法,如果没有调用则会造成内存泄漏
  • 对引用计数为1的对象实例发送 release 消息时,系统不会再对其进行 retainCount - 1 的操作。

MRC 和 ARC

使用对象实例的引用计数来进行 iOS的内存管理,分为两种方式:

  • MRC :手动引用计数,由程序员手动的管理对象实例的引用计数
  • ARC :自动引用计数,是基于 MRC 的,系统自动的管理对象实例的引用计数

实际上在 iOS 5 之后,Apple 就开始推荐使用 ARC 来进行 iOS 程序的内存管理工作,目前 MRC 已经非常少见。

ARC 中,编译器会在编译时在代码中插入合适的 retain 和 release 语句。

ARC 中的修饰符

ARC 中有四种修饰符

Name Description
__strong 强引用,默认值,持有所指向对象的所有权
__weak 弱引用,不持有所指向对象的所有权,所指向的对象销毁后,引用会自动置为 nil
__autoreleasing 自动释放对象的引用,一般用来传递参数
__unsafe_unretained 为兼容 MRC 出现的修饰符,可看成 MRC 下的 weak
属性的内存管理

常见的属性修饰符

Name Description
assign 直接赋值,一般用来修饰基本数据类型。修饰 Objc 对会造成野指针
retain 保留新值,再释放旧值,再设置新值
copy 拷贝新值,再释放旧值,再设置新值
weak ARC 新引入,可代替 assign,自动置 nil
strong ARC 新引入,可代替 retain
block 的内存管理

使用@property声明一个 block 时,使用 copy 来修饰。

block 会对内部使用的对象进行强应用,在使用时可能会造成循环引用,可通过添加弱引用标记来解决:

1
__weak typeof(self) weakSelf	=	self;

Autorelease & AutoreleasePool

在实际的情境中,经常会遇到不知道一个对象实例再什么时候不再使用,因而造成不知道应该何时才能将其释放的情况。Objective-C 中提供了 autorelease 方法来解决这个问题。

当给一个对象实例发送 autorelease 消息时,它会被添加到合适的自动释放池中,当自动释放池销毁时,会给自动释放池中的所有对象实例发送 release 消息。

autorelease 不会改变对象的引用计数。

创建自动释放池的两种方法:

1
2
3
4
5
6
7
// 1
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];
// 2
@autoreleasepool {

}

值得注意的是自动释放池实质上只是在销毁时给其中的所有对象发送了 release 消息,并不保证对象一定会被销毁。

内存管理问题和解决方案

僵尸对象和野指针

僵尸对象是指内存已经被回收的对象,而野指针是指向僵尸对象的指针。

向野指针发送消息会导致程序崩溃,就是经典的 : EXC_BAD_ACCESS 错误。

所以为了避免产生僵尸对象和野指针,在对象释放后,应将其指针置为 nil。

循环引用

当对象之间相互拥有彼此的强引用,形成闭环引用时,就称为循环引用。

循环引用会造成程序内存消耗过高、程序闪退等问题。

以下几种情况可能会造成循环引用:

  • 由于父类指针可以指向子类对象,当父类对象和子类对象相互引用时,就造成了循环引用

  • 作为对象属性的 block 中强引用了对象,造成循环引用,解决方法如下:

    1
    2
    3
    4
    5
    __weak typeof(self) weakSelf = self;
    self.testObject.testCircleBlock = ^{
    __strong typeof (weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
    };
  • 使用 strong 修饰符修饰代理属性,造成循环引用

  • 作为属性的 NSTimer,造成循环引用

循环中对象占用内存大

常见于循环次数较大,循环体生成的对象占用内存较大的情景。

可通过在循环中创建自己的 autoreleasePool 或及时释放占用内存大的 临时变量来解决。

作者

Y2hlbmdsZWk=

发布于

2015-12-01

更新于

2021-09-01

许可协议