本文是iOS 内存管理的基础篇,从最基本的堆栈开始一步步的了解iOS的内存管理。
内存管理基础结构
程序可执行文件的结构
一个程序的可执行文件在内存中的结果,从大的角度可以分为两个部分:只读部分和可读写部分。只读部分包括程序代码(.text)和程序中的常量(.rodata)。可读写部分(也就是变量)大致可以分成下面几个部分:
- .data: 初始化了的全局变量和静态变量
- .bss: 即 Block Started by Symbol, 未初始化的全局变量和静态变量
- heap: 堆,使用 malloc, realloc, 和 free 函数控制的变量,堆在所有的线程,共享库,和动态加载的模块中被共享使用
- stack: 栈,函数调用时使用栈来保存函数现场,自动变量(即生命周期限制在某个 scope 的变量)也存放在栈中
下面来具体解释一下:
data 和 bss 区
这两个都是存放全局变量的 他们之间的区别是:data区存放的是初始化了的全局变量和静态变量,而bss区存放的是未初始化过得
1 |
|
已经初始化的变量最开始会被放在.text中 因为值是卸载代码中的,程序运行起来之后就会被拷贝到.data区或者bss区。
答疑一: 静态变量和全局变量
全局变量
:在一个代码文件(具体说应该一个 translation unit/compilation unit))当中,一个变量要么定义在函数中,要么定义在在函数外面。当定义在函数外面时,这个变量就有了全局作用域,成为了全局变量。全局变量不光意味着这个变量可以在整个文件中使用,也意味着这个变量可以在其他文件中使用(这种叫做 external linkage)。当有如下两个文件时
a.c
1 |
|
b.c
1 |
|
在 Link 过程中会产生重复定义错误,因为有两个全局的 a 变量,Linker 不知道应该使用哪一个。为了避免这种情况,就需要引入 static
静态变量
: 指使用 static 关键字修饰的变量,static 关键字对变量的作用域进行了限制,具体的
限制如下:
- 在函数外定义:全局变量,但是只在当前文件中可见(叫做 internal linkage)
- 在函数内定义:全局变量,但是只在此函数内可见(同时,在多次函数调用中,变量的值不会丢失)
(C++)在类中定义:全局变量,但是只在此类中可见
对于全局变量来说,为了避免上面提到的重复定义错误,我们可以在一个文件中使用 static,另一个不使用。这样使用 static 的就会使用自己的 a 变量,而没有用 static 的会使用全局的 a 变量。当然,最好两个都使用 static,避免更多可能的命名冲突。
注意:’静态’这个中文翻译实在是有些莫名其妙,给人的感觉像是不可改变的,而实际上 static 跟不可改变没有关系,不可改变的变量使用 const 关键字修饰,注意不要混淆。
Bonus 部分 —— extern: extern 是 C 语言中另一个关键字,用来指示变量或函数的定义在别的文件中,使用 extern 可以在多个源文件中共享某个变量,例如这里的例子。 extern 跟 static 在含义上是“水火不容”的,一个表示不能在别的地方用,一个表示要去别的地方找。如果同时使用的话,有两种情况,一种是先使用 static,后使用 extern ,即:
1 |
|
这种情况,后面的 m 实际上就是前面的 m 。如果反过来:
1 |
|
这种情况的行为是未定义的,编译器也会给出警告。
答疑二 程序在内存和硬盘上不同的存在形式(不懂!!!)
这里我们提到的几个区,是指程序在内存中的存在形式。和程序在硬盘上存储的格式不是完全对应的。程序在硬盘上存储的格式更加复杂,而且是和操作系统有关的,具体可以参考这里。一个比较明显的例子可以帮你区分这个差别:之前我们提到过未定义的全局变量存储在 .bss 区,这个区域不会占用可执行文件的空间(一般只存储这个区域的长度),但是却会占用内存空间。这些变量没有定义,因此可执行文件中不需要存储(也不知道)它们的值,在程序启动过程中,它们的值会被初始化成 0 ,存储在内存中
栈
栈是用于存放本地变量,内部临时变量以及有关上下文的内存区域。程序在调用函数时,操作系统会自动通过压栈和弹栈完成保存函数现场等操作,不需要程序员手动干预。
栈是一块连续的内存区域
,栈顶的地址和栈的最大容量是系统预先规定好的。能从栈获得的空间较小。如果申请的空间超过栈的剩余空间时,例如递归深度过深,将提示stackoverflow。
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高
堆
堆是用于存放除了栈里的东西之外所有其他东西的内存区域,当使用malloc
和free
时就是在操作堆中的内存。对于堆来说·释放工作由程序员控制
,容易产生memory leak
。
堆是向高地址扩展的数据结构,是不连续的内存区域
。这是由于系统是用链表
来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
。
对于堆来讲,频繁的new/delete
势必会造成内存空间的不连续,从而造成大量的碎片
,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。
堆都是动态分配的
,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配
。动态分配由alloca函数进行分配
,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现
。
计算机底层并没有对堆的支持,堆则是C/C++函数库提供的,同时由于上面提到的碎片问题,都会导致堆的效率比栈要低。
全局区 / 静态区
存储全局变量和静态变量,程序结束后由系统释放
初始化区 非初始化区分开存放
文字常量区
存储字符串常量,程序结束后由系统释放
程序代码区
存储函数体的二进制代码
1 |
|
结构体(Struct)
1、定义
struct tag { member-list } variable-list;
2、成员访问
直接访问: 变量名.成员名
间接访问: 结构体指针名->成员名
3、成员存储
获得EXAMPLE类型结构体所占内存大小: int size_example = sizeof( struct EXAMPLE );
获得成员b相对于EXAMPLE储存地址的偏移量: int offset_b = offsetof( struct EXAMPLE, b );
内存分配
虚拟地址:用户编程时将代码(或数据)分成若干个段,每条代码或每个数据的地址由段名称 + 段内相对地址构成,这样的程序地址称为虚拟地址
逻辑地址:虚拟地址中,段内相对地址部分称为逻辑地址
物理地址:实际物理内存中所看到的存储地址称为物理地址
逻辑地址空间:在实际应用中,将虚拟地址和逻辑地址经常不加区分,通称为逻辑地址。逻辑地址的集合称为逻辑地址空间
线性地址空间:CPU地址总线可以访问的所有地址集合称为线性地址空间
物理地址空间:实际存在的可访问的物理内存地址集合称为物理地址空间
MMU(Memery Management Unit内存管理单元):实现将用户程序的虚拟地址(逻辑地址) → 物理地址映射的CPU中的硬件电路
基地址:在进行地址映射时,经常以段或页为单位并以其最小地址(即起始地址)为基值来进行计算
偏移量:在以段或页为单位进行地址映射时,相对于基地址的地址值
虚拟地址先经过分段机制映射到线性地址,然后线性地址通过分页机制映射到物理地址。
页面置换算法
FIFO算法
先入先出,即淘汰最早调入的页面。
OPT(MIN)算法
选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。
可惜,MIN需要知道将来发生的事,只能在理论中存在,实际不可应用。
LRU(Least-Recently-Used)算法
用过去的历史预测将来,选最近最长时间没有使用的页淘汰(也称最近最少使用)。
LRU准确实现:计数器法,页码栈法。
由于代价较高,通常不使用准确实现,而是采用近似实现,例如Clock算法。
内存抖动现象
:页面的频繁更换,导致整个系统效率急剧下降,这个现象称为内存抖动(或颠簸)。抖动一般是内存分配算法不好,内存太小引或者程序的算法不佳引起的
Belady现象
:对有的页面置换算法,页错误率可能会随着分配帧数增加而增加。
FIFO会产生Belady异常。
栈式算法无Belady异常,LRU,LFU(最不经常使用),OPT都属于栈式算法。
OC的内存管理
MRC于ARC 环境设置
Reference Counting
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release方法 |
废弃对象 | dealloc方法 |
alloc/retain/release/dealloc 实现
在 Xcode 中 设置 Debug -> Debug Workflow -> Always Show Disassenbly 打开。这样在打断点后,可以看到更详细的方法调用。
alloc
通过设置断点追踪程序的执行,下面列出了执行所调用的方法和函数:
1 |
|
下面我们来看这几个跟retainCount相关的方法到底都做了什么!
retainCount
1 |
|
retain
1 |
|
release
1 |
|
很明显 这几个方法都调用了__CFdoExternRefOperation
这个方法,下面我们来看一下这个方法的实现:
CFRuntime.c __CFDoExternRefOperation:
1 |
|
从BasicHash
这样的方法名可以看出,其实引用计数表就是散列表。key 为 hash(对象的地址) value为引用计数
所有的修饰符
_strong 修饰符
是id类型和对象类型默认的所有权修饰符
1 |
|
上面的源码与下面的相同
1 |
|
_weak修饰符
_weak修饰符出现是为了避免发生循环引用,循环引用容易发生内存泄漏.所为内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。
即使只有一个对象也有可能发生循环引用
1 |
|
__weak修饰符还有一个优点:在持有某对象的弱引用时,若该对象被废弃则弱引用将自动失效且处于nil被赋值的状态(空弱引用)。
1 |
|
输出结果:
1 |
|
_unsafe_unretained修饰符
是不安全的所有权修饰符,因此:
1 |
|
仍然会提示
1 |
|
这一点跟__weak是一样 因为自己无法持有自己创建的对象 创建完成之后就会被销毁
_autoreleasing 修饰符
ARC有效时,要通过将对象赋值给附加了__autoreleaseing修饰符的变量来替代调用autorelease方法,对象赋值给附有__autoreleaseing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool中
1 |
|
等价于
1 |
|
MRC(Manual Reference Counting)
dealloc
[super dealloc];一定在最后一行
不能直接调用
一旦对象被回收,继续使用会野指针
野指针 & 空指针
- 野指针即一个指针指向了“僵尸对象(不能再使用的对象)”
- 给野指针发消息报错:EXC_BAD_ACCESS
- 避免野指针发消息报错,对象释放后,将指针置为空指针
空指针即没有指向任何存储空间(存的nil)
向空指针发送消息没有任何反应
MRC @property参数
成员变量前加上@property,自动生成基本的setter/getter
property加上retain,自动生成有内存管理的setter/getter
property加上assign,自动生成基本的setter/getter,默认什么都不加就是assign
MRC 循环引用
当两端互相引用时,应该一端用retain,一端用assign
autoreleasepool
- [p autorelease] 给p发送一条autorelease消息,将p放到autoreleasepool,在autoreleasepool释放时做一次release操作
- autorelease方法返回对象本身,引用计数不会变化
autoreleasepool 注意
并不是放到autoreleasepool代码中,都会自动加入到自动释放池
1 |
|
autorelease是一个方法, 只有在autoreleasepool中调用才有效
1 |
|
autoreleasepool 循环
- 尽量避免对大内存使用autorelease
- 不要把for循环放在@autoreleasepool之间,会造成内存峰值上升
autoreleasepool 错误用法
不能连续调用autorelease
调用autorelease后又调用release
ARC(Automatic Reference Counting)
ARC自动引用计数内存管理,通过编译器(Clang Complier),本质上还是会使用到retain、release等关键字方法,只是不是开发者手动添加,而是编译器在编译过程中添加retain、release等关键字方法到相应的代码行。
ARC @property
- strong : 用于OC对象,相当于MRC中的retain
- weak : 用于OC对象,相当于MRC中的assign
- assign : 用于基本数据类型,跟MRC中的assign一样
ARC 注意
不能调用release
不能调用autorelease
不能调用[super dealloc]
NSThread & NSRunLoop & NSAutoreleasePool
- 1、每个线程(包括主线程)都拥有一个专属的NSRunLoop,并在需要时自动创建
- 2、主线程的NSRunLoop对象(包括系统级别的其它线程)的每个event loop开始前,自动创建一个autoreleasepool,并在event loop结束时drain
- 3、每个autoreleasepool对应且只对应一个线程
需要手动添加autoreleasepool的情况
- 编写的程序不是基于UI框架的,比如命令行工具
- 编写的循环中创建了大量的临时对象
- 创建了一个辅助线程
内存管理的实际应用
项目
- 1、YYKit :解决循环中创建的大量临时对象
- 2、AFNetworking: 创建了辅助线程
- 3、XX会 混编时, 标注MRC文件:-fno-objc-arc
ARC 实例
- 1、ARC想要主动释放,最好是提前置为nil
- 2、ARC下获取引用计数
KVC [obj valueForKey:@”retainCount”]
私有API _objc_rootRetainCount(obj)
CFGetRetainCount((__bridge CFTypeRef)(obj))
MRC 实例
NSString的引用计数是随机值,NSMutableString的引用计数是正常值
NSString的class是__NSCFConstantString,字符串常量
NSMutableString的class是__NSCFString,有引用计数
对于字符串常量、NSNumber做常量时?
retain 和 release都不会有影响,因为系统不会回收,也不会对其做引用计数
stringWithFormat创建的string?
为变量,所以会有引用计数
现在返回的已经是常量,见后面的例子
stringWithString创建的string?
取决于它后面的string对象,如果是常量则不做计数,如果是变量则做计数
除了alloc new copy mutableCopy retain显示增加retainCount以外还有哪些看不到的能够增加引用计数的操作?
容器类array、dic addObject;release时,里面的成员都会release一次,和autorelease pool一致
addsubview, 因为view有栈(subviews),加入栈中retainCount+1
navcontroller的push, 因为nav有栈(viewcontrollers),加入栈中retainCount+1
performSelector 调用时target和info都会加1,结束时减1
苹果不推荐使用retainCount方法,因为他对程序本身没有作用,retainCount可能永远不会反回0,有时候系统会优化对象的释放行为,在保留计数还是1的时候就释放了。
retainCount 关于NSString和NSMutableString的例子
1 |
|
1 |
|
1 |
|