大多数浏览器和
Developer App 均支持流媒体播放。
-
检测和诊断内存问题
探究如何了解和诊断 Xcode 的内存性能问题。我们将带您了解 Xcode 工具的最新更新,探索 Metrics,查看 XCTest 中的 memgraph 集合功能,并学习如何使用 Performance XCTest 发现性能退化。
资源
相关视频
WWDC23
WWDC22
WWDC21
WWDC19
WWDC18
-
下载
嗨 我是坦努加 我是一位程序员 任职于OS性能团队 今天斯特凡和我将会谈及 如何侦测并诊断 存在于你的应用程序上的内存问题 我们首先要来探讨内存占用 对一个应用程序所带来的影响
再来我们会讨论到可以用来分析 你的内存用量性能的工具 以及你可能会遇到的内存问题类型
让我们直接切入主题吧
你可能马上想问的第一个问题是 为什么我需要在乎 应用程序的内存占用? 最关键的原因是 它会让你的应用程序用户体验 变得更好 在系统里有一定的内存量 通过监控应用程序内存 来避免系统终止运行以回收该内存
这表示你的应用程序可持续 在背景保持它的状态 这样很棒 因为运行内存需要时间 而让你的内存占用保持紧凑 会增加你的应用程序 维持在内存的机会 让程序被激活得更快
降低你的内存使用 也会引起更活跃 反应更为敏捷的体验 这就是你的用户在探索新功能时 最为需要的
通过策略性安排你的应用程序 将如何被写入内存 它将免去用户 使用程序时等待回收内存的时间成本
策略性安排你的内存使用 也会让你能将更广泛的功能 加到你的应用程序中 像是加载视频 包括动画短片 以及其他更多
最后 我们的设备随着时间不断进化 而我们的新设备 比以前有更多物理内存 通过减少你的内存占用 你的程序在旧设备上 也会表现得一样好 并增加能愉快使用你的程序的人
通过监控你的程序内存占用 能让程序更快被激活 也更敏捷 能处理复杂的功能 并在更广泛的设备上有好的表现 我们现在来看内存占用的组成
我们将你的内存占用 分成三个类别做性能分析 脏内存、压缩内存与干净内存 我们很快地来看一下 它们分别涵盖什么
脏内存包含你的程序写入的内存 也包括了所有的堆分配内存 像是你使用的malloc函数 已解码的图像缓冲 以及框架
压缩内存是指任何 被内存压缩器进行过压缩的 那些近期未被访问过的脏页 这些页面会在被访问时被解压缩
要注意在iOS系统里 没有swap分区的概念 它是专属于macOS系统的
最后我们有干净内存 干净内存指的是那些尚未被写入 或可被系统清出并重新加载的数据 举例来说 这些可能是内存映射文件 像是存储在磁盘上 但已加载到内存的图片 或者它们也可能是框架
当我们提到你的程序内存占用 我们真的是在讨论程序的脏内存 以及压缩内存 不算进干净内存
这是对你的内存占用 较为高层的理解 要有更深入、更细节的说明 我们建议你查看2018年WWDC 所发布的iOS内存深入研究 我们现在来看一下你可以用来 对你的内存做性能分析的一些工具
Xcode提供一套可协助全面监控 你的程序内存表现的工具 涵盖开发及生产环境工作流程
XCTest测试框架直接在 程序项目单元和界面测试中 帮助你监控程序内存占用 而MetricKit跟Xcode Organizer 能让你在生产环境中监控 从你的用户身上获得的内存指标
这个讨论将会延伸到 使用XTest的成效分析 但这些技术仍适用于 一般内存分类跟侦测
通过XCTest成效分析 你可以估量系统资源 像是内存利用、CPU使用量 磁盘写入 还有其他很多信息 让我们一起来看一下 一个测试范例吧
如果我是Meal Planner的程序员 这个程序帮助你安排 一个星期会吃的每一餐 然后我想要测量 我新加入的存储餐点功能 所占用的内存使用量 它让用户能下载食谱到他们的设备上
在我的成效分析中 我使用measureWithMetrics API 而我特别指出我想要测量 目标程序的内存用量 在测量指令的程序代码区块里 我要启动该程序 手动调用测量的API 开始测量 然后点击存储餐点的按钮
通过查看界面上的更新 我等了快30秒直到食谱下载完成
现在我可以直接在Xcode的界面上 跑这个测试 来获得我的测量结果
我可以点击测试旁边的灰色钻石图示 来查看我的测量结果 该结果的弹出框界面有下拉选单 让我知道有哪些被测量的指标
底部的条线图则展示了 每个迭代的测量结果
五个迭代的整体平均 会被算出来并显示在上面
我现在可以决定 是否设定这次测试的平均 为未来测试的比较底线
未来测试平均如果比设定的底线大 就是失败了 我们将这个与底线间的偏差称作回归
回归代表我们应该停下来和侦测 并修复我们的程序代码让测试能够通过
我们很兴奋能在Xcode 13与大家分享 我们加入了一个 能够搜集诊断结果的功能 来协助将这些回归测试加以分类 有两个非常有价值的诊断结果 ktrace档案和memory graphs
Ktrace非常强大并具有多种功能 它们可以用于普通的系统侦测 或独立追踪特定问题 像是在侦测技术问题时 深入探讨渲染管线 或是查看你的主线程阻塞的原因 造成无响应的表现
这些ktrace档案可以用Instruments 在你正常的工作流程内被打开 并加以分析
第二个诊断结果 对特定内存进行侦测来说特别好 Memory graphs可以 跟Xcode visual debugger 以及许多命令行工具一起使用 我们稍后会谈到其中一些
Memory graph本质上就是你的进程 地址空间的实时映射 Memgraphs记录每个虚拟内存区域的 地址跟尺寸大小 以及每个malloc分配模块 以及那些区域及模块间的重点信息 这让你能够进一步查看 堆内存上的个别对象 一览跟相连框架有关的区域数据 以及更多
XCTest会自动化malloc日志堆栈 会为刚分配的对象获取backtrace函数
要开始搜集诊断结果 使用xcodebuild命令行工具 并结合 enablePerformance TestsDiagnostics旗标 这个旗标会为了nonmemory指标 让ktrace搜集开始运行 也为了内存指标驱动memgraphs
一旦我们之前写入的性能测试 跑完了之后 我们会看到下列信息 被打印在管理中心 这还挺多的 但可以查看其中 特别关键的几个东西
第一件事情是要看测试是否通过 在这个范例里 测试失败了
输出信息内也指出测试失败 特别归咎于一个回归 我们新的平均值比底线差了12%
最后我们能够找到 xcresult bundle的路径
当我们在Xcode里打开xcresult bundle 我们会在顶端看到内存测量结果 就在测试名称的旁边
我们就能展开测试日志 并在最底部 能够找到我们相连的memgraphs
下载并解压缩后 我们会有两个memgraphs 这是因为我们在你的测试上 加了一个迭代 来运行mallock日志堆栈 我们在被测量的迭代开始的时候 搜集最初的memgraph 文件名前缀为pre 然后在迭代结束之际 我们搜集到第二个memgraph 文件名前缀为post 这让你能够在需要的时候 分析内存在单个迭代过程中的成长
有了ktrace档案跟memory graphs 以及malloc日志堆栈的运行 你不但能判断回归是否发生 也能知道它为何会发生了 我现在会交棒给我的同事 斯特凡 来谈谈你可能 在查看搜集到的memgraph诊断 所碰到的不同内存问题的种类 谢谢坦努加 嗨 大家好 我是斯特凡 是一位任职于 OS开发表现团队的程序员 我将会谈到一些你可能会在程序里 所发现的常见内存问题 以及你能如何诊断 修复 并且避开它们
我会谈到两种内存问题 内存泄漏和堆区内存 可以进一步被归类为堆分配回归 以及碎片化问题 这不会是一张令人疲惫的列表 但已涵盖了最常见的一些问题 我也会谈到一些命令行工作流程 可以用来诊断这些问题 如果想要深入探讨命令式工具 请查看WWDC2018的 iOS内存深入研究谈话
我们先从内存泄漏开始讨论吧
在进程分配一个对象 但没有释放它的情况下 却失去所用相关引用时 就会产生泄漏 我在这里有一个范例对象图标 灰色箭头标示出对象之间的引用 注意每个对象都至少有一个相关引用
也看到对象A到B以虚线表示的引用 比方我把这个引用设定为nil 移除它
而那个引用不见了之后 对象B就被泄漏了 它就完全没有相关引用了 它还是脏的 但进程没有办法引用它 在它exit之前都没有办法释放它 因此你应该一直修复泄漏
对象在Swift很常因为循环引用被泄漏 在这个图示里 对象A和B在一个循环引用中 它们互相引用 但没有其他外部的引用跟任一相关 这表示进程没有办法调用或释放它们 所以它们被判定为泄漏
很幸运地 大多在Swift的对象是被 Swift的自动引用计数系统 也就是ARC所管理的 以避免更多的泄漏 如果你使用不是被ARC管理的对象 像是UnsafePointers 那要确保你在 失去所有相关引用前 要先释放它们
即使是ARC管理的对象 都很容易落入循环引用 因此要避免在你的程序代码里 创造强循环引用 如果循环引用真的很必要 那就考虑用一个弱引用吧 因为弱引用不会阻挡对象被释放
我们来看Meal Planner程序的例子 坦奴加寄给我一些从失败的XCTest里 获取的pre跟post memgraphs 我马上想要查看 post memgraph里的泄漏信息
为此我要在memgraph上运行Leaks 这会显示一些有用的信息 让我知道任何有发生的泄漏
输出结果显示我有 占240泄漏位元的4个泄漏
再来在输出结果内也包含了 每个泄漏的对象图内的细节面貌 给我一些线索找出可能泄漏的东西
在对象图的顶端标示着ROOT CYCLE 这表示我在处理一个循环引用
这边有一些有用的代号 看起来这个循环引用可能包含了 MealPlan跟MenuItem两个对象
因为XCTests有运行malloc日志堆栈 输出结果也同样包括每个泄漏 分配调用的堆栈 这对要找出哪些对象被泄漏 是非常有帮助的
常常你会想找出程序码里 那段有着代号的调用堆栈 这是我的程序码内调用堆栈的部分
泄漏的MealPlan对象被分配在 populateMealData功能里 我会打开Xcode 来看我可否修复该问题
这是我在泄漏中看到的 populateMealData功能 这里我正在分配一个MealPlan对象 跟一个MenuItem对象 是我在循环引用里 所查看到的两个对象 嗯 这个addMealToMealPlan功能 看起来有点可疑 我来看一下
所以看起来我是在Mealplan上 调用addItem 但我也在MenuItem上调用addPlan 这是个让我们能够一览餐点上 所有项目的功能 但也跟规划一份餐点有相关
在MealPlan里 addItem把MenuItem 加入到一个数组里 存储相关的引用 而在MenuItem中 addPlan则存储了跟mealplan的引用 所以这肯定是一个循环引用 因为它们同样对彼此拥有强引用
一旦populateMealData exit MealPlan跟MenuItem对象 也会不再作用 也就不会有相关的外部引用了 但它们还是会彼此引用 造成泄漏
我应该要来寻找一个 不用循环引用的解法 但先用快解 我会把MenuItem 对它的Mealplan对象换成 使用一个弱引用 这样就会打破那个循环引用 因为我们不再有两个强循环引用
我们现在转换一下去看堆分配回归
堆就是你的进程地址空间区 存放动态分配对象的地方 堆分配回归 就是由于进程 分配比以前还多的对象在堆上 所造成的内存占用 为了减少堆分配回归 要移除没有使用的分配 并缩小非必要的巨大分配 你也应该要注意 你同时掌控的内存有多大 释放你没有在使用的内存 等到你需要的时候再分配内存 这就会减少你的程序高峰占用值 让它不太会被终止运行
那我们回头来看Mealplanner程序 失败的XCTest 并查看是否有堆分配回归 为了了解我应该查看哪里 我会同时在在pre跟post memgraphs上 跑vmmap-summary 来获得一个不错的已使用内存总览
我在pre memgraph上的内存占用 大约有112兆字节 而在post memgraph 我的内存占用约有125兆字节 所以大概有13兆字节的差异
再来在输出结果中也看到 我的进程内存使用被分作不同区
由于我怀疑这是个堆分配回归问题 我想要开始用MALLOC_ 来查看那些区 因为那些区涵盖了 我所有在堆区内的对象
记得坦奴加的公式 内存占用=脏内存+压缩内存 在这个工具里 “交换”代表“压缩” 所以在这些栏位中我只在乎 “脏尺寸”跟“交换尺寸”
而当然输出结果也显示 MALLOC_LARGE区 大概有13兆字节的脏内存 那跟我的回归尺寸大小差不多 所以我当然想要进一步查看它 下一步要来找出是哪些种类的对象 造成那些13兆字节回归 为了获取那样的信息 我会在post memgraph上 跑heap -diffFrom
我会传pre跟postmemgraphs当作参数 这会显示什么对象 存在于post memgraph的堆区内 但不存在于pre memgraph的堆区里
靠近顶端输出结果显示我大概有 13兆字节大小的新对象 在post memgraph上
在下面堆区内存用对象类别来分类 针对每个对象类别 输出结果都会展示对象数量 以及那些对象 以兆字节计量的所占大小 我马上就发觉我有 大约13兆字节左右大小的 non-object类别 在Swift语言里这通常代表 原生的malloced字节 这种对象通常有点难追踪 但我可以用某些工具来获取一些信息 首先我想要这些non-object的地址
我会跑heap --addresses来找出它们
我会指定我只要尺寸大小 至少500千字节的non-objects
啊哈! 这个non-object大概有13兆字节 所以它会是这次调查的首要可疑之处 我会获取这个地址来看我能不能 有一些线索来找出它究竟是什么 至此我有几个选择 根据情境分别有个别的好处 所以我会简单地一个个说明
其中一个选择是我可以在这个地址上 跑leaks --traceTree
这可以让我看到 引用这个地址的树状对象图 当我想要获得一个特定对象的 更多信息时 这就会相当有用 或当我的memgraph 没有Malloc日志堆栈 或MSL运行时也很有用 我们记得XCTest memgraphs 自动允许MSL运行 但如果你碰到了一个 没有这样做的memgraph 记得用这个工具
我标记出在树状图里 看起来有关联的对象 我的巨大non-object 可能跟在MKTCustomMeal PlannerCollectionViewCell里 这个mealData对象有关 我也可以跑leaks --referenceTree
这给我一个从上至下的引用树状图 包含我的进程里的所有内存 来猜想哪些对象会是根源 根据这个输出结果我可以推论出 我的程序里内存都集中在哪 当我知道我有一个巨大的回归 但我不知道是哪些特定的对象 所造成的时候 这个工具会非常有帮助 我可以传-groupByType参数 来将同样类别绑在一起 缩小输出 让它更容易被解析
常常有很大一部分的回归 会在这个树状图里 一起被绑在一个节点下 让找到一些跟这个内存相关的线索 更为容易
同样地我也标记出 那个显示相关对象的段落 这是同样的mealData对象 跟我在leaks-traceTree输出结果中 看到的一样 输出结果显示大约有 13兆字节大小的内存 被分配给这个mealData对象 我会很想知道这个对象 是怎么被分配的 因为我的memgraph有MSL运行 我可以用malloc_history-fullStacks 找出来
我会传巨大non-object的地址 是我之前从heap-addresses中获取的
然后我会针对在那个地址的对象 获得分配调用堆栈 当我有MSL在运行 并且有我在乎的对象地址时 这是非常有用的
所以看起来我的mealData对象 是被分配到 saveMeal功能里的第三行里列出来了 我会换去Xcode看看到底发生什么事 这个saveMeal功能 在我的自定义表格视图控制器里 而这就是罪魁祸首 我要重新分配这个原生缓冲区域 然后用这个mealData对象将它包起来 我重新分配这个缓冲区域 是为了复用它 并将结果写入磁盘 写入磁盘完成后 我就不再需要这个缓冲区域了 那为什么它还在?
这个嘛 mealData是类别的一员 所以只要这个类别单例存在 引用也还是会存在 这表示当我在任何表格内 点击saveMeal 那个表格会被分配 并有一个很大的缓冲区域 直到表格被移除前都会存在着 如果我存储多个餐点 那个内存会变得相当大
那我要怎么修复它? 有一个选择是 在saveMeal功能里定义mealData 但我知道这在某处类别里已被用过 所以我不想这么做 另外一个方法是 在我完成磁盘写入之后 将mealData设定为nil 在Swift里的Data对象很聪明 能在我没了最后的引用时 自动释放缓冲 这样在功能跑完后 这个缓冲就不会存在了
最后我们来谈谈碎片化
我们很快地来看看 页在iOS里是如何运作的 一个页是固定大小、独立的内存单元 是系统给予你的进程的
也由于页是独立的 当你的进程写入任何一部分的页 整页就变成脏页 也会占到你的进程 就算大多都是未使用的也一样
碎片化会在进程有了脏页 却没有100%利用它时发生 为了了解发生的原因 我们看一下一个范例 首先我有三张连续的干净页面
当进程开始运行 分配会开始填入这些页 并弄脏它们
当对象被释放后 它们原本的空间会留下空区块 在图表里标记为“空闲内存” 但是这些页还是脏的 因为它们上面还是有些分配对象
系统会尝试着将未来的分配 填入这些空区块 我有一个很大的新内存分配 像是右边的盒子标示出的一样 不幸地 这个新的分配 对我的空闲内存空间来讲过大了 即使总空闲空间的大小是够的 它们也没有连续性 所以没办法被用于单次的分配
因为它无法被写入 任何已存的空闲空间 系统会开辟一个新的脏页 来放置我的分配 就如图表的右边部分所示 空闲内存空间仍然未被填满 也就被视为碎片化的内存了
减少碎片化的最佳方法 是让有相似生命周期的对象 在内存里尽可能连续靠近彼此 这会帮助确保全部的对象 都能同时被释放 让进程中比较巨大的连续性内存 能够被利用在未来的分配上
在这个例子中 我手动分配 所有标记为“我的对象”的对象 同时间我打算要释放它们 但我编码的时候不够小心 让系统最后将我的对象 与其他对象交错了
现在当我释放全部对象后 我有四个空闲内存空间 没有任何一个是连续的 因为它们都被这些 已分配的对象分割了 这造成50%碎片化 以及四张脏页 不太好
那如果我换成写一段编码 来将我的对象分配到一起呢? 它们现在就会一起在两张页面上 而当我释放我的对象 进程就会为系统释出两张干净页面 最后只会造成两张脏页跟0%碎片化 要注意碎片化会加大内存占用 从两张到四张脏页 我的内存占用就双倍放大为 50%碎片化 在大多数的真实情境中 有些碎片化是无可避免的 所以依据经验法则 把目标放在大概25% 或更少的碎片化即可
一种减少碎片化的方法 是用自动释放池 自动释放池会让系统知道 只要对象不再作用 就要释放其中所有的对象 这会帮助确保所有 在自动释放池内的对象 有着相似的生命周期
虽然所有的进程都有可能 碰到碎片化问题 但特别可能发生在 长时间运行的进程中 有可能是因为很多分配跟释放 导致地址空间被分成碎片 比方说如果你的程序 使用长时间运行的套件 记得要查看那些进程中的 碎片化状况
我们很快地来看一下 我的进程碎片化率 我可以跑vmmap -summary 然后下拉到输出结果底部
这个段落会以malloc zone分类 每一区都包含了不同种类的分配 通常我只会在乎DefaultMallocZone 因为这是默认情况下 堆分配最终结束的地方
但是因为这个memgraph有MSL运行 我真的在乎的是 MallocStackLoggingLiteZone 只要MSL有在运行 这个区就是最终所有堆分配 最终结束的地方
% FRAG栏位则告诉我 有多少占比的内存 因为每个malloc zone的碎片化 而被浪费掉 有些数字还满大的 但我就只专注在 MallocStackLoggingLiteZone
那是因为MallocStackLoggingLiteZone 有着最大比例的脏内存 总共五兆字节里占了4.3 所以这次我可以忽略其他区
“脏+交换Frag大小”栏位 会告诉我准确来说有多少内存浪费 是跟每个malloc区里的碎片化有关
在我的例子里 因为碎片化 我浪费掉大约800K的空间 看起来好像很多 但就如同我提过的 有些碎片化是无可避免的 所以只要我的碎片化小于25% 我就会认定这样的浪费尚可接受
看起来我好像有19%碎片化 在MallocStackLoggingLiteZone里 还在经验法则的25%之下 所以我并不担心
如果我真的有碎片化的内存问题 我可以使用Instruments工具里的 Allocations track
我特别想要查看 Allocations表单的总览来看哪些对象 在我有兴趣的范围里 分别被persist跟destroy
以碎片化的本质来说 destroy对象会释放出空闲内存空间 而persist对象被视为 仍未被释放的对象 即为脏页存在的原因 如果你想要深入探讨碎片化 两者都值得进一步的调查
要获得更多有关 使用Instruments工具的信息 查看我们在WWDC 2019的 Instruments入门指南
那现在我已经查看完内存泄漏 跟堆内存回归 也确认碎片化不是个问题了 我会再跑一次XCTest
太棒了! XCTest现在通过了 而回归也被解决了 现在你学会了如何侦测 并诊断内存问题 我们来回顾你可以在程序内使用的 作业流程 加入新功能的时候 写入一个XCTest的性能分析 来监控内存 或是用任何其他提供的系统指标 针对每一次测试 设定一个底线 然后用测试抓出回归 并用搜集的ktrace及memgraph档案 进一步调查问题
使用任何失败XCTests的memgraphs 来协助侦测你的内存问题 首先你要做的是查看内存泄漏 使用Leaks工具和MSL backtrace函数 来帮助找到及修复任何内存泄漏 如果回归不包含泄漏 那就去查看堆区 从vmmap -summary开始 以确认内存是否在堆里
如果是 跑heap -diffFrom来查看 哪种对象类别造成内存增加 如果原因很明显 用heap -addresses来获取它们的地址 如果不是 利用leaks-referenceTree 来找线索 最后调查罪魁祸首的对象地址 可以用leaks -traceTree和malloc_history
最后确保你在开发的时候 有将这些最佳范例记在心上 力求程序内无内存泄漏 如果你在处理的是unsafe类别 记得要释放每个你分配的东西 并注意你的程序代码里是否有循环引用
想办法减少你的堆内存分配 无论是缩小它们 减少运行它们的时间 或是移除不必要的分配 也可以都一起用 记得要将碎片化放在心上 将有相似生命周期的对象分配到一起 之后才能创造很好的 较大空闲内存空间 有了这些最佳范例和XCTest作业流程 你就能够侦测、诊断 并排除你的应用程序中的内存问题 仅代表坦努加和我自己 跟观看的各位说声谢谢 [打击乐]
-
-
4:52 - Monitor memory performance with XCTests
// Monitor memory performance with XCTests func testSaveMeal() { let app = XCUIApplication() let options = XCTMeasureOptions() options.invocationOptions = [.manuallyStart] measure(metrics: [XCTMemoryMetric(application: app)], options: options) { app.launch() startMeasuring() app.cells.firstMatch.buttons["Save meal"].firstMatch.tap() let savedButton = app.cells.firstMatch.buttons["Saved"].firstMatch XCTAssertTrue(savedButton.waitForExistence(timeout: 30)) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。