一些相关
【iOS】内存管理
什么是内存?
冯诺依曼结构
冯诺依曼结构中,存储器存放着程序的指令和数据,在程序运行时提供给CPU使用。
冯诺伊曼结构的瓶颈
CPU的运算速度远远大于了访存的速度,所以要找到一个速度、容量和成本都折中的方式 —— 存储器分层。
存储器分层
- L0是寄存器,读写速度最快,是CPU组成部分之一;L1 - L3的高速缓存速度比主存更快,集成在CPU芯片内部
- L5 - L6是设备成本较便宜且存储容量大的存储设备,但是存储速度低。
- L4的主存也就是我们所说的内存,是一个设备成本与存储速度、存储容量都折中的存储设备。可见内存是一个外存与CPU之间的桥梁。
内存
一个设备的RAM的大小,例如iPhone14ProMax的运行内存就是8GB
内存 = 主存 = 运行内存 = RAM
操作系统层面的内存管理
内存管理的概述:在软件运行时对计算机内存资源的分配和使用的技术。主要目的是高效、快速的分配,并且在合适的时候释放和回收内存资源。
CPU寻址方法
- 物理寻址:CPU直接通过物理地址去访问内存。
- 虚拟寻址:CPU通过访问虚拟地址,经过虚拟内存地址到物理内存地址的翻译获得物理地址,才能访问到对应的内存。
CPU是如何访问内存的呢?最简单直接的方式就是物理寻址,也就是CPU直接通过物理地址去访问内存,但物理寻址最大的一个问题就是地址空间缺乏保护:直接暴露物理地址,进程可以访问到任何物理地址,这是非常危险的,故引入了虚拟寻址。虚拟寻址就是CPU通过访问虚拟地址,经过虚拟内存地址到物理内存地址的翻译获得物理地址,才能访问到对应的内存,这个翻译过程由CPU的内存管理单元(MMU)完成。
虚拟内存
在虚拟地址到物理地址的翻译过程中可以增加一些权限判定,对地址空间进行保护,操作系统为每个进程提供了一个独立的、私有的、连续的地址空间,这就是虚拟内存。虚拟内存保护了进程的地址空间,使得进程之间互不干扰。
对于进程而言,它可见的部分只有虚拟内存,但实际上虚拟内存除了映射到物理内存以外,还有可能映射到磁盘,当物理内存的空间不足时,可以将部分内存数据交换到磁盘,也就是内存交换机制,有了该机制后,虚拟内存就可以利用磁盘拓展内存空间。
应用层面(iOS)的内存管理
iOS使用虚拟内存机制,但大多数移动设备包括iOS在內使用闪存,不存在内存和磁盘的交换,因此不支持内存交换机制。当内存不够用时,iOS会发出内存警告,didReceiveMemoryWarning方法就是在内存警告时会被触发,此时APP会清理一些不必要的内存来释放一定空间,当释放过后内存还是不够用时,就会发生OOM崩溃。在ios app maximum memory budget上统计了单个APP能够使用的最大内存,以iPhone12Pro为例,总共可用内存为5703MB,单个APP的可用内存达到了3054MB,占比54%,可以看出单个APP要发生OOM崩溃,绝大多数情况都是程序本身出现了问题。因此,合理控制APP的内存是至关重要的事情,应该尽可能的减少内存占用,并对内存警告以及 OOM 崩溃做好防范。
- iOS使用虚拟内存机制
- 没有内存交换机制
- 内存有限,但单个APP可用内存大
- 当内存不够用时,会触发内存警告
Clean Memory & Dirty Memory
对于一般的操作系统,Clean Memory可以理解为是能够进行Page Out的部分,但是因为iOS不存在内存交换的机制,所以对于iOS来说,Clean Memory指的是能被重新创建的内存,例如未写入数据的内存。
int *array = malloc(200 * sizeof(int));
array[0] = 32
array[199] = 64
iOS 内存管理
- 说明内存和内存管理是什么
- 介绍iOS的内存管理
- 结合业务代码一起看
什么是内存?
冯诺依曼结构
[图片]
冯诺依曼结构中,存储器存放着程序的指令和数据,在程序运行时提供给CPU使用。
冯诺伊曼结构的瓶颈
CPU的运算速度远远大于了访存的速度,所以要找到一个速度、容量和成本都折中的方式 —— 存储器分层。
存储器分层 - L0是寄存器,读写速度最快,是CPU组成部分之一;L1 - L3的高速缓存速度比主存更快,集成在CPU芯片内部
- L5 - L6是设备成本较便宜且存储容量大的存储设备,但是存储速度低。
- L4的主存也就是我们所说的内存,是一个设备成本与存储速度、存储容量都折中的存储设备。可见内存是一个外存与CPU之间的桥梁。
[图片]
内存
一个设备的RAM的大小,例如iPhone14ProMax的运行内存就是8GB
内存 = 主存 = 运行内存 = RAM
操作系统层面的内存管理
内存管理的概述:在软件运行时对计算机内存资源的分配和使用的技术。主要目的是高效、快速的分配,并且在合适的时候释放和回收内存资源。
CPU寻址方法 - 物理寻址:CPU直接通过物理地址去访问内存。
- 虚拟寻址:CPU通过访问虚拟地址,经过虚拟内存地址到物理内存地址的翻译获得物理地址,才能访问到对应的内存。
CPU是如何访问内存的呢?最简单直接的方式就是物理寻址,也就是CPU直接通过物理地址去访问内存,但物理寻址最大的一个问题就是地址空间缺乏保护:直接暴露物理地址,进程可以访问到任何物理地址,这是非常危险的,故引入了虚拟寻址。虚拟寻址就是CPU通过访问虚拟地址,经过虚拟内存地址到物理内存地址的翻译获得物理地址,才能访问到对应的内存,这个翻译过程由CPU的内存管理单元(MMU)完成。
虚拟内存
在虚拟地址到物理地址的翻译过程中可以增加一些权限判定,对地址空间进行保护,操作系统为每个进程提供了一个独立的、私有的、连续的地址空间,这就是虚拟内存。虚拟内存保护了进程的地址空间,使得进程之间互不干扰。
对于进程而言,它可见的部分只有虚拟内存,但实际上虚拟内存除了映射到物理内存以外,还有可能映射到磁盘,当物理内存的空间不足时,可以将部分内存数据交换到磁盘,也就是内存交换机制,有了该机制后,虚拟内存就可以利用磁盘拓展内存空间。
[图片]
应用层面(iOS)的内存管理
iOS使用虚拟内存机制,但大多数移动设备包括iOS在內使用闪存,不存在内存和磁盘的交换,因此不支持内存交换机制。当内存不够用时,iOS会发出内存警告,didReceiveMemoryWarning方法就是在内存警告时会被触发,此时APP会清理一些不必要的内存来释放一定空间,当释放过后内存还是不够用时,就会发生OOM崩溃。在ios app maximum memory budget上统计了单个APP能够使用的最大内存,以iPhone12Pro为例,总共可用内存为5703MB,单个APP的可用内存达到了3054MB,占比54%,可以看出单个APP要发生OOM崩溃,绝大多数情况都是程序本身出现了问题。因此,合理控制APP的内存是至关重要的事情,应该尽可能的减少内存占用,并对内存警告以及 OOM 崩溃做好防范。
- iOS使用虚拟内存机制
- 没有内存交换机制
- 内存有限,但单个APP可用内存大
- 当内存不够用时,会触发内存警告
Clean Memory & Dirty Memory
对于一般的操作系统,Clean Memory可以理解为是能够进行Page Out的部分,但是因为iOS不存在内存交换的机制,所以对于iOS来说,Clean Memory指的是能被重新创建的内存,例如未写入数据的内存。
int *array = malloc(200 * sizeof(int));
array[0] = 32
array[199] = 64
例如创建一个数组,只有写入了数据的部分array[0] 和 array[199]才属于Dirty Memory,未写入的部分都属于Clean Memory。Dirty memory会始终占据内存,直到内存不够用时,系统便会开始清理。
Compressed Memory
当内存不够用时,iOS会压缩部分内存,在需要读写这部分内存的时候再去解压,以达到节约内存的目的,对应的被压缩的内存,就是Compressed Memory。
综上,iOS的内存占用组成还可以如下图所示:
当可使用的内存达到低位时(比如有很多应用在后台,或者前台应用使用了过多物理内存),操作系统就会试图去减小内存压力,它会做以下几件事:
- 首先,系统会移除一些Clean Memroy pages
- 如果应用使用了太多的Dirty Memory,系统就会对应用发送警告以期望应用自己去释放一些内存
- 如果在数次警告之后,应用程序还是继续使用大量的Dirty Memory,系统就会杀掉这个应用
进程是分配资源的最小单位,每个进程都有独立的虚拟内存地址空间,分配的资源如右图所示。
- 全局区、常量和代码区有系统自动加载和释放
- 栈区存放局部变量、临时变量,由编译器自动分配和释放
- 堆区用于存放进程运行中被动态分配的内存段,由程序员分配和释放(目前iOS基本都使用ARC来管理对象)
Autorelease Pool
在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对象
- 自动释放池与线程一一对应
- 自动释放池(即所有的AutoreleasePoolPage对象)是以栈为结点通过双向链表的形式组合而成
- 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放内部的成员变量,其余的4040个字节用来存放autoreleasepool对象的地址
在MRC下,当我们不需要一个对象时,就调用release或autorelease来释放它
- release:对象的引用计数立即-1,若-1后对象的引用计数为0,对象就会被销毁
- autorelease:会将该对象放入自动释放池,交由自动释放池给池中的对象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的控制。
内存优化
对内存泄漏的处理
内存泄漏指的是应该释放但没有正确释放掉的内存,导致一直占据着内存。
block循环引用
__weak typeof(self) weakSelf = self;
self.block = ^{__strong typeof(weakSelf) strongSelf = weakSelf; // 防止self被释放NSLog(@"%@",weakSelf.app);
};
self.block();
ReactiveCocoa中潜在的内存泄漏及解决方案
ReactiveCocoa中潜在的内存泄漏及解决方案
原因是RAC强引用
NSTimer
对非OC对象的内存处理
- ARC模式仅对OC对象进行自动内存管理, 比如 CoreFoundation 框架下的 CI、CG、CF 等开头的类的对象,在使用完毕后仍需我们手动释放
使用@autoreleasepool来减少峰值内存占用
苹果官方文档Using Autorelease Pool Blocks中有通过使用使用@autoreleasepool来减少峰值内存占用的示例代码:
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {@autoreleasepool {NSError *error;NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error];/* Process the string, creating and autoreleasing more objects. */}
}
didReceiveMemoryWarning及时清除Cache
图片/视频的加载、VC的加载可能导致内存暴涨,可以通过监听UIApplicationDidReceiveMemoryWarningNotification或者VC自带的didReceiveMemoryWarning方法在内存警告时及时的清除cache
根据业务场景选择NSCache而非NSDictionary
场景举例:在收到内存警告时,我们尝试将Dictionary这部分的内容释放掉,但是Dictionary因未使用是Compressed Memory,处于被压缩的状态,解压、释放这部分内容之后,Dictionary处于未压缩状态,可能还会导致内存占用更大了,所以业务可以根据业务的具体场景,在允许的情况下更推荐使用NSCache而非NSDictionary,因为NSCache会在内存警告时由系统自动释放内存。
Memory Graph
通过 Debug Memory Graph 可以查看当前进程中所有生命周期内的对象。我们可以在调试时通过这个功能发现一些本来应该被释放但是却没有被释放的对象,从而确定哪些对象有内存泄漏的嫌疑。
- Memory Graph可以帮助我们找到循环引用和内存泄漏,正在使用的内存以及每个区域的大小。
- Memory Graph显示应用程序使用的内存的位置,以及这些使用内存之间的引用关系
Xcode运行App后,点击下图的图标,可以打开memory graph
Target Pointer
Apple在2013年9月推出了iPhone5s,配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念
对于64位程序,引入Tagged Pointer之后,相关逻辑能减少一半的内存占用,以及三倍的访问速度提升,100倍的创建,销毁速度提升。
原有的对象为什么会浪费内存?
- 以一个NSNumber为例,对象存储在堆上,NSNumber的指针中存储的是堆中NSNumber对象的地址值,
- 他所占用的内存与cpu的位数有关,在32位系统下占4个字节,在64位系统下占8个字节,、指针类型的大小通常也是与cpu位数相关,一个指针所占用的内存在32位下cpu为4个字节,在64位cpu下也是8个字节
- 如果没有Taggedpointer对象,NSNumber一类的对象从32位机器迁移到64位机器以后,所占用的内存会翻倍
- 对象需要在堆上分配内存,同时需要维护引用计数,管理生命周期,程序额外的逻辑造成运行效率上的耗损
Tagged Pointer的引入
- NSNumber,NSDate,NSString一类的变量本身的值需要占用的内存一半不需要8字节,拿整数来说,4个字节所能表示的数对于绝大多数情况都是可以处理的
- 因此可以将一个对象的指针拆成两部分,一部分用来直接保存数据,另一部份用来做一个特殊标记,表示这是一个特殊的指针,不指向任何一个地址