【翻译】WWDC 2019 :优秀的开发习惯

5,818 阅读27分钟

本文翻译自 WWDC 2019 Session 239
演讲者:Josh Tidsbury

成功的APP开发需要掌握方方面面的东西。了解可纳入开发流程的实践以提高你的生产力,提升你APP的性能和稳定性。学习如何提高通过Xcode编写的代码质量。获得一些有价值的开发技术的切实理解。

早上好,我是Josh,来自苹果技术布道团队。我们的团队与像你这样来自世界各地的开发者一起工作是难以置信的荣誉。我们的目标是帮助你们开发出真正优秀的APP。在与你们的交流中我们学习到了很多,得以了解您所采用的流程,面临的挑战,目标和愿景。我们学习可以帮助你摆脱困境的技巧和工具,虽然我们听到的每个故事有些许不同,但无论来自世界何方,它们均有诸多的共同主题。

当你想象手艺这个词时,你首先可能会联想到设计。开发人员和工程师从事的也是手艺,毕竟手艺的定义如下:

  1. 规划、制作和执行的技巧
  2. 用心、熟练或创造性地制作和生产

代码是用手写出来的,它涉及非常多的技能,在构建APP时需要创造力的技术和抉择。今天我想和你谈谈这门“手艺活”所在乎的事情,将这种在乎融入到你的代码、storyboards和产品中。开始看起来可能很容易,但结合对当今我们开发者提出的所有要求来看,有时会相当困难。手艺的技能水平随着时间而发展,这需要敬业、耐心和专注。这是关于学习去享受过程中带来的乐趣,几乎与到达目的地时的一样多。这个过程的一部分是将那些开始时需要强烈并有意识关注的事情转换为习惯,类似于实际中驾驶汽车,我们驾驶时有意识地关注的事物数量会随着时间的推移而减少,因为我们已经将这些事物转换为自然而然的习惯,在App开发中我们也能做到同样的事情。

要做到这一点,就意味着要拥抱好习惯抛弃陋习。当开发一个APP时,开发者需要关注非常多的细节,而这些细节很少有被使用我们APP的用户直接观察到,然而他们能感受到因这些细节影响的性能、可靠性和稳定性。

我们没有足够的时间关注这里所有的细节。今天我想花一些时间来回顾一些有希望改进我们开发工作中的实践,将它们纳入到常规的工作流程后再形成习惯,这在将来会把我们从挫败、麻烦和浪费时间的困境中拯救出来。我相信你们大部分人已经在实践很多相关东西了,但也许还有一些没能成为你的习惯,说不定在这里会受到一些启发而去实践更多。

首先我们来看组织

1. 组织

除了APP开发人员的身份外我还是一名木工,对我来说是很好的从现代生活逃离的一种方式。有一点可以肯定的是,在干净的工作间中更加容易打造出又好又漂亮的家具。如果你的工作台是杂乱无章、毫无条理的,那么很难去找寻所需的工具和材料,不得不为你目前的工作腾出空间而搅乱更多的东西,花费了较往常更多的时间同时过程中也伴随着更多的意外和错误。

我们团队每年接触很多Xcode项目,有一些实践经验可以帮助你确保工作空间的干净整洁,让你能够最好地进行工作。

Xcode项目受益于Group带来的代码结构和组织。让你程序各部分的代码文件一目了然,在你尝试修复Bug时,快速地为你做好准备。

利用Group以功能模块的角度来组织你的项目,这与用户操作App的方式逻辑上一致。我们经常看到有些项目使用文件类型来分组,或者干脆就不分组,这样对想要快速了解源文件如何相关联的人来说没有任何好处。

此外分组有助于你的Xcode项目结构与实际的文件系统结构相匹配。从Xcode 9开始,当你在项目中新建Group同时会在磁盘上创建一个文件夹来存放组内的文件。这意味着无论当你从项目、源代码管理或者磁盘上查看都是相似的结构,有助于减少疑惑和混乱。

Storyboard是非常强大的工具,通过可视化的方式构建用户界面。我们确实遇到了许多项目将所有的UI一股脑的全部放在一个Storyboard中,没有理由可以这么做。

感谢Storyboard之间可以引用。应该针对应用程序的主要部分分别创建各自的Storyboard,然后通过引用将它们连接起来。你会发现这样能很好的隔绝各自独立的变化,并使你在大型团队合作中更加简单,避免那些令人讨厌的合并冲突的风险。就像你不会把所有的代码放在一个文件中一样,所以千万也不要把所有的UI放在一个Storyboard文件中。

将项目文件保持在最新状态是确保Xcode帮你解决并避免问题累积的关键方法。如果你定期处理这真不能算是一个问题,要是放置不理,在未来确实会引起问题。首先,当你更新到新版本的Xcode时,有时候需要你来决定让Xcode更新项目设置和项目文件到最新的格式。因此,除非你有重要的原理不这么做,否则我们建议你在出现弹出框提示或在issue navigator出现警告时执行此操作。

其次,确保你的项目使用新的构建系统New Build System,该功能于2017年首次推出。它在性能、依赖管理方面作出了重大改进,对采用Swift包起到至关重要的作用。它从Xcode 10起始作为默认的构建系统,你可以通过从File菜单下的Projects Settings中进行查看验证。

作为木工,我们常常会保留一些小的废料以防以后可能会用到,直到都要装满它们时我们才不得不接受这样一个现实:这些小废料从未真正被使用到项目中去。开发人员也有这个偏好,好在有一个(比起木工)更容易的决定,由于你已经将项目代码加入源代码管理之中,你有吧? 去掉那些不需要并且没有使用的代码,不要仅仅将它们注释掉。万一有天你想要把它找回来,如果你真的需要的话,你也可以从那个文件的历史版本将其找回来,扔掉这些“小废料” 吧。

另外一件我们不想失去控制的事情是警告Warning。为此,你和你的团队应该开始零警告的实践,包含警告的代码不应该被签入,你应该像对待错误一样对待警告,尽快地修复它们。如果我们的项目有着成千上万个警告,其大部分的原因都是因为开发人员放弃并且没有安排时间去修复它们而累积起来的。此外,如果你在维护这样的一个项目,当新的警告产生时你也根本发现不了。

通过以下这些方式让你的工作空间和项目有条无紊并整洁干净,这对你的APP的长期健康和成功起到至关重要的作用:

  • 利用Group来组织你的项目,与文件系统结构相对应
  • 利用Reference拆分过大的Storyboard
  • 确保项目文件是最新的
  • 清除废弃、旧的代码;
  • 出现警告时就找到并解决引起它的根本原因。

做好这些事情使得您的项目变得灵活,你的开发流程在项目生命周期中将运转得更好。

2. 追踪

说起源码控制,在搭建项目时你就应该启用它。我们确实遇到过许多没有源代码管理的项目,特别是那些独立开发团队的项目。在你搭建Xcode项目时,你只需要确保这个复选框被选中就可以了,这样你的项目就会使用Git进行源代码管理。

现在你可以随时回过头看看你过去的修改,当你提交当前的变更集时会发生的变化以及更容易发现任何类型的错误。启用源代码管理之后,那么还应该注意这些事情使得更加有用和高效:

首先,每次提交的粒度小一点。经常性地检查让你的当前代码和分支,保持小增量的更新,让这些变更尽可能的局部性和自包含。当你以后需要线索或解决回归bug的时候,这会给你提供一条回顾的路径。同时因为你做的这些都是小的变更,从而降低引入侵害的几率。

其次,编写有用的commit messages。因为有一天我们都会问出这个希望自己能回答的问题:当时究竟在想什么。当你试图回忆当时代码发生变更时的情形和原因时,你的commit messages就是给你未来的笔记。

即使你是独立开发人员也像参与大型团队那样进行源代码控制。这样意味着修复bug和添加新功能都可以通过分支来完成,一旦你完成之后便可压扁合并回主分支或dev分支,同时使用有意义的commit message。如今你有多种选择和模式可以遵循,建议你尝试使用它们,找到最适合你的那种并将其集成到你的开发流程中。

这就是追踪。源代码的管理对成功的现代App开发工作流程至关重要,请将它作为你项目的一部分、视作日常实践的一部分。保持较小的提交,填写有用的commit message,以及利用分支的帮助进行管理和隔离因修复bug或添加新功能引起的代码变更。

3. 文档

在我看来,对代码清晰度和可维护性贡献最大的是注释和文档,它们对你和你的同事们来说就像面包屑导航那样的作用。有人说我不需要代码注释,我的代码是自记录(self-documenting)的,我对此并不买账。从算法角度看,良好编写的代码确实清晰的描述了它所做的事情,确实是自记录的。但是它并没有说明为什么,为什么会有这些代码的原因,这些代码该如何在更大的上下文中进行适配,也没有描述当时编写时采取这种方法的背后原因。我工作过的最好的开发人员,不仅编写令人难以置信得清晰简洁的代码,而且还花时间在代码中留下有用的审查记录或注释,引导未来的读者领会原作者的意图。初级开发人员能在这个过程中受益更多,因为你的经验会在项目开始到结束时变化很大,开始时做出的决定可能实际上与最后做出的决定不一致。(译者注:初级开发人员因为经验的缘故,导致代码变动的因素更多,因此更加需要清晰描述的文档和注释)

怎么样算作好的注释呢?好的注释假设读者了解所用的编程语言,能够走查代码序列以及采用的步骤,它真正关注的是代码起初写在这里的原因和支撑这么做的理由。举个例子,这是我们经常看到而实际没什么价值的注释:

//A constant string id value
let id = "2ADA155F-1529-4D2D-98C4-0E4BD06940E8"

我假设你们大部分人都使用Swift写过一些代码,知道这里是创建了一个字符串常量来携带这个值,但是我们不知道这个id是什么,它的用途以及被硬编码到APP中的原因。给它加了一点注释后,我们就明白了这个值存在的原因,以及它来自哪里。

// The permanent identifier for this application when interacting
// with the CMS, provided by the vendor of the CMS.
let id = "2ADA155F-1529-4D2D-98C4-0E4BD06940E8"

我们还可以更进一步,给变量换个名字后变得更加清晰。

// The permanent identifier for this application when interacting
// with the CMS, provided by the vendor of the CMS.
let cmsApplicationIdentifier = "2ADA155F-1529-4D2D-98C4-0E4BD06940E8"

如果你发现你自己的变量使用了单个字母或者类似id等方式命名,也许这是你给它们选择一个更具描述性的名字的好机会。自动补全功能让Xcode充满魅力,你甚至都不用再输入任何更多东西了。这个案例中的标识符在任意时间使用时在贯穿整个代码库都是清晰的。

文档带来的好处和注释相似,但这些好处是遍及整个应用程序甚至之外的。 当你编写自己的APP时,你创建了一层层的抽象和算法,将大段蜿蜒的代码分解成整齐可测试、可重用的函数。如果你没有选择给这些函数添加文档,那么你是在强迫你自己每次使用该函数时都得在头脑中过一遍这个函数的文档,常常必须从头了解这个函数内部的实现,包括每个参数的用途以及如何产生的结果。

如果你不知道如何在Xcode中添加文档,很简单,只要你将光标定位至函数签名的第一行,再按option + command + /组合键,您需要的所有占位符文本将自动生成,填充空白就好了。

按住option并点击任何使用该函数的地方都会显示跟你已经熟悉并喜爱的原生SDK/Swift标准库风格类似的快捷帮助弹出框。

注释和文档对你的时间来说真是低投入高回报的投资,收益在整个项目周期中都会源源不断。所以请为你的代码添加有用的注释,让读者能更好地理解原始作者的思路。为你的变量选择更具描述性的名字,为你的函数、属性、结构体和枚举都添加上说明文档。

4. 测试

接下来,我要谈论一下测试,特指单元测试。为此我还要介绍一下Marshall, 他是我们Swift和开发工具的布道师,一个非常聪明和友善的伙伴,恰好也是一部Swift领域的Lint 对讲机

每当我提交代码进行评审的时候,我都做好了迎接海啸般深刻见解和反馈的准备,这帮助我从形式和功能上改进所写的内容。前几天,Marshall在单元测试的另外一个主题上也把我引向了正确的方向。现在,我必须承认,我在写单元测试方面并没有无懈可击的记录,我并不是不欣赏它们带来的潜在价值或者是对它们不熟悉,而是我总是倾向于在写完实际代码之后再来写单元测试,我老想把它放到最后来做。

然而,有一天我在为某APP 的新功能实现数据模型层的时候,Marshall跟我说:“你在做这些事的时候,最好添加一个单元测试来确保字典和结构体这两种表示之间的来回转换能够正常工作”。

我的真的想不通这在将来怎么可能会出问题,不过我还是听从Marshall的意见给这个简单的流程加上了单元测试。跑了下单元测试,看到绿色的复选标记表示测试通过时,我感到无比的满足,所以我提交了代码进行评审。我再次想起这个测试是几周后我们需要给这个结构体添加额外的数据,修改之后运行时也没有问题,我的工作做完了,对吗?就准备签入代码了,然后我想起来运行一下那个单元测试,果然我因为忘了修改字典反序列化的方式被单元测试抓了现行。这个Bug可能会在我们实现UI的时候才会暴露出来,那样毫无疑问会浪费我们相当多的时间去找到原因。所以感谢Marshall提醒我将单元测试作为日常实践的一部分,Marshall:"不客气,Josh!"

编写单元测试相当重要,即使那些看上去貌似比较简单的代码正如我碰到的这个例子一样。代码的易受影响性带来了潜在的回归bug,鉴于此我们并没有那么多的时间进行彻底地测试,让我们将Xcode作为一组额外的“眼睛”。

因此,将单元测试的实现作为常规开发实践的一部分,并在每次提交之前运行它们。单元测试也是持续集成的关键组成部分,所以你可以为此做好准备。 测试是用户永远不会真正看到的隐藏细节之一,但是可能会带来不同的后果,给用户带来极致的体验或是APP重要数据受到破坏造成的令人沮丧。

5. 分析

如今有多种形式的分析可以作为你部分日常工作流程,其中一些需要额外时间的投入,也有一些可以在后台中为你工作,你甚至都不需要去管它。

一个叫Network link Conditioner的工具非常有用,APP开发往往在网络性能良好的家中或办公室,却不是你的APP通常被使用的典型环境。

因此,通过启用Network link Conditioner可以人为地将网络性能限制为类似典型蜂窝网络的性能,甚至是较差的网络环境,你会惊讶于在这种加载和争用条件中产生的问题数量,这样你的客户就可以避免。

在你的Scheme设置中有一些Sanitizer选择器,通过它们可以在开发周期里发现多种多样的错误。Address Sanitizer将监视类似内存错误和缓冲区溢出的问题,内存问题通常是导致安全漏洞的原因,因此使用Address Sanitizer可以帮助你确保不会将这些问题暴露给线上。

通过启用Thread Sanitizer可以在你使用模拟器测试或调试你的APP时发现数据竞争问题。数据竞争是指两个未同步的线程,而且至少其中一个试图对同一块数据进行写操作,这可能的恼人错误会让程序运行出现不可预料的问题甚至出现内存异常。

Undefined Behavior Sanitizer可以捕获诸如除零错误、浮点数的转换超出范围或溢出、以及未对齐指针等bug,当程序出现未定义的行为时它可能会崩溃,也可能不如预期的方式工作,又或者看上去没问题,但在不同的时间看似没有任何理由的不同结果。Sanitizer可以在帮助你摆脱令人沮丧的bug,在它们对你的项目造成严重破坏之前。

最后要介绍的是Main Thread Checker,它确保你没有在后台线程中对AppKitUIKitAPI的无效调用。举个例子,如果你在主线程之外的线程上去更新界面,它可能会导致界面更新丢失、视觉错乱、数据丢失和崩溃。有时候这些bug因为间歇性地出现而变得非常难以追踪,而启用这个功能对性能的影响非常少,所以我们推荐你尽可能就留着启用它。

在调试你的App时请时刻关注性能和资源利用率,确保app尽可能高效地利用系统资源。首先可以使用调试仪表盘,这些仪表盘可以在你构建和运行项目时随时在Xcode中的Debug Naviagtor中找到,你可以通过它们查看整个App生命周期的CPU内存磁盘网络利用率,快速了解你的App是否通过网络正在连接未知的服务器或者不断地轮询某个端点,消耗大量的带宽和电量。

最后你可以通过Profile中的Instruments按钮进行进一步的深入分析。我经常使用的其中一个是Time Profiler,它允许你查明代码的哪些段落占用了最多的周期,并且允许我们缩小那段也许应该变成异步或者被不可扩展的方式实现的代码的范围。

分析是一个相当广泛的主题,但我在这描述的大多数工具只需要你记得启用就行了。使用Newwork link Conditioner模拟典型的或者较差网络条件;经常使用SanitizersCheckers,只要把它们打开就好了;定期的查看调试仪表盘,并密切关注你APP的足迹和性能;利用Instruments分析你的App,深入研究并更精确地解决那些问题。将这些小小的努力转化为习惯后将大大提高你APP的性能和可靠性。

6. 评估

我住在多伦多的时候有一个车库,后来将它改成了我的木工车间,这是一个舒适的空间,它完全都属于我。

但是自从搬到湾区以后,我就再也没有属于自己的空间了。我去过这里的各种共用的社区木工店,因为现在必须与其他人共享工具和空间所以有时候会感到沮丧。但是我没有意识到我反而应该感谢的是有机会向商店里的其他人交流想法以及问问他们对于在做事情的意见。

我觉得这在App开发中的类比就是代码评审。过去几年我都是以独立开发人员的身份完成许多App的开发,这就好像我拥有自己的工作间一样,不可思议得快速和灵活,只有自己的意见才重要。缺点是你自己没有从同事或同行中学习更好地使用语言、框架和SDK的机会。

虽然解决问题的办法通常有多种,但总有更好的那一种。比如那种更简洁或者在性能、可维护性和可靠性方面更突出的方法。不能因为它起作用了就意味着它必然是正确的,就不能通过某种方式可以去明显地改进它。(译者:结合个人工作间 vs 共享工作间, 个人开发 vs 团队开发的优缺点来理解这段话)

苹果的所有团队都有一项未经评审的代码不能并入项目的政策。我们团队通过这个过程互相学到了很多东西,代码风格更加一致,更不用说在可靠性方便的改进了。它还确保我们整个团队更熟悉更广泛的代码库,使得我们可以解决的bug和功能的范围也更广。我有幸处在这么一只经验丰富的团队,这让事情变得简单。但是如果你经营着自己的公司或者是独立开发者,试着找找你所在地区或者来自世界各地的伙伴,并与他们互相进行代码评审。或许还可以从聚会、当地会议或者共享办公室去找找。所以,现在你已经打算将代码评审纳入你的开发实践了吧。

好的代码评审是怎么样的?首先,意味着需要花时间去理解每一行变更的代码,匆匆一瞥是没有意义的。其次,构建项目并跑起来看看,不要假设原作者之前做过,特别是在你记录中看到最后的提交是一次合并时。运行这些测试,首先这么做是提醒你去检查实际上是否有这些测试以及测试是否通过,记住编译通过不代表不存在某种方式上的破坏。彻底阅读这些注释和文档,我的意思是它们有的,对吧?接着寻找是否有拼写和语法错误。继续查找变量名的拼写错误,作为一个加拿大人,我有长期使用colour作为颜色变量名的习惯,这让我的团队在查找color变量时简直抓狂。确保代码库中的函数、变量名的一致性可以更容易的查找,也节省时间。

可能会觉得这个过程在短期内也许减缓了您的速度,但从长远来看,通过减少潜在的错误和问题,将毫无疑问地为您节省时间、金钱和客户。你作为开发人员的经验在将来处理类似的模式或挑战时受益匪浅。

7. 解耦

开发人员致力于创建小而精的可重用、可测试的代码,毕竟我们不想总是一次次的编写重复代码。

包和框架给了我们更集中的方式来维护代码的机会,并且以便携的方式提供功能,不仅仅是你当前的App,其他的App也能利用这些成果。 如果你的App包含Extensions并且将它们的共享代码打包成Framework,那么你的应用二进制包大小会减少,因为你的主AppExtensions可以共享这个Framework。当然,创建包也给你了在社区分享你努力成果的机会,特别现在与Xcode 11包的集成更紧密了。但是除了你App、共享框架、包和库中的代码外,还需要附带优秀的文档才能让其他人更好去使用。

因此,将框架和包作为你拆分代码库的一种方式,这可以让你的成果作用于多个正在开发或维护的appFramework可以帮你减少二进制包的大小,然后你可以将成果与社区共享,但请务必附带优秀的文档。

8. 管理

最后一个我想谈论的是依赖管理,尤其是搞清楚它给你的项目带来的好处和风险。使用Swift包、框架和其他库有很多好处,但是在你开始使用之前,了解库里面包含的内容以及在带来潜在的问题是非常重要的。

你要确保了解依赖包对数据的影响,你要对App的内容负责,包括对用户数的所作所为。 确保Framework没有收集不必要的指标或设备信息,确保它没有把数据从你的设备发送出去。

同样也要了解和研究给定依赖包的依赖。 毕竟引入包含依赖的依赖包后意味着你App的安全和成功与这整条链挂钩了。

最后还有另外一种可能性会破坏你的App,如果依赖包不维护了怎么办?或者直接就消失了呢?重要的是你要有一个计划决定你在任何时候遇到这些情形时该如何去处理,毕竟你在项目中引入了一个新的依赖后,项目的未来就真的依赖它了。 那么是你准备自己来修复这个公开的bug呢?还是将它转为内部的项目并维护它,又或者你计划不得不把那个依赖在以后全部清除出去以及包括因此带来的必要工作?

使用外部依赖包(如Swift包)可以让你更快地完成工作,并避免重复创造社区中已经存在的工具。本着掌握它们用途的态度, 确保它们只按照你期望的那样做,务必要它们尊重使用你App的用户的隐私。做好它们在未来出错或者消失的应对措施计划。在项目中添加新的依赖时问问自己这些问题,把这件事变成你的习惯,你将最大限度地发挥出它们的长处并得到长远的回报。

总结

对于App开发项目,有时候觉得项目的最后10%的工作花费的时间与前90%的时间一样长。但是我认为尝试通过将这些实践转化为习惯之后,你可以避免这种感觉。

通过有效的组织你的工作区,让你更快更高效的工作,专注于实际代码。 利用源码控制追踪你的代码库,可以减少回归bug的几率,加快调查可能导致bug的原因。 编写有帮助有意义的注释和文档,你可以在未来阅读这些代码以及每次使用自己编写的类、结构体、函数时减少负担成本。单元测试能在最后一刻拯救你,在引入一些回归bug的时候。 Sanitizerscheckers提供对你的代码持续分析的能力,它们在后台运行,你都甚至不用去操心。仪表盘和Instruments确保你高效地利用系统资源,允许你精确地追踪性能和其他问题。代码评审不仅仅是评估代码样式和功能,更是一个巨大的学习机会,让你提升你的开发技能并与你的开发团队在社区进行分享。将你的项目分解为更小的、可重用的包和框架,帮助你让你的成果延伸到多个项目,并允许你共享它,这样做同样有利于减少二进制包的大小。最后,使用外部依赖(比如Swift包)可以重用社区中已经开发好的功能,帮助你更快完成工作。但是一定要把关好它们的所作所为, 记住了吗?包括它对你用户数据做了什么,也要做好没有它们的准备。

将这些实践作为你工作的一部分,只会给项目每个阶段增加一点点时间,但是从长远来看将会为你节省不可思议的时间,确保你的项目基业长青。我希望今天为你提供的想法和建议可以让你思考如何进一步提升作为开发人员的手艺。你正在想纳入的这些实践可以提升你工作质量和稳定性,把这些自觉的努力转换为习惯将允许让你将精力投入到更重要的领域。那些使用你APP的人也许不能说出个所以然,但是能感觉到你投入这些工作所带来的关爱,你能为自己打造的这些而自豪。谢谢!

【全文完】