SketchK's Studio.

第 17 章 图像和 UI:示例(iOS和macOS性能优化)

字数统计: 8.9k阅读时长: 31 min
2022/08/29 Share

这一部分的文章是我早年间参与iOS和macOS性能优化这本书翻译时的原稿, 当时的水平有限, 翻译的可能会有一些纰漏, 还请多多指教.
将文章同步到个人博客主要是为了同步和备份.

本章将通过两个具体示例来详述如何优化大型应用程序:一款以图像为核心的天气应用和 Wunderlist 3 任务管理器(译者注:一款生活实用类软件)。

优美的天气应用

几年之前,我与一家刚刚成立的柏林创业公司进行了接触,迄今为止主要业务是开发益智类应用程序,并且取得了相当优秀的成绩。他们的目标是构建一款最优美的天气应用,现在已经基本完成了 UI 设计(参见图 17.1),但是同时遇到了一些阻碍,延缓了项目的交付。

IMAGE

这款应用程序依赖大量图片资源,以致于经常因为内存错误而引发崩溃。此外性能问题也令人堪忧。我已经在前面几章中讲述了一些经验教训,但是最明显的一点仍然是内存警告与线程之间恼人的交互:若主线程运行内存消耗大的程序,进程会收到内存警告并处理,这将阻塞当前进程,甚至可能会被销毁,即使原则上你可以做些事情来挽救它。另一方面,如果后台线程上运行内存消耗大的程序,那么即使主线程尝试处理内存警告,但是后台线程可能仍会继续占用内存,从而导致进程被销毁。

解决方案如下:通过间断性发送消息的方式,后台线程向主线程“登记”申请内存,然后等待主线程返回的结果,特别是在分配大量内存之前。这样做的结果就是让主线程有时间对所有内存警告作出回应,并且在必要的时候停止后台线程。

更新

然而,本章所述的主要任务是对应用进行相应的更新。截至目前,iOS 设备的屏幕尺寸和分辨率数目又有所增加,并且为了同时兼容旧设备与新设备,我们需要处理新设备与旧设备之间存在的巨大功能差异。当然,设计团队可能还需要更多的动画、更逼真的图像以及更强的视差效果。

当我介入这个项目的时候,可能需要花费几分钟的时间才能够启动应用,单单高分辨率版本的图像资源本身就占用了 491 MB,并且这还不是全部的资源。为每个设备添加优化过的资源以及所缺少的图像,很容易就能让应用大小超过 1 GB,但是 Apple 仅允许 100 MB 以下的应用程序使用空中下载技术进行购买与更新(译者注:OTA Over-The-Aire 的简称,是通过移动通信(GSM或CDMA)的空中接口对SIM卡数据及应用进行远程管理的技术,可以简单理解为2G 3G 4G网络)。

所以当务之急是将资源的容量减少五分之四以上,并且还要在不增加资源的基础上支持当前所有 iOS 设备,此外还得大大缩短加载时间。我不得不承认,这似乎是个不可能完成的任务。

探索 PNG

应用初始版本使用的是 PNG 图像,尽管这些图像有着媲美照片的真实画质。我当时对这个选择已经有所质疑,但是在版本发布的前一夜再来做任何事情就真的太晚了,而且这个团队似乎也知道他们在做什么。

此外,虽然这些图像看起来与真实照片无异,但实际上是通过合成得到,而且 PNG 通常认为比 JPEG 更适合进行图像合成。因为 PNG 进行了充分的优化,它使用了 8 位 / 256 色的调色板图像来进行有损压缩,甚至可以将调色板颜色减少到 8 位以下,以为了空间分辨率而牺牲色彩分辨率。

原则上来说,牺牲色彩分辨率是一个好主意,因为人类视觉对于亮度变化的分辨要远远强于对色彩变化的分辨,但是这种做法可能会有所偏差:使用某种交互工具将原始尺寸的图像进行多次压缩,并调整相应的参数,直到图像达到最小尺寸,也就是实现一种“看起来还行”的效果。这种方法的问题在于,由于图像将会被放大显示,空间上的缺陷将会超过色彩上的缺陷。解决方案是同时“增加”颜色分辨率,但是我不知道该如何实现。

原型应用试图避免为每个设备配置多个版本的资源,这种方式也是有问题的——它对某些特定出现问题的设备保存了优化过的资源版本(实际上,这是为了将其降低到适当的分辨率上)。为了不影响现有的渲染代码,这种二次采样和保存操作是在正常加载代码之前完成的(参见图 17.2),这导致第一次启动的用户体验非常糟糕:应用会在加载屏幕停留好几分钟,不停的转圈圈,无法进行交互,并且设备会变得很烫手。

IMAGE

此外,iOS 的 PNG 写入代码不像外部工具那样具备相应的优化机制,因此图像将被保存为 32 位的 RGBA,显然会比原始文件要大很多,尽管图像的分辨率很低。

头脑风暴

就我而言,很显然,“至少” PNG 格式需要重新考虑是否继续使用。我的第一个想法是围绕类似金字塔 (pyramidal) 编码方案进行,或者直接使用诸如 JPEG 2000 之类的小波 (wavelet) 编码。金字塔编码方案的优点是,它通过不停提取图像的较低分辨率版本来压缩图像,并且只存储这些版本之间的差距。这意味着解压缩操作将会自动提取较低分辨率的版本,因此可以仅仅只提供一个图像文件,就可以实现多个分辨率。

不过,iOS 和 Mac OS X 对 JPEG 2000 的支持相当慢,所以使用 JPEG 2000 并不是一个好选择。还有相当多的证据表明,这种性能上的缺陷并不是没有人尝试去修复,而是被现有的格式和技术手段所制约住了。所以这种方法似乎相当令人生畏,尽管我们的需求允许使用较少层次的金字塔简单实现,也可以使用其他的压缩机制来编码这些基本图像。

我们还研究了 PNG 当中所使用的 flate 压缩的替代方法。flate 压缩是一个非常好用的通用无损压缩器,但是我们的需求并不是很广泛,而是非常具体的。例如,我们只需要压缩图像,并且比起压缩速度而言,我们更关注解压缩速度(flate 有时会平衡两者的速度)。我们看到的一个替代方案是 LZ4,它的解压缩速度比起最差压缩比的 flate 速度至少要快 10 倍。

此外,我们其实还有其他神奇的选择,比如说 Apple 的移动芯片组直接支持的预压缩 PVRTC 结构格式。该数据格式不需要 CPU 解码;它可以直接馈送到 GPU,从而得到最佳的性能。另一方面,它的压缩率和质量都很一般。我还尝试使用 MPEG 影片格式,它也有一个硬件解码器,但是在 iOS 上一次只能存在一个 MPEG,而且它很难与场景中的其他对象组合在一起。

JPEG 数据点

直至最后,我们的面前仍然摆放了很多选择,而且依旧不清楚什么是正确的选择。我们需要相关的数据,所以我开始进行试验,从最普通的 JPEG 和 PNG 图像格式开始。

毫无疑问,JPEG 格式的文件在尺寸大小方面遥遥领先,对于 491 MB 的资源来说,使用非常保守的 0.7 质量设置就能够压缩到 87 MB,并且图片质量没有明显的损失。更令人惊讶的是,Apple 推荐将 Xcode “优化过的” PNG 作为默认格式,但是 JPEG 压缩文件的解码速度明显更快,至少在我运行测试的 Mac 上是这样的。

我们决定更加深入研究 JPEG 解码,得到了更多的好消息——使用 TurboJPEG 库能够将速度额外提升 20% 到 200%。更重要的是,对于我们在第 16 章“为何绝对不要绘制缩略图”一节中提到的需求而言,CGImageSourceCreateThumbnailAtIndex() 函数变得完全无用,也就是从 JPEG 2000 当中快速提取低分辨率图像!

测量时的小错误

当然,这里我犯了一个严重错误,没有实际在设备上运行这些测试,真正运行测试的时候就出现了一个巨大的断层:性能变得更加糟糕,特别是相对于 PNG 而言,而 PNG 格式现在竟然变得更加迅速了!这种性能差异对我来说没有任何意义,因为 CPU 是非常相似的,即便设备上的速度要稍微慢一些,但是两个解编码器之间的性能差异也不应该像我们测量的那么大才对。随后我对 Independent JPEG Group 的 libjpeg 软件的其中一个版本,也就是 JPEG 的官方参考实现,发现得到了比 Apple 标准库更好的结果,事情变得棘手起来。

其原因在于,Apple 实际上在 iPhone 系统集成芯片 (SOC) 中包含了一个 JPEG 解码硬件。这个解码硬件的工作效率实际上可能比软件还要慢,但是所使用的功率要更少一些,所以即便它的工作效率较慢,Apple 仍然更加偏好它。此外使用 Mach IPC 接口与硬件通信也会产生一些性能开销。

幸运的是,事实证明这些性能开销是固定的,因为我对一些小图像进行了测量。图 17.3 至 17.5 展示了一组更具代表性的测量结果,我们对不同的尺寸和不同的二次采样设置的图像进行了测量。

IMAGE

IMAGE

IMAGE

对于较大的图像而言,在对更多数据进行解码的时候,固定的性能消耗将会被摊销,此时 Apple JPEG 轻松地击败了 TurboJPEG,这个替代品的性能位于第二位,而 PNG 的速度依旧慢得让人难以接受,因为 JPEG 解码器确实能够只进行部分解码来获得性能的大幅度提升。

最后,尽管小图像相对性能的下降非常显著,但是由于图像很小,绝对性能的下降并没有那么大。一个中等大小的 JPEG 所“耗费”的性能抵得上将近一百个小图像,并且如果有一个非常大的图像,那么结果就显而易见了。此外,硬件性能的下降因模型而异,我所测试的是最慢的设备之一。

所以实际上,这个结果比我们一开始用设备测试后得出的结果要好,如果其他的方案都失败了,那么我们就可以回到 libjpeg 或者使用 TurboJPEG。因此,看起来我们似乎不需要那种高精尖的技术,JPEG 能够满足我们所需的一切。

JPNG 与 JPJP

现在只剩最后一道障碍了:我们需要将许多资源组合在一起,从而形成最终的场景,并且还是用了大量的透明度设置,然而 JPEG 不支持透明度设置。幸运之神再次眷顾了我们,有人曾经遇到并解决了这个特殊问题:Nick Lockwood 提出了 JPNG 文件格式,也就是将 JPEG 和 PNG 组合成一个单独的文件,JPEG 提供颜色信息,PNG 提供 alpha 遮罩。

首先,使用 PNG 来提供 alpha 通道似乎听起来有些矛盾。没错,PNG 支持 alpha,但是我们并不会使用 alpha 值来编码图像,我们只需要编码出一个简单的灰度图,这样就与对另一个图像应用 alpha 通道的表现类似。另一方面,alpha 通道通常比图像更具备块状特性,因此 flate 压缩的效果应该也会很不错。尽管如此,实际上并没有一个很好的理由让 alpha 通道以 PNG 的形式提供,对于我们来说这同样是一个严重的限制,因为这意味着对于最高分辨率而言,必须要砍掉四分之一的数据。

相反,我们决定更新 JPNG 格式,以便它也可以使用(灰度)JPEG 图像来作为 Alpha 通道。此外,我们还会修改库的 API,以允许指定图像的分辨率/大小,并且还可以使用 CGImageSourceCreateThumbnailAtIndex() 来提取较低分辨率的图像。

优美的启动

最后,我们实现了这个看似不可能完成的任务。让这个应用保持在 100 MB 的限制之内,并且还可以支持所有的新设备,设计师对于他们可以添加新图形和动画感到十分满意。用户呢?他们非常喜欢,应用在美国和德国的 App Store 上都获得了 4.5+ 的评分,并且很多评论表示早上打开这个应用还能够驱赶瞌睡。

使用 JPEG 子集化机制的一个好处就是:即便是在非常老旧的设备上,我们仍然可以非常快速地加载这些分辨率显著降低的图像(分辨率只有之前的四分之一甚至八分之一),以便在高分辨率图片正在加载的过程中,用户仍然能够看到最终的场景。

我们能做的还有很多。首先我们并没有集成 JPEG 软解码,所有的解码都要通过硬件解码器完成。显然,从 CPU Profile 中可以看到,CPU 的利用率显著低于 100%。虽然硬件通常要更快一些,但是添加两个软件解码器似乎可以让解码吞吐量至少增加一倍,特别是如果我们还设法对图像进行排序,以便让硬件解码器优先解码较大的图像,软件解码器优先解码较小的图像。我们还可以添加一些 PVRTC 格式的图像,使用这种压缩还能够进一步提升性能,此外或许还可以从 MPEG 视频中解码某些动画序列。这种想法是尽可能多地利用可用的硬件资源,只要它们不会互相干扰即可。

但是这是之后的优化目标了。

Wunderlist 3

2013 年底,Wunderkinder 团队请我去帮忙推出 Wunderlist 3,在这期间,我们一起推出了名为 Objectice-C 客户端:Mac 和 iOS 的架构(译者注:没搞明白),它的表现格外突出,至今仍然无与伦比。这个团队以及参与的这款产品给我留下了深刻印象,。

一年半后,微软对这个团队和他们所构建的产品表现出了浓厚的兴趣,与此同时收购了这个公司,这意味着忠实的 Apple 程序员现在已被邪恶的 Redmond 帝国所雇佣了。并且还深深喜爱上了它!

Wunderlist 2

Wunderlist 2.0 版本就许多方面而言是一款非常优异的产品,大部分用户都非常喜欢,但是它的性能和稳定性仍然亟待改善。当我首次下载并启动应用的时候竟然直接闪崩了,直至最后版本稳定下来之前,依旧连续崩溃了好几次。

Mac 和 iOS 客户端的数据模型使用 Core Data 来构建,此外也用来关联 UI 组件。正如我们在第 12 章所看到的那样,对于少量数据和简单用例而言,Core Data 的表现还行,毕竟性能的要求并不高。但是在处理中等或者大量数据的时候,保持高性能就变成了极大的挑战,开发团队发现他们得创建更复杂、同时也更加脆弱的权衡措施,才能够保证 Core Data 不会由于 I/O 而阻塞主线程。

整体架构

Wunderlist 3 Objective-C 客户端的整体架构如图 17.6 所示。这个架构并没有什么特别的地方。其中包含了一个内存模型 (in-memory model),它会在启动的时候从持久化(硬盘)存储中进行初始化。内存模型将会与 UI (双向)和后端(也是双向)保持同步。我们还会让磁盘存储与内存模型保持同步,但是由于磁盘存储对于应用而言是无法直接看见的,因此这个同步是单向访问。

IMAGE

然而,正是这种简单的架构才可能成就优秀的性能:通过明确界定不同子系统之间的界限,使得责任分工清晰明了。例如,模型对象和内存中数据库都不需要知道存储或者网络 I/O 中的任何内容,这样就不会有出人意料的数据交互发生。它们最多知道如何将自身转换成字典,也就是让外部代码可以将其序列化为某种持久化的数据格式。

我们用另一种方法来表示该架构,如示例 17.1 所示,这次是用代码的形式来表示的。|==|= 运算符(类似图 17.6 当中的实线箭头)表示数据流约束 (dataflow constraints),其行为与 Excel 公式非常类似,并且可以视作永久分配 (permanent assignment),因此它的行为与正常分配类似,只不过系统会维护它们之间的关系。

1
2
3
4
memory-model := persistence.
persistence |= memory-model.
ui =|= memory-model.
backend =|= memory-model.

URI 与进程中 REST

内存模型和持久化存储的基础架构模型是 In-Process REST,这是一种适用于应用当中的 REST 架构风格。所有的实体都由标识符对象所引用,这些标识符对象则是发挥 URI (通用资源标识符,Uniform Resource Identifier) 的作用;在 Wunderlist 中,指的是 WLObjectReference 类的实例,它会将实体类型 (entity type)容器 ID (container id)对象 ID (object id) 进行编码。容器 ID 是封闭对象 (enclosing object) 的 ID,比如说任务所属列表的列表 ID。并非所有对象都有明确的容器;例如,列表或者已登入用户是直接位于 URI 根结构下面的。示例 17.2 展示了用字符串 URI 表示的 WLObjectReferences 实例:

示例 17.2 内部 URI

1
2
3
4
task://container/2/id/3
list://id/2/
task://container/2/
task://id/3

URI 是结构化的。例如,示例 17.2 中的第一个 URI 引用了 id 2 列表中的 id 3 对象所表示的任务。第二个 URI 表示 id 2 列表对象。第三个 URI 是一个数组,表示包含在 id 2 列表当中的所有任务。最后一个 URI 指示表示 id 为 3 的一个任务,并没有提供任何列表 id。在我们目前的实现中,这会在所有列表中搜索满足此条件的任务。

数据存储被组织成一系列对象,其行为类似于 Web 服务器,只是它们不会使用 HTTP 协议来进行通信,不过这与示例 17.3 所示的标准 Objective-C 消息协议类似。如你所见,消息会一一与 GET、PUT 和 DELETE 这几个动词相对应,唯一的区别是,我们通常传入对象数组,而非单个对象。

1
2
3
4
5
6
7
@protocol WLStorage <NSObject>

- (NSArray*)objectsForReference:(WLObjectReference*)ref;
- (void)removeObjectsForReference:(WLObjectReference*)ref;
- (void)setObjects:(NSArray*)new forReference:(WLObjectReference*)ref;

@end

内存存储、硬盘存储和表示 REST 后端的对象都遵循相同的协议,因此大部分的存储操作都是可以互相替换的。为了进行测试,我们可以将第二个内存存储替换为磁盘存储,或者替换为后端同步,也可以同时实现两者的功能,这样就加快了测试的速度。事实上,这个协议非常简单,这同样意味着可以相互组合。例如,我们将计算实体 (computed entities) 的匹配器 (filter) 添加到存储层次结构中,这样就可以使用相同的方式来对其进行访问了,或者也可以针对某个特定的实体,将其放到多种持久化存储当中。

我们可以独立于 WLObjectReferences 所引用的对象来执行相关的计算。例如,我们可以确定磁盘路径和后端 URL。正如我们在示例 17.2 当中所看到的那样,我们还可以剔除 URI 的最后一个部分来确定对象所属的组。

最终一致的异步数据存储

回想我们之前使用 Core Data 的经验,让数据存储保持简洁、快速是初期设计中的优先考虑事项之一。我认为我们成功做到了这一点:我们的 CTO 很喜欢在讲座中震惊听众,他这样说:我们将数据以单独的 JSON 文件形式存储在磁盘上。这种做法其实非常有效——如果您回想一下第 12 章的内容,使用 Foundation 方法来对 JSON 格式进行编码和解码是最快的,恰好 JSON 也是我们与后端通信的数据格式,因此让存储格式和后端通讯格式保持相同,已经证明对于调试而言是非常不错的方法。

我们已经证明,这种简单的机制所带来的性能提升是非常惊人的。我们的其它客户端使用了数据库,或者其他复杂的序列化格式,但是 Objective-C 客户端在性能上始终遥遥领先,特别是在处理压力测试的时候,考虑到一个理智的——呃,不,一个不理智的用户很有可能会创建很多个列表和任务。尽管从事实上来说,使用这种格式就很多方面而言存在很多不足。例如,我们可能会写入太多的小文件。然而,每当我以为某个问题需要换用更复杂的方法才能解决时(否则会很伤脑筋),但实际上最后我会发现这个问题是由一个简单的错误所引起的,并且解决起来非常简单。

数据存储之所以如此简单,完全要归功于我们的后端,它由一个松散的微服务集合所组成,可以保证不同的实体之间最终能够保持一致。这意味着我们的一致性需求并不只满足于存储的规范,因此将 YES 传递给 NSDatawriteToFile:atomically: 方法来保证独立文件的一致性,这个做法非常有效。

所有写入到磁盘的操作都是与主线程异步的;不过,这些操作都是在一个负责写入磁盘的后台线程中同步循环执行的。主线程只会将需要保存到后台写入线程的 对象 URI,通过队列发送出去。当后台写入线程在队列中获取到特定的 URI 后,就会从内存存储中获取当前的最新条目,然后将其序列化到磁盘当中。

由于磁盘写入器总是保存当前最新版本,所以可以简单地剔除队列中重复的写入请求 URI,以合并多个写入请求。这有助于减少磁盘子系统的负载。

RESTOperation 队列

在上一节中,我提到写入请求将通过队列发送到文件写入模块当中。这个队列就是 WLRESTOperationQueue,这个实例我们在整个系统中用来异步连接代理实体 (acting entities)。可以这么说,这是让 Wunderlist 在处理网络交互和管理持久化的同时,仍然可以响应用户操作的秘密武器。

顾名思义,WLRESTOperationQueue 由一个 REST 操作队列组成,而每个操作又由一个 WLObjectReference 和一个 REST 动词(GET、PUT、DELETE)组成,该动词用于告知目标应该对引用执行何种操作。操作的含义取决于具体的目标。对于磁盘存储而言,如果收到了 PUT 就意味着要将这个由 URI 指定的对象存储到磁盘;对于 Web 接口而言,则是意味着发送 HTTP PUT 请求给后端。

可以从任意线程中来添加队列,并且队列可以维护子集的工作线程,以便为条目进行服务。队列可以选择将结果传递给指定的目标线程,这个线程与服务线程不同;比如说主线程。与 GCD 相比,让每个队列都具备单独的工作线程,可以大大减少线程的数量,以及相应的资源消耗。

WLRESTOperationQueue 对象通过自动拒绝重复条目 (entry),来支持合并操作。要实现这个功能,最关键的一点取决于:队列当中的条目只能是引用。我们花了很多时间来证明这一点,因此 WLRESTOperationQueue 目前的版本历经了大约一年左右的完善才得以出现。

如果我们尝试使用实际对象指针的任意一种变体,都会出现不合意的结果(比如说以这个写入磁盘的示例应用为例)。

  • 将可变对象写入到队列当中,并在写入之后对其进行修改,因此这种对象在保存的时候仍有可能会被修改。这是一个很糟糕的想法,如果要解决这个问题,那么就需要添加数不胜数的锁,即便如此仍可能允许进行有冲突的修改。
  • 将副本发在队列当中可能意味着:每当对其进行修改,那么所有相同对象都会执行一次写入。这在高负载的情况下会导致性能严重下降,特别是您需要保证性能良好的时候。
  • 清除最新添加的对象(通过 URI 的方式)意味着:只有第一条更新操作会被写入;后面执行的所有更新都会丢失,直到对象再次执行更改。
  • 清除最老添加的对象,很容易导致所修改的对象永远无法写入到磁盘的情况出现。

借助 URI 队列,即便是在高负载的情况下也仅仅只意味着会有多余的更改操作累积下来,但是磁盘子系统仍会尽可能保持最高的吞吐量。到目前为止,我们已经很少遇见磁盘写入速率跟不上系统写入速率的情况,而这些由小错误引起的问题也很容易进行修复。

流畅、反应灵敏的 UI

对于 UI 而言(图 17.7),实际上我们使用了一种经典的 MVC 方法,在 Wunderlist 的架构中可以表示为 ui =|= model。在经典的 MVC 中,当 UI 准备好进行更新并需要数据时,UI 便会去模型中拉取数据,对应到 Apple 的 MVC,其特征是控制器负责将数据从模型推送到 UI 中。

IMAGE

让 UI 在准备就绪后,就自行完成更新其实才是 MVC 的基本准则,而这在目前流行的大部分 ViewController 编程实践中往往会被忽略,但是当在动画运行的时候,需要通过异步操作来为其添加更多数据,从而协调动画的运行,因此这个准则变得至关重要,因为尝试依赖模型推送来协调几乎是不可能实现这一点的,并且还可能会导致各种复杂解决方案的出现,例如说 FRP 和 React,但是传统的 MVC 对此就有解决方案:我们只需要通知用户界面(无需推送数据),并让决定何时该自行更新即可。

在我们的示例中,UI 元素将由 URI 进行参数化,URI 当中包含了 UI 元素所应该展示的对象。之后,可以使用这个 URI 来发送 objectsForReference: 消息,从内存存储中获取最新的对象版本。

这个 URI 同样也可以用在更新通知当中。我们可以使用 Cocoa 最基本的 NSNotificationCenter 方法,将对应的 URI 与其一同进行参数化。随后,UI 元素可以将此 URI 与其内部维护的 URI 进行比较,以确定是否需要更新自身。

如前所述,URI 还会互相关联,因此假设有一个 URI 为 task://container/2/id/3 的任务被修改了,那么展示该列表 task://container/2 的列表视图同样也会进行更新。

我们可以使用 WLRESTOperationQueue 对象分离 UI 线程和任何可能会发生模型修改的线程。当模型对某个特定对象进行修改后,它会将该对象的 URI 发布到队列当中,并将配置为表示“模型已更改”的 NSNotification 传递到 UI 线程上的默认 NSNotificationCenter 当中。

队列的合并行为巧妙地解决了这样一个问题:我们尽可能降低每个单独更新的等待时间,同时避免在发生大量连续更改时频繁重载 UI 。此外,这种行为同样也不会丢失任何更新。

对于 UI 而言,我们实际上还需要添加一项功能:自动组合。在正常操作下,我们希望每个单独元素会立即独立进行更新。然而,随着负载的增加,这种做法变得越来越无意义。当您将数百条新列表任务发送给设备时,让每个列表都执行一遍动画效果不仅毫无意义,并且还会让用户感到非常烦人、感觉非常混乱。

自动合并的工作方法就是监视队列的深度。对于进入到队列当中的 URI 而言,随着队列越来越长,合并级别也将逐步增加,URI 后面的元素移除得也越来越多。如果将合并级别设置为默认值,那么诸如 task://container/2/id/3 之类的 URI 将原样进入到队列当中,并且合并只会影响到特定任务的更改。

如果将自动合并的合并级别设置为 1,那么就会从 URI 后面移除一个元素,仅仅只留下前面的容器:task://container/2。这会造成两种影响:一方面,当前 UI 中的整个列表会全部刷新,而不是针对某个特定任务进行更新;另一方面,这还会将列表当中所有独立的项目更新给合并在一起。因此,这就不会对列表当中的项目进行多次更新,因为我们对整个列表执行了更新。

最后,如果更新的频率仍然超出了用户界面展示的能力,那么合并级别 2 将会移除 URI 末尾的所有内容,只留下一个通用的 “UI 需要更新”的消息,并且还会将所有的 UI 更新请求合并成一个,以让 UI 自行进行刷新。

通过这种机制,我们就再也不用担心 UI 在大量更新的过程中跟不上变化,或者出现无响应的情况。除非我们引入了新的 BUG。

简评 Wunderlist

这里所展现的架构元素显然不是 Wunderlist 3 之所以性能如此强劲的全部原因。我们还有一个庞大的后端团队,为我们提供快速的 HTTP 和 WebSocket 接口,此外整个团队还进行了深入、详细的性能调查,并根据需求适当进行调整。不过,架构元素会确保审查的次数并不会很多,并且目的也十分明确,因此所做的调整都是非常微小、简单的,而不是时刻都在奋战性能问题。

我的意思是,这并不是实现高性能的唯一途径,换句话说:那些我们没有使用的技术并不是不能实现高性能。我认为,应用这些技术和基本原则,不仅可以在使用其他技术的同时实现高性能,并且还可以任意使用我们所提供的工具来直接获得出人意料的性能。优秀的性能是应用每月获取 500 万活跃用户,并且在 App Store 中获取 4.5 到 5 星评价的关键因素。

总结

在本章中,我们讨论了两个示例,通过将所有调优手段“结合在一起”,从而开发出优秀的、高性能的应用。优美天气应用是一个很极端的例子,因为它将调优目标推动到了一个非常具体的调优方向(即加载并显示大型图像集),此外还要实现一些看起来完全不可能的功能,最后还得为应用预留充足的空间。我们需要仔细分析硬件和软件的功能,并恰当调整需求,并根据需求作出适当调整,此外还需要一些来自外界的支持……我们自定义一种图像文件格式,它是另一种自定义图像格式的改版。

Wunderlist 是当代移动应用的一个典型示例,其中混杂了数据存储、实时网络访问以及 UI 的频繁更新等功能。我们将之前章节中所学到经验教训结合在了一起——例如,都要尽可能在内存中处理大部分工作,避免使用数据库引擎,无论性能是否与之相关,都尽可能使用简单、快速的存储机制。我们将第 16 章当中所述的更新机制进行了概括,将 UI 更新限制在某个架构元素当中,从而用于协调和简化应用的所有部分:网络层、数据存储、内存模型和 UI。

这两个示例都展示了如今手机的无限可能,并且最后,我们还是需要将硬件的能力推向极致,从而才能实现非凡的性能。

CATALOG
  1. 1. 优美的天气应用
    1. 1.1. 更新
    2. 1.2. 探索 PNG
    3. 1.3. 头脑风暴
    4. 1.4. JPEG 数据点
    5. 1.5. 测量时的小错误
    6. 1.6. JPNG 与 JPJP
    7. 1.7. 优美的启动
  2. 2. Wunderlist 3
    1. 2.1. Wunderlist 2
    2. 2.2. 整体架构
    3. 2.3. URI 与进程中 REST
    4. 2.4. 最终一致的异步数据存储
    5. 2.5. RESTOperation 队列
    6. 2.6. 流畅、反应灵敏的 UI
    7. 2.7. 简评 Wunderlist
  3. 3. 总结