你必定要知道的iOS 内存管理|干货满满

iPhone 做为一个移动设备,其计算和内存资源一般是很是有限的,而许多用户对应用的性能却很敏感,卡顿、应用回到前台丢失状态、甚至 OOM 闪退,这就给了 iOS 工程师一个很大的挑战。html

网上的绝大多数关于 iOS 内存管理的文章,大可能是围绕 ARC/MRC、循环引用的原理或者是如何找寻内存泄漏来展开的,而这些内容更准确的说应该是 ObjC 或者 Swift 的内存管理,是语言层面带来的特性,而不是操做系统自己的内存管理。git

若是咱们须要聊聊”管理“内存,那么就须要先了解一些基础知识。github

内存基础概念复习

物理内存

一个设备的 RAM 大小。如下是维基百科上的资料:web

简单来讲,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及以前都是 2G 内存,iPhone 6 和 6 plus 及以前都是 1G 内存。面试

虚拟内存(VM for Virtual Memory)

每一个进程都有一个本身私有的虚拟内存空间。对于32位设备来讲是 4GB,而64位设备(5s之后的设备)是 18EB(1EB = 1000PB, 1PB = 1000TB),映射到物理内存空间。缓存

内存管理、映射中的基本单位是页,一页的大小是 4kb(早期设备)或者 16kb(A7 芯片及之后)网络

由于有页的存在,每次申请内存都必须以页为单位。然而这样一来,若是只是申请几个 byte,却不得不分配一页(16kb),是很是大的浪费。所以在用户态咱们有 “heap” 的概念。session

Page In/Out

因为虚拟内存的空间远远大于物理内存,在任意一个时间点,虚拟内存中的一个页并不必定老是在物理内存中,而是可能被暂时存到了磁盘上,这样物理内存即可以暂时释放这部分空间,供优先级更高的任务使用,所以磁盘能够做为 backing store 以扩展物理内存(MacOS 中有,iOS 没有)。另外一种多是加载一个比较大的文件/动态库,每次使用咱们可能只须要加载其中的一部分,那么就可使用 mmap 映射这个文件到虚拟内存空间,这样当咱们访问其中一部分时,系统会自动把这一部分从磁盘加载到内存,而不加载其他部分。数据结构

这样把磁盘中的数据写到内存/从内存中写回磁盘成为 page in/out。架构

Wired memory

没法被 page out 的内存,主要为系统层所用,开发者不须要考虑这些。

VM Region

一个 VM Region 是指一段连续的内存页(在虚拟地址空间里),这些页拥有相同的属性(如读写权限、是不是 wired,也就是是否能被 page out)。举几个例子:

  • mapped file,即映射到磁盘的一个文件
  • __TEXT,r-x,多数为二进制
  • __DATA,rw-,为可读写数据
  • MALLOC_(SIZE),顾名思义是 malloc 申请的内存

VM Object

每一个 VM Region 对应一个数据结构,名为 VM Object。Object 会记录这个 Region 内存的属性

Resident Page

当前正在物理内存中的页(没有被交换出去)

与其余App共存的状况

  • app 内存消耗较低,同时其余 app 也很是“自律”,不会大手大脚地消耗内存,那么即便切换到其余应用,咱们本身的 app 依然是“活着”的,保留了用户的使用状态,体验较好
  • app 内存消耗较低,可是其余 app 很是消耗内存(多是使用不当,也多是自己就很是消耗内存,好比大型游戏),那除了当前在前台的进程,其余 app 都会被系统回收,用来给活跃进程提供内存资源。这种状况咱们没法控制
  • app 内存消耗比较大,那切换到其余 app 之后,即便其余 app 向系统申请不是特别大的内存,系统也会由于资源紧张,优先把消耗内存较多的 app 回收掉。用户会发现只要 app 一旦退到后台,过会再打开时就会从新加载
  • app 内存消耗特别大,在前台运行时就有可能被系统 kill 掉,引发闪退

在 iOS 上管理杀进程释放资源策略模块叫作 Jetsam,这里推荐五子棋的文章,其中有详细的介绍。

OOM的断定

苹果官方关于 OOM 的文档和接口很是少,以致于 facebook 在判断应用是否上次由于 OOM 而闪退时,须要通过一个漫长的逻辑判断,当不知足全部条件时才能断定为 OOM(想象一下若是系统能提供一个接口,告诉开发者上次的退出缘由,会方便多少!)

咱们会好奇,当一个普通 app 启动时,内存消耗究竟有多少?

如何查看内存占用量

咱们刚讨论到内存的不一样类别,那么应该选用哪一个值做为内存占用量的标准呢?

Memory Footprint

在 WWDC13 704 中,苹果推荐用 footprint 命令来查看一个进程的内存占用。

关于什么是 footprint,在官方文档 Minimizing your app’s Memory Footprint 里有说明:

Refers to the total current amount of system memory that is allocated to your app.

因为该命令只能在 MacOS 上运行,而且 iOS 上也没有 Activity Monitor,咱们新建一个 Mac app,而后用不一样手段测量内存占用

  • Instruments 中的 All Heap & Anonymous VM: 8.32MB
  • Xcode: 47.4MB
  • 系统 Activity Monitor: 47.4MB
  • 使用 footprint 命令(能够经过 man footprint 查看文档): 47MB
  • task_vm_info.phys_footprint: 47.4MB
  • task_info.resident_size: 80MB

能够看到,Xcode、系统、footprint 工具和 phys_footprint 获得的数据是一致的,而既然官方推荐了 footprint,所以咱们以这几个方法获得的结果做为标准。猜想 footprint 比 Instruments 数据更大的缘由是存在一些”非代码执行开销“,如把系统和应用二进制加载进内存。iOS 中虽然不能使用系统 Activity Monitor 和 footprint 命令,也能在 Xcode 中和 phys_footprint 获得一样的结果。

至此咱们能够获得一个结论: Instruments 中显示的部分,其实也只是整个应用进程里内存的一部分。可是因为咱们可以控制的只有这一部分,所以应该把精力投入到 Instruments 的分析中去。

使用 Instruments 分析

应用的详细性能分析老是须要依赖 Instruments 的强大功能。从 Allocations 角度来看,总的内存占用 = All Heap Allocations + All Anonymous VM:

  • All Heap Allocations,几乎全部类实例,包括 UIViewController、UIView、UIImage、Foundation 和咱们代码里的各类类/结构实例。通常和咱们的代码直接相关。
  • All Anonymous VM,能够看到都是由”VM:”开头的

主要包含一些系统模块的内存占用。有些部分虽然看起来离咱们的业务逻辑比较远,但实际上是保证咱们代码正常运行不可或缺的部分,也是咱们经常忽视的部分。通常包括:

  • CG raster data(光栅化数据,也就是像素数据。注意不必定是图片,一块显示缓存里也多是文字或者其余内容。一般每像素消耗 4 个字节)
  • Image IO(图片编解码缓存)
  • Stack(每一个线程都会须要500KB左右的栈空间)
  • CoreAnimation
  • SQLite
  • network
  • 等等

咱们平时最常常会作的 debug 之一,就是查找循环引用。而循环引用形成的 leak 数据一般是 UIKit 或咱们本身的一些数据结构,会被归类到 heap。这些是咱们相对熟悉的,网上也有很是多的文章,这里再也不讨论。而就 VM 这块来讲,由于不受咱们直接控制,文档也较少,因此相对神秘一些,每每容易被忽视。

对于 VM 中的线程栈开销、网络 buffer 等,咱们其实没有太大的控制能力,一般这些也不会是内存开销的主要缘由(除非有成百上千的线程和频繁大量的网络请求)。而对于即刻和绝大多数 app 来讲,尤为是采用了 AsyncDisplayKit(用空间换时间)的状况下,渲染开销是绝对不可忽视的一块。

我一直认为,移动设备上无论是 CPU、GPU 仍是内存,最大的性能杀手必定是布局和渲染。布局数据和通常数据结构相似,单个内存开销最多以 KB 计,而渲染缓存很容易就用“兆”来计算,更容易影响到总体开销。

任意打开一个 app,能够看到渲染无非就是两大部分:图片和文字。

图片渲染开销

咱们知道,解压后的图片是由无数像素数据组成。每一个像素点一般包括红、绿、蓝和 alpha 数据,每一个值都是 8 位(0–255),所以一个像素一般会占用 4 个字节(32 bit per pixel。少数专业的 app 可能会用更大的空间来表示色深,消耗的内存会相应线性增长)。

下面咱们来计算一些一般的图片开销:

  • 普通图片大小,如 500 600 32bpp = 1MB
  • 跟 iPhone X 屏幕同样大的:1125 2436 32bpp = 10MB
  • 即刻中容许最大的图片,总像素不超过1500w:15000000 * 32bpp = 57MB

有了大体的概念,之后看到一张图能简单预估,大概会吃掉多少内存。

缩放

  • 内存开销多少与图片文件的大小(解压前的大小)没有直接关系,而是跟图片分辨率有关。举个例子:一样是 100 * 100,jpeg 和 png 两张图,文件大小可能差几倍,可是渲染后的内存开销是彻底同样的——解压后的图片 buffer 大小与图片尺寸成正比,有多少像素就有多少数据。
  • 一般咱们下载的图片和最终展现在界面上的尺寸是不一样的,有时可能须要将一张巨型图片展现在一个很小的 view 里。若是不作缩放,那么原图就会被整个解压,消耗大量内存,而不少像素点会在展现前被压缩掉,彻底浪费了。因此把图片缩放到实际显示大小很是重要,并且解码量变少的话,速度也会提升不少。
  • 若是在网上搜索图片缩放方案的话,通常都会找到相似“新建一个 context ,把图画在里面,再从 context 里读取图片”的答案。此时若是原图很大,那么即便缩放后的图片很小,解压原图的过程仍然须要占用大量内存资源,一不当心就会 OOM。可是若是换用 ImageIO 状况就会好不少,整个过程最多只会占用缩放后的图片所需的内存(一般只有原图的几分之一),大大减小了内存压力。

解码

图片解码是每一个开发者都绕不过去的话题。图片从压缩的格式化数据变成像素数据须要通过解码,而解码对 CPU 和内存的开销都比较大,同时解码后的数据如何管理,如何显示都是须要咱们注意的。

  • 一般咱们把一张图片设置到 UIImageView 上,系统会自动处理解码过程,但这样会在主线程上占用必定 CPU 资源,引发卡顿。使用 ImageIO 解码 + 后台线程执行是 WWDC(18 session 219) 推荐的作法。
  • ImageIO 功能很强大,可是不支持 webp
  • AsyncDisplayKit 的一大思想是拿空间换时间,换取流畅的性能,可是内存开销会比 UIKit 高。一样用一个全屏的 UIImageView 测试,直接用UIImage(named:)来设置图片,虽然不可避免要在主线程上作解压,可是消耗的内存反而较小,只有4MB(正常须要10MB)。猜想神秘的 IOSurface 对图片数据作了某些优化。苹果有这么一段话描述 IOSurface:
Share hardware-accelerated buffer data (framebuffers and textures) across multiple processes. Manage image memory more efficiently.

渲染

网上关于渲染的资料不少,可是不少都是人云亦云,咱们来讲一些比较少讨论的点:

  • 咱们常常会须要预先渲染文字/图片以提升性能,此时须要尽量保证这块 context 的大小与屏幕上的实际尺寸一致,避免浪费内存。能够经过 View Hierarchy 调试工具,打印一个 layer 的 contents 属性来查看其中的 CGImage(backing image)以及其大小

做为 backing image 的 CGImage

  • 一旦涉及到 offscreen rendering,就可能会须要多消耗一块内存/显存。那到底什么是离屏渲染?无论是 CPU 仍是 GPU,只要不能直接在 frame buffer 上画,都属于offscreen rendering。在 Core Animation: Advanced Techniques 书里有 offscreen rendering 的一段说明:

    Offscreen rendering is invoked whenever the combination of layer properties that have been specified mean that the layer cannot be drawn directly to the screen without pre- compositing. Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed.

  • layer mask 会形成离屏渲染,猜测多是因为涉及到”根据 mask 去掉一些像素“,没法直接在 frame buffer 中作
  • 圆角要慎用,但不是说彻底不能用— — 只有圆角和 clipsToBounds 结合的时候,才会形成离屏渲染。猜测这二者结合起来也会形成相似 mask 的效果,用来切除圆角之外的部分
  • backgroundColor 能够直接在 frame buffer 上画,所以并不须要额外内存

文字渲染的CPU和内存开销

关于文字渲染的文档资料并非不少,所以咱们须要作一些实验来判断。 新建一个项目,添加一个全屏的 label,不停切换文字,获得 cpu 占用率稳定在 15%,gpu占用率 0%。而且 Time Profiling 显示

排名第一的方法主要是在调用 render_glyphs,说明主要是 CPU 参与了文字渲染。

  • 文字渲染中,主要内存开销调用栈:

  • 虽然文字比较多,可是只占用了 2.75MB(2883584 byte,能够看到这边苹果仍然是用1024KB = 1MB的换算)的内存。那么问题来了,咱们上面提到一块跟屏幕同样大的显示区域占用空间大约是 10MB,为何文字占用这么少呢?

理论上 iPhone X 全屏有 1125 * 2436 = 2740500 个像素,距离实际占用内存很是接近,只多了 143084 byte(139.73kb),说明差很少正好是一个像素对应一个字节。这印证了 WWDC(WWDC18 219和416)上的结论,即黑白的位图只占用 1 个字节,比 4 字节节省 75% 的空间。固然实际使用过程当中很难限制文字只采用黑白两种颜色,可是仍是应该了解苹果的优化过程。

在以上测试基础上,若是咱们尝试把第一个字符加上红色属性,或者添加 emoji,那么渲染结果就再也不是黑白的了,而是一张彩色图片,相似普通图片那样每一个像素须要 4 个字节来描述。所以理论上所消耗的内存会变成 2.75MB * 4 = 10MB 多一点。测试获得:

结果占用了 11468800 bytes,是原来 2740500 的 3.97 倍,与理论值 4 很是接近(可能内存中还存在一些附属的其余元数据,而这些不会如同像素数据同样线性放大,所以不彻底是精确 4 倍关系)。比较好的印证了以前的结论。

整整一屏的文字,在 3x 设备上,只占用了 2MB 多一点的内存,能够说是很是省了。

总结

iOS 的内存管理有如下几个特色:

  • 文档较少,系统提供的接口也较少,所以你们本身生产的轮子较多,须要多作实验才能获得可靠的结论。多利用 Instruments 也会发现一些以前忽略的点
  • 内存问题的暴露有必定延时性,OOM 在本地很难复现,须要投入大量时间测试,同时配套相应的监控系统
  • 技术变化较慢,操做系统这一层的知识在过去和将来的很长一段时间都不太会改变,或只是微调,值得花时间来研究
  • 经典的时空取舍问题,在资源有限的设备上,如何平衡 CPU/GPU 和内存的开销,来达到性能最大化
  • 可以帮助咱们了解一些文字和图片渲染的本质,更好的了渲染系统的工做原理,毕竟这是客户端工程师不可替代的职责之一

推荐?:

  • 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。
  • 结实人脉、讨论技术 你想要的这里都有!
  • 抢先入群,跑赢同龄人!(入群无需任何费用)
  • (直接搜索群号:789143298,快速入群)
  • 点击此处,与iOS开发大牛一块儿交流学习

申请即送:

  • BAT大厂面试题、独家面试工具包,
  • 资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,

做者:即刻技术团队
连接:https://juejin.im/post/5bec0e...

相关文章
相关标签/搜索