【iOS】内存管理
冯诺依曼结构中,存储器存放着程序的指令和数据,在程序运行时提供给CPU使用。
CPU的运算速度远远大于了访存的速度,所以要找到一个速度、容量和成本都折中的方式 —— 存储器分层。
一个设备的RAM的大小,例如iPhone14ProMax的运行内存就是8GB
内存 = 主存 = 运行内存 = RAM
内存管理的概述:在软件运行时对计算机内存资源的分配和使用的技术。主要目的是高效、快速的分配,并且在合适的时候释放和回收内存资源。
CPU是如何访问内存的呢?最简单直接的方式就是物理寻址,也就是CPU直接通过物理地址去访问内存,但物理寻址最大的一个问题就是地址空间缺乏保护:直接暴露物理地址,进程可以访问到任何物理地址,这是非常危险的,故引入了虚拟寻址。虚拟寻址就是CPU通过访问虚拟地址,经过虚拟内存地址到物理内存地址的翻译获得物理地址,才能访问到对应的内存,这个翻译过程由CPU的内存管理单元(MMU)完成。
在虚拟地址到物理地址的翻译过程中可以增加一些权限判定,对地址空间进行保护,操作系统为每个进程提供了一个独立的、私有的、连续的地址空间,这就是虚拟内存。虚拟内存保护了进程的地址空间,使得进程之间互不干扰。
对于进程而言,它可见的部分只有虚拟内存,但实际上虚拟内存除了映射到物理内存以外,还有可能映射到磁盘,当物理内存的空间不足时,可以将部分内存数据交换到磁盘,也就是内存交换机制,有了该机制后,虚拟内存就可以利用磁盘拓展内存空间。
iOS使用虚拟内存机制,但大多数移动设备包括iOS在內使用闪存,不存在内存和磁盘的交换,因此不支持内存交换机制。当内存不够用时,iOS会发出内存警告,didReceiveMemoryWarning方法就是在内存警告时会被触发,此时APP会清理一些不必要的内存来释放一定空间,当释放过后内存还是不够用时,就会发生OOM崩溃。在ios app maximum memory budget上统计了单个APP能够使用的最大内存,以iPhone12Pro为例,总共可用内存为5703MB,单个APP的可用内存达到了3054MB,占比54%,可以看出单个APP要发生OOM崩溃,绝大多数情况都是程序本身出现了问题。因此,合理控制APP的内存是至关重要的事情,应该尽可能的减少内存占用,并对内存警告以及 OOM 崩溃做好防范。
对于一般的操作系统,Clean Memory可以理解为是能够进行Page Out的部分,但是因为iOS不存在内存交换的机制,所以对于iOS来说,Clean Memory指的是能被重新创建的内存,例如未写入数据的内存。
int *array = malloc(200 * sizeof(int));
array[0] = 32
array[199] = 64
iOS 内存管理
int *array = malloc(200 * sizeof(int));
array[0] = 32
array[199] = 64
例如创建一个数组,只有写入了数据的部分array[0] 和 array[199]才属于Dirty Memory,未写入的部分都属于Clean Memory。Dirty memory会始终占据内存,直到内存不够用时,系统便会开始清理。
当内存不够用时,iOS会压缩部分内存,在需要读写这部分内存的时候再去解压,以达到节约内存的目的,对应的被压缩的内存,就是Compressed Memory。
综上,iOS的内存占用组成还可以如下图所示:
当可使用的内存达到低位时(比如有很多应用在后台,或者前台应用使用了过多物理内存),操作系统就会试图去减小内存压力,它会做以下几件事:
进程是分配资源的最小单位,每个进程都有独立的虚拟内存地址空间,分配的资源如右图所示。
在ARC下,自动释放池会自动创建,在NSAutoreleasePool | Apple Developer Documentation的有关介绍:
AppKit 和 UIKit 框架在事件循环(RunLoop)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。
简单介绍@autoreleasepool的原理:
如下代码,@autoreleasepool的底层是创建了一个__AtAutoreleasePool结构体对象,在构造函数中调用了objc_autoreleasePoolPush()函数,释放结构体时会调用objc_autoreleasePoolPop()函数。
@autoreleasepool {
// ...
}
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
/* @autoreleasepool */
__AtAutoreleasePool __autoreleasepool;
进一步看下objc_autoreleasePoolPush()和objc_autoreleasePoolPop()函数的源码,可以看到其实这两个函数时调用了AutoreleasePoolPage的两个方法push()和pop(),所以@autoreleasepool底层就是使用AutoreleasePoolPage类来实现的。
// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
每个线程(包括主线程)都维护自己的NSAutoreleasePool对象栈。新创建的池子会被添加到栈的顶部,销毁池子时,池子会从栈的顶部移除。自动释放的对象会被放入当前线程的顶部自动释放池中。当一个线程终止时,它会自动清空所有与其关联的自动释放池。
因此在程序运行过程中,可能会有多个AutoreleasePoolPage对象
在MRC下,当我们不需要一个对象时,就调用release或autorelease来释放它
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[[Person alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[Person dealloc]
// -[ViewController viewDidAppear:]
对象的dealloc方法不是在viewDidLoad结束后释放,而是viewWillAppear方法结束后释放的,系统干预释放是由RunLoop来控制,会在当前RunLoop每次循环结束时释放,person对象在viewWillAppear方法结束后释放,说明viewDidLoad和viewWillAppear在同一次循环里。
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
HTPerson *person = [[[HTPerson alloc] init] autorelease];
}
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[Person dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
添加在手动创建的@autoreleasepool中的对象,在@autoreleasepool的大括号结束时就会释放,不受RunLoop的控制。
内存泄漏指的是应该释放但没有正确释放掉的内存,导致一直占据着内存。
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 防止self被释放
NSLog(@"%@",weakSelf.app);
};
self.block();
ReactiveCocoa中潜在的内存泄漏及解决方案
原因是RAC强引用
苹果官方文档Using Autorelease Pool Blocks中有通过使用使用@autoreleasepool来减少峰值内存占用的示例代码:
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
图片/视频的加载、VC的加载可能导致内存暴涨,可以通过监听UIApplicationDidReceiveMemoryWarningNotification或者VC自带的didReceiveMemoryWarning方法在内存警告时及时的清除cache
场景举例:在收到内存警告时,我们尝试将Dictionary这部分的内容释放掉,但是Dictionary因未使用是Compressed Memory,处于被压缩的状态,解压、释放这部分内容之后,Dictionary处于未压缩状态,可能还会导致内存占用更大了,所以业务可以根据业务的具体场景,在允许的情况下更推荐使用NSCache而非NSDictionary,因为NSCache会在内存警告时由系统自动释放内存。
通过 Debug Memory Graph 可以查看当前进程中所有生命周期内的对象。我们可以在调试时通过这个功能发现一些本来应该被释放但是却没有被释放的对象,从而确定哪些对象有内存泄漏的嫌疑。
Xcode运行App后,点击下图的图标,可以打开memory graph
Apple在2013年9月推出了iPhone5s,配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念
对于64位程序,引入Tagged Pointer之后,相关逻辑能减少一半的内存占用,以及三倍的访问速度提升,100倍的创建,销毁速度提升。