上一章节我们认识了计算机中的内存相关知识,本章节我们整理一些 iOS中内存需要关注的点以及 iOS 的内存管理。
关于虚拟内存系统
iOS 包含一个完全集成的虚拟内存系统,这个虚拟内存系统是无法关闭的,会一直开启。这个虚拟内存系统可以为每个32位进程提供4GB的可寻址空间。
虚拟内存管理器会创建一个逻辑地址空间(或虚拟地址空间)用于每个进程(APP),并将其划分为大小均匀的内存块,称为页。早期的版本,页面大小是4kb,A9系统之后,每页大小为16kb。MMU 负责虚拟地址和物理地址的转换。
低内存警告
当iOS 遇到内存不足时,系统可能会向当前运行中的APP发送低内存警告。APP在接收到低内存警告时,应该及时做出响应,尽可能的释放未在使用的内存对象。
OOM
如果在收到低内存警告时,未能释放足够多的内存资源,那么系统可能会杀死一个或多个APP,这就会产生所谓的OOM(out-of-memory)崩溃。oom发生时从现象上看,与普通的Crash 并无区别,只是无法像监控Crash那样监控到OOM崩溃。不过在发生OOM崩溃后,当前设备的设置-隐私-分析与改进中会生成 JetsamEvent开头的日志。
iOS 内存占用
当内存不足的时候,系统会按照一定策略来腾出更多空间供使用,比较常见的做法是将一部分低优先级的数据挪到磁盘上,这个操作称为 Page Out
。之后当再次访问到这块数据的时候,系统会负责将它重新搬回内存空间中,这个操作称为 Page In
。
然而对于移动设备而言,频繁对磁盘进行IO操作会降低存储设备的寿命。从 iOS7 开始,系统开始采用压缩内存的办法来释放内存空间,被压缩的内存称为 Compressed Memory
。下面依次介绍一下 iOS App 通常情况下的三种内存类型:Clean Memory
、Dirty Memory
以及Compressed Memory
。
Clean Memory
可以简单理解为能够被重新写入数据的干净内存。对开发者而言是read-only,而iOS系统可以写入或移除。
-
System Framework、Binary Executable占用的内存
-
可以被释放(Page Out,iOS上是压缩内存的方式)的文件,包括内存映射文件Memory mapped file(如image、data、model等)。内存映射文件通常是只读的。
-
系统中可回收、可复用的内存,实际不会立即申请到物理内存,而是真正需要的时候再给。
-
每个framework都有DATA_CONST段,当App运行时使用到了某个framework,该framework对应的DATA_CONST的内存就由clean变为dirty了。
注意:如果通过文件内存映射机制memory mapped file载入内存的,可以先清除这部分内存占用,需要的时候再从文件载入到内存。所以是Clean Memory。
Dirty Memory
Dirty Memory
是指那些被 App 写入过数据的内存,包括所有堆区的对象、图像解码缓冲区,同时,类似 Clean memory
,也包括 App 所用到的 frameworks。每个 framework 都会有 _DATA
段和 _DATA_DIRTY
段,它们的内存是 Dirty
的。
值得注意的是,在使用 framework 的过程中会产生 Dirty Memory
,使用单例或者全局初始化方法是减少 Dirty Memory
不错的方法,因为单例一旦创建就不会销毁,全局初始化方法会在 class 加载时执行。
Compressed Memory
当内存吃紧的时候,系统会将不使用的内存进行压缩,直到下一次访问的时候进行解压。
例如,当我们使用 Dictionary
去缓存数据的时候,假设现在已经使用了 3 页内存,当不访问的时候可能会被压缩为 1 页,再次使用到时候又会解压成 3 页。
iOS 内存管理
iOS 采用引用计数(Reference Count)作为对象的内存管理方式,在当前的Swift和OC工程中,默认使用ARC(Automatic Reference Count)。我们可以在 Build Setting:Apple Clang - Language - Objc查看当前APP是否打开了ARC。此外,在Build Phases:Compile Sources 中为单个文件配置是否打开ARC。
引用计数
引用计数是一种对象生命周期的管理方式。当创建一个对象时,它的引用计数初始化为1,如果有新的指针指向这个对象时,它的引用计数加一,当然如果这个指针不再指向这个对象,那么引用计数就会减一。当对象的引用计数为0时,系统将该对象销毁并回收内存。
ARC与MRC
我们知道早期的iOS 开发是需要研发人员自己手动管理引用计数的,也就是MRC。在2011年之后,苹果推出了自动引用计数 - ARC,ARC的原理是依赖编译器的静态分析能力,在编译时于合适的位置插入引用计数管理代码。
虽然ARC能够帮助我们解决绝大部分场景下的内存问题,但是在实际研发中,还是有一些场景需要研发人员手动管理。
- 循环引用
- 与底层 Core Foundation 对象交互
循环引用
两个对象相互引用或者多个对象形成环状引用,都会产生循环引用,导致对象无法正常的释放。比如A控制器,持有一个Block - B,该B内又引用了A控制器的某个属性,这样就导致了A与B的循环引用。循环引用可以通过弱引用(weak reference) 的方式来打破循环引用。所谓弱引用就是持有对象的同时,且不增加引用计数。
弱引用的实现原理
弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。
从原理上看,弱引用的使用是有额外的开销,不建议在项目中无脑使用弱引用。
与底层 Core Foundation 对象交互
由于 Core Foundation 对象底层是通过MRC的方式来管理生命周期,所以我们在使用的过程中,还是需要自己手动管理。小技巧:底层的Core Foundation对象创建时大多是以XxxCreateXxx,XxxCreateWithXxx的方式使用的。
// 创建一个 Core Foundation 对象
CTFontRef src = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, nil);
// 引用计数加 1
CFRetain(src);
// 引用计数减 1
CFRelease(src);
此外,在涉及到Core Foundation对象与OC对象转换时,需要引入bridge
关键字。
-
__bridge
: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。 -
__bridge_retained
:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。 -
__bridge_transfer
:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。
参考:
网友评论