iOS Memory 内存详解

本文以 iOS Memory 的相关内容做为主题,主要从通常操做系统的内存管理、iOS 系统内存、app 内存管理等三个层面进行了介绍,主要内容的目录以下:css

1

iOS 是基于 BSD 发展而来,因此先理解通常的桌面操做系统的内存机制是很是有必要的。在此基础之上,本文会进一步在 iOS 系统层面进行分析,包括 iOS 总体的内存机制,以及 iOS 系统运行时的内存占用的状况。最后会将粒度缩小到 iOS 中的单个 app,讲到单个 app 的内存管理策略。html

操做系统的内存机制

为了从根本上更好地理解和分析 iOS 系统上的内存特性,咱们首先须要正确理解通常操做系统通用的内存机制。git

冯·诺依曼结构

2

冯·诺依曼结构(Von Neumann architecture)在 1945 年就已经被提出了, 这个概念当时十分新颖,它第一次将存储器和运算器分离,致使了以存储器为核心的现代计算机的诞生。程序员

在冯·诺依曼结构中,存储器有着重要地位,它存放着程序的指令以及数据,在程序运行时,根据须要提供给 CPU 使用。能够想象,一个理想的存储器,应该是兼顾读写速度快、容量大、价格便宜等特色的,可是鱼和熊掌不可兼得,读写速度越快的存储器也更贵、容量更小。github

但冯·诺依曼结构存在一个难以克服的问题,被称为冯·诺依曼瓶颈 —— 在目前的科技水平之下,CPU 与存储器之间的读写速率远远小于 CPU 的工做效率。简单来讲就是 CPU 太快了,存储器读写速度不够快,形成了 CPU 性能的浪费。面试

既然如今咱们没办法得到完美的存储器,那咱们如何尽可能突破冯·诺依曼结构的瓶颈呢?现行的解决方式就是采用多级存储,来平衡存储器的读写速率、容量、价格。算法

存储器的层次结构

3

存储器主要分为两类:易失性存储器速度更快,断电以后数据会丢失;非易失性存储器容量更大、价格更低,断电也不会丢失数据。随机访问存储器 RAM 也分为两类,其中 SRAM 速度更快,因此用做高速缓存,DRAM 用做主存。只读存储器 ROM 实际上只有最开始的时候是只读的,后来随着发展也可以进行读写了,只是沿用了以前的名字。swift

4

上图就是多层存储器的具体状况,咱们平时常说的内存,实际上就是指的 L4 主存。而 L1-L3 高速缓存和主存相比,速度更快,而且它们都已经集成在 CPU 芯片内部了。其中 L0 寄存器自己就是 CPU 的组成部分之一,读写速度最快,操做耗费 0 个时钟周期。api

简单来讲,存储器的分级实际上就是一种缓存思想。金字塔底部的部分容量大,更便宜,主要是为了发挥其存储属性;而金字塔尖的高速缓存部分读写速度快,负责将高频使用的部分缓存起来,必定程度上优化总体的读写效率。数组

为何采用缓存就可以提升效率呢?逻辑上理解起来其实很简单,具体来讲就是由于存在局部性原理(Principle of locality) —— 被使用过的存储器内容在将来可能会被屡次使用,以及它附近的内容也大几率被使用。当咱们把这些内容放在高速缓存中,那么就能够在部分状况下节约访问存储器的时间。

CPU 寻址方式

那么,CPU 是如何访问内存的呢?内存能够被看做一个数组,数组元素是一个字节大小的空间,而数组索引则是所谓的物理地址(Physical Address)。最简单最直接的方式,就是 CPU 直接经过物理地址去访问对应的内存,这样也被叫作物理寻址。

物理寻址后来也扩展支持了分段机制,经过在 CPU 中增长段寄存器,将物理地址变成了 "段地址":"段内偏移量" 的形式,增长了物理寻址的寻址范围。

不过支持了分段机制的物理寻址,仍然有一些问题,最严重的问题之一就是地址空间缺少保护。简单来讲,由于直接暴露的是物理地址,因此进程能够访问到任何物理地址,用户进程想干吗就干吗,这是很是危险的。

5

现代处理器使用的是虚拟寻址的方式,CPU 经过访问虚拟地址(Virtual Address),通过翻译得到物理地址,才能访问内存。这个翻译过程由 CPU 中的内存管理单元(Memory Management Unit,缩写为 MMU)完成。

具体流程如上图所示:首先会在 TLB(Translation Lookaside Buffer)中进行查询,它表位于 CPU 内部,查询速度最快;若是没有命中,那么接下来会在页表(Page Table)中进行查询,页表位于物理内存中,因此查询速度较慢;最后若是发现目标页并不在物理内存中,称为缺页,此时会去磁盘中找。固然,若是页表中还找不到,那就是出错了。

翻译过程实际上和前文讲到的存储器分级相似,都体现了缓存思想:TLB 的速度最快,可是容量也最小,以后是页表,最慢的是硬盘。

虚拟内存

刚才提到,直接使用物理寻址,会有地址空间缺少保护的严重问题。那么如何解决呢?实际上在使用了虚拟寻址以后,因为每次都会进行一个翻译过程,因此能够在翻译中增长一些额外的权限断定,对地址空间进行保护。因此,对于每一个进程来讲,操做系统能够为其提供一个独立的、私有的、连续的地址空间,这就是所谓的虚拟内存。

6

虚拟内存最大的意义就是保护了进程的地址空间,使得进程之间不可以越权进行互相地干扰。对于每一个进程来讲,操做系统经过虚拟内存进行"欺骗",进程只可以操做被分配的虚拟内存的部分。与此同时,进程可见的虚拟内存是一个连续的地址空间,这样也方便了程序员对内存进行管理。

7

对于进程来讲,它的可见部分只有分配给它的虚拟内存,而虚拟内存实际上可能映射到物理内存以及硬盘的任何区域。因为硬盘读写速度并不如内存快,因此操做系统会优先使用物理内存空间,可是当物理内存空间不够时,就会将部份内存数据交换到硬盘上去存储,这就是所谓的 Swap 内存交换机制。有了内存交换机制之后,相比起物理寻址,虚拟内存实际上利用硬盘空间拓展了内存空间。

总结起来,虚拟内存有下面几个意义:保护了每一个进程的地址空间、简化了内存管理、利用硬盘空间拓展了内存空间。

内存分页

基于前文的思路,虚拟内存和物理内存创建了映射的关系。为了方便映射和管理,虚拟内存和物理内存都被分割成相同大小的单位,物理内存的最小单位被称为帧(Frame),而虚拟内存的最小单位被称为页(Page)。

注意页和帧大小相同,有着相似函数的映射关系,前文提到的借助 TLB、页表进行的翻译过程,实际上和函数的映射很是相似。

内存分页最大的意义在于,支持了物理内存的离散使用。因为存在映射过程,因此虚拟内存对应的物理内存能够任意存放,这样就方便了操做系统对物理内存的管理,也可以能够最大化利用物理内存。同时,也能够采用一些页面调度(Paging)算法,利用翻译过程当中也存在的局部性原理,将大几率被使用的帧地址加入到 TLB 或者页表之中,提升翻译的效率。

iOS 的内存机制

根据官方文档 Memory Usage Performance Guidelines(如今已经不更新了)咱们能知道 iOS 的内存机制有下面几个特色:

使用虚拟内存

iOS 和大多数桌面操做系统同样,使用了虚拟内存机制。

内存有限,但单应用可用内存大

对于移动设备来讲,受限于客观条件,物理内存容量自己就小,而 iPhone 的 RAM 自己也是偏小的,最新的 iPhone XS Max 也才有 4GB,横向对比小米 9 可达 8GB,华为 P30 也是 8GB。根据 List of iPhones 能够查看历代 iPhone 的内存大小。

可是与其余手机不一样的是,iOS 系统给每一个进程分配的虚拟内存空间很是大。据官方文档的说法,iOS 为每一个 32 位的进程都会提供高达 4GB 的可寻址空间,这已经算很是大的了。

没有内存交换机制

虚拟内存远大于物理内存,那若是物理内存不够用了该怎么办呢?以前咱们讲到,其余桌面操做系统(好比 OS X)有内存交换机制,在须要时能将物理内存中的一部份内容交换到硬盘上去,利用硬盘空间拓展内存空间,这也是使用虚拟内存带来的优点之一。

然而 iOS 并不支持内存交换机制,大多数移动设备都不支持内存交换机制。移动设备上的大容量存储器一般是闪存(Flash),它的读写速度远远小于电脑所使用的硬盘,这就致使了在移动设备就算使用内存交换机制,也并不能提高性能。其次,移动设备的容量自己就常常短缺、闪存的读写寿命也是有限的,因此这种状况下还拿闪存来作内存交换,就有点太过奢侈了。

须要注意的是,网上有少数文章说 iOS 没有虚拟内存机制,实际上应该指的是 iOS 没有内存交换机制,由于在 Windows 系统下,虚拟内存有时指的是硬盘提供给内存交换的大小。

内存警告

那么当内存不够用时,iOS 的处理是会发出内存警告,告知进程去清理本身的内存。iOS 上一个进程就对应一个 app。代码中的 didReceiveMemoryWarning() 方法就是在内存警告发生时被触发,app 应该去清理一些没必要要的内存,来释放必定的空间。

OOM 崩溃

若是 app 在发生了内存警告,并进行了清理以后,物理内存仍是不够用了,那么就会发生 OOM 崩溃,也就是 Out of Memory Crash。

在 stack overflow 上,有人对单个 app 可以使用的最大内存作了统计:iOS app max memory budget。以 iPhone XS Max 为例,总共的可用内存是 3735 MB(比硬件大小小一些,由于系统自己也会消耗一部份内存),而单个 app 可用内存达到 2039 MB,达到了 55%。当 app 使用的内存超过这个临界值,就会发生 OOM 崩溃。能够看出,单个 app 的可用物理内存实际上仍是很大的,要发生 OOM 崩溃,绝大多数状况下都是程序自己出了问题。

iOS 系统内存占用

分析了 iOS 内存机制的特色以后,咱们可以意识到合理控制 app 使用的内存是很是重要的一件事。那么具体来讲,咱们须要减小的是哪些部分呢?实际上这就是所谓的 iOS 内存占用(Memory Footprint)的部分。

上文讲到内存分页,实际上内存页也有分类,通常来讲分为 clean memory 和 dirty memory 两种,iOS 中也有 compressed memory 的概念。

Clean memory & dirty memory

对于通常的桌面操做系统,clean memory 能够认为是可以进行 Page Out 的部分。Page Out 指的是将优先级低的内存数据交换到磁盘上的操做,但 iOS 并无内存交换机制,因此对 iOS 这样的定义是不严谨的。那么对于 iOS 来讲,clean memory 指的是能被从新建立的内存,它主要包含下面几类:

  • app 的二进制可执行文件

  • framework 中的 _DATA_CONST 段

  • 文件映射的内存

  • 未写入数据的内存

内存映射的文件指的是当 app 访问一个文件时,系统会将文件映射加载到内存中,若是文件只读,那么这部份内存就属于 clean memory。另外须要注意的是,连接的 framework 中 _DATA_CONST 并不绝对属于 clean memory,当 app 使用到 framework 时,就会变成 dirty memory。

未写入数据的内存也属于 clean memory,好比下面这段代码,只有写入了的部分才属于 dirty memory。

int *array = malloc(20000 * sizeof(int));
array[0] = 32
array[19999] = 64复制代码复制代码

8

全部不属于 clean memory 的内存都是 dirty memory。这部份内存并不能被系统从新建立,因此 dirty memory 会始终占据物理内存,直到物理内存不够用以后,系统便会开始清理。

Compressed memory

当物理内存不够用时,iOS 会将部分物理内存压缩,在须要读写时再解压,以达到节约内存的目的。而压缩以后的内存,就是所谓的 compressed memory。苹果最开始只是在 OS X 上使用这项技术,后来也在 iOS 系统上使用。

实际上,随着虚拟内存技术的发展,不少桌面操做系统早已经应用了内存压缩技术,好比 Windows 中的 memory combining 技术。这本质上来讲和内存交换机制相似,都是是一种用 CPU 时间换内存空间的方式,只不过内存压缩技术消耗的时间更少,但占用 CPU 更高。不过在文章最开始,咱们就已经谈到因为 CPU 算力过剩,在大多数场景下,物理内存的空间相比起 CPU 算力来讲显然更为重要,因此内存压缩技术很是有用。

根据 OS X Mavericks Core Technology Overview 官方文档来看,使用 compressed memory 能在内存紧张时,将目标内存压缩至原有的一半如下,同时压缩和解压消耗的时间都很是小。对于 OS X,compressed memory 也能和内存交换技术共用,提升内存交换的效率,毕竟压缩后再进行交换效率明显更高,只是 iOS 没有内存交换,也就不存在这方面的好处了。

本质上来说,compressed memory 也属于 dirty memory。

内存占用组成

9

对于 app 来讲,咱们主要关心的内存是 dirty memory,固然其中也包含 compressed memory。而对于 clean memory,做为开发者一般能够没必要关心。

当内存占用的部分过大,就会发生前文所说的内存警告以及 OOM 崩溃等状况,因此咱们应该尽量的减小内存占用,并对内存警告以及 OOM 崩溃作好防范。减小内存占用也能侧面提高启动速度,要加载的内存少了,天然启动速度会变快。

按照正常的思路,app 监听到内存警告时应该主动清理释放掉一些优先级低的内存,这本质上是没错的。不过因为 compressed memory 的特殊性,因此致使内存占用的实际大小考虑起来会有些复杂。

10

好比上面这种状况,当咱们收到内存警告时,咱们尝试将 Dictionary 中的部份内容释放掉,但因为以前的 Dictionary 因为未使用,因此正处于被压缩状态;而解压、释放部份内容以后,Dictionary 处于未压缩状态,可能并无减小物理内存,甚至可能反而让物理内存更大了。

因此,进行缓存更推荐使用 NSCache 而不是 NSDictionary,就是由于 NSCache 不只线程安全,并且对存在 compressed memory 状况下的内存警告也作了优化,能够由系统自动释放内存。

iOS app 内存管理

前文讲了 iOS 系统层面上的内存机制,在系统层面上的内存管理大多数状况下都已经由操做系统自动完成了。iOS 中一个 app 就是一个进程,因此开发者平时常常讨论的内存管理,好比 MRC、ARC 等等,实际上属于进程内部的内存管理,或者说是语言层面上的内存管理。这部份内存管理语言自己、操做系统均会有一些管理策略,可是做为开发者来讲,不少时候仍是须要从语言层面直接进行操做的。

iOS app 地址空间

前文咱们说过,每一个进程都有独立的虚拟内存地址空间,也就是所谓的进程地址空间。如今咱们稍微简化一下,一个 iOS app 对应的进程地址空间大概以下图所示:

11

每一个区域实际上都存储相应的内容,其中代码区、常量区、静态区这三个区域都是自动加载,而且在进程结束以后被系统释放,开发者并不须要进行关注。

栈区通常存放局部变量、临时变量,由编译器自动分配和释放,每一个线程运行时都对应一个栈。而堆区用于动态内存的申请,由程序员分配和释放。通常来讲,栈区因为被系统自动管理,速度更快,可是使用起来并不如堆区灵活。

对于 Swift 来讲,值类型存于栈区,引用类型存于堆区。值类型典型的有 struct、enum 以及 tuple 都是值类型。而好比 Int、Double、Array,Dictionary 等其实都是用结构体实现的,也是值类型。而 class、closure 都是引用类型,也就是说 Swift 中咱们若是遇到类和闭包,就要留个心眼,考虑一下他们的引用状况。

引用计数

堆区须要程序员进行管理,如何管理、记录、回收就是一个很值得思考的问题。iOS 采用的是引用计数(Reference Counting)的方式,将资源被引用的次数保存起来,当被引用次数变为零时就将其空间释放回收。

对于早期 iOS 来讲,使用的是 MRC(Mannul Reference Counting)手动管理引用计数,经过插入 retainrelease 等方法来管理对象的生命周期。但因为 MRC 维护起来实在是太麻烦了,2011 年的 WWDC 大会上提出了 ARC(Automatic Reference Counting)自动管理引用计数,经过编译器的静态分析,自动插入引入计数的管理逻辑,从而避免繁杂的手动管理。

引用计数只是垃圾回收中的一种,除此以外还有标记-清除算法(Mark Sweep GC)、可达性算法(Tracing GC)等。相比之下,引用计数因为只记录了对象的被引用次数,实际上只是一个局部的信息,而缺少全局信息,所以可能产生循环引用的问题,因而在代码层面就须要格外注意。

那么为何 iOS 还要采用引用计数呢?首先使用引用计数,对象生命周期结束时,能够马上被回收,而不须要等到全局遍历以后再回首。其次,在内存不充裕的状况下,tracing GC 算法的延迟更大,效率反而更低,因为 iPhone 总体内存偏小,因此引用计数算是一种更为合理的选择。

循环引用

内存泄漏指的是没能释放不能使用的内存,会浪费大量内存,极可能致使应用崩溃。ARC 可能致使的循环引用就是其中一种,而且也是 iOS 上最常发生的。什么状况下会发生循环引用,你们可能都比较熟悉了,swift 中比较典型的是在使用闭包的时候:

class viewController: UIViewController {
    var a = 10
    var b = 20
    var someClosure: (() -> Int)?
    
    func anotherFunction(closure: @escaping () -> Int) {
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {print(closure)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        someClosure = {return self.a + self.b
        }
        anotherFunction(closure: someClosure!)
    }
}复制代码复制代码

上面这段代码中,viewController 会持有 someClosure,而 someClosure 也由于须要使用 self.a + self.b 而持有了 viewController,这就致使了循环引用。注意,闭包和类类似,都是引用类型,当把闭包赋值给类的属性时,其实是把闭包的引用赋值给了这个属性。

12

解决方法也很简单,利用 Swift 提供的闭包捕获列表,将循环引用中的一个强引用关系改成弱引用就行了。实际上,Swift 要求在闭包中使用到了 self 的成员都必须不能省略 self. 的关键词,就是为了提醒这种状况下可能发生循环引用问题。

someClosure = { [weak self] inguard let self = self else { return 0 }return self.a + self.b
}复制代码复制代码

weak 和 unowned

weak 关键字能将循环引用中的一个强引用替换为弱引用,以此来破解循环引用。而还有另外一个关键字 unowned,经过将强引用替换为无主引用,也能破解循环引用,不过两者有什么区别呢?弱引用对象能够为 nil,而无主引用对象不能,会发生运行时错误。

好比上面的例子咱们使用了 weak,那么就须要额外使用 guard let 进行一步解包。而若是使用 unowned,就能够省略解包的一步:

someClosure = { [unowned self] inreturn self.a + self.b
}复制代码复制代码

weak 在底层添加了附加层,间接地把 unowned 引用包裹到了一个可选容器里面,虽然这样作会更加清晰,可是在性能方面带来了一些影响,因此 unowned 会更快一些。

可是无主引用有可能致使 crash,就是无主引用的对象为 nil 时,好比上面这个例子中,anotherFunction 咱们会延迟 5s 调用 someClosure,可是若是 5s 内咱们已经 pop 了这个 viewController,那么 unowned self 在调用时就会发现 self 已经被释放了,此时就会发生崩溃。

Fatal error: Attempted to read an unowned reference but the object was already deallocated

若是简单类比,使用 weak 的引用对象就相似于一个可选类型,使用时须要考虑解包;而使用 unowned 的引用对象就相似于已经进行强制解包了,不须要再解包,可是若是对象是 nil,那么就会直接 crash。

13

到底什么状况下可使用 unowned 呢?根据官方文档 Automatic Reference Counting 所说,无主引用在其余实例有相同或者更长的生命周期时使用。

Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

一种状况,若是两个互相持有的对象,一个可能为 nil 而另外一个不会为 nil,那么就可使用 unowned。好比官方文档中的这个例子,每张信用卡必然有它的主人,CreditCard 必然对应一个 Customer,因此这里使用了 unowned

class Customer {let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}复制代码复制代码

而另外一种状况,对于闭包,在闭包和捕获的实例老是相互引用而且同时销毁时,能够将闭包的捕获定义为 unowned。若是被捕获的引用绝对不会变为 nil,应该使用 unowned,而不是 weak

If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.

好比下面这个例子中的闭包,首先 asHTML 被声明为 lazy,那么必定是 self 先被初始化;同时内部也没有使用 asHTML 属性,因此 self 一旦被销毁,闭包也不存在了。这种状况下就应该使用 unowned

class HTMLElement {let name: Stringlet text: String?

    lazy var asHTML: () -> String = {
        [unowned self] inif let text = self.text {return "\(text)"} else {return ""}
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

}复制代码复制代码

总的来讲,最关键的点在于 weakunowned 更加安全,可以避免意外的 crash,这对于工程来讲是很是有益的。因此大多数时候,就像咱们经过 if let 以及 guard let 来避免使用 ! 强制解析同样,咱们也一般直接使用 weak

不会致使循环引用的情形

因为闭包常常产生循环引用的问题,并且加上 weak 以及 guard let 以后也不会出现错误,因此不少时候咱们遇到闭包就直接无脑使用 weak,这实际上就太过粗糙了。

好比,若是在 viewController 中使用了相似下面的闭包,就不会发生循环引用,由于 DispatchQueue 并不会被持有:

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    self.execute()
}复制代码复制代码

更典型的好比使用 static functions 的时候:

class APIClass {
    // static 函数
    static func getData(params: String, completion:@escaping (String) -> Void) {
        request(method: .get, parameters: params) { (response) incompletion(response)
        }
    }
}
class viewController {

		var params = "something"
		var value = ""override func viewDidLoad() {
        super.viewDidLoad()
        getData(params: self.params) { (value) inself.value = value
        }
    }
}复制代码复制代码

此时并不会产生循环引用,由于 self 并不会持有 static class,所以也不会产生内存泄漏:

14

OOM 崩溃

Jetsam 机制

iOS 是一个从 BSD 衍生而来的系统,其内核是 Mach。其中内存警告,以及 OOM 崩溃的处理机制就是 Jetsam 机制,也被称为 Memorystatus。Jetsam 会始终监控内存总体使用状况,当内存不足时会根据优先级、内存占用大小杀掉一些进程,并记录成 JetsamEvent

根据 apple 开源的内核代码 apple/darwin-xnu,咱们能够看到,Jetsam 维护了一个优先级队列,具体的优先级内容能够在 bsd/kern/kern_memorystatus.c 文件中找到:

static const char *
memorystatus_priority_band_name(int32_t priority)
{
	switch (priority) {	case JETSAM_PRIORITY_FOREGROUND:		return "FOREGROUND";	case JETSAM_PRIORITY_AUDIO_AND_ACCESSORY:		return "AUDIO_AND_ACCESSORY";	case JETSAM_PRIORITY_CONDUCTOR:		return "CONDUCTOR";	case JETSAM_PRIORITY_HOME:		return "HOME";	case JETSAM_PRIORITY_EXECUTIVE:		return "EXECUTIVE";	case JETSAM_PRIORITY_IMPORTANT:		return "IMPORTANT";	case JETSAM_PRIORITY_CRITICAL:		return "CRITICAL";
	}	return ("?");
}复制代码复制代码

而如何监控内存警告,以及处理 Jetsam 事件呢?首先,内核会调起一个内核优先级最高(95 /* MAXPRI_KERNEL */ 已是内核能给线程分配的最高优先级了)的线程:

// 一样在 bsd/kern/kern_memorystatus.c 文件中
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);复制代码复制代码

这个线程会维护两个列表,一个是基于优先级的进程列表,另外一个是每一个进程消耗的内存页的列表。与此同时,它会监听内核 pageout 线程对总体内存使用状况的通知,在内存告急时向每一个进程转发内存警告,也就是触发 didReceiveMemoryWarning 方法。

而杀掉应用,触发 OOM,主要是经过 memorystatus_kill_on_VM_page_shortage,有同步和异步两种方式。同步方式会马上杀掉进程,先根据优先级,杀掉优先级低的进程;同一优先级再根据内存大小,杀掉内存占用大的进程。而异步方式只会标记当前进程,经过专门的内存管理线程去杀死。

如何检测 OOM

OOM 分为两大类,Foreground OOM / Background OOM,简写为 FOOM 以及 BOOM。而其中 FOOM 是指 app 在前台时因为消耗内存过大,而被系统杀死,直接表现为 crash。

而 Facebook 开源的 FBAllocationTracker,原理是 hook 了 malloc/free 等方法,以此在运行时记录全部实例的分配信息,从而发现一些实例的内存异常状况,有点相似于在 app 内运行、性能更好的 Allocation。可是这个库只能监控 Objective-C 对象,因此局限性很是大,同时由于没办法拿到对象的堆栈信息,因此更难定位 OOM 的具体缘由。

而腾讯开源的 OOMDetector,经过 malloc/free 的更底层接口 malloc_logger_t 记录当前存活对象的内存分配信息,同时也根据系统的 backtrace_symbols 回溯了堆栈信息。以后再根据伸展树(Splay Tree)等作数据存储分析,具体方式参看这篇文章:iOS微信内存监控

OOM 常见缘由

内存泄漏

最多见的缘由之一就是内存泄漏。

UIWebview 缺陷

不管是打开网页,仍是执行一段简单的 js 代码,UIWebView 都会占用大量内存,同时旧版本的 css 动画也会致使大量问题,因此最好使用 WKWebView

大图片、大视图

缩放、绘制分辨率高的大图片,播放 gif 图,以及渲染自己 size 过大的视图(例如超长的 TextView)等,都会占用大量内存,轻则形成卡顿,重则可能在解析、渲染的过程当中发生 OOM。

内存分析

关于内存占用状况、内存泄漏,咱们都有一系列方法进行分析检测。

  • Xcode memory gauge:在 Xcode 的 Debug navigator 中,能够粗略查看内存占用的状况。

  • Instrument - Allocations:能够查看虚拟内存占用、堆信息、对象信息、调用栈信息,VM Regions 信息等。能够利用这个工具分析内存,并针对地进行优化。

  • Instrument - Leaks:用于检测内存泄漏。

  • MLeaksFinder:经过判断 UIViewController 被销毁后其子 view 是否也都被销毁,能够在不入侵代码的状况下检测内存泄漏。

  • Instrument - VM Tracker:能够查看内存占用信息,查看各种型内存的占用状况,好比 dirty memory 的大小等等,能够辅助分析内存过大、内存泄漏等缘由。

  • Instrument - Virtual Memory Trace:有内存分页的具体信息,具体能够参考 WWDC 2016 - Syetem Trace in Depth

  • Memory Resource Exceptions:从 Xcode 10 开始,内存占用过大时,调试器能捕获到 EXC_RESOURCE RESOURCE_TYPE_MEMORY 异常,并断点在触发异常抛出的地方。

  • Xcode Memory Debugger:Xcode 中能够直接查看全部对象间的相互依赖关系,能够很是方便的查找循环引用的问题。同时,还能够将这些信息导出为 memgraph 文件。

  • memgraph + 命令行指令:结合上一步输出的 memgraph 文件,能够经过一些指令来分析内存状况。vmmap 能够打印出进程信息,以及 VMRegions 的信息等,结合 grep 能够查看指定 VMRegion 的信息。leaks 可追踪堆中的对象,从而查看内存泄漏、堆栈信息等。heap 会打印出堆中全部信息,方便追踪内存占用较大的对象。malloc_history 能够查看 heap 指令获得的对象的堆栈信息,从而方便地发现问题。总结:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。

做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群,能够加小编的QQ3268829585,无论你是小白仍是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。

来源:本文为第三方转载,若有侵权请联系小编删除。

相关文章
相关标签/搜索