大多数浏览器和
Developer App 均支持流媒体播放。
-
深入了解 iOS 内存
探索如何利用内存图来掌握 app 内存占用空间的具体构成情况。理解一幅图像的真实内存开销。了解减少 app 内存占用空间的技巧和窍门。
资源
相关视频
WWDC23
WWDC21
WWDC18
-
下载
大家好 我是 Kyle 我是一名 Apple 的软件工程师 今天我们要将深入 研究一下 iOS 内存 要知道的是 尽管这里说的是 iOS 我们接下来涉及到的很多内容 也适用于其他平台 我们首先要讨论的 是为什么要减少内存
当我们提到减少内存时 我们实际上 是在讨论减少内存占用 那么我们就来谈谈这点
我们将讨论一些 可以用来分析内存占用的工具
我们会有一些针对图像的提醒 还会讲到后台优化 最后 我们会用一个很好的演示作为总结
为什么减少内存呢
简单的回答是 为了使用户获得更好的体验 不仅你的 App 会启动得更快 系统会表现得更好 你的 App 也会在内存中保留 更长的时间 其他 App 也会在内存中保留 更长的时间 几乎一切都变得更好 现在 如果你看看周围 你实际上也在通过 减少内存 来帮助其他开发者
我们讨论的是减少内存 但实际上减少的是内存占用 内存和人不同 它们生而“不”平等 这是什么意思呢
我们需要谈谈 “Pages” 不是这个 “Pages 文稿” 我们将要讨论内存的页面 系统给了你一个内存页 它可以堆的形式 存储多个对象 有些对象实际上 可以跨越多个内存页 它们的大小一般是 16KB 可以是净页 也可以是脏页
App 的内存使用 实际上指的是页面数量 乘以页面大小 这有一个净页和脏页的例子 假设我分配了一个含有 20000 个 整数的数组 系统可能会分配给我 6 个内存页面
当我分配这些页面时 它们是净页 但是 当我开始 对数据缓冲区进行写入时 例如 如果我写入到这个数组的第一个位置 这个内存页就会变成脏页
类似地 如果我写入到 缓冲区中的最后一个位置 最后一页也会变成 脏页 请注意 中间的四个页面 仍然是 净页 因为 App 还没有写入它们 另一个有趣的话题是内存映射文件 它是一种在磁盘上的文件 但加载到了内存中 如果你用的是只读文件 这些将一直是净页 内核实际上 是在它们离开磁盘写入 RAM 时进行管理的 JPEG 就是一个很好的例子 如果我有一个 JPEG 文件 比如说 它有 50KB 大 当它被映射到内存中时 它实际上被映射到 大约 4 页内存中 第 4 页实际上 并没有被完全填满 所以它可以用来做其他的事情 内存就是像这样复杂 但是之前的那三页 总是可以被系统释放 当我们讨论某个典型的 App 时 它们的内存占用和分析文件 都会有一个脏的 一个压缩的 以及一个干净的内存段 让我分别来看看
净内存是可以被分页的数据 这些是我们刚刚讨论过的内存映射文件 可以是图像文件 Blob.data 或者 Training.model 也可以是框架
每个框架都有一个 _DATA_CONST 部分
它通常是 净内存 但是如果你做了任何运行时的小把戏 比如 “Method Swizzling(方法交换)” 那么它就会变成脏内存 脏内存是 App 写入的任何内存
它们可以是对象 比如 malloc 字符串 数组等等 它可以是已解码的图像缓冲 我们稍后会讲到这点 它也可以是框架 框架也有一个 _DATA 部分 和一个 _DATA_DIRTY 部分 它们总是指向脏内存
你可能注意到了 我曾两次提到了框架 是的 你链接的框架 实际上使用内存 和脏内存 它是一个 连接框架的必要部分 但如果你要保持自己的框架 可以使用单例 和全局初始化器 来减少他们使用的 脏内存 因为单例 在被创建之后 才会进入内存 初始化器也只有 在框架被链接或类被加载时
才会运行 压缩内存非常酷 iOS 没有传统的磁盘交换系统 取而代之 它使用内存压缩器 它是在 iOS 7 中被引入的内存压缩器 接收未访问的内存页 并压缩它们 这实际上可以创建更多的空间 但在访问时 压缩器会对它们进行解压 以便读取内存 让我们看一个例子
假设我有一个用于缓存的字典 它占用了 3 页的内存 但是如果我有一段时间 没有访问过它 且系统需要一些空间 系统就可以把它 压缩到一个内存页中 它现在被压缩了 但同时我节省了空间 或者说我现在有了两个额外的内存页 如果在未来的某个时刻我想访问它 它将会恢复原来的大小 我们来谈谈内存警告 App 并不总是 引起内存警告的原因 如果你在一个低内存的设备上 接到一个电话 那也可能会触发一个内存警告 你就有麻烦了 所以不要想当然地认为 内存警告是你造成的
压缩器使内存的释放 变得复杂 因为根据压缩的内容 实际上你可以 比以前使用更多的内存 因此我们建议修改策略 比如暂时不缓存任何内容 或者在发生内存警告时 限制一些后台工作
我们中的一些人可能在 App 中 遇到这种情况 我们得到一个内存警告 并决定从缓存中 删除所有对象 回到压缩字典的例子 会发生什么呢 既然我正在访问这个字典 我现在比以前 使用了更多的内存页 在内存受限的环境中 我们并不希望这样 因为我删除了所有的对象 我做了很多工作 只是为了让它回到 之前被压缩时的样子 也就是只占用一个内存页
所以我们要注意记忆警告
这就提出了一个关于缓存的重要问题 当我们缓存时 我们实际上是在试图 避免 CPU 重复工作 但是如果我们缓存太多 我们将耗尽所有的内存 这可能会给系统带来问题 所以请记住 我们有 内存压缩器和缓存 最好平衡一下 缓存与重新计算的内容
另一个注意事项是 如果使用的不是字典 而是 NSCache 这将是存储缓存对象的安全方法 由于 NSCache 分配内存的方式 它实际上是可被释放的 所以在内存受限的环境中 它的效果更好 回到典型的 App 中的 这三个部分 当我们讨论 App 的内存占用时 我们实际上是在讨论那些脏的 和压缩的部分 净内存在这里并不重要
每个 App 都有一个内存占用限制
这个限制对于一个 App 来说 是相当高的 但是请记住 根据设备的不同 这个限制也会改变 因此 你不能像 在内存 4GB 设备上那样 在 1GB 的设备上 使用同样多的内存 还有扩展 扩展的内存占用要小得多 所以在使用扩展时 你需要更加注意这一点
当你超过了内存占用限制 就会出现异常 这种异常就是 EXC_RESOURCE_EXCEPTION
现在我想邀请 James 来谈谈 如何分析我们的内存占用
谢谢 James 谢谢你
谢谢 Kyle 好的 我是 James 我是一名 Apple 的软件工程师 我想向你们介绍一些 更高级的工具用于分析和研究 App 的内存占用情况
你可能已经熟悉了 Xcode 内存测量计 它就在调试导航器中 它是帮助你快速查看 App 内存占用 很好的一种方式 在 Xcode 10 中 它现在可向你输出 系统对你的评分值 所以如果看起来与 Xcode 9 不同 不要太在意
我正在 Xcode 中运行我的 App 我发现它消耗了更多的内存 那么下一步我应该用什么工具呢 显然是 Instruments
它提供了许多方法 来调查 App 的内存占用 你可能已经熟悉 “Allocations” 和 “Leaks” “Allocations” 分析由你的 App 所分配的堆 “Leaks” 会检查一个进程中的内存泄漏 但是你可能不太熟悉 “VM Tracker” 和 “Virtual memory trace(虚拟内存追踪)” 如果你还记得 Kyle 谈论过的 iOS 内存的主要类别 他谈到了脏内存 和压缩内存 VM Tracker 提供了一种 很好的分析方式
它为脏内存 以及交换内存即 iOS 中的压缩内存 分别提供了独立的追踪 并告诉你 关于常驻内存大小的信息 我认为这对于 研究 App 的脏内存大小 非常有用 Instruments 中的最后一项是 “Virtual memory trace(虚拟内存追踪)” 你可以凭借它 与你 App 相关的虚拟内存系统的性能 进行深入的了解
我发现这里的 “By Operation” 标签页 非常有用 它为你提供了一个 虚拟内存系统文件 并向你展示虚拟内存的内存页缓存命中 以及内存页零填充之类的内容
Kyle 前面提到 如果你接近设备的内存限制 你将收到 EXC_RESOURCE_EXCEPTION 的异常 如果你正在 Xcode 10 中 运行你的 App Xcode 将会捕获这个异常 并暂停你的 App 这意味着你可以启动内存调试器 并从那里开始调查 我认为这真的十分有用 Xcode 的内存调试器 是在 Xcode 8 中提供的 它可以帮助你跟踪对象的依赖 声明周期和泄漏 在 Xcode 10 中 内存调试器更新了布局 它非常适合用来查看 非常大的内存图文件 在内部 Xcode 使用 Memgraph 文件格式 存储有关 App 的 内存使用的信息 你可能不知道 你可以搭配我们的多种命令行工具 使用 Memgraph
首先 你需要从 Xcode 中 导出 Memgraph 这很简单
你只需点按 “File(文件)”菜单中的 “Export Memory Graph...(导出内存图)” 并将其保存 然后 你就可以将 Memgraph 传递给命令行工具 而不是目标本身 这样就可以了
我在 Xcode 10 中运行我的 App 然后收到一个 内存资源异常 这可不太好 我也许应该提取 Memgraph 来进一步研究 但接下来我该怎么做呢 显然 去终端 我经常使用的第一个工具 是 vmmap 通过输出 分配给进程的虚拟内存区域 它给你的 App 提供了 内存消耗的高级分析
-summary 参数 是一个很好的起点 它可以打印出很多细节 比如该区域的内存大小 脏区域的数量 以及交换内存 也就是 iOS 中的压缩内存的数量 请记住这里的脏和交换区域大小 是非常重要的
值得注意的一点是 交换区域大小指的是 数据压缩前的大小 而不是压缩后的大小
如果你真的需要深入了解 想要更多的信息 你可以在 Memgraph 上 运行 vmmap 你会得到所有区域的具体信息 我们首先向你们展示 “Non-writable regions(不可写入区域)” 比如程序的文本 或可执行代码 然后是 “Writable regions(可写入区域)” 比如数据部分 这就是你的进程堆 所在的位置 除了这些之外还有很酷的一点 就是所有这些工具 都可以很好地使用 标准命令行实用程序 例如 前几天 我在 VM Tracker 中分析我的 App 然后我看到了 脏内存增加的情况 所以我导出了 Memgraph 文件 我想知道这些脏数据 是否有一部分是由我链接的 框架或库造成的 于是我在这个 Memgraph 上 运行 vmmap
我使用了 -pages 参数 这意味着 vmmap 将输出 内存页的数量 而不仅仅是原始字节 然后我将它传输到 grep 并在那搜索 ‘.dylib’ 所以我在这里需要动态库
最后 我将它导入到 一个特别简单的 awk 脚本中 来合计脏列 然后最终将其输出为 脏内存页的数量
我觉得这很酷 而且我一直在使用它 它让你能够 为你和你的团队编写 非常强大的调试工作流 另一个 macOS 开发人员 可能已经熟悉的 命令行实用程序是 leaks 它在运行时跟踪堆中 没有根的对象 所以请记住 如果你在 leaks 中看到一个对象 那它占用的是你无法释放的脏内存 让我们看看 内存调试器中的内存泄漏
这里我有 3 个对象 它们相互之间都有很强的引用 创建了一个经典的 “Retain Cycle(留置环)” 让我们在 leaks 工具中 看看这个泄漏 今年 leaks 已被更新 不仅可以显示 泄漏的对象 还可显示它们所属的 Retain Cycle 如果进程中启用了 malloc 堆栈日志记录 我们甚至为你提供了
根节点 回溯 我经常问自己的一个问题是 内存都去哪了 我查看了 vmmap 发现堆很大 但是接下来要做什么呢 heap 工具提供了 关于进程堆中 对象分配的各种信息 它可以帮助你追踪 非常复杂的分配 或者很多同类的对象
我这里有一个 Memgraph 文件 它是我在 Xcode 捕获到 内存资源异常时得到的 我想研究它的堆 所以我把它传递给了 heap 它告诉我 每个对象的类名 它们的数量 它们的平均大小 以及这类对象的总大小 在这里 我看到了很多很多小对象 但我不认为 这是什么问题 我不认为 这是主要的问题
默认情况下 堆将按数量排序 但是我希望看到的 是最大的对象 而不是数量最多的对象 因此将 -sortBySize 参数传递给堆 能让它们按大小排序
这里我看到了一些 硕大的 NSConcreteData 对象 我应该将这个输出 和 Memgraph 附加到 Bug 报告中 但这还不够 我得弄清楚 这些对象是怎么来的
首先 我需要获得 其中一个 NSConcreteData 对象的地址 然后是 heap 工具中的 -addresses 参数 当你将 -addresses 参数 与一个类名一起传给 heap 工具时 它将为你提供堆上的 每个实例的地址
现在我有了这些地址 我可以知道它们每一个都来自哪里
这就是 malloc 堆栈日志记录 派上用场的地方 当启用时 系统将记录每个分配的回溯 当我们记录一个 Memgraph 时 这些日志就会被捕获 它们将用于为我们的一些工具 注释现有的输出 你可以在 “Scheme Editor(Scheme 编辑器)”中的 “Diagnostics(诊断)”标签页中 轻松启用它 我建议你们 在 Memgraph 中 使用实时分配选项 我的 Memgraph 文件 在 malloc 堆栈日志记录中被捕获 现在我们要找到分配的回溯 这就是 malloc_history 发挥作用的地方 你只需传递 malloc_history Memgraph 以及内存中实例的地址 那么 如果捕获到它的回溯 malloc_history 就会将其提供给你 这里我取了其中 一个很大的 NSConcreteData 的地址 我把它传递给了 malloc_history 然后我就得到了一个回溯记录 有趣的是 看起来我的 NoirFilter.apply() 方法 创建了一个巨大的 NSConcreteData 我应该将这个和 Memgraph 附加到一个 Bug 报告中 其他人就可以查看它 这些只是几种 可以深入研究 App 行为的方法 当遇到内存问题时 你会选择哪个工具
有 3 种思考方式 你想看到对象的创建吗 你想要查看内存中 引用对象或地址的内容吗 或者你只是想看看 一个实例有多大
如果你在进程启动时 启用了 malloc 堆栈日志记录 那么 malloc_history 可以帮助你查找 该对象的回溯
如果你只是想看看 在内存中引用对象的内容 你可以使用 leaks 和在内存页面中 提供的其他工具来帮助你 最后 如果你只是想了解 一个区域或一个实例有多大 vmmap 和 heap 是首选工具 作为起始点 我建议在进程的 Memgraph 上 运行带有 -summary 命令的 vmmap 然后顺着线程继续进行 现在 我想请回 Kyle 他将会讨论 iOS App 中最大的对象 那就是图像 有请 Kyle
谢谢 James
说到图像 关于图像需要记住的 最重要的就是 内存使用与图像的尺寸有关 而不与它的文件大小有关
举个例子 我有一张非常漂亮的图片 并且我想把它作为 一个 iPad App 的壁纸
它的尺寸是 2048*1536 磁盘上文件的大小是 590KB 但是它实际使用了多少内存呢
10MB 10MB 这可够大的 这是因为 把像素的宽度乘以高 即 2048 乘以 1536 然后每像素乘以 4 字节 就会达到 10MB 那么为什么它会大这么多呢
我们要谈谈图像 是如何在 iOS 上工作的 有加载 解码 和渲染三个阶段 在加载阶段 这个被压缩的 590KB 的 JPEG 文件被接收 并被加载到内存中
在解码阶段 JPEG 文件 将被转换为 GPU 可以读取的格式 图像需要被解压 这使得文件大小增至 10 mb 被解码之后 图像就可以被随意渲染了 要了解更多关于图像的信息 以及如何对它们进行优化 我建议你们查看 本周早些时候举行的 “Images and Graphics Best Practices” 的讨论会 在 SRGB 格式中 每个像素有 4 个字节 这通常是图形中 图像最常见的格式 它是每个像素 8 位 所以红色 1 字节 绿色 1 字节 蓝色 1 字节 Alpha 通道 1 字节
但是 我们还可以将其继续变大 iOS 硬件可以渲染宽格式 宽格式中 为了得到有表现力的颜色 每个像素需要 2 个字节 所以我们将图像的大小加倍 iPhone 7 iPhone 8 iPhone X 以及一些 iPad Pro 上的摄像头 非常适合捕捉这种 高保真的内容 你也可以用它来制作 非常精确的颜色 比如运动商标等等
但是这些只在 宽格式显示器中有用 所以我们不希望 在不需要的时候使用它
另一方面 我们也可以使图像变小 比如 AL8 格式 这种格式只存储灰度值 和 Alpha 值 它通常用于着色器 比如 Metal App 等等 这个格式并不常用 实际上 我们还可以让它继续变小 我们可以使用所谓的 Alpha 8 格式 Alpha 8 只有 1 个通道 每个像素 1 个字节 非常小 它比 SRGB 小 75% 这很适合蒙版 或单色文本 因为我们节省了 75% 的内存
如果我们分开来看 我们可以从 Alpha 8 格式的 每个像素 1 个字节开始 一直增加到宽格式的每个像素 8 个字节 这个范围很大 所以我们真正需要做的是 知道如何选择正确的格式 那么我们如何选择正确的格式呢 简短的回答是不要选择格式 让格式来选择你
如果你不再使用 自 iOS 诞生起就存在于 iOS 的 UIGraphicsBeginImageContext WithOptions API 而是切换到 UIGraphicsImageRenderer 格式 你可以节省很多内存 因为 UIGraphicsBeginImage ContextWithOptions 总是一个 每像素 4 字节的格式 它总是 SRGB 格式 所以只要你不想 你就不会得到宽格式 也不会得到 每像素 1 字节的 A8 格式 如果你使用在 iOS 10 中引入的 UIGraphicsImageRenderer API 在 iOS 12 中 它会自动为你选择最好的图形格式 这有一个例子 假设我画了一个圆作为一个蒙版 使用旧 API 高亮的部分是 我的绘制代码 只是为了绘制一个黑色圆圈 我得到的却是每个像素 4 字节的格式
如果我转而使用新的 API 我使用的是完全相同的绘制代码 通过使用新的 API 我现在得到的是每个像素 1 字节的图像 这意味着它减少了 75% 的内存使用 在保证相同保真度的同时 也获得了可观的内存节省 另外一个好处是 如果我想再次使用这个蒙版 我可以在一个 imageView 上 改变 tintColor 而且只用一个点号就可以做到 这意味着我不必再分配内存了 我不仅可以把它 设成一个黑色的圆圈 还可以设成蓝色的 红色的 绿色的圆圈 且没有额外的内存占用 这很酷
我们通常对图像做的另一件事是 对它们进行下采样 当我们想要制作 比如缩略图的时候 我们想要缩小它 我们不应该 用 UIImage 进行缩小 如果我们使用 UIImage 绘图 由于内部坐标空间变换 这种方法性能并不高 就像我们之前看到的 它会解压缩内存中的整个图像 取而代之 我们可以使用 ImageIO 框架 ImageIO 可以对图像进行下采样 它使用 Streaming API 这样你只需为生成图像 使用一些脏内存 这将为你节省一个内存峰值 例如 这里有一些代码 以及我在磁盘上获得的一个文件 也可以是我下载的一个文件 我现在用 UIImage 绘制一个 更小的矩形 仍然会有一个大峰值
如果切换到 ImageIO 我仍然需要从磁盘加载文件 因为它是一个较低级的 API 我设置了一些参数 来表示我希望这个图像有多大 所以我让它用 CGImageSourceCreateThumbnailAtIndex 创建图像 现在 我可以用 UIImage 封装这个 CGImage 并准备好进行下一步了 我有一个小得多的图像 而且比之前的代码快 50%
我们要讨论的 另一件事是 如何进行后台优化 假设我在一个 App 中 有一个全屏的图像 它很美 我很喜欢 但之后 我需要 到我的主屏幕上处理通知 或者转到 另一个 App 上 那张图像还在内存中
经验之谈 我们建议你卸载 看不到的大型资源 有两种方法可供选择 第一种是 App 生命周期 如果你把你的 App 放在后台或者前台 App 生命周期事件 可以帮助你了解它 这主要适用于 屏幕上的视图 因为它们不遵循 UIViewController 外观的生命周期 UIViewController 方法 适用于标签控制器 或导航控制器 因为你会有多个视图控制器 但只有一个出现在屏幕上 如果你利用 viewWillAppear 和 viewDidDisappear 的代码或回调 就可以使 内存占用更小
举一个例子 如果我为进入后台的 App 注册通知 我可以卸载我的大型资源 在这里就是图像
当 App 回到前台时 我就会收到通知 如果我在这里重新加载图像 当用户返回时 我就可以在后台保存内存 并保持同样的保真度 这对他们来说是完全一样的 但是系统有 更多的内存可用 与此类似 如果我在导航控制器 或标签控制器中 我的视图控制器可以 在图像消失时卸载它们 在返回 viewWillAppear() 方法之前 我可以重新加载它们 用户还是不会注意到 有什么不同 我们的 App 如今使用更少的内存 这很好 现在 我想邀请 Kris 用一个很好的演示 向你们展示之前的内容 Kris 好的 我现在要 切换到演示机器 我们开始吧 我一直在开发这个 App 这些是我从 NASA 那里得到的 太阳系的高分辨率图像 这个 App 可以让你 对它们应用不同的滤镜 接下来我们会看到一个 简单的例子 在太阳上应用一个滤镜 我对目前的进展非常满意 所以我把它发给 James 征求他的意见 他给我回了一封 带有两个附件的邮件 一个附件是 Memgraph 文件 另一个是这个图像
James 是一个相当保守和低调的人 所以当他发送了两个红色的惊叹号 和一个尖叫的表情符号时 我知道他很难过 所以我去找 James 我说 “你知道 我不明白这有什么大不了的 很明显我需要 再使用至少 0.5 GB 才能出现内存不足的情况 然而我还有一些可用的内存 我难道不能用它吗”
James 一个比我 优秀得多的开发者 他指出了一些 我的逻辑有问题的地方 首先 这个测量计 测量的是一个有 2GB 内存的设备 并不是所有的设备 都有那么多的内存 如果这段代码运行在 只有 1GB 内存的设备上 那么很有可能 我们的 App 已经被操作系统终止了 其次 操作系统 在决定何时 终止 App 时 不仅依照你的 App 使用的内存大小 还依照操作系统中的其他内容 所以仅仅因为 我们还没有耗尽内存 并不意味着我们没有被终止的危险 最后 这对用户来说 是一种糟糕的体验 事实上 如果你查看 使用比较图表 你可以看到其他进程的 内存为 0KB 那是因为它们都被 操作系统抛弃了 只是为我们的 App 腾出空间 你们可能都在静静地看着我 然后摆出一个嫌弃的表情 因为当用户想去 看你们的 App 时 它必须从头开始加载
James 说得很有道理 我认为 总的来说 我们应该让这个内存指针 尽可能向左 而不是向右 看看我们能做什么 让我先来看看 Memgraph 文件 我有一些 在使用 Memgraph 文件 的日常技巧 或者说是策略 第一个 我需要把它向上拖动一下 就是寻找泄漏 如果我前往过滤器工具栏 轻点泄漏过滤器 它就会显示 Memgraph 文件中的 每一个泄漏 这个 Memgraph 文件没有泄漏 这既是好消息 又是坏消息 好处在于 没有泄漏 但现在我得弄清楚 到底发生了什么 Memgraph 的 另一个好处是 告诉我一个对象 在内存中有多少个实例 以及是否比我预期的要多 当我查看这个 Memgraph 文件时 如果我特意关注 代码中的对象 就可以看到 内存中只有 5 个对象 且每种都只有 1 个 如果内存中 有多个 RootViewController 多个 NoirFilter 多个滤镜 或其他预料之外的对象 那些就是我可以调查的东西 这里的实例数量 在我的预料内 但也许其中存在一个很大的实例 尽管不太可能 我还是得检查一下 所以我需要使用内存检查器 我要看看这些 它们中的每一个 都列出了每个对象的大小 我可以看到我的 AppDelegate 是 32 字节 DataViewController 是 1500 字节 当我浏览每一个的时候 没有一个明显地占用了 我的 App 正在使用的 1GB 多的内存 这就是我在 Xcode 中 处理 Memgraph 的技巧 我接下来要做什么 我刚刚看了这个 关于在 Memgraph 文件中 使用命令行工具的 WWDC 讨论会 让我来试试 能不能找到什么 回想起来 James 提出的第一件事 就是使用 vmmap 的 -summary 参数
所以我来试试 传入 Memgraph 文件 我们来看看这个输出
现在 我应该在这里 寻找什么呢 总的来说 我在寻找非常大的数字 我想弄清楚 是什么在使用这些内存 大的数字意味着更多的内存使用 这里有很多列 有些列比其他列更重要 首先 “VIRTUAL SIZE(虚拟内存大小)” 虚拟意味着不是实际的 我几乎可以忽略这一列 它是 App 所需的内存 但不一定要使用 脏内存听起来像是 我绝对不希望在 App 里存在的东西 我希望我的 App 是干净的 而不是脏的 所以我想要这个数字尽量小 然后交换内存 因为我们谈的是 iOS 所以指的是压缩内存 正如 Kyle 和 James 之前提到的 操作系统凭借 脏内存大小 加上压缩内存大小的总和 来确定我的 App 实际 实际使用了多少内存 所以这就是我想要 关注的两列 我们再来看一些较大的数字 我马上就看到了 “CG image” 非常显眼 它占用了非常多的 脏内存和交换内存 这是一个危险信号 让我们继续观察 我可以看到 “IOSurface” 占用了很多的 脏内存 但不占用交换内存 “MALLOC_LARGE” 占用了很多脏内存 但占用较少的交换内存 之后就没有 这么大的数字了 基于我在这里看到的 我认为我应该集中处理 CG image 的虚拟内存区域 让我们把它复制下来 下一步该是什么呢 我们想要了解更多 关于虚拟内存的信息 所以 vmmap 似乎还是我们要用的工具 这次我将不再 使用 -summary 参数 而是传递我的 Memgraph 文件 但我只关心 “CG image” 的内存 并不关心 vmmap 会告诉我的其他 虚拟内存区域 所以我应该使用 “grep” 只向我展示 关于 “CG image” 的行 让我们看看会发生什么 现在 我有三行信息 我可以看到 有两个虚拟内存区域 在那里我可以看到 它们的起始地址和终止地址 然后我可以看到 和之前相同的列 分别是虚拟内存 常驻内存 脏内存和压缩内存 这里显示的最后一行 是总结行 也就是和上面一样的数据 看看这两个区域 我有一个很小的区域 和一个很大的区域 我显然更想了解 这个大一点的区域 那么我怎样才能找到 更多关于这个虚拟内存区域的信息呢 我查看了 vmmap 的文档 然后注意到一个 -verbose 参数 顾名思义 它会输出更多的信息 我想知道它能告诉我什么
让我们继续 传入 -verbose 和 Memgraph 文件
同样 我只关心 “CG image” 区域
所以我用 “grep” 来进行过滤 现在我看到了 更多的区域 为什么会这样 默认情况下 如果 vmmap 找到连续的区域 它会把它们 合并在一起 实际上 如果你从第二行开始看 这个区域的终止地址 和这个区域的起始地址 是一样的 下面也一样 因此 vmmap 在默认情况下 将其折叠成一个区域 但是看看这里的细节 却能发现一些 不同之处 特别是其中一些区域 使用了更多的 脏内存 而另一些使用了更多的压缩内存 这就会帮助我找到 应该关注的地方 但这里我要用 另一种策略 我知道 操作系统中 虽然不一定 但一般来说 虚拟内存区域创建得越晚 在 App 生命周期中 它发生得就越晚 由于这个 Memgraph 文件 是在内存使用峰值时获取的 所以很有可能 这些后面的区域 与导致峰值的原因更有关联 所以我不想寻找 最大的 脏内存 和压缩内存数字 而是要从底部这里开始 我要获取最后一个区域 的起始地址 我该怎么做呢 James 提到的一个工具是 heap 但它作用于堆上的对象 而我正在处理一个 虚拟内存区域 所以它并不适用 还有 leaks 工具 但是我这里并没有泄漏 我已经从 Memgraph 中 知道了这里没有泄漏 所以它看起来不像是 我可以使用的工具 但是我查看了 关于 leaks 的帮助信息 发现 leaks 可以做很多事情 包括告诉我 哪些对堆上的对象 或虚拟内存区域有引用 我们来看看 它会告诉我们什么 我将使用 leaks 然后传递 -traceTree 参数 它的作用是 给了我一个树形视图 可以涵盖所有 与我要传入的地址有关的东西 在这个例子中 我传入的是 虚拟内存区域的 起始地址 最后我们提供 这个 Memgraph 文件 它会是什么样子呢 这里我们能看到 所有引用的树 如果我们向上滚动到顶部
在这里 我可以看到 这是我的虚拟内存区域 这是我的 “CG image” 区域 然后我可以看到 这个树视图 包含了所有具有引用的东西 以及引用它们的东西 以及引用这些东西的东西 等等等等 如果我们回到 Xcode 我们实际上过滤了 相同的地址 我来看看这个对象 这个树视图和我从 leaks 中 看到的一样 如果我想的话 我可以沿着树走下去 并展开每一个节点 看看每个节点的细节 但是这需要一段时间 而且有点乏味 leaks 的输出的优点是 我不仅可以 快速浏览它 还可以随意搜索或筛选 或者我可以把它放进 一个 Bug 报告或电子邮件中 然而我却不能对 Xcode 中的图形视图 进行上述的操作 那么在这个 leaks 的输出中 我要找什么呢 理想情况下 我会找到一个 我负责的类 一个来自我的 App 的类 我之前看过这个 我知道这里 没有我的类 那么我还能找到什么呢 我正在创建的类 比如一个框架类 它也许是以我的名义创建的 也可能是我 直接创建的 我知道我的 App 有 UIViews 它有 UIImages 我可以用这些 Core Image 类 来进行过滤 我们继续看这里 我用的是一个非常复杂的 叫做 “My Eyeballs” 的调试工具 我们继续寻找 我看看能不能找到我想要的 这是一个很大的终端输出 所以让人更加困惑 举个例子 这里有一个字体引用 我知道我的 App 使用字体 但是字体并不会导致 很多的内存使用 所以它没有帮助 我们再往下看 我可以看到有很多 这样的 CI 类 它们是 Core Image 滤镜 或者是 Core Image 在我的 App 中 起滤镜作用的东西 它也许也是我该 进一步研究的东西 我已经做了 但没有发现任何有用的东西 我无法进一步研究 leaks 的输出 这很不幸 接下来我该做什么呢 幸运的是
James 在捕获 这个 Memgraph 时 打开了配置内存的回溯记录 这意味着我可以使用 他谈到的另一个工具 来查看对象的创建回溯 我将使用 malloc_history
这一次 我先传入 Memgraph 文件 然后再传入这个从帮助文档中获知的 -fullStacks 参数 它的功能就是 使每一帧都在它自己的行上显示 这样让人更容易阅读 然后我将传递 虚拟内存区域的 起始内存地址 让我们看看是什么样子
实际上这并不是一个 很大的回溯 我可以看到我的代码 出现在这里的几行 第 6 行到第 9 行 实际上来自我的 App 代码 我可以在第 6 行看到 NoirFilter.apply() 函数 负责创建 这个特定的虚拟内存区域 这是一个很好的证据 展示了我如何在 App 中 找到造成这些内存使用的东西 如果我们回到 Memgraph 文件 就可以发现这和 Xcode 中 出现的回溯是一样的 你可以看到这里 也是 NoirFilter.apply() 方法 我们没有像 通常在回溯视图中看到的那样 得到很好的高亮显示 因为我们没有调试一个活动进程 我们正在加载一个 Memgraph 文件 但是你可以看到它的输出 和我们从 malloc_history 中得到的输出 是完全一样的 事实上 为了进一步确认 我需要查看 “CG image” 虚拟内存区域的完整列表 接下来我选取了 倒数第二个 我们来看看 这个区域的回溯
结果是相同的回溯 同样的代码路径 也指向这个区域 如果继续观察 其中的几个区域 实际上仍使用了相同的回溯 这样我就明白了 在我的 App 中 是什么创建了这些 占用了 App 中 大量内存的 虚拟内存区域 那么我们能做些什么呢 让我们回到 Xcode 我现在可以关闭 Memgraph 文件
我要做的第一件事是 看看这里的代码 如果看看与我的滤镜相关的代码 我可以看到这里是 apply() 函数 我可以马上看到 一些东西跳出来 它们是我正在使用的 UIGraphicsBeginImageContextWithOptions 以及 UIGraphicsEndImageContext 我记得 Kyle 说过 你们不应该使用它 在那些情况下有更好的 API 可以使用 这是我肯定想要 再次讨论的内容 但我首先需要的 是某种基线 我需要知道 我的 App 使用了多少内存 这样我就可以确保我的更改 能让结果变得不同 我要运行 我的 App 然后找到调试导航器 查看内存报告 现在 我可以看到我的 App 在运行时使用的内存 我真的很喜欢这个 土星北极的图像 它是一个奇怪的六边形 炫酷的同时 又有点古怪 我们来看看这个 应用这个滤镜 然后看看会得到什么 1GB 3GB 4GB 6GB 7GB 这可不太好 不过可以 很好地告诉我们 它根本不会在设备上流畅运行 所以当你在模拟器中运行时 你必须记住 它对于调试和 测试变更很有用 但是你也需要在设备上 验证所有的东西 但同时 另一件好事是 模拟器永远不会耗尽内存 如果我遇到了 App 在设备上 被关闭的情况 可以在模拟器中试试 我可以等待一个 非常大的分配 而不会被关闭 然后从那里进行调查 我想指出的一点是 我们其实给你们看了 这里标志的内存峰值 在这个例子中 我最多使用了 7.7GB 这很糟糕 我们来看看能做些什么 回到我的 apply() 函数 现在 我想使用这个 beginImageContextWithOptions 但是回想一下 Kyle 说过的 当你处理图像时 内存使用中 最重要的是什么 是图像的尺寸 让我们看看它是什么样子 我要再次 使用这个滤镜 我在调试器中停止时 就可以看到 这个图像的尺寸 在我按回车 我要先喝一小口水 我其实并不想喝水
它是 15000*13000 我检查过文档 在 UIImage 上 那是点(pt) 不是像素(px) 如果这是 2X 设备 或 3X 设备 你必须把它乘以一个很大的数字 Kyle 会很生气 因为一张图片就占用了 10MB 没人告诉他这件事 为了证实这一点 我想尝试一下 我要将 15000 乘以 13000 iPhone X 是一个 3X 设备 所以是 3 倍的宽度 乘以 3 倍的高度 再乘以 4 字节每像素 这个数字看起来很熟悉
所以我很确定 我清楚地知道到底是什么 使用了 7.5GB 的内存 并不一定是 我的 beginImageContext 而是图像的尺寸 没有理由需要 这么大的图像 我要做的是把它缩小成 和我的视图 相同的尺寸 这样 它就会占用 更少的内存 那么 我要回到 上面的图像加载代码 实际上 在我这么做之前 我想先禁用这个断点 让我们看看 它是做什么的 很简单 它从一个 Bundle 中获取 URL 它从那个 URL 中 加载一些数据 并将其加载到 UIImage 中 然后传递给滤镜 我想做的是 在我把它发送到滤镜之前 我想缩小这个图像 然而 我还记得 Kyle 说的 我不应该在 UIImage 上进行缩放 因为它仍然会 把整个图像 加载到内存中 这是我要避免的 把这个函数 折叠起来 我要用 Kyle 建议的代码 来替换它 好的 我们来看看 这段代码在做什么
这里是一样的 我们从 Bundle 中获取图像 但是这一次 我得稍微调宽一点 我调用了 CGImageSourceCreateWithURL 来获取对图像的引用 然后将其传递给 CGImageSourceCreateThumbnailAtIndex 现在 我可以将图像 缩放到我想要的大小 而不需要将整个内容载入内存 让我们试一试 看看会不会有什么不同 我将重新构建 然后等待它 在 App 上启动
重新生成中 哦 有一个警告 我需要再调整一下这个
再来看一下
好了 正在构建
构建 构建 构建 好的 正在启动 没有问题 现在 让我们来看看 内存报告
让我们回到 我一直很喜欢的 土星北极的图像
我们应用这个图像 看看会占用多少内存 现在是 75 93MB 在这里 我们的内存峰值 是 93MB 明显地改善
这要比 几乎百分百被关闭的 7.5GB 的内存占用要好得多 但现在 我记得有件事 我想回去 先停止运行 我还是想回到 我的 filter() 方法 改变这个 UIBeginImageContext 代码 然后按照 Kyle 的建议去做 所以我要删除这段代码 然后添加新的滤镜
在这里 我要创建一个 UIGraphicsImageRenderer 我要在这个渲染器中 使用 CIFilter 来应用这个滤镜 让我们运行这段代码 希望它可以成功构建 然后看看是否会对我的内存使用 产生影响
让我们回到调试导航器 和内存报告中 再一次 我们回到土星图像 然后应用我们的滤镜 让我们来看看 这次的内存峰值是多少 98MB 这和上次基本是一样的 但是如果你仔细思考 这个其实就是我所期望的结果 在这种情况下 我的图像仍然是 每个像素 4 个字节 所以我不会使用 这个新方法来节省内存 然而 如果有一个 节省内存的机会 例如 如果操作系统可以判断 图像可以每像素使用更少 或更多的字节 那么系统就可以做出正确的处理 我就不需要担心了 因此 在代码经过这些更改后 即使我没有看到很大的改进 我也知道它变得更好了
我还可以做更多 我想确保当 App 进入后台时 我们会卸载图像 而且我们不会在屏幕之外的视图中 显示任何图像 我还有很多可以做的 但是我对这些结果 已经很满意了 所以我想把它们送回给 James 我要抓取一个屏幕快照 并给 James 添加一点备注 让他知道 我对这一切是多么的满意 我想我们可以 给他发一个
星星眼的表情符号 希望 James 会对这些结果 感到满意 现在 我想请回 Kyle 他会帮我们整理一遍 谢谢大家 谢谢 Kris
谢谢 Kris
太棒了 只做了一点点工作 我们就能大大减少内存的使用 总结起来 内存是有限的 也是共享的
我们使用得越多 系统就为其他 App 分配越少的内存 我们真的需要做一个好市民 并注意我们对内存的使用 只使用我们需要的内存
在调试时 Xcode 中的内存报告是至关重要的 当我们的 App 运行时我们就可以打开它 因为当我们监视它的时候 随着调试的进行 我们能注意到内存使用的消退
我们要确保 iOS 可以为我们选择图像格式 通过使用新的 UIImage 的 GraphicsRenderer API 我们可以从 SRGB 到 alpha 8 的转变过程中 节省 75% 的内存使用 这对蒙版和文本来说都很重要
除此之外 我们可以使用 ImageIO 来对图像进行下采样 它可以防止过高的内存峰值 相较于将 UIImage 绘制到 更小的环境中时 它也会更快 最后 我们要卸载 不在屏幕上的 大型图像和资源 使用这些内存是没有意义的 因为用户看不到它们
即使经历了所有这些努力 我们仍然没有完成
正如我们刚才看到的 使用 Memgraph 可以帮助我们 进一步了解发生了什么 并减少内存占用 结合 malloc_history 我们可以深入了解 内存的去向以及用途
所以我建议 大家能在讨论会后打开 malloc_history 分析你的工具 然后开始深入研究
要了解更多的信息 你们可以查看我们的幻灯片 除此之外 如果你们还有其他问题的话 我们稍后会去技术实验室
谢谢大家 希望你们享受 WWDC 中的其他讨论会
[ 掌声 ]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。