iOS内存管理详解

777 阅读16分钟

一、iOS的内存


如下图,iOS用户态内存分为5大区,分别为

  • 栈区(stack)
  • 堆区(heap)
  • 全局区
  • 常量区
  • 代码区

iOS五大内存区域划分.png

1.1、栈区(Stack)

定义

  • 栈是向低地址扩展
  • 栈是一块连续的内存区域,遵循先进后出(FILO)原则
  • 栈的地址空间在iOS中是以0X7开头
  • 栈区一般在运行时分配

存储

栈区是由编译器自动分配并释放的,主要用来存储

  • 局部变量
  • 函数的参数,eg:函数的隐藏参数(id self,SEL _cmd)

特点

  • 优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效

  • 缺点:栈的内存大小有限制,数据不灵活

    • iOS主线程栈大小是1MB
    • 其他线程是512KB
    • MAC只有8M

以上内存大小的说明,在Threading Programming Guide中有相关说明

image.png

1.2、堆区(Heap)

定义

  • 堆是向高地址扩展
  • 堆是不连续的内存区域,类似于链表结构(便于增删,不便于查询),遵循先进先出(FIFO)原则
  • 堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态的
  • 堆区的分配一般是在运行时分配

存储

堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收,主要用于存放

  • OC中使用alloc或者 使用new开辟空间创建对象
  • C语言中使用malloc、calloc、realloc分配的空间,需要free释放

特点

  • 优点:灵活方便,数据适应面广泛
  • 缺点:需手动管理速度慢、容易产生内存碎片

访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区

1.3、全局区(静态区,即.bss & .data)

全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放

  • 未初始化全局变量静态变量,即BSS区(.bss)
  • 已初始化全局变量静态变量,即数据区(.data)

其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量

1.4、常量区(即.rodata)

常量区是编译时分配的内存空间,在程序结束后由系统释放,主要存放

  • 已经使用了的,且没有指向的字符串常量

字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存

1.5、代码区(即.text)

代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存

地址以0x1开头,说明是存放在常量区 地址以0x6开头,说明是存放在堆区 地址以0x7开头,说明是存放在栈区

二、什么是内存管理


  • 程序在运行的过程中,往往涉及到创建对象、定义变量、调用函数或方法,而这些行为都会增加程序的内存占用
  • 而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的。
  • 当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要再使用的对象、变量等。
  • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。

所以,内存管理进行合理的内存分配、内存回收,释放不需要再使用的对象的管理过程

从上边iOS内存分布图可以看出:只有堆区存放的数据需要由程序员分配和释放,其他区的数据由系统分配和释放,所以通常所说的iOS内存管理是对堆区的内存管理

三、内存管理的机制


Objective-c中提供了手动管理内存(MRC-Mannul Reference Counting),和自动管理内存(ARC-Automatic Reference Counting),两者都是基于引用计数机制

3.1、 引用计数机制

3.1.1、引用计数器:

一个整数,表示为「对象被引用的次数」。系统需要根据对象的引用计数器来判断对象是否需要被回收。

  • 从字面意义上,可以把引用计数器理解为「对象被引用的次数」,也可以理解为: 「有多少人正在用这个对象」。

  • 系统根据引用计数机制来判断对象是否需要被回收。在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。

3.1.2、关于「引用计数器」,有以下几个特点:

  • 每个 OC 对象都有自己的引用计数器。
  • 任何一个对象,刚创建的时候,初始的引用计数为 1。
  • 即使用 alloc、new 或者 copy 创建一个对象时,对象的引用计数器默认就是 1。
  • 当没有任何人使用这个对象时,系统才会回收这个对象。也就是说:
  • 当对象的引用计数器为 0 时,对象占用的内存就会被系统回收。
  • 如果对象的引用计数器不为 0 时,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出)。

3.1.3、 引用计数器操作

  1. 为保证对象的存在,每当创建引用到对象需要给对象发送一条 retain 消息,可以使引用计数器值 +1 ( retain 方法返回对象本身)。
  2. 当不再需要对象时,通过给对象发送一条 release 消息,可以使引用计数器值 -1。
  3. 给对象发送 retainCount 消息,可以获得当前的引用计数器值。
  4. 当对象的引用计数为 0 时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送 dealloc 消息发起这个过程。
  5. 需要注意的是:release 并不代表销毁 / 回收对象,仅仅是将计数器 -1。
// MRC下举例,ARC不能直接使用retain和release。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要创建一个对象默认引用计数器的值就是 1。
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 1

        // 只要给对象发送一个 retain 消息, 对象的引用计数器就会 +1。
        [p retain];

        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 2
        // 通过指针变量 p,给 p 指向的对象发送一条 release 消息。
        // 只要对象接收到 release 消息, 引用计数器就会 -1。
        // 只要对象的引用计数器为 0, 系统就会释放对象。

        [p release];
        // 需要注意的是: release 并不代表销毁 / 回收对象, 仅仅是将计数器 -1。
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

        [p release]; // 0
        NSLog(@"--------");
    }
//    [p setAge:20];    // 此时对象已经被释放
    return 0;
}

3.1.4、dealloc 方法

  • 当一个对象的引用计数器值为 0 时,这个对象即将被销毁,其占用的内存被系统回收。
  • 对象即将被销毁时系统会自动给对象发送一条 dealloc 消息(因此,从 dealloc 方法有没有被调用,就可以判断出对象是否被销毁)
3.2、手动管理内存(MRC-Mannul Reference Counting)
  • MRC模式下,所有的对象都需要手动的添加retain、release代码来管理内存。
  • 使用MRC,需要遵守谁创建,谁回收的原则。也就是谁alloc,谁release;谁retain,谁release。
  • 当引用计数为0的时候,系统调用dealloc方法回收。
对象操作OC中对应方法对应的引用计数的变化
生成并持有对象alloc/new/copy/mutableCopy等+1
持有对象retain+1
释放对象release+1
废弃对象dealloc-

主要有以下四种情况,所以一定要小心管理内存

 /*
 * 自己生成并持有该对象
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];
/*
 * 持有非自己生成的对象
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象
/*
 * 不在需要自己持有的对象的时候,释放
 */
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
 * 指向对象的指针仍就被保留在obj这个变量中
 * 但对象已经释放,不可访问
 */
/*
 * 非自己持有的对象无法释放
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为

3.3、自动管理内存(ARC-Automatic Reference Counting)

3.3.1、自动引用计数

  • Automatic Reference Counting,自动引用计数,即 ARC,WWDC 2011 和 iOS 5 所引入的最大的变革和最激动人心的变化。ARC 是新的 LLVM 3.0 编译器的一项特性,使用 ARC,可以说一 举解决了广大 iOS 开发者所憎恨的手动内存管理的麻烦。

  • 使用 ARC 后,系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在何时的地方插入 retain、release 和 autorelease,通过生成正确的代码去自动释放或者保持对象。我们完全不用担心编译器会出错。

3.3.2、ARC 所有权修饰符

「引用计数式内存管理」的本质部分在 ARC 中并没有改变,ARC 只是自动帮我们处理了「引用计数」的相关部分。

为了处理对象,ARC 引入了以下四种变量所有权修饰符。

  • __strong:强指针,默认所有对象的指针变量都是强指针类型。只要还有一个强指针指向某个对象,则这个对象就会一直存活。
  • __weak:弱指针,不能持有对象实例。如果一个对象没有强指针引用,则弱指针引用会被置为 nil。
  • __unsafe_unretained:和 __weak 相似,是一种弱引用关系。区别在于如果一个对象没有强指针引用,则 __unsafe_unretained 引用不会被置为 nil,而是会变成一个野指针。
  • __autoreleasing:用于通过引用传递对象,指示以引用(id*)传入的参数并在函数返回时自动释放。

3.3.3、ARC 的使用规则

  • 不能使用 retain / release / retainCount / autorelease,使用会导致编译器报错。
  • 不能使用 NSAllocateObject / NSDeallocateObject,使用会导致编译器报错。
  • 对象的生成/持有的方法必须遵循以下命名规则:alloc / new / copy / mutableCopy。
  • 不能显式调用 dealloc。重写父类的 dealloc 方法时,不能再调用 [super dealloc];。
  • 使用 @autorelease 块代替 NSAutoreleasePool。

3.3.4、ARC下 @property 参数

  • strong:表示指向并拥有该对象。用于 OC 对象,相当于 MRC 中的 retain。
  • weak:表示指向但不拥有该对象。
  • assign:用于修饰基本数据类型,跟 MRC 中的 assign 一样,不涉及内存管理。
  • copy:与 strong 类似,不同之处在于 copy 在对象进行赋值(调用 setter 方法)时执行的是 copy 操作而不是 retain 操作。

这里说一下 strongcopy 的区别。

@property 参数会帮我们生成对应的 settergetter 方法。不同的修饰符生成的 settergetter 方法也不同。

strong 对应的 setter 方法,是将参数进行了 retain 操作,而 copy 对应的 setter 方法,是将参数内容进行了 copy 操作。

copy 操作在原对象是可变类型和不可变类型两种不同情况下是有区别的:

  • 当赋值参数为不可变类型(比如 NSString)时,在进行赋值操作时,copy 操作跟 strong 效果一样,只是对参数做了一次浅拷贝,地址不变。
  • 当赋值参数为可变类型(比如 NSMutableString)时,在进行赋值操作时,strong 的指针还是指向原地址。而 copy 操作则是对参数内容做了一次深拷贝,生成了一个新的对象,地址发生了改变。
  • 这样,如果赋值参数为可变类型,当赋值参数发生改变的时候,使用 strong 修饰的对象也会跟着改变,因为两者指向的是同一个地址。而使用 copy 修饰的对象则不会跟着改变,这是因为 copy 指针指向的是一个新的对象。

所以 copy 多用于修饰带有可变类型的不可变对象上(NSString / NSArray / NSDictionary)。这是为了避免可变类型数据赋值给不可变类型数据时,内容发生改变的情况。

3.4、管理策略
  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有对象的时候,释放。
  • 非自己持有的对象无需释放。

四、内存管理问题


由于MRC被苹果藏入历史的抽屉,故下面主要说的是ARC的

4.1、内存泄漏

4.1.1、含义

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃(OOM)等严重后果。

在 ARC 环境下,导致内存泄漏的根本原因是代码中存在循环引用。从而导致dealloc() 方法无法被调用,最终导致内存无法释放。

循环引用:简单理解为A引用了B,而B又引用了A,双方都同时保持对方的一个引用,导致任何时候引用计数都不为0,始终无法释放。

OOM

OOM(Out of Memory)是指当 App 占用的内存达到了 iOS 系统对单个 App 占用内存上限后会被系统强杀掉的现象。这是一种由 iOS 的 JetSam 机制导致的一种“另类”崩溃,并且日志无法通过信号捕捉到。

  • 存在 Foreground Out-Of-Memory (FOOM)Background Out-Of-Memory (BOOM) 两种超出内存限制的 OOM 崩溃现象

  • JetSam 机制,指的是操作系统为了控制内存资源过度使用而采用的一种资源管控机制。

  • Jetsam是一个独立运行的进程,每个进程都有一个内存阈值,一旦超过这个阈值,Jetsam将立即杀死该进程。

4.1.2、常见内存泄漏和解决方法

NSTimer循环引用

[NSTimer scheduledTimerWithTimeInterval:1.0 
                                 target:self 
                               selector:@selector(updateTime:) 
                               userInfo:nil 
                                repeats:YES];

delegate

@class AButton;
@protocol AButtonDelegate <NSObject>

- (void)animationButton:(AButton *)button;
@end

@interface AButton : UIButton

@property (nonatomic, weak) id <AButtonDelegate> delegate;
  • 原因:如果代理用strong修饰, 当前对象(self)会强引用delegate,delegate内部强引用 当前对象(self),造成循环引用

  • 处理方法:代理使用weak修饰

Block

__weak typeof(self) weakSelf = self;
[self.block addBlock:^{
     __strong typeof(weakSelf) strongSelf = weakSelf;
    }
}];
  • 原因:如果block被当前对象(self)持有,这时,如果block内部再持有当前对象(self),就会造成循环引用

  • 处理方法:在block外部对弱化self,再在block内部强化已经弱化的weakSelf

WKWebView 造成的内存泄漏

  • 原因: 但是其实 “addScriptMessageHandler” 这个操作,导致了 wkWebView 对 self 进行了强引用,然后 “addSubview”这个操作,也让 self 对 wkWebView 进行了强引用,这就造成了循环引用

  • 处理方法: 在合适的机会里对 “MessageHandler” 进行移除操作

CoreFoundation 方式申请的内存,忘记释放

  • 原因: CoreFoundation属于MRC方式的管理

  • 处理方法: 用完申请的内存后进行手动free

4.1.3、检测方法

手动检测

实现 dealloc 方法,离开当前类是否会调用。不会则内存泄漏。

工具检测

  • 静态分析【Analyze(command + shift + b)】
    • 1、逻辑错误:访问空指针或未初始化的变量等;
    • 2、内存管理错误:如内存泄漏等;
    • 3、声明错误:从未使用过的变量;
    • 4、Api调用错误:未包含使用的库和框架。
  • 动态分析【(product → profile → leaks)或者( Xcode → Open Developer Tool → Instruments → leaks)】

自动化检测

  • MLeaksFinder
  • FBRetainCycleDetector
4.2、野指针

4.2.1、含义

野指针:又叫做'悬挂指针', 不是NULL指针,是指向“垃圾”内存的指针。

  • 野指针出现的原因是因为指针没有赋值,或者指针指向的对象已经释放了, 比如指向僵尸对象;
    • 僵尸对象: 已经被销毁的对象(不能再使用的对象),内存已经被回收的对象。一个引用计数器为0对象被释放后就变为了僵尸对象;
  • 野指针指向一块“垃圾”内存,给野指针发送消息会导致程序崩溃

4.4.2、解决方法

  • Malloc Scribble

  • Zombie Objects

4.3、内存泄漏和野指针的区别

用一个比喻来说明什么是内存泄漏,什么是野指针

张三指的是变量a;房产证指的是内存空间的使用权;钥匙指的是内存空间的首地址。

内存泄漏情况:

  1. 当张三拿到房子1的房产证和钥匙,这个时候他就可以使用这个房子1,
  2. 过了一段时间,张三又买了一套房子,我们简称房子2,但是这个时候张三把房子1的钥匙给丢了,由于房产证依然是张三的,所以别人不能住这个房子,但是由于张三没有了钥匙,所以他也不能进到房子1里,房子1就这样被空置下来。
  3. 以此类推,当张三买了很多套房子,但是由于他弄丢了钥匙住不了,而别人是因为没有房产证,所以也住不了,所以这些房子都空置下来了。当空置的房子过多时,就变成了内存泄漏

野指针情况:

  1. 当张三拿到房子的钥匙和房产证,当房产证被free了(内存被释放),也就等于说房产证丢了。
  2. 这个时候如果变量a没有赋NULL值,也就等于说钥匙还在张三手里(首地址还在变量a中),所以虽然钥匙还在张三手中,但是没有房产证,他再进这个房子就变成非法进入了,这就是野指针

参考资料