可能是史上最全面的内存管理文章

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看501 回复回复10 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 501|回复: 10
收起左侧

可能是史上最全面的内存管理文章

[复制链接]
蚊香酱 发表于 2016-8-8 19:24:14 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑
本帖最后由 蚊香酱 于 2016-8-8 19:24 编辑

neicunguanli.png

可能是史上最全面的内存管理文章 - 敏捷大拇指 - neicunguanli.png


前言: 我是一个我是一个有点强迫症的人,如果一个知识点不弄明白,就总认为对这方面知识根本就不会,做其他的任何事儿都无法全身心投入.今天这篇文章我从上家公司写到这家公司,写写停停经历了大半年的时间,今天终于算是一个完整些的版本了,终于了却了一桩心事,可以向着下个目标迈进了,加油!

# iOS内存管理
### 概述
##### 什么是内存管理

应用程序内存管理是在程序运行时分配内存(比如创建一个对象,会增加内存占用)与清除内存(比如销毁一个对象,会减少内存占用)的过程

##### 为什么要管理内存

目前iPhone手机内存大多为1G,分配给每个应用程序的内存空间极其有限,当应用程序运行过程中所占用的内存较大时,便会收到系统给出的内存警告,如果应用程序所占用的内存超过限制时,便会被系统强制关闭,所以我们需要对应用程序进行内存管理,一个好的程序程序也应该尽可能少地占用内存

##### 内存管理的方式

在OC中提供了两种管理内存的方式

- MRR(Manual Retain-Release),也被人称作MRC(Manual Reference Counting,手动引用计数)
- ARC(Automatic Reference Counting,自动引用计数)

> 注: 在Build Setting中的Objective-C Automatic Reference Counting设置为YES即为ARC

##### 内存管理的范围

任何继承自NSObject的对象都需要管理内存(因存放在堆里面),基本数据类型int、float、double、char、结构体struct以及枚举enum都不需要管理内存(因存放在栈里面)

- 堆: 一般由程序员分配释放内存,若程序员不释放,程序结束时可能由OS释放,其操作方式类似于数据结构中的链表
- 栈: 由操作系统自动分配释放,存放函数的参数值,局部变量值等,其操作方式类似于数据结构中的栈(先进后出)

##### 内存管理的规则

内存管理模型基于对象所有权机制,一个对象的所有权可以拥有一个或者多个拥有者,只要一个对象的所有权拥有至少一个拥有者,该对象就会一直存在于内存中.如果一个对象的所有权没有任何拥有者,那么系统便会自动地将该对象所占用的内存释放掉.为了明确何时拥有某个对象的所有权、何时放弃某个对象的所有权,系统设置了如下规则

- 你拥有你创建的对象的所有权: 你拥有使用alloc,new,copy或者mutableCopy方法创建的新对象的所有权
- 你可以通过retain来获取一个对象的所有权: 在两种情形中会使用到retain来获取一个对象的所有权
  - 在init方法和setter方法中,使用retain来获取一个想要保存为属性值的对象的所有权
  - 防止一个对象在一些操作中被设置为无效,具体参见"内存管理中需要注意的两个使用场景"
- 你必须放弃你不再需要拥有其所有权的对象的所有权: 你必须通过release或者autorelease方法来放弃你不再需要拥有其所有权的对象的所有权,在Cocoa术语中"放弃一个对象的所有权"一般被称作"释放一个对象"
- 你不能放弃一个你不拥有的对象的所有权: 这个规则是以上几点规则的推论

# iOS内存管理之MRR&MRC
### 概述

在iOS5以前,程序员普遍使用MRR&MRC方式来管理内存,程序员需要自己添加retain、release和autorelease等内存管理代码来跟踪自己所拥有的对象以明确地管理对象的内存,在需要使用该对象的时候保证该对象一直存在于内存中,在不需要使用该对象的时候保证该对象所占用的内存被系统正常回收

为了让系统知道何时需要将某个对象所占用的内存清理掉,系统引入了引用计数器的概念

### 引用计数器
##### 概述

系统为每个OC对象内部都分配了4个字节的存储空间存放自己的引用计数器,引用计数器是一个整数,表示“对象被引用的次数”,当对象的引用计数器为0时,对象所占用的内存空间就会被系统自动回收,当对象的引用计数器不为0时,在程序运行过程中所占用的内存会一直存在,直到整个程序退出时由OS自动释放

##### 操作引用计数器

- 当使用alloc、new、copy、mutableCopy创建一个新对象时,该新对象的引用计数器为1
- 当给对象发送一条retain消息时,对象的引用计数器+1(方法返回对象本身)
- 当给对象发送一条release消息时对象的引用计数器-1(方法无返回值)
- 当给对象发送一条retainCount消息时,返回对象的当前引用计数器(不要以该数据来判断对象是否被释放)

> 注: 给对象发送一条release消息,不代表对象会被释放,只有对象的引用计数器为0时才会被释放

##### 示例

下面通过一个小示例,来演示一下如何操作对象的引用计数器

```objc
Person *p = [[Person alloc] init]; // 使用alloc创建一个新对象,对象引用计数器 = 1
[p retain]; // 给对象发送一条retain消息,对象引用计数器 + 1 = 2
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 1
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 0,指针所指向的对象的内存被释放
```

### 僵尸对象、野指针与空指针
##### 概述

下面介绍几个关于内存管理方面常常提到的术语

- 僵尸对象: 所占用的内存已经被回收的对象,僵尸对象不能再使用
- 野指针: 指向僵尸对象的指针,给野指针发送消息会报错EXC_BAD_ACCESS错误:访问了一块已经被回收的内存
- 空指针: 没有指向任何对象的指针(存储的东西是nil,NULL,0),给空指针发送消息不会报错,系统什么也不会做,所以在对象被释放时将指针设置为nil可以避免野指针错误

> 注: 默认情况下,Xcode是不会监听僵尸对象的,所以需要我们自己手动开启,开启监听僵尸对象步骤为: Edit Scheme ->; Run ->; Diagnostics ->; Objective-C的Enable Zombie Objects打钩,这样便可以在因为僵尸对象报错的时候给出更多错误信息

##### 示例

```objc
Person *p = [[Person alloc] init]; // 引用计数器 = 1
[p release]; // 引用计数器 - 1 = 0,指针所指向的对象的内存被释放
[p release]; // 这句给野指针发送消息,会报野指针错误,开启监听僵尸对象会给出错误信息**- -[Person release]: message sent to deallocated instance 0x100206fd0
```

> 注: 如果在第一次给对象发送release消息后,立刻将指针置空,便不会出现野指针错误,因为给空指针发送消息不会报错,系统什么也不会做,所以在对象被释放时将指针设置为nil可以避免野指针错误

### 单对象内存管理
##### 概述

单对象的内存管理比较简单,当使用alloc、new、copy、mutableCopy创建一个新对象时,该新对象的引用计数器为1,我们只需要在对象不再使用的时候给其发送一次release消息,让对象的引用计数器-1变为0即可正常释放

##### 示例

下面通过一个小示例,来演示一下如何对单对象进行内存管理

```objc
// 在堆中开辟一块内存存放Person对象,在栈中开辟一块内存存储局部变量p,p中存放的是Person对象的地址
// 当出了大括号,局部变量p出了作用域便会被系统自动回收,而Person对象引用计数器为1仍然存在,这样便造成内存的泄漏,所以我们需要在局部变量p被系统回收之前,给p所指向的Person对象发送一条release消息让对象引用计数器-1变为0
Person *p = [[Person alloc] init];
[p release];
```

### 多对象内存管理
##### 概述

相对于单对象内存管理而言,多对象内存管理显得比较复杂,但是只要我们遵从基本的内存管理规则,即可避免大部分内存管理相关的问题

- 当A对象想要拥有B对象的所有权时,一定要对B对象进行一次retain操作,这样才能保证A对象存在期间B对象一直存在
- 当A对象想要放弃对B对象的所有权时,一定要对B对象进行一次release操作,这样才能保证A对象释放了,B对象也能正常释放

##### 存取器方法

在涉及到多对象内存管理时,平时我们使用的存取器方法便需要针对内存管理进行一些调整

- getter方法规范: 直接返回实例变量

```objc
// 针对基本数据类型
- (int)age
{
    return _age;
}

// 针对OC对象
- (Room *)room
{
    return _room;
}
```

- setter方法规范:

```objc
// 针对基本数据类型,直接将新值赋值给实例变量
- (void)setAge:(int)age
{
    _age = age;
}

// 针对OC对象,方式一: 判断两次传入的对象是否相同,如果不同便release旧对象retain新对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
    if (_room != room)
    {
        [_room release];
        _room = [room retain];
    }
}

// 针对OC对象,方式二: retain新对象,release旧对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
    [room retain];
    [_room release];
    _room = room;
}
```

> 注1: release旧对象retain新对象原因: 避免设置两次不同的room,在Person对象释放时,第一次设置的对象无法被释放,造成内存泄露

> 注2: 判断当前对象与传入对象是否相同原因: 避免设置两次相同的room,在设置第二次时,将room已经释放了,再调用retain造成野指针错误

##### dealloc方法

当系统回收对象的内存时,系统会自动给该对象发送一条dealloc消息,我们一般会重写dealloc方法,在这里给当前对象所拥有的资源(包括实例变量)发送一条release消息(基本数据类型不用),保证自身所拥有的资源也可以正常释放(因为在使用该资源的时候,采用retain获取了该资源的所有权,在自身释放的同时,也应该放弃对该资源的所有权)

```objc
- (void)dealloc
{
    NSLog(@"Person dealloc");

    // release对象所拥有资源
    [_room release];
    // 设置为nil可以避免野指针错误(其实可以不设置,只是写了显得有