大多数浏览器和
Developer App 均支持流媒体播放。
-
分析堆内存
深入探索 App 动态内存的基础:堆!了解如何利用 Instruments 和 Xcode 来衡量、分析并修复常见的堆问题。我们还将介绍一些相关的技巧和推荐做法,帮助你为自己的 App 诊断瞬时增长、持久增长以及内存泄露问题。
章节
- 0:00 - Introduction
- 1:05 - Heap memory overview
- 3:45 - Tools for inspecting heap memory issues
- 7:40 - Transient memory growth overview
- 10:34 - Managing autorelease pool growth in Swift
- 13:57 - Persistent memory growth overview
- 16:00 - How the Xcode memory graph debugger works
- 20:15 - Reachability and ensuring memory is deallocated appropriately
- 21:54 - Resolving leaks of Swift closure contexts
- 24:13 - Leaks FAQ
- 26:51 - Comparing performance of weak and unowned
- 30:44 - Reducing reference counting overhead
- 32:06 - Cost of measurement
- 32:30 - Wrap up
资源
相关视频
WWDC24
WWDC22
WWDC21
WWDC18
-
下载
大家好 欢迎观看“分析堆内存”! 这位是 Ben 这位是 Daniel! 今天我们来谈谈堆内存和 App App 会直接和间接使用堆内存 而作为开发者 你能够控制和优化堆内存 堆内存是存储 App 引用类型 的地方 而且非常重要 因为经常有数据写入堆内存 导致它被标记为脏 这会影响 App 的内存限制 因此 本讲座的重点是 测量和减少堆内存
如果你希望进一步了解 其他类型的内存 包括图形内存或是内存限制 还有其他一些精彩的讲座 会更详细地介绍这些内容 因此 无论是你的 App 使用了过多内存 还是像我一样好奇 只想窥探一下背后的秘密 我们都可以一起深入探讨! 我们今天将讨论五个主题: 测量堆 处理瞬时增长 跟踪持久增长 修复内存泄漏 以及提高运行时性能 那么 让我们从这些问题开始! 什么是堆内存? 我们可以使用哪些工具 来测量 App 正在使用多少内存? 要了解堆内存 我们需要了解它 在 App 整个虚拟内存中的位置 当 App 启动时 它会获得自己的 虚拟内存空地址空间 App 启动时 系统会载入 主要可执行文件、 链接库和框架 并从磁盘映射到只读资源区域 运行时 App 还会为每个线程的 本地变量和临时变量使用堆栈区域 而动态内存和长期内存 则放置在统称为堆的 内存区域中 今天 我们将重点关注这一部分 让我们放大来看! 堆不仅仅是一个内存块 它还由多个虚拟内存区域组成 再放大一点 我们看看区域级别 每个区域都被分割成单独的堆分配 在后台 每个区域都 由来自操作系统的 16KB 内存页组成 但每次分配可大可小 这些内存页可能 处于三种状态之一: 干净页、脏页或交换页 干净页是没有数据写入的内存 这是已分配但尚未使用的空间 这类页面也可能代表着 从磁盘映射为只读的文件 干净页开销非常低 因为系统可以随时丢弃这些页面 之后再通过缺页异常处理重新载入 脏页是最近被应用程序写入的内存 脏页暂时没有使用时 也不能将它们丢弃 如果存在内存压力 系统可以交换脏页 将它们压缩或写入磁盘 这样 在需要时 内存就可以从磁盘解压缩 或触发缺页异常处理 在这三种页面中 只有脏页和交换页会计入 应用程序的内存占用 而在大多数应用程序中 堆是大部分内存占用的原因所在
堆区域是使用 malloc 函数 或使用类似的 calloc 或 realloc 分配原语 创建的内存 在很多情况下 你不会直接调用这些函数 但编译器和运行时经常会用到它们 例如 当你创建 Swift 或 Objective-C 类的实例时 malloc 可让你的 App 动态分配长期内存 分配的内存会一直存在 直到显式释放为止 这意味着这些内存可以超越 创建内存的代码所对应的作用域 这类函数强制执行一些规则 例如 它们的最小分配大小 和对齐方式是 16 字节 这意味着如果请求 4 字节 你的请求会被四舍五入到 16 字节 此外 这类函数还有一项安全功能 大多数小分配在释放时都会清零 语言运行时使用堆来分配长期内存 例如 Swift 会扩展 这个类构造器 调用一系列 Swift 运行时函数 然后最终调用 malloc
malloc 还有一些调试功能 其中一项功能是 MallocStackLogging 这个功能可以记录调用堆栈 和每次分配的时间戳 启用 MallocStackLogging 后 可以更轻松地跟踪 内存分配的时间和地点
在 Xcode 中 你可以在方案的 “Diagnostics”标签下 选中相应的复选框来启用 MallocStackLogging 在今天的所有演示中 我们都启用了这样的 malloc 堆栈日志记录功能 要跟踪内存使用情况 我们可以使用的第一个工具 是 Xcode 内存报告 这份报告可以显示应用程序 在一段时间内的占用空间 应用程序的占用空间 不仅仅由堆组成 而内存报告可以显示显著的 内存问题和一些最近的历史记录 遗憾的是 这份报告无法告诉你 内存使用量增长的原因 我们需要其他工具来帮助我们了解 我们今天要介绍的另一款工具 也是 Xcode 的一部分 内存图调试器可以捕获内存图 这是所有分配 以及分配间引用的快照 使用 MallocStackLogging 时 内存图包括每次分配的回溯栈跟踪 如果需要关注特定的分配 那么这款工具非常适合你使用 而且可直接从 Xcode 的 调试栏访问
Xcode 还包含一些用于 内存分析的强大命令行工具 leaks、heap、vmmap 和 malloc_history 可以直接分析 macOS 和模拟器进程 或使用已捕获的内存图调查问题 我建议查看这些工具的 man 页面 进一步了解它们的高级功能
如果要剖析一段时间的 内存使用情况 Instruments 应用程序 提供了几个模板 Allocations 工具会记录 一段时间内所有分配 和释放事件的历史记录 汇总统计数据和调用树 帮助你追溯到代码 Leaks 工具会定期 对 App 的内存拍摄快照 以便检测内存泄漏 让我们来看看如何使用 Allocations 来调查 “Destination Video” 示例 App 中的一个问题
Daniel 和我正在为 “Destination Video”App 开发一项新功能 但我发现其中存在一些内存问题 这项新功能允许用户为视频 选择新的背景图片 我打开并关闭背景图片库几次后 就发现 App 崩溃了 因为它占用了太多内存 每次我打开图库时 Xcode 内存报告都会显示内存 使用量再次激增 几乎达到千兆字节 我们可以使用 Allocations 工具 对这种情况进行分析 我会在我的设备上进行测试
要在 Instruments 中进行剖析 可使用 Product > Profile 菜单项 这样操作可对我的 App 执行发布构建 然后打开 Instruments 将构建好的 App 作为分析目标选中 打开后 Instruments 会要求我们 选择一个模板进行剖析 在本例中 我们想要剖析 App 的堆 因此我会选择 Allocations Allocations 模板包含两个工具: Allocations 和 VM Tracker Allocations 实时记录 堆和 VM 事件 帮助我们实时查看活动 VM Tracker 可以定期拍摄快照 来测量所有虚拟内存 今天 我不会启用这个工具 因为我的主要关注点是堆 为了开始跟踪 点按跟踪文档左上角的 “Record”按钮 随着跟踪的开始 关于 App 的数据流开始涌入 跟踪视图会显示 App 的内存使用量 在 App 载入后保持稳定 我要打开再关闭图库视图 查看内存使用量激增时的情况
这次比上次要慢一些 但也在意料之中 每次调用 malloc 和 free 时 我都会得到堆栈跟踪 这些数据马上就会派上用场
点按左上角的“Stop”按钮 停止跟踪 在“Allocations”跟踪中 我们可以 清楚地看到峰值模式再现 现在我得到了一个跟踪文件 我可以把文件发给我的搭档 他很喜欢修复内存错误 我使用 File > Save 菜单项 来保存跟踪文件 现在 这个问题归 Daniel 了
Daniel 你想谈谈诊断 瞬时内存增长的问题吗? 当然 Ben!让我们深入了解一下 Ben 记录的内存峰值 看看我们能否做些什么 App 中的内存峰值是 瞬时内存增长的一种类型 这种增长不是好现象 原因有三
内存峰值会带来内存压力 导致系统做出反应 交换和压缩脏内存 丢弃只读内存 甚至终止后台任务 最糟糕的情况是 这些峰值 可能意味着你的 App 也会终止 内存峰值的长期影响也很不利 因为它会导致碎片化 或堆内存区域出现漏洞 有两种方法可以进行跟踪 我们可以查看特定的峰值 从低点到高点 找出状态为 Created & Still Living 的分配 或者 改用汇总形式 我们可以选择一个大范围 在这个范围内 找出状态为 Created & Destroyed 的所有分配 现在 让我们用 Ben 发给我的 跟踪信息来试试
我在时间线上选择一个峰值区间 在跟踪视图中点按低点 一直拖移到峰值顶点 下面的详细统计信息应该可以表明 造成峰值的原因 让我按总字节数对这些行进行排序 并查找字节数最大的行 尽管我们在本讲座中 重点讨论的是堆内存 但排名靠前的类别看起来像是 IOSurface 虚拟内存 显然 这个提示表明 我们的临时内存问题 与我们如何处理背景图片有关 如果我按照持久性排序 在本例中 位于峰值顶点的对象中 有一个节点引起了我的注意: @autoreleasepool content 系统创建了数百个这样的节点 而这 对于 autoreleasepool 不是小数目 我稍后再谈这个问题 另一种解决临时内存问题的方法 是在较大范围内找到 负责处理已创建且已销毁对象的 代码 在窗口底部 我将“Lifespan”筛选器 更改为“Created & Destroyed” 在时间线上 我选中所有三个峰值 现在 我可以使用中间的转跳栏 将详细视图切换为调用树 调用树是通过回溯栈跟踪 分解分配的好方法 能让我了解哪些代码分配了 最多的内存 看看总数 有 8GB 的临时分配? 哇哦 这些分配都是从哪里产生的? 右边的 Heaviest Stack Trace 为我提供了很好的探查线索 我把这个窗口拉宽一点
这里突出显示了代码中的帧 看一下这个列表 makeThumbnail() 代码 看起来是一个很好的开始 我可以点按一次快速打开调用树 也可以双击来查看源代码
噢 这是我们应用的图像滤镜 其中一行显示 创建和销毁了大量内存 达到数千兆字节 这应该是临时内存 但我们看到 内存持续增长 直到峰值顶点 然后一次性全部释放 我再往上看几帧 首先点按返回到转跳栏中的调用树 再往上看几帧 这次我想去看一下 ThumbnailLoader 的 loadThumbnails 代码
缩略图载入发生在循环中 在循环运行时 内存不断增加 在循环结束后 内存下降了 结合之前的 autoreleasepool 线索 我想我找到了背后的原因 虽然我使用的 Swift 具备 自动引用计数功能 但自动释放池也是 内存暂时增长的常见原因 Objective-C 使用这些池 来延长函数返回值的 对象生命周期 自动释放池通过延迟释放 来确保这些返回值继续存在 不过 这也意味着 Swift 在调用那些使用或公开 Objective-C API 的框架时 会生成自动释放的对象 这个简单的示例会打印当前日期 但同时也会创建 一个自动释放的字符串 这个字符串将一直保留在堆上 直到当前自动释放范围结束 这可能需要一点时间
线程通常有一个顶层自动释放池 但并不经常清理 当代码使用对象将释放池填满时 清理就很重要 循环中很容易出现这种情况
每次迭代 对象都会 自动释放到同一个池中 它们的生命周期可能会 超过所需时间 在本例中 等到循环全部结束后 才会释放对象 在内部 自动释放池会分配 内容页来引用对象 由于这些内容页可在 “Allocations”工具中查看 因此可以很好地帮助你 发现这类问题 之后 当自动释放池清空时 池会发送延迟释放 大量对象得以一次性释放
解决这个问题的办法通常是 定义一个嵌套的 本地自动释放池作用域 来缩小这些生命周期的范围 在本例中 自动释放的对象 由内部的每个循环池保留 并在每次迭代时释放 这意味着 累积的对象数量更少 跟踪引用所需的内容页也更少 我们转回之前的界面 看看能否解决这个问题 在 Instruments 中 我使用 源代码视图右上角的菜单 在 Xcode 中打开这个文件
为了解决这个问题 我们在循环 主体中添加一个自动释放池作用域 它在每次迭代后会清空对象
让我们看看效果如何
哦 我没带开发专用手机 嘿 Ben 我能用你的手机 测试一下我的修复方法吗? 不行!这是我的手机! 你为什么不用模拟器呢? 好吧 有道理 对于大多数剖析 重要的是 在真实设备上 运行发布版本 以获得准确的计时 不过 对于堆分析 模拟器环境在行为上更为接近 用来进行内存剖析也不错 我会切换回内存仪表板 试试 Ben 刚才展示的功能
在模拟器中 我会打开一次图库 然后再关闭
仪表显示 情况改善了 内存虽有上升 但这次没有出现巨大的峰值
第二次也是一样 但现在 我开始发现 另一种不太理想的模式
在调出工作表三次后 我可以确认 内存峰值消失了! 我们再也没有接近过千兆字节 但现在 我们的问题是 内存每次都以阶梯模式上升 这很奇怪 因为 即使创建缩略图的开销很高 它也应该只在我第一次 打开图库时才会增长 我等下就会推送我的 自动释放池修复 但我不想夺走所有的乐趣 在 Xcode 的调试栏中 我要在内存图调试器中暂停 这将捕获应用程序堆中的 每一次分配 如果我已经知道 是什么类型导致增长 我现在就可以进行搜索 或者在右边进行共享 虽然我可以直接在 Instruments 中导入这个内存图 但我更想直接把它 隔空投送给 Ben 也许他会有办法找出原因 但这无异于大海捞针 祝你好运 Ben! 想得美 Daniel 但我可不会再上当了! 我会用我自己的内存图 来研究这个持久增长问题 持久性内存是 不会被解除分配的内存 持久增长一般是这样的 内存随时间推移不断增加 这种增长由多个分配组成 “Allocations”工具中的 “Mark Generation”功能可以 按时间跨度分解增长 点按“Mark Generation”按钮 Instruments 会创建一个 新的分配组 这次生成会收集在这之前 创建的所有分配 这也会持续到跟踪结束 当我选择较晚的时间 并再次点按“Mark Generation”时 Instruments 会创建一个新组 后面的这次生成将收集上一次之后 新时间戳之前的所有持续分配 我在 Xcode 中生成了自己的内存图
并将它导入 Instruments Instruments 会显示 Allocations、 Leaks 和 VM Tracker 工具中的数据 我现在将重点放在 Allocations 跟踪上 在跟踪视图中 我可以看到 Daniel 在 Xcode 内存报告中 指出的相同阶梯模式 我将使用生成标记功能 来隔离在增长区间创建的持续分配 我会在增长期之间选择几个区间 然后按下“Mark Generation”按钮
Instruments 现在显示三次生成 生成 B 和 C 显示的是 打开图库后产生的持续增长 我可以展开其中一次生成 查看分配情况 并按照增长大小排序 查看哪些类型导致的增长最多 看起来大部分增长 来自 Data 存储 我可以扩展这一类型的条目 查看单个分配及分配地址 哈!大海里捞到针了!
看起来所有这些 Data 存储分配 均由 ThumbnailLoader 代码创建 那么 是什么在保留 Data 呢? 我们可以从 Instruments 中 获取其中一个地址 并将地址放入内存图调试器 看看引用地址的是什么 这应该能表明 为什么在图库关闭后它仍然存在 我将从展开的详细视图中拷贝地址 并将它放入内存图调试器的筛选栏 然后选择分配
为了更好地理解 内存图调试器反映的信息 让我们来谈谈它是如何工作的
调查内存增长就是要询问: 为什么这一分配仍然存在? 保留分配的是什么? 内存图调试器可以 帮助回答这些问题 要充分利用这一工具 我们需要了解类型信息和引用扫描 引用主要有四种类型: 强引用是绝对指针 位于 ARC 管理的位置 带有显式所有权保证 弱引用和无主引用 也是绝对指针 带有显式非所有权保证 非托管引用是指运行时知道 但不会自动管理的 位置的指针 这些可能是手动拥有的引用 但也可能不是 最后一种类型是不确定引用 也叫保守引用 当工具不知道所扫描内存的类型 只是看到原始内存时 就会记录这些引用 如果值看起来像指针 也许就是指针 但如果没有类型信息 就真的无法确定了 工具扫描进程堆时 会使用每次分配时 可用的最佳类型信息 在 Swift Swallow 的示例中 前两个字段是标准字段 不包含对引用扫描 重要的内容 之后 我们要扫描的是 coconut 引用! 这个字段确实包含一个 指向堆分配的指针 它是指向 Coconut 对象的 强引用! Swift 和 Objective-C 的 类型信息很棒 但 C 和 C++ 没有 引用所有权信息 所以你只能看到保守引用 工具最擅长的就是查找具有 虚拟方法的 C++ 类型的名称 这个类的实例将被视为 Coconut 对于没有虚拟方法的类型 或其他分配 堆栈跟踪可以帮助提供名称 有了 MallocStackLogging 数据 这个类的实例可能会 在 PalmTree::growCoconut() 中 标记为 malloc 这就很好地提示了它可能是什么 现在 我们讨论了类型信息和引用 让我们回过头来看看为什么 我们的数据存储会永远持续下去 在内存图调试器中 我们可以看到我们选择的分配 由一个 __DataStorage 对象保留 这个对象由 PhotoThumbnail 保留 而 PhotoThumbnail 又由一个词典保留 回过头来看 保留分配的似乎是静态属性 ThumbnailLoader.globalImageCache 由于我在运行时 启用了 MallocStackLogging 因此可以在右侧的检查器中 看到分配回溯栈跟踪 我将使用检查器导航到 负责分配的源 让我们选择保留数据的 PhotoThumbnail 看起来代码中的一个闭包 负责分配这个数据 我使用堆栈跟踪跳转到这段代码
看起来这个 faultThumbnail 方法 负责缓存缩略图 并在缓存未命中时创建新的缩略图 我猜 它一定是将缩略图 存储在我们刚才看到的 globalImageCache 中 从注释中可以看出 我们是根据 URL 和 creationDate 来缓存的 这似乎很合理 但这里有个错误! 这显然不是文件的创建时间戳! 而是当前时间 这意味着我们永远无法 在缓存中找到任何东西 而且每次调用这个方法时 我们总是会缓存 一个新的 PhotoThumbnail 这就解释了为什么我们会 看到缩略图数量持续增长! 为解决这个问题 让我们根据文件的 创建日期进行实际缓存 我将删除使用错误时间戳的代码 现在我需要获取文件的创建时间戳 很好 Xcode 推荐了我想要的代码 我按下 Tab 键接受 我将再次运行 App 验证问题是否已经解决 同时确保阶梯模式不会 出现在 Xcode 的内存报告中
让我们再试试这个功能
好的 我们已经生成缩略图 现在再试一次
很好 没有增长 为确保万无一失 我再试一次
让我们来看看内存图调试器 确保没有其他问题
我看到发现了一些内存泄漏 就是旁边显示了 黄色三角形图标的那些分配 Daniel 你是不是又故意 在代码中引入了内存泄露? 是的!我想这样正好 因为接下来的话题就是内存泄露 要了解并修复内存泄露问题 我们首先要谈谈可达性 程序中的所有内存都应该 可通过非弱引用 从某处到达 以便将来使用 堆上有三种内存 第一种是有用的内存 是程序可达 并且将来还会使用的内存 第二种是遗弃内存 这种内存可达且可使用 但实际上不会再使用 这些内存会计入 App 的 占用空间 造成浪费 这种情况很容易出现 例如过度缓存 或在单一实例上保留高开销数据 App 中的第三类内存 是泄漏的内存 这些是不可达、 无法再次使用的内存 这种情况通常发生在 最后一个指针丢失时 常见的场景要么是手动管理分配 要么是对象的引用循环 对于大多数泄漏 我们的目标是 找到并修复循环中的一个引用 这可能是删除一个意外引用 或者将所有权限定符 从强变为弱或无主 为了更轻松地调查这些泄漏 我将使用筛选栏中的“Show only leaked allocations”按钮 这个按钮也有一个三角形图标
导航器会显示根据 App 中 不同二进制文件分组的类型 你的代码有可能 从系统二进制文件泄漏类型 但泄漏通常是由项目中的 问题直接造成的 我点按筛选栏的另一个按钮 只筛选我的项目的类型 这样更容易理解! ThumbnailLoader 类 有 3 个泄漏 ThumbnailRenderer 也有 3 个泄露 我选择其中一个 这看起来像是一个小型引用循环 发生在 ThumbnailRenderer、 ThumbnailLoader 和闭包上下文之间 但闭包上下文是什么呢? 我们先来谈谈这个问题 当 Swift 闭包需要捕获值时 它们会在堆上分配内存 来存储捕获的值 内存图调试器会将这些分配 标记为闭包上下文 App 堆中的每个闭包上下文 都与实时闭包一一对应 闭包默认强捕获引用 从而可能创建引用循环 你可以使用弱捕获 或无主捕获来打破这些循环 比方说 我有一个 Swallow 对象 它有一个完成处理程序 当 swallow 传递 coconut 时 就会调用这个处理程序 如果我不小心搞错 就会通过强捕获 Swallow 本身 创建一个引用循环 内存图调试器会 将引用显示为强捕获 但闭包元数据并不包括变量名 所有来自闭包上下文的引用 都被简单地标记为捕获 让我们回去看看能否解决泄漏问题 我打开检查器 点按其中的几个这类引用
ThumbnailRenderer 有一个 指向载入器的 cacheProvider 引用 载入器有一个引用闭包上下文的 completionHandler 如果我选择返回渲染器的这个捕获 检查器会显示这个引用是强引用 要打破这种引用循环 我们需要 找到创建闭包的代码 从闭包上下文的堆栈跟踪 跳转到 PhotosView 中的代码
这段代码负责创建一个 ThumbnailLoader 对象 为这个对象分配一个完成处理程序 然后告诉它开始载入 但我们刚刚看到的问题是 闭包会强捕获 ThumbnailRenderer 而这造成了引用循环 那么该如何解决呢? 我们也许应该更改这段代码 改为使用 Swift 并发 而不是完成闭包 但现在 我们可以指定一个 捕获列表 弱捕获或无主捕获都能打破循环 我要为渲染器设置弱捕获
由于我们现在有了一个可选的弱引用 所以我添加了这个 guard let 以确保我们使用渲染器时 它的目标仍然存在 我刚刚做的修复 是针对 3 节点循环的 但像这样的小改动 会产生很大的影响 再次尝试我们的功能 然后看看内存图调试器
现在我没有看到任何类型泄露 如果我关闭类型筛选器 还有一个惊喜: 其他泄漏也得到解决! 这些其他的类型被我们 刚刚修复的泄漏所引用 现在它们也得以解除分配 在这个例子中 找到并修复泄漏非常简单 不过 代码的泄漏方式多种多样 查找泄漏可能是问题最多的地方 让我们来谈谈其中的 几个问题 首先 为什么泄漏检查 不能发现所有泄漏? 假设我故意制造一个泄漏 为什么工具总是找不到它? 对于很多内存 工具都没有类型信息 而 C 语言允许使用非托管指针 这意味着工具必须允许 看起来可能是指针 但实际不是指针的东西存在 当工具进行保守扫描时 它们会逐个字节地查找指针 查找看起来是引用的值 然后将它们与分配列表进行核对 如果值匹配 工具就会将这个内存块记录为 不确定引用 也就是保守引用 但请记住 值可能是一个数字值、标志 或者只是看起来 像是有效指针的随机字节 因此 这个问题的答案是 真正的泄漏可能 会因为保守引用而漏掉 如果我想故意制造泄漏 并确保能够发现泄露 那么把泄漏放在一个循环中 泄漏 100 次也无妨 在实际 App 中 泄漏代码 通常会运行多次 因此 即使工具不能发现每一个泄漏 它们仍能捕捉到导致泄漏的错误 另一个相关问题是 报告的泄漏次数怎么会 随时间的推移而增减? 错误会随时间而导致更多泄漏 但堆可能是非常嘈杂和随机的 这种噪声使得保守引用 具有非确定性 因此它们可能出现 也可能消失 因此 即使程序在启动时 泄漏了 5 个对象 工具也可能一开始发现其中 5 个 而后来只发现 4 个 另一个常见问题是 为什么非返回函数 有时好像会泄漏内存 这可能是因为具有 noreturn 属性的 C 函数或 返回 Never 类型的 Swift 函数 由于这些函数永远不会返回 编译器可优化掉 它们通常需要进行的清理工作 包括释放它们创建的 本地分配或引用 如果将这类函数用于致命断言 没什么问题 因为 无论如何程序都会崩溃! 但有时它们用来永远暂停线程 如果你发现 调用 noreturn 函数时 本地状态报告为泄漏 例如本例中的 Server 对象 一种解决方法是将对象 显式存储到全局变量中 通过将对象存储 在本地函数作用域之外 引用最终会出现在 工具可以看到的地方 因为工具可以看到引用 所以对象会被认为是可达的 而不是泄露的 即使编译器没有以其他方式 保留本地变量 也是这样 现在我们讨论了泄漏问题 我想让 Ben 来谈谈 运行时速度和椰子 谢谢 Daniel! 减少内存可以大大提高 App 的性能 但还有一些运行时细节需要注意 以便进一步提高性能
弱引用和无主引用 是 Swift 中的两种常用工具 可避免产生强引用循环 让我们来谈谈它们的区别 以及使用情况 弱引用始终是可选类型 在反初始化这些引用的目标后 它们会变为 nil 无论源和目标的生命周期如何 你总是可以使用弱引用 看一下燕子和椰子的例子 燕子可以携带椰子 但椰子并不拥有燕子 如果我们想让椰子引用燕子 就不应该使用强引用 我们可以使用弱引用 不过这也会带来开销 为了实现弱引用 Swift 在第一次弱引用目标对象时 就会为目标对象分配 一个 Swift 弱引用存储空间 这一分配位于 Swallow 和所有传入的弱引用之间 允许弱引用在 Swallow 消失后 延迟归零 与弱引用不同 无主引用 直接保留它们的目标 这意味着它们不会使用 任何额外的内存 访问所需的时间也比弱引用少 它们甚至可以是非选择性的 也可以是常数 不过 使用无主引用 并不总是有效的 假设我们将 Coconut 的 “holder”引用 设为无主引用 而不是弱引用 如果 Swallow 在这个引用之前消失 情况会怎样? Swallow 将反初始化 但不会解除分配 这就是无主引用安全的原因 无主引用必须指向某些东西 因此运行时会保留前鹦鹉 或者燕子 我这里的比喻搞混了 此时 如果我试图使用 Coconut 的无主引用 来访问 Swallow 就会发生确定性崩溃 从这个角度看 无主引用 很像强制解析弱引用 即使我不访问无主引用 把它留下来也不好 只要有无主引用存在 它的目标就无法解除分配 从而造成内存浪费 如果你不知道目标将存在多长时间 那么采用弱引用 产生少量开销 是一种可取的做法
如果你在内存图中没有看到 弱引用或无主引用的报告 你可能需要在 Xcode 中 检查项目的 Reflection Metadata Level 构建设置 我们建议尽可能使用 默认的 All 级别 这一设置包含 工具需要的所有元数据 并让工具为 Swift 提供 更高的准确性
让我们来看一个具体的例子 这个 ByteProducer 类的 生成器属性 是一个闭包 一开始会分配给 defaultAction 方法 问题是 这会创建一个强引用循环 因为 defaultAction 方法 隐式使用 self 将方法作为闭包使用时 要非常小心
为了解决这个问题 我们可以定义一个 调用 defaultAction() 的闭包 它仍然执行对 self 的捕获 但现在捕获是显式的 而且我们可以使用捕获列表 来防止强捕获 我们需要指定一个引用限定符 弱引用在这里无疑是 很好的默认设置 在这种情况下 无主引用也没有问题 因为生成器闭包的 生命周期与它的目标 也就是 ByteProducer 实例相同 闭包没有提供给其他代码 也没有异步派发 因此它的生命周期 不可能超过捕获的 self 这些选择之间的性能差异 有时会非常大 如果我分配一百万个 这样的 ByteProducer 并导出内存图 堆命令行工具 就能快速汇总成本 每个 ByteProducer 都有 一个弱引用存储分配 它们使用的内存几乎与 ByteProducer 本身一样多! 如果使用无主引用 就不需要这些内存了 我想说明的是 弱引用是很好的默认设置 而无主引用可以节省内存和时间 因为你可以保证引用的 生命周期不会超过它的目标 要查找引用带来 CPU 开销的位置 请剖析并查看运行时函数的调用 如 swift_weakLoadStrong()
你可以在《Swift 编程语言指南》的 “自动引用计数”章节中 进一步了解 Swift 引用计数
除了弱引用和无主引用 有时自动保留和释放调用 也会成为剖析热点 虽然这可能很有诱惑力 但不要绕开 ARC 除了使用非托管指针 或将对性能敏感的代码 移动到内存不安全的语言 还有更好的解决方案 确保启用 “-whole-module-optimization” 因为它可以通过更多内联 来减少开销 此外 还应剖析并查找 可能需要显式特化的泛型
确保拷贝次数最多的结构 具有简单字段 也会很有帮助 剖析可以帮助识别 开销高的结构拷贝 对于这些结构 请尽量 避免使用引用类型、 写时复制类型以及任意类型
有关优化 Swift 性能的更多技巧 请观看“探索 Swift 性能” 以及“在 Swift 中 使用不可拷贝的类型” 对于 Objective-C 代码 也有一些方法可以 减少保留和释放开销
同样 不要绕开 ARC 因为手动引用计数 造成的泄漏极难调试 将方法标记为 objc_direct 允许内联 Objective-C 方法调用 这有助于减少保留和释放流量 在无法内联的情况下 objc_externally_retained 属性 非常适合向编译器传达 参数生命周期得到保证的情况 从而避免保留和释放
提升性能的一个关键在于 了解观察开销 MallocStackLogging 和 Allocations 会跟踪实时数据 这需要一定的内存和 CPU 来记录每次分配的信息 Leaks、VM Tracker 和 Memory Graph 都基于快照 这需要在分析期间暂停目标 App 这可能导致 App 在快照过程中 短暂卡顿或挂起 总结一下 今天我们介绍了 如何用 Instruments 来测量堆 并查找瞬时增长和持续增长的模式 一旦发现某些分配存在问题 请使用 Xcode 的内存图调试器 和 MallocStackLogging 来找出分配仍然存在于 App 堆中的原因 但最重要的是 一定要采取主动! 分析并优化 App 的堆内存 发现内存泄漏和持久增长可让用户 更长久地尽情使用你的 App 再次感谢大家的参与!
-
-
10:01 - ThumbnailLoader.makeThumbnail(from:) implementation
func makeThumbnail(from photoURL: URL) -> PhotoThumbnail { validate(url: photoURL) var coreImage = CIImage(contentsOf: photoURL)! let sepiaTone = CIFilter.sepiaTone() sepiaTone.inputImage = coreImage sepiaTone.intensity = 0.4 coreImage = sepiaTone.outputImage! let squareSize = min(coreImage.extent.width, coreImage.extent.height) coreImage = coreImage.cropped(to: CGRect(x: 0, y: 0, width: squareSize, height: squareSize)) let targetSize = CGSize(width:64, height:64) let scalingFilter = CIFilter.lanczosScaleTransform() scalingFilter.inputImage = coreImage scalingFilter.scale = Float(targetSize.height / coreImage.extent.height) scalingFilter.aspectRatio = Float(Double(coreImage.extent.width) / Double(coreImage.extent.height)) coreImage = scalingFilter.outputImage! let imageData = context.generateImageData(of: coreImage) return PhotoThumbnail(size: targetSize, data: imageData, url: photoURL) }
-
10:23 - ThumbnailLoader.loadThumbnails(with:), with autorelease pool growth issues
func loadThumbnails(with renderer: ThumbnailRenderer) { for photoURL in urls { renderer.faultThumbnail(from: photoURL) } }
-
10:33 - Simple autorelease example
print("Now is \(Date.now)") // Produces autoreleased .description String
-
11:08 - Autorelease pool growth in loop
autoreleasepool { // ... for _ in 1...1000 { // Autoreleases into single pool, causing growth as loop runs print("Now is \(Date.now)") } // ... }
-
11:50 - Autorelease pool growth in loop, managed by nested pool
autoreleasepool { // ... for _ in 1...1000 { autoreleasepool { // Autoreleases into nested pool, preventing outer pool from bloating print("Now is \(Date.now)") } } // ... }
-
12:16 - ThumbnailLoader.loadThumbnails(with:), with nested autorelease pool growth issues fixed
func loadThumbnails(with renderer: ThumbnailRenderer) { for photoURL in urls { autoreleasepool { renderer.faultThumbnail(from: photoURL) } } }
-
17:27 - C++ class with virtual method
class Coconut { Swallow *swallow; virtual void virtualMethod() {} };
-
17:40 - C++ class without virtual method
class Coconut { Swallow *swallow; };
-
18:41 - ThumbnailRenderer.faultThumbnail(from:), caching thumbnails incorrectly
func faultThumbnail(from photoURL: URL) { // Cache the thumbnail based on url + creationDate let timestamp = UInt64(Date.now.timeIntervalSince1970) // Bad - caching with wrong timestamp let cacheKey = CacheKey(url: photoURL, timestamp: timestamp) let thumbnail = cacheProvider.thumbnail(for: cacheKey) { return makeThumbnail(from: photoURL) } images.append(thumbnail.image) }
-
19:28 - ThumbnailRenderer.faultThumbnail(from:), caching thumbnails correctly
func faultThumbnail(from photoURL: URL) { // Cache the thumbnail based on url + creationDate let timestamp = cacheKeyTimestamp(for: photoURL) // Fixed - caching with correct timestamp let cacheKey = CacheKey(url: photoURL, timestamp: timestamp) let thumbnail = cacheProvider.thumbnail(for: cacheKey) { return makeThumbnail(from: photoURL) } images.append(thumbnail.image) }
-
22:19 - Code creating reference cycle with closure context
let swallow = Swallow() swallow.completion = { print("\(swallow) finished carrying a coconut") }
-
23:11 - PhotosView image loading code, with leak
// ... let renderer = ThumbnailRenderer(style: .vibrant) let loader = ThumbnailLoader(bundle: .main, completionQueue: .main) loader.completionHandler = { self.thumbnails = renderer.images // implicit strong capture of renderer causes strong reference cycle } loader.beginLoading(with: renderer) // ...
-
23:40 - PhotosView image loading code, with leak fixed
// ... let renderer = ThumbnailRenderer(style: .vibrant) let loader = ThumbnailLoader(bundle: .main, completionQueue: .main) loader.completionHandler = { [weak renderer] in guard let renderer else { return } self.thumbnails = renderer.images } loader.beginLoading(with: renderer) // ...
-
24:24 - Intentional leak of manually-managed allocation
let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16) // intentional mistake: missing `oops.deallocate()`
-
25:12 - Loop over intentional leak of manually-managed allocations
for _ in 0..<100 { let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16) // intentional mistake: missing `oops.deallocate()` }
-
26:11 - Nonreturning function which can see leaks of allocations owned by local variables
func beginServer() { let singleton = Server(delegate: self) dispatchMain() // __attribute__((noreturn)) }
-
26:22 - Fix for reported leak in nonreturning function
static var singleton: Server? func beginServer() { Self.singleton = Server(delegate: self) dispatchMain() }
-
27:21 - Weak reference example
weak var holder: Swallow?
-
27:43 - Unowned reference example
unowned let holder: Swallow
-
29:07 - Implicit use of self by method causes reference cycle
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = defaultAction // Implicitly uses `self` } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
29:25 - Break reference cycle cause day implicit use of self by method, using weak
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = { [weak self] data in return self?.defaultAction(data) } } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
29:41 - Break reference cycle cause day implicit use of self by method, using unowned
class ByteProducer { let data: Data private var generator: ((Data) -> UInt8)? = nil init(data: Data) { self.data = data generator = { [unowned self] data in return self.defaultAction(data) } } func defaultAction(_ data: Data) -> UInt8 { // ... } }
-
31:14 - Struct with non-trivial init/copy/deinit
struct Nontrivial { var number: Int64 var simple: CGPoint? var complex: String // Copy-on-write, requires non-trivial struct init/copy/destroy }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。