在 Teehan + Lax,我们已经从事 Krush 项目好几个月了。 从 iOS 体系结构的角度来看,Krush 是一个有趣的应用程序,因为它涉及到了新手 iOS 在许多常见领域的疑惑。 具体来说,它是一个需要通过访问 API 来联网的应用程序,具有磁盘缓存功能,并提供有趣的内容。 在这篇文章中,我将探讨有关应用程序方面的一些案例研究:为什么我们选择某种方法论,其在实践中的工作方式以及事后的看法。
我们在 90 天之内推出了 Krush,将其作为最低限度的可行性产品,所以 "为什么" 我们选择某些方法背后的动机主要是基于速度:我们能以多快的速度达到最低限度的功能和能力集,这是将可测试的东西推向市场所需的,以及我们在之后能以多快的速度对其进行迭代?这些动机影响了我们做出的决定,所以如果你的动机不同,你应该从这个角度来看待我们的决定。
案例研究1:网络层
网络层主要由我的天才同事 Brendan Lynch 构建。网络层负责 Krush 的所有外部连接,无论是调用服务器的 API 还是调用我们的 CDN 进行资产交付。所有的东西都要通过一个通用的接口。
我们没有使用 NSURLSession
等新的 API,而是选择使用更熟悉的网络操作技术。具体来说,我们使用了一个属于应用程序委托(app delegate)的请求客户端,管理所有的网络活动。这个请求客户端持有一个 NSOperationQueue,我们的网络请求在这里排队。
网络请求本身由 URL、parameters 参数和 OAuth 的编码规范组成。请求对象知道如何构造 OAuth NSURLRequests
,使得在连接请求失败的情况下重放请求变得微不足道。网络请求子类 NSOperation
,遵守 NSURLConnectionDataDelegate
协议。
如果网络请求失败或超时,请求客户端会自动重新请求,最多达到一定次数,在这种情况下,请求最终失败。
每个操作都有一个回调 Block 块。当一个操作完成或失败时,该 Block 块就会被调用,传递从网络返回的数据和操作的结果。回调 Block 块在请求客户端中定义,将这些数据转化到磁盘缓存中,我们将在下一节中介绍。
这种网络架构在实践中效果很好。当一个请求确实失败时,它会自动重启,所以我们的应用是非常健壮的。通过采用熟悉的方法,而不是较新的 iOS 7 API,我们能够更快地将产品推出去。
如果我们必须重新来过,为了减少代码工作量,并充分利用 iOS 7 的通过后台获取 API 特性,可能值得研究 NSURLSession
。我还想探索使用从视图控制器发送的命令在响应者链上传递给应用委托的想法,然后委托可以将它们转发给请求客户端。这样一来,我们就可以将视图控制器完全解耦,完全不用知道请求客户端的情况。
案例研究2:磁盘缓存
Krush 是一个非常可视化的应用程序 - 它下载并显示大量的图像。这些图像,一旦从 JPEG 解压成位图进行显示,就会占用大量的内存。将应用程序的全部内容保存在内存中是不可取的,而且每次要显示时下载每个资产都会占用用户太多的网络资源。解决方案是使用磁盘缓存。
为了实现可读性,Brendan 使用他熟悉的 SQLite 构建了一个磁盘存储系统。但是,他忙着搭建网络层,而我却在搭建磁盘缓存,并且我的 SQLite 基础很弱。因此我选择依靠我熟悉的技术:Core Data。
Core Data 本身并不是一个对象持久化库,而是一个对象图管理框架,刚好可以将数据持久化存储到磁盘存储。我们把它作为缓存;每次启动应用时,存储都会被删除。
应用程序的启动是应用程序最关键的环节之一。如果一个应用程序不能在合理的时间内启动和运行,用户就会抛弃它。在 Krush 的案例中,我们从用户和客户那里得到的反馈是,应用程序启动时很慢。呃,哦。
我打开 Instruments,在一台设备上测试了应用启动时间。
哦,有很多网络连接正在进行。在一个跟踪中,我测量了应用程序第一次启动时的170个网络请求。事实证明,我们是先发制人地发出了大量请求,而不是按需分配。我改变了我们的网络请求,使其不那么乐观,而更多的是按需分配,这很容易改变。然而,这个改变导致了很多界面抖动。再次,我进行了测量。
我们使用一个非常简单的 Core Data 缓存来启动 Krush,因为我们没有很多时间来投资更复杂的东西。堆栈由主线程上的单个托管对象上下文组成。我从来都不喜欢过早地优化一个问题,反正我更喜欢测量-调整-测量的循环。当我测量接口中的抖动时,我立刻看到了问题。Core Data 阻塞了主线程。
我做了一些研究,决定使用不同的方法。请求客户端实例将拥有一个后台上下文,它将在自己的队列上进行工作;后台队列和主线程队列将共享一个单一的持久化存储协调器。
我们来看一个通过网络请求获取用户详细信息的例子。
用户对象已经存在于主管理对象上下文中,但不一定存在于后台上下文中。我们必须保存主上下文,确保对象存在于持久化存储中。然后我们从用户中抓取 objectId,在网络请求的回调块中,从后台上下文中抓取相应的用户对象。在这里,在后台线程中,我们执行我们的 JSON 解析,并形成后台上下文用户和后台上下文中其他对象之间的关系。最后,我们保存后台上下文,后台上下文会发出通知,将我们后台上下文的变化合并到主上下文中。相应的视图会通过 KVO 进行更新。Phew!
结果是戏剧性的。我们大大缩短了启动时间,并使整个界面的响应速度大大提高。
理想情况下,我们所有的变化都是在后台管理对象上下文上进行的。如果我必须重做这个解决方案,我会把主管理对象上下文模型实例变成只读(语义上),并且只在后台上下文上执行更改。这样一来,我就不用在访问后台上下文中的对象之前保存主上下文了。
这里的教训是,在启动前一定要衡量你的应用程序。只花了几天时间就真正支撑起了界面和启动时间。如果我们在启动前投入这些天的时间,我们可能会有一个更平滑的体验,而不是在我们的迭代阶段。
案例研究3:使用配置视图
Krush 用户配置是一个复杂的东西。无论是从设计的角度还是从代码的角度来看,都要把它做好。我们设想的设计有三个标签栏:Krushes, Influence, 和 Network.
不过,更多的是,标签需要模块化,因为,对于一个有 brand 的用户页面,我们会希望有不同的标签。这是一个有趣的架构问题;如何将代码结构化,使其能够以模块化的方式重复使用?
我们本可以使用子视图控制器,但我想尝试更多数据驱动的东西。相反,我只使用了一个列表视图,由一个 UITableViewController
控制。该控制器有一个 datasource
强引用属性,它遵守数据源协议。
当选择不同的标签页时,数据源就会改变。此外,当数据源改变时,列表视图被重新加载。现在,控制器会基于列表视图要显示的内容,去查询数据源。
数据源被用来填充我们自己编写的标签选择控件。根据被显示的用户是否是一个 brand,不同的数据源是可用的。通过使用 ReactiveCocoa,我们能够在 viewDidLoad
中导出视图控制器的数据源状态。我们的列表视图控制器本身的逻辑非常轻,而是将布局的关注点委托给数据源。
每个数据源负责提供信息,比如任何给定行的行数或高度,还负责布局各个单元格。每个数据源也有一个类属性和 reuseIdentifier
重用标识符,用于在 viewDidLoad
中用列表视图注册自定义 UITableViewCell
子类。最后,每个数据源还负责暴露一个ReactiveCocoa 信号,该信号将触发列表视图的重新加载。
在项目的迭代阶段,当设计发生变化时,这种数据源的方法非常有效。它还保持了代码的干净和解耦。这种方法的一个弱点是,当网络选项卡设计的某些方面被集成到 Krushes 选项卡设计中时,没有一个简单的方法来在两个不同的数据源之间共享该逻辑。我希望 Objective-C 有语言级的抽象类支持,因为这可以帮助减少数据源对象之间的代码重复问题。
案例研究4:MVVM 在 Feed 上的应用
早期的预发布版本的应用有一个简单的 feed 和一个简单的用户 onboarding 导览。当我们向办公室的同事们演示时,我们发现导览是最初用户体验的一个弱点。Geoff 建议在第一次启动时将信息卡集成到 feed 中,向用户展示如何使用该应用。这样一来,他们就不必在使用应用之前就记住教程中的说明。
此时,我们的 feed 视图控制器使用 NSFetchedResultsController 来显示 Core Data 存储的内容。我没有将新逻辑集成到我们的 feed 视图控制器中,而是探索了Objective-C 中的一种新兴模式。Model-View-ViewModel。
简而言之,我们将视图控制器中呈现内容的所有逻辑抽象成一个视图模型,这个模型与实际的 UI 无关。视图模型只提供一些信息,比如是否应该显示确认和保存按钮,或者为特定的列表视图单元格使用的图片。我们还将获取结果控制器的委托代码从视图控制器移到了视图模型中,它将把 onboarding 模型插入到它维护的内部那个数组中。
当用户即将到达 feed 终点时,视图模型也会得到通知,以便可以获取更多的结果,或者当用户拉到刷新时,视图模型也会得到通知。
当我们将标签整合到应用程序中时,这种方法效果很好。使用了相同的视图控制器,只是采用了不同的视图模型,不同的展现逻辑。通过让我们不同的视图模型符合视图控制器可以依赖的通用协议,我们能够让我们的控制器不可知地呈现什么,以及如何呈现。
我对这种方法的效果非常满意。如果我必须重新来过,我会更努力地减少不同视图模型之间的代码重复。同样,一个抽象类可以在这里帮助我们。
总结
这对我们在 Teehan+Lax 来说是一个令人兴奋的项目。在整个项目过程中,我们学到了很多东西,并从中获得了很多乐趣。我们希望通过分享我们在这个项目中学到的一些经验,让开发者们能够制作出他们自己很棒的应用程序。去做伟大的事情吧!
网友评论