iOS内存基础知识

操作系统的内存机制

冯·诺依曼结构

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

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

为了解决CPU与存储器之间的速度不同步的问题,目前普遍的解决方案就是存储器的分级方案。

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

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

寻址方式

目前,大部分通用的计算机的内存管理使用 段页式存储结构;用户程序先分段,每个段内再分页;而 页是存储的最基本单位,iOS设备的 arm64 架构后,页大小是16KB;

利用 逻辑地址 (段号 + 段内页号 + 页内地址) 进行地址变化,获得物理地址;这样的话,在段页式结构中,须三次访问内存才能获取数据或指令

当进程访问一个虚拟内存的页时,而对应的物理内存却不存在时,会触发一次 Page Fault(缺页中断),将需要的数据 or 指令从磁盘加载到物理内存页中,建立映射关系,然后再恢复现场,程序本身是无感知的;

更详细的段页式介绍可以参考之前的博客:OS内核(xv6)探究-分段&分页

虚拟内存

在使用了虚拟寻址之后,由于每次都会进行一个翻译过程,所以可以在翻译中增加一些额外的权限判定,对地址空间进行保护。所以,对于每个进程来说,操作系统可以为其提供一个独立的、私有的、连续的地址空间,这就是所谓的虚拟内存。

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

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

Swap In/Out & Page In/Out
  • 磁盘内部有一个区域叫做 交换空间 (Swap Space),MMU(内存管理单元) 会将 暂时不用的内存块内容 写在交互空间上(硬盘),这就是 Swap Out;当需要时候再从 Swap Space 中读取到内存中,这就是 Swap In;Swap in和swap out的操作都是比较耗时的, 频繁的Swap in和Swap out操作非常影响系统性能;
  • Page In/Out 和 Swap In/Out 概念类似,只不过Page In/Out是将 某些页 的数据写到内存/从内存写回磁盘交互区;而Swap In/Out是将 整个地址空间 的数据写到内存/从内存写回磁盘交互区;本质都是交互机制。
  • macOS支持这类交换机制,但是iOS不支持。

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 的内存分区

从高地址到低地址各区域如下:

  • 栈区(stack): 由编译器⾃动分配,存放 函数的参数值,局部变量的值等,作用域执行完毕之后,就会被系统收回。(栈区的地址从高到低分配)
    • 栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。【先进后出】
    • alloc 在堆上申请一块空间返回一个指针,这个指针在栈上,申请空间在堆上,这里指的局部变量不是对象地址,而是这个对象的指针在栈上。
    • 栈区存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。
    • 只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    • 栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    • 由系统自动分配,速度较快,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,不会产生内存碎片。

根据官方文档 苹果线程成本,iOS的主线程栈大小不超过1MB,OS X主线程栈最大8MB,子线程栈最大512KB。子线程在创建的时候可以更改栈的大小,子线程允许设置的最小栈大小为16 KB,并且栈大小必须为4 KB的倍数。主线程的栈大小无法修改。

  • 堆区(heap): 一般由程序员分配和释放,用于存放程序运行中被动态分配的内存段;iOS 中的 Objective-C 对象存放在这里,由ARC管理;注意它与数据结构中的堆是两回事,分配方式类似于链表。(堆区的地址是从低到高分配)

    • 如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在iOS 中 alloc 都是存放在堆中。优点是灵活方便,数据适应面广泛,但是效率有一定降低。【顺序随意】
    • 堆空间的分配总是动态的虽然程序结束时所有的数据空间都会被释放回系统。
    • 堆区申请后的系统响应
      1. 首先应该知道操作系统有一个记录空闲内存地址的链表。
      2. 当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
      3. 由于找到的堆结点的大小不一定正好等于申请的大小
      4. 系统会自动的将多余的那部分重新放入空闲链表中。
  • 全局区/静态区: 由编译器分配,主要是存放全局变量 和 静态变量,程序结束后由系统释放;主要分两个区:

  • BSS区: 未初始化 的全局变量 和 静态变量;

  • 数据区: 已初始化 的全局变量 和 静态变量;

  • 常量区: 存放的是常量,如常量字符串,程序结束后由系统释放;

  • 代码区: 存放函数体的二进制代码,程序结束后由系统释放;

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微信内存监控

常见原因:

  1. 内存泄漏:最常见的原因之一就是内存泄漏。
  2. UIWebview 缺陷:无论是打开网页,还是执行一段简单的 js 代码,UIWebView 都会占用大量内存,同时旧版本的 css 动画也会导致大量问题,所以最好使用 WKWebView
  3. 大图片、大视图:缩放、绘制分辨率高的大图片,播放 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 Memory 内存详解 (长文)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值