跳过导航
An illustration showing a 3D stopwatch on a yellow background
Sketch 内部

技术讲座:如何通过改进渲染使 Mac 应用程序更快

了解我们如何在 Sketch 中提高渲染性能,以及未来的发展方向

在 Sketch,我们一直在努力提高平台的速度和响应能力。例如,我们最近讨论了Web 应用程序 Inspector 的速度改进。但这其中最明显的部分是在 Mac 应用程序的 画布上进行渲染——快速准确地将 Sketch 图层转换为屏幕上的像素。我们非常关注此管道,并不断改进。

这些更改通常会在多个更新中逐步推出,因此很少有某个版本会显著提高性能。 但是,回顾几个更新版本,您肯定会注意到差异。

因此,今天我们将进行比较——将今天的 Sketch 与几个月前的版本进行比较,以了解我们在渲染性能方面取得了多大的进展。

但首先,是一些快速背景介绍。

将图层转换为渲染命令

我们的渲染堆栈有很多活动部件。 因此,对于这篇文章,我们将专注于一个渲染子系统——迭代 Sketch 文档树并将其转换为 CoreGraphic 原始调用的部分。 从本质上讲,这会将您的图层树转换为诸如“填充此路径”或“绘制该描边”之类的调用。

我们最初在 2010 年编写这个子系统时,使用的是 Objective-C。现在我们认为时机已到,可以退后一步,进行现代化改造,并看看我们可以改进什么——而不会牺牲渲染质量。

从 Objective-C 切换到 Swift

我们做的第一件事是用 Swift 重写代码。 这不是我们一时兴起做的事情——仅仅为了重写而重写有效的代码通常是个坏主意。 您可能会在此过程中引入细微的新错误。 但是这一次,我们有一些很好的理由进行更改。

用 Swift 重写代码不是我们一时兴起做的事情。 但是这一次,我们有一些很好的理由进行更改。

首先,Swift 与 CoreGraphics 通信的接口要好得多,并且我们认为 Swift 更强大的类型系统可以派上用场。 当然,它也是 Apple 平台的首选语言。 最后,重写代码还迫使我们真正重新审视每个类和行,这有助于我们进行许多其他小的改进。

简化复杂的过程

将图层转换为渲染命令起初看起来很简单。 当您遇到一个形状时,绘制该形状,可能添加边框和阴影,然后继续到下一个形状。 但它比这更复杂。

在某些情况下——例如在使用蒙版时——您必须向前看才能看到接下来会发生什么,或者记住以前发生的事情。 并且在单个图层的情况下,天真地绘制填充和边框会导致不必要的锯齿效果。

将图层转换为渲染命令起初看起来很简单。 但在某些情况下,您必须向前看才能看到接下来会发生什么,或者记住以前发生的事情。

还有其他例子,但它们都有一个共同点——它们使本来可以是一系列简单命令的过程变得复杂和缓慢。 所有这些意味着我们必须采取更复杂的路线。

棘手的部分? 我们并不总是需要使用这种复杂的路线——有时我们可以走捷径,从而获得更好的性能。 真正的挑战是知道何时采取哪条路线。

改进屏幕外位图

在用 Swift 重写代码后,我们开始研究这些渲染复杂性,并研究如何有效地处理它们。 我们怀疑我们可以在这里获得很多性能,因为 Mac 应用程序在绘制时没有完整的信息——因此它做了比严格需要做更多的工作。 我们特别感兴趣的是应用程序如何使用屏幕外位图。

Mac 应用程序在渲染过程中大量使用屏幕外位图,尤其是在它首先需要将多个绘图组合在一起时。 例如,如果您使用低于 100% 的不透明度值,应用蒙版或使用诸如模糊之类的效果。

当应用程序使用屏幕外位图时,它必须为其分配内存,在其中绘制,将生成的位图绘制到其最终目的地,然后在自身之后进行清理。 您可以想象,与仅将输出直接绘制到目标相比,这些步骤会为处理这些像素带来一些成本。

屏幕外位图在操作中

让我们使用一个特定示例,该示例通常会使用屏幕外位图——跨多个图层共享的不透明度

A screenshot showing two shapes on the Canvas of a Sketch file. The first has two squares intersecting, and each square is at 50% opacity — the area where the two squares intersect is darker. The second has a similar layout with the squares intersecting, but the image is handled as a single shape and color is uniform across the whole thing.

如果您有 Sketch 经验,您将知道如何实现以上两个示例之间的差异。 第一个是两个正方形,每个正方形的不透明度设置为 50%。 第二个是包含两个正方形的组,该组的不透明度设置为 50%。

现在让我们将这些转换为绘制图元。 在第一个示例中,我们正在一个接一个地绘制两个半透明的正方形——这使得第一个渲染可以通过第二个正方形看到。 在第二个示例中,首先将两个正方形不透明地绘制到屏幕外位图,然后将整个结果作为一个具有不透明度的图像进行合成。

但是,如果该组在此示例中仅包含一个正方形,则屏幕外位图路线将毫无意义。 结果将没有差异,但是如果我们以这种天真的方式处理它,性能将会下降。 因此,为了获得最佳性能,我们必须提前查看以确保该组中有多于一个图层,然后才能采取屏幕外路线。

建模复杂性

此示例很简单——但真正的挑战是拥有足够的信息来预先确定我们是否真的需要屏幕外位图,或者是否可以直接路线。

我们决定投入一些时间来构建一个数据结构以捕获所有这些细节。 构建额外的树很昂贵,但我们认为,如果它可以帮助我们优化渲染(例如,通过跳过不必要的屏幕外位图),那么成本将是值得的。

它以小增量开始。 在 Sketch 72 中,我们使用新的树来渲染的单个图层。 在 73 中,我们将树扩展到整个组。 在 74 中,我们为需要在一次传递中进行渲染的文档的整个部分构建了树。 每一步都释放了进一步的优化——到 Sketch 75 推出时,我们已经掌握了足够的信息来尽可能跳过昂贵的路径。

回到上面的示例——我们知道设计师有充分的理由将单个图层放置在半透明组中,而不是直接将不透明度应用于形状。 例如,在 符号中,您可以将符号源设为黑色字形,然后为实例指定特定的着色。 现在,代码足够智能,可以查看符号内部并检测我们是否可以直接使用正确的填充渲染形状,并完全绕过屏幕外位图。

显示结果

虽然每个屏幕外位图本身都很便宜,但它们加起来很快。 因此,这些优化确实有所作为——正如您从下面的图表中所看到的。

A graph showing how the time taken to process documents has dropped over time as we have implemented rendering improvements.

此图表仅关注处理文档图以生成原始命令所花费的时间。 除此之外还有其他过程正在发生,因此我们无法声称总体性能提高了“高达 3 倍”。 但是在许多文档中,差异是显而易见的。

此图表还显示了另一个重要的一点——存在各种各样的文档,设计师选择以不同的方式在这些文档中堆叠效果。 没有灵丹妙药,但有些文档显示出非常令人印象深刻的下降。

我们还没有完成。 在将来的更新中,您将看到进一步的改进,这些改进将使 Sketch 工作更快,感觉更好。 请密切关注将来帖子中的那些内容。

您可能还喜欢

Sketch 内部

Sketch 和人工智能

一段时间以来,我们一直在思考人工智能对 Sketch 的意义。以下是我们可能会如何使用它,我们永远不会如何使用它,以及指导我们思考的原则。

免费试用 Sketch

无论您是 Sketch 的新手,还是回来看看有什么新功能,我们都会让您在几分钟内完成设置,并准备好完成您的最佳作品。

免费开始
免费开始