-
实现出色 App 性能的实用方法
注重性能并提高总体响应能力可以让所有 app 从中受益。这个视频中包含大量信息,旨在为您提供使用 Instruments 和其他工具解决性能问题的策略。此外,还可以通过调整 Apple 自有 app (包括 Xcode 和 iOS 上的“照片”) 的相关经验,获得实用建议。
资源
相关视频
WWDC20
WWDC18
WWDC16
-
搜索此视频…
[ 音乐 ]
[ 掌声 ] 大家下午好 我是 John Hess 今天我将和 Matthew Lucas 一起 与在座的各位 谈谈提高 App 性能的 一些实用的方法 我现在是一名 Xcode 团队的工程师 在过去的七年 我有幸一直专注于 提高性能的工作 首先是在 Xcode 的两个领域 “Project Find” 和 “Open Quickly” 二者的主要功能 都是提高性能 最近 我有机会去 做一项关于 Xcode GY 响应性的调查 我想与你们分享的是 在进行性能工作时 我所使用的方法 其中包括我十分熟悉的代码 也包括我 第一次接触到的代码
那么 如果各位想从今天的演讲中有所收获 我希望各位至少能 记住一点 那就是 你们所有提高性能的工作 都要基于测量的结果 在开始解决一个性能问题时 你需要进行测量 来建立一个基线 让你知道自己所处的位置
当你重复解决 同一个性能问题时 你应该测量每一步所使用的方法 以确保性能的改变 能够带来你所期待的效果
当你解决了一个性能问题 你还需要再测量一遍 这样你可以与原先的基线 进行对比 从而给出一个量化的结果 关于你将 App 的性能 真正提高了多少 你想要将成果报告给你的老板 分享给你的同事和用户
那么 当你想要为你的用户提高性能时 你需要考虑这一点 我喜欢称它为 总体性能影响 如果你将 App 的 一个领域的性能 提高了 50% 但你的用户 只体验到了其中的 1% 那么这种提升的影响广度 还不及你只提高了其他领域的 10% 但所有用户都能体验到 这全部的 10% 所以一定不要去优化边缘的案例 要确保你的性能的优化 能够为全部的用户所体验
那么我们如何修复 性能漏洞呢 如何修复常规的漏洞呢 一般来说 最开始用户反馈给我们 某方面的缺陷报告 我们接收到这个报告 了解到用户对 App 的性能 有一些不满意的地方 于是我们会设法 来综合步骤进行故障重现 以便我们可以随意地使程序出错 一旦我们成功的做到 我们就给 程序附加一个调试器 以便我们看到我们的程序出错的时候 在做什么
再结合我们的知识 想想代码应该如何工作 在必要时修改它 来消除那些 不如意的部分 我们要确保我们没有 带来任何不想要的副作用 同时 要进行必要的重复 直到我们完全地 解决了这个漏洞
我已经用相同的方法 修复了许多性能漏洞
只不过我并没有使用调试器 而是使用了一个分析器 分析器就是一个很好的测量工具 我用一些步骤 来重现运行很慢的 程序 我用一个附加的分析器 来执行这些步骤 因此我可以深入地了解 当我的代码运行缓慢时 它发生了什么
然后再结合 我的程序必须要如何运行 才能完成手边的任务 我找到其中正在进行的步骤 并删除了它们 因为想让你的代码变快的 首要方法就是删除多余的步骤 无论它正在计算的是什么
现在 我修改了源代码 在必要时进行反复测量 直到我对整体的结果 感到满意为止
当我通过这种方法提高性能时 我发现自己常常处于 一些场景当中 这些不同的场景 改变了我测试有问题代码的方式 以及复制这些 漏洞的方法 有时我会遇到 严重的性能衰退问题 对吗 开始一切都在平稳地运行 然后我们的团队里 有人对其进行了检测 这个人也许是我 然后发现性能出现了严重的衰退 那么我们就需要回去 找出这种衰退的原因 如果这种衰退十分明显 或者是它出现在一个 我认为近期 不可能再次衰退的地方 我也许会自己手动地 运用附加的分析器 对它进行测试
但是 想提高你的性能 这是一个不大容易的过程 因为很可能 只是一系列轻微的衰退 就会让你功亏一篑 我建议你们所有人 创建自动化的性能测试 来捕捉你的 App 的性能 这样你就可以 保证它不会随着时间而衰退
我经常所处的另一个场景是 App 的性能 在很长一段时间后 还能保持不变吗 也许在一些绘图测试中 它运行的速度是 45 帧每秒 但是我们希望的运行速度是 60 每秒 所以我们需要小幅地提高它 并且我们有理由相信 通过我们先前的性能工作 我们可以利用现场修复 以及增量式变化 来对它进行提高 在这种场景下 我或许也已经自动地 开始进行测试了 因为随着时间的推移
我已经很了解我的性能了 还有第三种场景 我们的 App 因为设计不良而受到损害 性能也远远比不上 它原应达到的效果
我们知道我们不能用 简单的场地修复来提高性能 因为过去我们这么尝试过 但并没有成功 无法提高性能 在这样的情况下 你会想要做一次彻底的性能检修 你会想重新设计一些特性的核心部分 或是有问题的算法 以使性能成为一个 首要的约束条件 在这些情况里 你必然要对性能进行检测 以测量你确实 达到了你的性能目标
知道你要测试什么 很重要 我想要提醒你的是 我从来都不会立刻投入 这些性能检修 将其视为解决 某种性能问题的方法 我喜欢这么做 这是一种未曾开发的工程 你能够从头开始 设计这些东西 但这也颇具风险 你最终要的是 一个性能更好的产品 但这却是一个曲折的过程 因为你需要重新设计 整个特性 当你做这种工作的时候 你不仅需要知道 这些代码的 功能约束条件 你还要知道 性能约束条件 以及你的用户应用这一特性时 最常使用的 典型的模式 而你只能从 过去这方面的性能工作中 获得这方面信息 我想和你们分享一件轶事 是我们的一个 在 Xcode 里工作的案例
在 Xcode 9 中 我们重做了 “Project Find” 将性能作为一个 首要的目标 我们的目标就是要 在短短几十毫秒中 传递出搜索结果 当我们与同事 讨论这个特性时 我们常常遇到一些挑战 比如让我们在一些大的项目上 搜索一些类似于字符串 或者甚至是字母 E 这样的东西 这些东西能搜索出成千上万种结果 不是吗 当然如果你的 App 能够迅速生产出成千上万的结果 它搜索任何东西 都会很快 但是如果你思考一下 什么是典型的模式 我们搜索我们使用的 API 我们自己的类名称 还有我们引用的图片名称 诸如此类的东西 它能搜索出很多结果 也许上百种结果 当然十分重要的是 当有一百万种搜索结果时 你的 App 能够 正常地工作运转 但是通常的使用情况是 有数百种搜索结果 那么 在进行搜索这样的任务时 你所做的一些工作 要与生成原始结果 这样的东西相对应 而其余的工作则要基于 你能多有效地索引 项目中的文本 并预先避免工作 在这两种场景中 你可能会有完全 不同的优化目标 让其中一个目标的搜索速度 快于其他的目标 对吗 因而你有必要了解 你的用户将要 如何使用该产品 这样你就可以优化 应该优化的案例
在所有的这些案例中 我需要进行一些形式的测试 无论是手动地 还是自动地
我想要与你分享 两类典型的性能测试 我通常用它们 来测量 Xcode 的性能
我们或者进行单元测试 或者进行综合测试 我们来对比一下这两种测试
在性能的单元测试中 你的目标就是对你的 App 的 某些特征进行隔离 并单独地对其进行测量 你也许会抹去它的相关性 然后在一个 隔离的环境中 启动它 如果我要为 Xcode 的代码完成 性能的单元测试 那我也许会编写 一系列的三个小测试 其中一个测试会测量 与编译器的对话 并得到原始的结果 和原始的代码完成候选的结果
另一个性能测试则会测量 那些结果的相互关联 对它们进行排列以及评分 以便我们知道要将哪些结果 显示给用户
第三个测试会选取 那些已经准备好的结果 并测量将它们放入 UI 元素 后最终展示出来的情况 为了涉及到全部三个方面 我会很好地覆盖 IDE 中 代码完成的主要的组件
这些性能单元测试 有一些很好的方面 我们将高度聚焦这些方面 这意味着如果它们 在将来出现衰退 我会很清楚地知道 这些衰退出现在哪里 因为这些正在运行的代码 已经处于检查之中了 而它们也将在不断地运行过程中 产生出更多的 可重复的结果 而它们产生的时间 也不会有太大的波动 因为这些代码 是高度聚焦的
现在 我们来将单元测试与 综合测试进行对比
在综合测试中 你要做的是 在用户使用 App 时 对其性能进行全面地测量 所以 如果我为 Xcode 编写 代码完成单元测试 哦不对 是综合测试 我会启动全部的 Xcode App 我会打开一个源文件 我会定位到该源文件 然后进行书写 再反复地进行代码完成 当我执行这些步骤的时候 看看 Xcode 在做什么 看看它花了多少时间 我会发现这个测试 一点也不聚焦
Xcode 会在我书写的时候 进行绘制和布局 当我归类时 它还会同时进行语法着色 在后台 Xcode 也许正在进行索引 读取获取状态 并决定把新的文件 显示在 Assistant Editor 中 而所有的这些 都将与代码完成一起 来竞争 CPU 的资源 也许当我查看 Profiler 时 我会看到我们花费了 80% 的时间在做语法着色 而剩下的 20% 则用在了代码完成上 从这个数据我可以知道 要提高代码完成性能 最好的方法就是 推迟语法着色 这样的知识 是我在高度聚焦的单元测试中 所无法获得的 所以如果今天各位听完这个演讲 只能带走两样东西的话 第二样就是 要调查你的性能 必然应该从这些 广泛的综合测试开始 它们会测量你的用户 使用 App 时的用户体验
对 我说的就是测试 测量以及分析 现在我想向你们 介绍一下用工具 在 Xcode 中进行分析 让我们来看一下 demo machine
今天我们要看的是一个 我们在 Xcode 9 和 Xcode 10 之间修复了的性能问题 我想展示给你们 下面我先启动 Xcode 9 打开我们的 Solar System App
那么 我们所 面临的问题就是创建标记 接着我要快速地 按几下快捷键 “Command-T” 如你所见 整个屏幕闪现了黑色 并且创建这些标记 花了好几秒的时间 这肯定达不到 我对性能的预期 我们需要来对它进行修复 那么我们来看看 如何进行修复
首先 我将启动 Instruments 这是我们的分析工具 你可以从 Xcode 菜单中打开它 在 Instruments 中的 Open Developer Tool 里面 现在 我在 Xcode 9 里面 所以如果我选择这个选项 那我启动的是 Xcode 9 中的 Instruments 当然我想要从 Xcode 10 中启动 Instruments 我已经把我的文件放在这里了 我将隐藏 Xcode 打开 Instruments 当 Instruments 启动时
就会出现一系列的分析工具 我们可以用它们 测量我们的 App 这里是各种各样的工具 它们能够测量 图形的利用率 内存消耗 IO 以及整体的时间
你们也许会对从哪个工具 先开始学习 而感到不安
那么我对你们的建议是 如果只学习其中的一个工具 那么你们应该学习 Time Profiler 在我的性能工作中 有 95% 甚至更多的情况下都在使用它 当你的用户抱怨 你的 App 运行很慢时 他们是在抱怨 它花费了太多的时间 假如你的 App 太慢了 是因为你运行了太多的 IO 从而花费了太多的时间 那么你就能在 Time Profiler 上看到这一点 所以如果你只学习一个工具 那就学 Time Profiler
我们来看看它是如何工作的
我要启动 Time Profiler 只需要双击这里 然后将 Instruments 全屏显示
下面 我想要记录 Xcode
在 Instruments 视窗的左上角 你可以控制 你想要附加和记录的进程 在默认情况下 只要按下这个记录按钮 我的 Mac 上的所有进程 都会被记录 而我只想锁定到 Xcode
我会将这个弹出框 切换到 Xcode 然后点击记录 现在 当我记录的时候 我就要关注视窗中的这个区域 来追踪轨迹视图 我要调整 Xcode 视窗的大小 将它调小一点 我就能看到那个区域 然后我就要进行让它变慢的操作 我要多创建几个标记
你可以看到这里的图表 发生了变化 现在 我要退出 App 然后回到 Instruments
所以刚刚发生了什么 当分析器在运行时 它被附加在我们的进程之中 像一个调试器 这个分析器每秒钟 都数千次地停止 我们的进程 并且它采集了跟踪轨迹 那么 提醒一下 跟踪轨迹描述了 你的程序如何到达 它目前所处的位置 所以如果你现在 处于 C 函数的第 6 行 你到达那里的原因是因为 Main 函数调用了 A B C 那么你的跟踪轨迹就是 Main A B C 当 Instruments 捕捉到 这些跟踪轨迹中的其中一个时 它会说 嘿 我们刚刚在 C 函数中 花了一毫秒 它说一毫秒 是因为我们的 记录采样间隔为 一毫秒每次
那么 在主线程上 所有的这些跟踪轨迹都要 从 Main 函数开始 它们也许将要调用 Application Main 之后它们将会通过你的源代码 不断地进行扩展 我们可以将这些跟踪轨迹 折叠在一起 将它们覆盖放进一个前缀树 以便它们从 Main 开始 进行下去 我们可以将我们 捕捉到的这些毫秒计数器 放在顶端 这样我们就能够 看到在我们源代码的 所有不同层级的区域上 我们都花费了多少时间 我们要看看这个数据 试着找出多余的 和不必要的操作 这样我们就可以提高速度 这是我们提高 App 性能的 首要方法 现在 正如你能想象到那样 我们每秒能捕捉 上千的跟踪轨迹 在 Instruments 里 有大量的数据需要你去处理 我首先要给你的建议就是 你想要尽可能多地 过滤这里的数据 你需要看整个过程的性能 不要把精力集中在细节点上 好吧 我想要给你演示 如何应用一堆强大的过滤器和工具
因为我之前做了记录 还记得吧 我让轨迹视图显示了出来
我这么做是因为我想要 看看当我创建标记的时候 CPU 的利用率是如何变化的 以及在哪里发生了变化 然后我告诉自己 就在这儿 我只是拖动鼠标并选中了 轨迹所在的区域 我让 Instruments 将它的跟踪轨迹数据只专注于 那里的时间间隔上 所有在这里的东西 都是我创建标记之前的 而所有这里的东西 都是我创建了标记 退出 App 之后的 那不是我现在要优化的地方 所以我不需要 看那里的数据 现在 在 Instruments 视窗的底部区域 Instruments 会显示出所有它 所采集到的轨迹 默认情况下 每个线程上只有一行在运作 在这个例子里 看起来只有四个线程在运作 而有时候则会更多 这取决于你的 App 是如何并行的 我通常喜欢以聚焦的名义
我也喜欢折叠它们 让他们建立在每个线程中的 函数执行的最顶层上 而不是线程 ID 上 因为前者与我使用 Grand Central Dispatch 的方式 更为契合
接下来我们看看 Instruments 视窗的底部 我要点击这个叫 Call Tree 的按钮 然后放大它 这样你就能看到 我要做什么了 这里有几个可用的过滤器 其中一个被线程分开了 默认情况下它就已经开启了 接下来我要禁用它 相反地 所有的线程都将按照 它们的最顶层入口点来分组 而不是它们的线程 ID
现在 来看这个轨迹 可以看到所有这些 正在运作的线程 顺便提一下 在主要轨迹的下面 是总的 CPU 使用率 CPU 使用率被分解到每一个线程上 我们可以看到在这个轨迹中 几乎其他所有的线程 大部分都是闲置的 我可以只聚焦主线程 从这里选择它 现在我们就可以只看 在这个时间段内 主线程的轨迹
我准备开始深入到 这个调用层级当中 这样就能看到我的 App 在做什么 通常我会键入这一过程 只要按下向右箭头和向下箭头 然后不断地进行重复就可以了 但是我想要给你们演示 Instruments 提供的 最密集的跟踪轨迹检测 如果你的检测是不可见的 你可以用这个按钮 进行切换 最密集的追踪轨迹就都会在这 在这个标记里 “Extended Detail” 那么 最密集的追踪轨迹 正是出现得最频繁的 那个轨迹 当我们在当前选择下 进行记录的时候 追踪轨迹出现得最为频繁 你可以利用这个 在一段时间内 快速浏览许多帧 我通常会浏览这里 寻找我自己的 API 还有值得我花这么多时间 来寻找的东西 或者是寻找 我们在样本数中 有重要分支点的地方
现在我们看这里 我看到这个给 IDE 的调用 Navigator Replacement View Did Install View Controller 我十分熟悉这个 API
在这个轨迹中 我可以查看这里 在视窗的左手边 这里负责的是 我们记录的总时间中的 或者 45% 的时间中的 1.19 秒 这种方法要花费的时间 远远超出了我的预期 但是 我们很难聚焦在这里 看看究竟发生了什么 对吗 这是这个轨迹下面的 其他所有的东西 那么 它看起来就好像 30 到 40 的堆线范围深度 这是很吓人的 我要给你演示如何聚焦这里 第一种方法就在这里 我们又要使用 “Call Tree” 弹出框 我要在这个弹出框里 选择 “flattened recursion”
让我们来继续完成这步 现在你就可以看到 就在这里 一组重复的方法调用 被折叠了
不好意思 让我滑下来
它被折叠了 事实上 我就是想要 继续我的性能测试 在这个 IDE Navigator 范围里 在这个 API 调用中 我可以根据上下文 重新聚焦整个 Call Tree 点击这里 然后选择 “Focus on Subtree” 之后 Instruments 就会 将这个符号放到 整个调用图的最顶层 其他的所有东西都会被删除 它会重新将百分比设置为 100% 因此我可以就聚焦在这里 现在 我可以用键盘上的箭头键 来继续运行这个样本 看看我们在做什么 我很熟悉这些 API 看起来我们好像正在进行 状态恢复 当我继续扩大这个 我可以看到我们几乎 深入了这个表格视图 除了有这种 热调用路径以外 你知道 这种调用路径占了 大部分的总百分比 同时还有其他的这些附带样本
它们很容易分散 我们的注意力
它们的其中之一就是 OPC Message Send 它可以遍布你的追踪器 只要你在写 Objective C 即使你在写 Swift 代码 当你按照你的方式 进入系统库时 你也会看到这个 你会经常看到它的对应函数 OPC LoadStrong LoadWeak Retain 等等 你可以 将所有的内容从调用关系树里删除 根据上下文点开这里 选择 “Charge OPC to Callers” 它就会告诉 Instruments 来选取所有 来自于 OPC 库的样本 然后将它们从调用数据中删除 但是要将时间的属性归于 调用它们的父框架 我倾向于将那些 Objective C 运行时的函数 视为在书写 Objective C 代码时 的代价成本 我很少会去试图优化它们 所以我只会 将它们从数据中删除 这样我就可以聚焦在 我可能要处理的地方
还有一个你们可以应用的 强大的过滤器 我要用它来删除所有 出现在这一组帧中的小样本 它就在这里 Call Tree 约束条件的部分 让我来给你演示
我将要告诉 Instruments 我想要看到的轨迹范围 只包括我们所说的 20 或者更多的样本 我选择 20 是因为 我知道我已经选择了 一个两秒的间隔 那么 20 毫秒 将会呈现出整个工作的大约 1% 而就包含了 在默认情况下 我想进行工作的粒度
因此将 Call Tree 约束条件
设置为一个最小值 20 我就能更有效地聚焦在这里 那么 我在这里提到过 我们在扩展我的视图项目 我看到在这里 我们其实在调用 NS 大纲视图 扩展项目 扩展子项 在这里 很多人都会将调用图 停在这里 我们会看到 我正在调用一个系统框架 我在那里花了很多时间 这不是我的错 对吗 那么我能做什么呢 我不能优化 NS 大纲视图 扩大项目
但你肯定有能力 改变这些情况 例如 花费在系统框架上的 所有这些时间 都是因为 它在运算你提供给它的数据 这需要花费很多的时间 因为你在成千上万次地 调用这个方法 它会花这么多时间 还因为它还要通过授权 回调你的代码 而最重要的是 你可以深入地了解 系统框架在做什么 只要沿着 Instruments 树 不断地进行扩展 并查看这些被调用的函数的名称 其实 我就是这样学会 修复这个漏洞的 当我将这个轨迹 扩展到大纲视图中 我能看到 它正在调用这里的两种方法
用项目入口来 批量处理扩展项目和子项 然后再进行最后更新后的工作
现在 那些对我来说都是重要的线索 说明我们或许 有可能通过批量处理来提高效率 正如你能想象的那样 大纲视图从一小组项目开始 然后我们试着 在我们代码的这个区域内 不断修复扩展状态 我们会告诉它打开 比如说顶部的项目 而当我告诉它打开顶部的项目时 你能够想象 它同时将里面的其他项目 都向下移了 然后我继续扩展第二个项目 它就会再次下移其他项目 接着是第三个项目 以此类推 而等到你做完这些 你已经将这些底部的项目 向下移动了数千次 那这些都是多余的工作 而它们正是 我在提高性能时 想要消除的东西 现在 这些方法调用 谈到的批量处理 让我觉得或许 在一些 API 中我可以 让大纲视图批量地进行工作 因此它就可以对所有的位置 只进行一次计算 而不是在我进行调用的时候 反复地计算
我还看到一个调用 用来进行最后更新之后的工作 那么 有时 API 会提供那种 在一组列阵上进行运算的 整体方法 而有时它则会提供一种 事务型 API 它会说我要开始进行更改 然后就做了一系列的变化 然后你说你完成了 之后它会对你变化的全程 进行计算 这比它自己独自完成这些 要更有效率
那么这时候 我会前往 NS 大纲视图或者 NS 表格视图 API 我会找一些这样的方法 那里确实有一个 在 NS 表格视图里 有一些开始和结束更新 所使用的方法 它们允许合并表格视图 并能够大大提高这些工作的效率 当然 我们要在 Xcode 10 中使用它 让我来给你演示 我要启动 Xcode 10
我要把 Source 作为一个 App 打开 然后我来创建一些标记 你可以看到 这里屏幕没有出现闪黑 标记也打开得更快了 现在 我想让标记打开的速度 比这个更快 对吗 那么我接下来应该怎么做呢 我很幸运 因为你不是每天 都需要进入轨迹中去寻找 一些如此明显且容易修复的东西 而这样的东西在样本里 占到了 50% 对吗 其实并没有 什么巨大的严重问题 等待我来解决 相反 我要做的其实就是 检查整个样本 并在此过程中应用过滤器 所以我只是寻找 一些只占到 1% 或者更多时间的操作 我要寻找每一个 我认为能够运用一些方法 来将其速度 提高一点的东西
我会将它们都记下来 放在一张纸上或一个文档里 或者其他什么东西上 然后我就会着手解决它们 现在 我需要选择一个 解决它们的顺序 对吗 因为有的时候 用修复第二件事情时 所用的方法来修复 列表中的第五件事情 这方法可能会是过时的 如果没有排好顺序 你就会做多余的工作 这是十分糟糕的 因为我们首先要删除的就是 多余的工作 但是这些工作都是如何进行的 我们又很难去预测 你通常无法事先知道这些 所以不要因为这个 停下你开始的脚步 因为你想要把速度提高 30% 就得进行 103% 的 改善工作
明白吗
现在 回到我们的幻灯片 我要给你演示 一些我们常用的方法 用来持续改善我们的性能
毫无疑问 我们最常见的就是 使用那些与大纲视觉里 相同的方法 批量处理和推迟处理 对吗 你有一个 API 而当这个 API 被调用时 会出现一些副作用 然后你用一些代码 调用你在循环中的 API 这就是你在做的 被请求的首要工作 这其中有一个副作用 好 如果没有人读取 这个副作用的结果 那你就是在反复地 进行多余的工作 你通常可以 通过使用成批的接口 得到一个更加有效的接口 在这里客户端会给你一系列的 或者某种集合 包括所有要完成的工作 以便你能够一次性地 计算那个副作用
那么 有时你有很多个客户端 对吗 你无法对它们进行 批量处理 那也没关系 你仍然可以通过推迟工作 以及缓慢地进行工作 来获得一样的性能风格
第三种提高你的性能的 简单的方法 是检查整个 Instruments 的轨迹 来寻找你看到它 对同样的东西进行反复计算 的地方 比如说 有一个方法 在它计算某个文档的大小的过程中 你看到同样的事情 在之后的几帧中也出现了 对于相同的文档 不断地重复 好 在这样的情况下 当然 你肯定想 一次性计算出该值
在最顶端计算它 让它不断地传下去 或者缓存它
另一个你可以在你的 UI App 中使用的方法 就是考虑 你使用了多少视图 来渲染你的 UI 使用一些带有小的 函数集的很小的视图 然后再将它们一起 生成大的函数 以此来组织你的源代码 这是很好的 但是你使用的视图越多 你就越难负担绘图 进行系统的布局
现在 这是一条双向道 因为更小的视图通常会 让你有更多精密的捕捉 它们也可以提高你的性能
但是一般来说 你可以微调 你拥有的视图的数量 以对性能产生 显著的影响 但视图更少的话 也并不都是最好的 否则我们所有的 App 都会只有一个包括所有内容的 巨大视图了
另一个经常 用到的方法就是直接观察 我们的源代码中通常 有两个松散耦合的方面 也许它们彼此知道对方 而它们是通过一些 间接的机制 进行交流的 也许它们用 NS Notification Center 一些基于代码块的回调 授权或者关键值观察
我常常见到 我们会有一些模型代码 它们在一个循环中 被不断地改变 而每次它走向循环 就会引起许多 KVO 通告机制 当然 你其实并不能 在模块代码中看到它 但是在一些其他的控件里 它会十分活跃地作出回应 并试图与模块里的变化 保持一致 这时你就花费了许多 CPU 时间 最终当你考虑整个的 变化的时候 这些都是多余的工作 那么 如果这是模块代码中的 直接调用 那么 不论是通过通告机制或授权 还是手动进行基于代码块的回调 改变都会发生地更加明显 当你编辑该模块代码时 你也许认为 将一些通告机制从循环中 拖出到循环之外 以对性能产生 较大的影响 是完全合适的 那么 或者 在控件这边 你可以用这些 推迟和批量处理方法中的一个 来避免多余的工作 只是它们的回应 是不同步的
最后 这是最简单的一个 一旦你的代码已经在 一个很好的路径上了 你知道 它已经是线性的了 而且不会有比线性更好的了 这是一种你要
获得的最低性能 毕竟 你要尽可能地 改进常数时间 那么 很简单 如果你将词典 作为对象使用 那你或许已经看到这点 如果你有一堆为秘钥准备的 字符串常数 那么你就可以大幅度提高 代码的清晰性 提高代码完成和重构 以及源代码验证 通过使用特定的形式 用它们的 strucks 和 swift 用它们隐含的初始化器和一致性 来平等哈希值 这再简单不过了 而这能轻而易举地 改进你的源代码 如果你在许多小目标上 完成数百万次的 字符串哈希映射 和字符串 equation 的话 你会惊讶于你花费了 多少时间
那么接下来的时间 我想请 Matthew 上台来 谈谈我们如何将这些方法 应用到“照片”中去
[ 掌声 ] 谢谢 Jim
大家好 我是 Matthew Lucas 一名“照片”团队的工程师 今天我想给你们一些 直接从“照片”中来的 实用的性能的例子
那么首先 让我们先来 简单谈谈“照片” 我们都十分熟悉这个 App 它可以供你存储 浏览 并体验你最喜爱的时刻 因此你可以浏览你最喜欢的时刻 从这个“时刻”视图里 在这里可以看到 这是默认视图 但是你也可以 切换到另一种视图“精选” 或者“年度” 这个我一会再细说 现在图片库里可以有 1000 到 100000 张先前的素材 这取决于你对拍照的喜爱 我们都喜欢捕捉每天的生活中 那些快乐而珍贵的瞬间
所以我们都很耐心地去捕捉它们 但是当这样的事发生时 我们并不是那么地有耐心 在你启动“照片” 这个 App 的时候 如果你的“时刻”变成了这样 你会有什么感受 那么 你也许 还会遇到这样的情况 在这里只有许多的 空白图片框 这样看起来 并不是非常好 也许你轻轻滑动 这个灰色的地方就会消失 照片就会开始加载 但你继续滑动 你就会遇到一些掉帧 因为视图被更新了
好 我们的目标就是 不要出现这样的视图
我们认为这种视图 并不是好的用户体验 但是我们知道有时候 这是不可避免的 但是当它过于频繁时 就无法忍受了
现在 当你运行一个 App 时 你想要确保它一开始就能用 能够做出响应
你还要确保 动画是很流畅的 而这两个属性 对用户体验来说 都是十分重要的 如果用户发现你的 App 并不相关 他们也许不会再使用它了
现在我来举例说明这两点 我要给你们 举两个例子 第一个就是我们如何 对这个“时刻”视图的启动 进行优化 第二个则是我们如何 创建这个“精选”和“年度”视图 以更好地适应用户的偏好
首先是启动“时刻” 那么什么是启动 我们有三种启动
第一种也是更为昂贵的一种 即我们所说的冷启动 它取决于在重启后 第一次重新启动你的 App
所以里面基本上 还没有缓存任何东西 也许你需要载入一些后台过程 或者一些库
下面在这样的情况也会冷启动 系统在内存压力下运行 开始回收一些内存 那么 如果你关闭一个 App 也许不会触发代码运行 因为是系统决定了资源 应该何时被放在 页面上 而如果你关闭了一个 App 然后几秒之后重新打开它 几乎可以肯定 你进行的是一个热启动 我们称其为热启动 是因为资源或从属的东西 都仍然在缓存里 所以它能更快地启动
那么 最后一种类型就是 我们所说的热启动 根本上说它就是继续启动 因为这时 App 已经处于运行状态 直接把它带回前台就可以了 所以当你开始测量启动时 你应该从测量 热启动开始 它所用的时间 要比冷启动所花的时间 变量更小 而测试迭代也更快 因为你不需要 重启你的设备
那么 我们用来测量启动的方法 是对从你点击这个 App 的图标的那一刻 到你可以开始与你的 App 进行互动 的整个时间进行评估 我说互动的意思就是 它真的可以使用了 而不再有旋转的圈
它通常会在显示 这个旋转的圈的同时 分派一些工作 但并不能让 App 更快地投入使用 所以我们想要 避免这些 现在 我们有三个针对 “照片”的目标 第一个就是我们想要瞬时 我们不想出现任何 等待的圆圈 我们不想出现任何
空白的图片框
坦白地说 你也许会看到一些空白图片框 在你第一次 同步 iClub 的时候 但是当数据本地化后 就尽可能地不显示了 那么 我所说的瞬时是什么意思呢 就是它启动所花的时间 应该和从主屏幕上 放大的动画过程的时间 保持一致 这个时间通常是 500 到 600 毫秒之间 这样一来 从主屏幕到 App 的转换 就是无缝连接的了 用户可以立刻与 App 互动 只要它显示完动画过程 顺便说一下 这是最基本的建议 这不只是给“照片”的 所以它适用于所有的 App 那么让我们来看看“照片” 现在是如何启动的
如果我们近距离地 看看到底发生了什么 你就能看到“照片” 在动画过程完成之前 就已经准备好了
如果我们深入剖析这个启动 我们就会看到 这里主要有两个部分 第一部分时间花在 DYD 中 这是载入器 它将加载并连接 你所有的动态链接器 而它还要运行你的 静态初始化器
虽然你对这部分 的控制是有限的 但也有可能做到 我鼓励你们去看看 去年 DYD 的部分
就能得到这个方面 更多的细节
那么 DYD 也要 在你的项目表中调用 Main 这就将我们带到了第二个部分 在这里你有很多控件 这个部分里 你需要确保 将它的时间保持在 500 毫秒以内 现在 第一个布局正好被排在 Did Finish Launching 之后 它将标记启动的结束 而这时你基本上 就可以使用你的 App 了
在这个部分 我们将要提到 一些原则 它们的确是我们在 进行性能工作时 最大的收获
第一是我们需要惰性 推迟我们不需要 的工作 第二则是要积极主动 而这两个方面 都有它的效用 积极主动能够 十分有效地预料 我们之后要做的工作 我们想要积极主动地 快速捕捉到衰退 因而你要确保 你设置了连续的综合测试
最后一点就是 无论我们总共需要 加载多少数据 我们都要持续不变
现在 如果我们用这个方法 我们载入所有在启动中 要用到的东西 这是它大概会在一个 有 30000 项的库里 花费的时间
首先你需要初始化数据库 然后你需要 准备一些视图控件 接着需要配置数据资源 加载一些库图像 然后读取云状态 你要记住 这个时间也许会 随着数据的生长发生变化 而其实数据将会一直生长 只要人们每天都拍照 所以在“照片”里一定要记住 我们要处理的是没有边界的 数据集 那么让我们来看看 如何优化“照片”的每一个步骤 就让我们从初始化数据库开始
首先 数据库通常 是在执行第一个查询的时候 被初始化和载入的 我们发现了一个非常 值得优化的地方 就是尽快地 在后台线程中完成初始化 这样它就不需要 在执行第一个查询时 再进行初始化工作了
这是个问题 特别是 如果第一个查询已经 从主线程当中完成了
现在 我们已经花了很多时间 而我们仍要花费很多时间 来检查我们在启动过程中的 所有的查询 我们想要确保 我们正在做的工作 是必要的 我们不再做更多的工作
那么最后 我们要确保 我们所做的所有查询 都要尽可能地高效 我们也想尽可能地 避免复杂的查询 我们有时知道 我们需要这样做 针对这类情况 我们设置了一些索引 以便我们能提高速度
现在我们的目标是 在初始化上花费的时间 不超过 30 毫秒 那么接下来让我们看看 我们如何准备视图控件
这里我们有四个标记 呈现在 App 的 主要特性上 那么我们需要注意的第一件事 就是我们想要 通过做最少的工作 来初始化这三个 不可见的标记 这里我们要遵循的原则 就是做尽可能地 少的工作来进行初始化 我们想要将工作最小化 同时记录载入的视图中的 所有的数据
这也让我们可以 在恒定时间内 初始化我们的控件
那么最后 我们想要 确保我们只加载了可见视图 这很简单 而我们通常是 在那个部分出现衰退的 因此你应该格外小心 那么在准备视图控件的工作上 我们现在的目标是 花费 120 毫秒 但是准备视图控件意味着 要配置数据资源 接下来我们就来看这一块
所以在“时刻”视图里 会呈现出这些东西 呈现你生活中的事件 而 UI 则通过获得 这组照片来呈现出 这些标题栏 比如说在这个图库中 我们也许有 500 张照片 为了创建一个视图 我们需要预先加载所有的图片
但是我们只是需要 这些照片的元数据 这样我们就可以创建视图了 我们不需要你的内容 因此我们首先要做的就是 执行那个超高速的询问 然后我们只加载 我们这里需要的内容
既然是这样 我们只要加载可见内容 在我们这个例子里 可见内容在 7 到 10 个图片之间
因为我们的可见内容 是有限值 所以我们可以 在主线程上 同时进行加载 同时 我们还想预先安排工作 这样我们就可以 开始异步加载 剩下的数据 我们在后台线程上 以正确的特性进行加载 来确保它不会 抢占主线程的运行
在这里我们的目标是 100 毫秒
那么最后 我们的数据源 还提供一些图像 我们来看看如何优化这个部分
所以目前为止 这是我们面对的 最大的数据块 当我们意识到在启动时 我们花费了好几秒的时间 来加载这个图像 我们就意识到 我们做了太多的工作 所以我们所做的第一件事 就是估计我们在启动时 需要用到多少张图像 然后在第一个事务中 我们只加载那些图像 这种情况下 这一数量 可以达到 60 张 包括上下堆起的 接下来 为了先加载那些图像 我们需要确保 我们只加载 低分辨率的图像 这样我们加载在 内存里的像素就越少 这样就能更高效
呈现出这个数据块 现在需要 200 毫秒
这是目前为止 我们提速最多的部分 我要它成为一个常数时间 真的很不错 那么 有时候你禁不住 问问自己这个问题 这在启动的时候真的需要吗 我们这里的一个例子 是页脚视图 它通过网络或数据库 来搜集信息 而其实我们最先的设计 是不要在启动时显示它 以优先所有我们 在这里看到的图像 我们想要显示尽可能多的图像 那这样可能就更简单了
我们现在只要安排 启动后的工作 我们缓存并加工之后要 显示的信息
现在 如果我们已经需要 显示这个信息 有一个方法可以办到 利用后台来 从 UA 工具包中 刷新 API 它便会主动清除你的 App 这样当用户要启动 App 的时候 你就可以开始 准备一些内容了
所以现在 这个部分已经从启动中删除了 为我们节省了 400 秒的 CPU 时间
现在我们看这里的 更新后的分解图 可以看到这些工作 只值得花费 450 毫秒 而我们现在在这个 500 毫秒 窗口之中 而且不论如何 将其同时呈现出来 最重要的是 一定要确保 你要考虑你 准备内容的时间成本 我这里说的考虑 是要真的去测量它
现在 你应该争取 在常量时间里进行工作 无论你加载的 数据总共有多少 在我们的例子里 我们的确有许多数据集 我们需要保持常量时间
既然我们已经启动了 App 我们要开始使用它 那么我们来看看我们是 如何创建“精选”和“年度”视图的 以便我们提高性能 正如我之前提到过 我们的用户能够进行 从动画过程到“时刻”视图的无缝转换 再通过“精选” 到“年度”视图
这是一个复杂的层级 我们要展示数千张的照片 我们要支持实时更新 我们也要支持 这些图层间的动画过程 我们还有一些手势
那么 我们这里还有一些目标 对于带给我们用户的体验
第一个和之前一样 我们不想有任何 等待的圆圈 我们不想有空白的图片框 当然我们想要 流畅的动画过程 这里所说的流畅 是指 60 到 120 帧每秒 这依赖于你正在运行的屏幕
记得之前我们说过的原则 现在 它们在这里都是有用的 我们想要懈怠并推迟 我们预先递交的工作 我们想要积极地 快速捕捉衰退 但是我们也想要 在布局关口中保持不变 无论我们要加载多少数据
那么 这次我们还想要及时 我们想要记住 绘制循环周期
这里我的意思是 你要记住 我们只有 8 或 16 毫秒 来绘制那一帧 所以我们需要确保 我们不超时 否则就会开始掉帧
现在 让我们返回一步 看看我们要在这里 获取什么 我们要这个便捷视图 里面有分栏和小单元格
而这基本上就是 “精选”视图给你提供的 对吗 只不过在这样极端的例子里 我们会限制 我们使用这个基本的方法 可以获得的极限 那样就会导致过多的 视图和过多的图层
但是随着分层的复杂性不断增加 花在内存上的时间 也在不断增加
所以这里我们需要创新 我们大幅地限制视图的数量 同时仍使用一个 “精选”视图
我们使用的这种方法 在电子游戏里更为常见 它叫做图谱法 它基本上就是 把一组图像组合 成为一个
我们高效地完成了它 开始只用很小的缩略图 然后我们将所有的原始图像数据 标记在一个我们用作 带状的画布上
接着 我们使用图像原始数据 这样就能够避免去解码每一个 我们发送的缩略图 因此基本上我们会显示一条 随机的图像
那么 我们飞速生成并缓存它们 因此我们可以更加灵活
当我们将多个图片绘制成一个时 我们会大量寄存 单元格 图层及目标的数量 这样便简化了布局 节省了创建它要花费的时间 现在这样很好 但是也要有所取舍 这就是它们的其中一个
如果有人试着长按 或强制搜索一个这里的项目 我们就需要计算它的位置 这样我们才能 正确地获得预览 因为只要我们显示一张图像 我们就需要维持 每个图像的映射 以及它的绘制条
现在你也许在想 我们为什么要飞速地生成它们
我们要支持实时更新 这就是原因 我们也需要不一样的视图大小 比如说 我们这里有风景图 但也有人像图
但是这没有关系 因为我们的用户增长 在很长一段时间里 是十分典型的 而我们需要生成 数千张图像这样的例子 相当地罕见
现在 你或许想知道 那我们为什么不生成 整个这部分呢
答案就是我们的设计记录 就是完成这个动画过程 在这里你能看到 在“精选”视图中它们 都扩展到自己的那部分中 或者相反地 折叠进它们的小组里
所以如果说第二部分里 你只要记住一件事情 那就是你应该考虑 你的层级的布局成本 然后对它进行测量
最后 你要时刻考虑性能 在“照片”里我们十分关心这点 这正是我们日常工作的一部分
要了解更多的信息 你们可以观看我在这里提到的 三个实验室 祝你们 参会愉快 谢谢 [ 掌声 ]
-