<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"><channel><generator>Typlog 3.1 (https://typlog.com)</generator><title><![CDATA[👋 Hi, I'm SIQI]]></title><description><![CDATA[I'm a Swift fan, focus on iOS/visionOS/macOS! 
On this blog I share my working processes, tip and tricks, tools, and problems I found along the way.]]></description><link>https://swiftsiqi.com/</link><copyright><![CDATA[Copyright 2023 👋 Hi, I'm SIQI]]></copyright><image><url>https://i.typlog.com/siqi/8280846817_4783535.png?x-oss-process=style/sl</url><title><![CDATA[👋 Hi, I'm SIQI]]></title><link>https://swiftsiqi.com/</link></image><atom:link href="https://swiftsiqi.com/feed.xml" rel="self" type="application/rss+xml"/><atom:link href="https://pubsubhubbub.appspot.com/" rel="hub"/><pubDate>Fri, 15 May 2026 09:00:54 +0000</pubDate><item><title><![CDATA[SIQI 的笔记系统搭建方法论]]></title><guid>https://swiftsiqi.com/posts/using_INK_to_organize_note</guid><link>https://swiftsiqi.com/posts/using_INK_to_organize_note</link><description><![CDATA[利用 INK 理论构建笔记系统]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 15:34:53 +0000</pubDate><content:encoded><![CDATA[<h1>利用 INK 理论构建笔记系统</h1>
<p>这是我搭建个人笔记体系的一套理论，目前也还在探索中，未来或许还会持续迭代。</p>
<p>当然，我主要目前有两个工具，</p>
<ul>
<li>Heptabase，主要用于记录知识点和学习笔记，不会记录用于发表和发布的文章，它的使用更像卡片笔记法的思路。</li>
<li>Craft，主要记录最终的文章，而这些文章的论据和内容大部分都是记录在 Heptabase，它的使用更像写一个比较严谨，且有逻辑性文章的思路。</li>
</ul>
<p>虽然有两个软件，但这两个软件的背后，其实包含着一套相同的方法论，只不过在使用上因为不同阶段有不同的诉求，所以需要两个工具载体！</p>
<p>这也就是我今天要说的这套 INK 方法论！</p>
<h2>含义</h2>
<ul>
<li><p>I：Inbox（收集箱），用来收集任何时刻、任何方式得到的零碎资讯，用关键词记下，放在 Inbox 下随便一个页面里。称为关键词笔记。</p>
</li>
<li><p>N：Note（条目），用来存放 Inbox 中经过整理的完整笔记。每一则笔记都有完整的时间、标题及脉络。称为参考笔记。</p>
</li>
<li><p>K：Knowledge （知识），这是重点。用来将 Note 里面储存的笔记主题化。称为主题笔记。</p>
</li>
</ul>
<h2>核心</h2>
<ul>
<li>就是将 Note 中的笔记，按照某一个主题，或者某一种用途，有组织地放到一起。</li>
</ul>
<h2>执行流程</h2>
<h3>收集 —— 完善 —— 整理 ——（应用）—— 归档</h3>
<h4>收集</h4>
<p>任何时刻，看到任何觉得有价值的资讯，用手机和平板（视哪个更方便而定）记下来。结合文字、画图、拍照等方式，记在印象笔记里，自动同步。</p>
<p>回到电脑前，打开印象笔记电脑端，同步，将之前记下来的关键词转移到Inbox里。</p>
<h4>完善</h4>
<p>定期进行，一般1-3天一次。将所有Inbox里的每一个关键词，通过搜索，扩充成一则有价值的详细笔记，存放在Note里面。如果在Inbox里记下的是一篇文章的链接，则进行通读，整理出要点，同样存放在Note里面。然后将Inbox清空。</p>
<p>例如，我在Inbox里记下一个关键词，叫做「长尾效应」，那么，我就会通过搜索，整理出：长尾效应是什么意思？谁提出的？得到了怎样的印证？业界对其态度如何？存在怎样的问题？等等。然后把这部分内容，在Note里新建一页，放进去。</p>
<h4>整理</h4>
<p>我会经常翻阅Note笔记，并且一边翻阅一边思考，当突然想到「诶，这个内容似乎可以用」的时候，就会进行思维整理。比如，我看到「长尾效应」时，可能会想到它也许可以解释某个案例，或者与某个理论之间存在矛盾，就会在Knowledge中另开一页，把这个思考写上去，并且附上「长尾效应」这一页的链接，以便查阅。</p>
<p>怎么附上链接呢？这里要提到 Craft 和 Heptabase 特别优秀的一个功能：在这两个软件里面，任何一个 Block（它可以是一页笔记、一段话、甚至一行字），都可以生成一个内部链接，只需要选中，右键，选择「复制指向页/段落的链接」即可。</p>
<p>这个功能可以大大提高思维整理的效率。比如，在一页Knowledge里面，我可能会引用到10页Note的内容，但我并不需要把它们的内容全部复制进去，我贴10个链接就可以了。</p>
<h4>应用</h4>
<p>定时翻看自己的笔记，尤其是主题笔记，有灵感了，就将其输出。
写一篇文章，在知乎回答一个问题，或者告诉别人，都可以。
关键是要去用。将笔记和思考转化成为实实在在的实践，把思想落实成为行动，这才是笔记的本来目的。</p>
<h4>归档</h4>
<p>对于完全物尽其用的笔记，或者已经记得滚瓜烂熟的笔记，删掉，为今后的输入腾出空间。</p>
<h2>注意点</h2>
<ul>
<li><p>Inbox 里使用关键字作为标题存放相关内容</p>
</li>
<li><p>Notes 里存放着经过阅读，整理，完善，扩充后的笔记</p>
</li>
<li><p>Knowledge 里存放 Notes 里的具体链接，看起来更像一个目录</p>
</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240667979_206152.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_553/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240667979_206152.jpg" alt="F2F6B5308EA78206F2940AF3BAF82D90.jpg"loading="lazy" decoding="async" width="553" height="222" /></picture></figure></div><h2>相关链接：</h2>
<ol>
<li><p><a href="https://www.zhihu.com/question/23427617">https://www.zhihu.com/question/23427617</a></p>
</li>
<li><p><a href="http://www.wenjukong.com/forum.php?mod=viewthread&amp;tid=2098">http://www.wenjukong.com/forum.php?mod=viewthread&amp;tid=2098</a></p>
</li>
<li><p><a href="https://www.douban.com/note/462721812/">https://www.douban.com/note/462721812/</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 06]]></title><guid>https://swiftsiqi.com/posts/justask006</guid><link>https://swiftsiqi.com/posts/justask006</link><description><![CDATA[Design Token]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 13:46:35 +0000</pubDate><content:encoded><![CDATA[<h1><strong>Design Token</strong></h1>
<h2>感想</h2>
<p>基于之前 《现代 Web 开发的现状与未来（JSDC 2019 演讲全文）》的演讲，去深入了解了一下 Design Token 的相关内容，说一下我学习后的感想：</p>
<h2>Design Token 是啥？</h2>
<p>首先设计系统这个概念现在已经很火了。提出原子设计理念的 Brad Frost 曾说过：</p>
<div class="blockquote"><blockquote><p>“设计系统就是一个讲述我们如何构建一个产品的故事。”—— Brad Frost on Let’s Work Together! at SmashingConf Barcelona 2017</p>
</blockquote></div>
<p>在一个公司内，设计系统可以提高团队间的协作效率。目前设计师和工程师遇到的一个普遍问题是，如何共享产品的品牌和界面信息。在开发过程中需要尽可能地按照设计产出还原，但是我们都知道这件事没那么简单。我们需要保持产品的品牌一致性，而设计规范和设计系统可以帮我们达成这个目标。</p>
<p>让我们先看一下 Nachos —— Trello 的设计系统。在大部分设计系统中，我们总能找到一个定义&quot;核心样式&quot;的章节。我所指的&quot;核心样式&quot;是指那些界面中不可拆分的部分，比如说颜色、字号、间隔、动画、阴影、层级、响应式断点等等。这些集中在一起的不可拆分的样式信息将会被应用于不同平台的产品设计中，它们就是——Design Token。</p>
<p>这个概念出自 Salesforce，Jina Anne 给其命名</p>
<p>Design Token 可以让产品设计团队更好地协作，同时保持产品跨平台的一致性。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673619_399515.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673619_399515.jpg" alt="C8D79A40F6D48BFCCBBDFFB01E9AD7AD.jpg"loading="lazy" decoding="async" width="1080" height="694" /></picture></figure></div><p>可以想象，上图中这些颜色将被用在 Trello 不同平台的界面中，不同的是每一种平台都有其自己的格式和要求：</p>
<ul>
<li><p>在 Web 中是一个写满了 RGB 颜色的 CSS 文件</p>
</li>
<li><p>在 iOS 中是一个写满 RGBA 颜色的 JSON 文件</p>
</li>
<li><p>在 Android 中是写满 8 位十六进制色值的 XML 文件</p>
</li>
</ul>
<p>当然，这些文件中的色值会随着时间不断变化，这也是为什么需要集中管理的原因，否则每次更新我们都需要到处寻找它们并修改，太浪费时间了。</p>
<h2>现状</h2>
<p>作为一个大前端，我假设设计师想要更换一些样式，比如说替换颜色，修改字号，或者其他一些样式。理想的状态是，他们更新之后在各个平台都可以立即看到修改后的效果，可事实并不是这样。</p>
<p>举个例子，假如现在我们需要替换一个颜色，现在的工作流程是这样的：</p>
<ul>
<li><p>设计师在设计工具中更新颜色</p>
</li>
<li><p>设计师将设计更新提供给开发</p>
</li>
<li><p>开发依据设计稿更改对应的代码</p>
</li>
<li><p>设计师可以在开发环境中看到最新的结果</p>
</li>
</ul>
<p>这个流程不好的地方在于：</p>
<ul>
<li><p>设计师想要看到最终实际效果如何，需要等待开发完成所有工作。这会阻碍设计流程，让设计师很难受</p>
</li>
<li><p>在代码中更改一处色值不需要太多时间，但是如果把这个时间用在其他更有意义的事情上会更好。如果经常这样更改，开发也会很难过</p>
</li>
<li><p>由于我们通常会把它记在任务管理工具中，这件「小事」会给人造成一定心理压力</p>
</li>
<li><p>从一个更加普遍的视角来看，这些变更会推迟产品走向市场的时间</p>
</li>
</ul>
<p>这个流程其实是可以被优化的</p>
<p>让 Design Token 来拯救你
现在闭上眼睛，想象你正在使用一个带有 Design Token 生成器的设计工具，理想的设计流程就像下面这样子：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673611_787325.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673611_787325.jpg" alt="BA4C9D280F4F6E02408943B15A39E5B6.jpg"loading="lazy" decoding="async" width="1080" height="1064" /></picture></figure></div><ul>
<li><p>设计师在设计工具中更改颜色。</p>
</li>
<li><p>设计工具自动将 Design Token 文件更新到指定平台。</p>
</li>
<li><p>开发只需要拉取或更新这个文件到自己的项目中即可。</p>
</li>
</ul>
<p>这将会给设计和开发节省大量时间，并保证产品整体一致性，无论是设计师还是开发或者产品负责人都会很开心</p>
<p>不过，正如我上面所说，这是一个理想的工作流程。如果要实现它，需要在设计工具和开发工具之间架起桥梁。目前据我所知可以生成 Design Token 的工具只有一个 Theo，这是由 Salesforce 开发的。Theo 做的事情很简单，它把一些原始的 JSON、YML 文件转换为适用于各个平台的代码文件，比如适用于 Web 的 Less 文件。</p>
<p>Theo 如果能在设计工具中使用的话，那么这个流程就有可能实现，尤其是自从 Sketch 可以导出 JSON 文件之后</p>
<p>想得更深远一些的话，这种中间工具应该可以直接让设计师在设计工具中生成 Design Token，接着这些变更被自动同步到设计系统中。</p>
<p>在这个例子中，Design Token 是颜色，其它属性诸如字号、投影或间隔都是一样的道理。</p>
<h2>适合开发的相关工具</h2>
<ul>
<li><p>24ways 的设计规范就是使用一个叫做 Fractal 的工具生成的。它的维护者 Paul Robert Lloyd 在构建规范的过程中就使用了 Design Token，这些 JSON 文件中的 Token 会被 postcss-map 插件导入到 CSS 中去。</p>
</li>
<li><p>EightShape 做了一个静态站点生成器，用来生成设计系统文档。他们通过一个 Gulp 任务来将 JSON 或 YAML 文件中的 Design Token 生成为一个叫 tokens.scss 的文件。</p>
</li>
<li><p>还有一个叫做 Dragoman 的项目，它是一个 Gulp 插件，也可以帮助你将 Design Token 转换为不同平台的样式文件，可惜它现在已不再更新了。</p>
</li>
</ul>
<h2>总结一下</h2>
<p>Design Token 是一些集中存储的 UI 元素信息，比如颜色、字体、间隔、动画等等。他们可以根据需要被转换为不同平台的代码，比如 Android、iOS 或 Web。</p>
<p>Design Token 让团队间的协作更加流畅，并且保证了不同平台间的品牌一致性。</p>
<p>然而，目前 Design Token 似乎只能在开发那边发挥作用，在设计工具内目前还没法直接和这些 Design Token 生成器产生联系，但目前 Sketch 可以导出 JSON 的数据了。另外 Figma 通过其开放 API 可以将 Design Token 导出，有一个插件叫做 CSSGen 甚至可以直接生成 Web 样式变量</p>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 05]]></title><guid>https://swiftsiqi.com/posts/justask005</guid><link>https://swiftsiqi.com/posts/justask005</link><description><![CDATA[Design Lint]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 13:43:38 +0000</pubDate><content:encoded><![CDATA[<h1><strong>Design Lint</strong></h1>
<p>设计产出后的那些事，大概可以分为 Design Review, UI Testing, Design Lint 三个环节。</p>
<h2>Design Review</h2>
<p>设计评审，应该是设计师最熟悉和参与度最高的一个环节。不过对于大部分设计师来说，关注的重点和经验主要集中在前期需求分析和完成设计稿，对于设计评审往往没有太多的经验，不知道如何表达，这篇文章很好地讲解了设计评审的流程：如何在會議中有效表達設計</p>
<h2>UI Testing</h2>
<p>UI 测试，通常是在通过前端框架自动化测试设计界面中存在的潜在问题，比如国外知名的 Chromatic 2.0，网易的 Airtest，阿里也有自己家的测试工具。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674075_760613.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674075_760613.jpg" alt="2B644F563BCAEC28CD765C2FB167892E.jpg"loading="lazy" decoding="async" width="1080" height="899" /></picture></figure></div><p>本文不讨论前面两个环节，主要说说 Design Lint 这件事。我们姑且把它翻译成设计审查吧。</p>
<h2>Design Lint</h2>
<p>Lint，这个在代码世界非常普及的事情，如今在设计工具里也渐渐流行了。</p>
<p>很早以前我就在想，为什么设计工具里一直没有 Lint 这类功能？我想可能有两个主要原因：</p>
<p>1.设计工具的最终产出和设计源文件没有直接关系。不像是代码，不符合规范会报错，语法错误根本无法运行。而设计过程中图层和命名等基本操作全凭设计师自觉，除非是团队制定规范，强制执行，通常也只能人工审核。但就算不遵守，也不会影响最终生产，老板更不关心。</p>
<p>2.历史遗留问题。早期的互联网设计和开发还是相对独立的两个环节，设计也没有组件，设计系统这些概念，和开发的联系没那么紧密，也就不需要语义化的命名规则，更不需要检查太多逻辑上的问题。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674055_075372.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674055_075372.jpg" alt="4357DC62D479AC18A4403752CABBDE81.jpg"loading="lazy" decoding="async" width="1080" height="636" /></picture></figure></div><p>同样的设计产出，表面看起来完全一样，但对组件和样式的运用、图层的命名却有天壤之别。</p>
<p>目前市面上已经出现很多 Design Lint 工具了，其中最广为人知使用最广泛的就是 color contrast 这类工具，比如：</p>
<h2>Cluse</h2>
<p>免费的 Sketch 插件，相当于把 Chrome DevTools 内置的 color contrast 功能拿到了软件里，可以实时查看修改结果。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674041_202524.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_639/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674041_202524.jpg" alt="F1C4CA26DF9F15E9892C1E54104E95C1.jpg"loading="lazy" decoding="async" width="639" height="561" /></picture></figure></div><h2>Contrast</h2>
<p>大概是目前设计和口碑最好的一款 Mac 小工具，由知名的 @mds 大神出品。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674027_601993.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674027_601993.jpg" alt="6DCB9BD76240A4FD3288EA7467E9FA04.jpg"loading="lazy" decoding="async" width="1080" height="540" /></picture></figure></div><h2>ontraste</h2>
<p>前苹果设计师做的另一个免费小工具。</p>
<p>同类的颜色对比度工具还有很多很多，因为标准已经比较完善成熟，不管是原生桌面软件，在线网站工具，还是设计软件里面的插件，甚至浏览器开发工具，都有不少非常不错的产品。</p>
<p>随着 Figma 这种基于 Web API 的设计工具的普及，越来越多更专业功能更全面的 Lint 工具也都出现了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674015_446463.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1052/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674015_446463.jpg" alt="4E853FDCD1E7C9CCFEA3542405776A50.jpg"loading="lazy" decoding="async" width="1052" height="468" /></picture></figure></div><h2>Design Lint</h2>
<p>前段时间 Discord 产品设计师做的一个 Figma 插件，受限于官方 API 的能力，虽然功能不算强大，但可以说很好的展示了这类工具的雏形和未来发展方向。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240674000_327924.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240674000_327924.jpg" alt="DEAA603FE89FFA0FB248BC1C1776D455.jpg"loading="lazy" decoding="async" width="1080" height="746" /></picture></figure></div><p>基于现在设计系统的思想，通常我们需要定义好一套颜色、描边、阴影等基础样式，以便在图层中方便地引用，它可以检测出设计稿中没有用到定义的样式的所有图层，统计并报错。尽管不是完全自动化的，需要你点一下插件执行，但好在是全局的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673988_626507.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673988_626507.jpg" alt="3C480F9F64CE780189AF54A25930FABF.jpg"loading="lazy" decoding="async" width="1080" height="858" /></picture></figure></div><p>然后就可以逐条点击并跳转到错误的图层进行修复，并且提供了简单的提示。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673978_126594.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673978_126594.jpg" alt="7AAA8115218E52874681E650ED3DB039.jpg"loading="lazy" decoding="async" width="1080" height="746" /></picture></figure></div><h2>Roller</h2>
<p>TOYBOX 团队也开发的另一款类似的 Figma 插件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673936_572578.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673936_572578.jpg" alt="F855159ACC6C8DC54C76CC02BC0F5AF0.jpg"loading="lazy" decoding="async" width="1080" height="540" /></picture></figure></div><p>这个插件和上面那个大同小异，同样也是检查颜色、文字、描边、样式和圆角等基本属性，但是界面的设计更加合理一些，把不同的分类整合在一起显示了，并且还可以直接添加没有样式的部分。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673926_516273.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673926_516273.jpg" alt="BD77AF98B1F8015E6373B7C8D043F665.jpg"loading="lazy" decoding="async" width="1080" height="761" /></picture></figure></div><p>同时它还提供了可以直接在插件里修改的界面，这样就不需要不停地跳转到对应图层了，比 Design Lint 更高级一些，唯一的缺点是免费版只能检查颜色和文字两个属性。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673910_172306.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673910_172306.jpg" alt="35647920A2FBC4988BC1314C76AD646C.jpg"loading="lazy" decoding="async" width="1080" height="1102" /></picture></figure></div><h2>Sketch-lin</h2>
<p>Sketch Lint 是一个比较早期的 Sketch 插件，应该是目前功能最强大的，但也是作为一个试验性项目，已经一年多没更新了。因为是基于 Sketch 和 Mac 系统原生的 API，它除了可以做到上述基本属性，还可以检查拼写和边距（Padding）。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673891_531788.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_600/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673891_531788.jpg" alt="F6AD2639B4403BBF23968A7BA99724B9.jpg"loading="lazy" decoding="async" width="600" height="468" /></picture></figure></div><h2>Sleuth</h2>
<p>最近才发布的另一个 Sketch 插件，基于 Sketch 的组件 Library 系统来检测组件的使用情况。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673882_707427.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673882_707427.jpg" alt="E1CFCAE18A014127799D6EF9E4E2E225.jpg"loading="lazy" decoding="async" width="1080" height="540" /></picture></figure></div><p>分析结果界面：有点像 Figma 企业版里面才有的那个设计系统分析工具。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673871_807005.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673871_807005.jpg" alt="C8EDF3E3D54C3B1D4B2C8FB60381825E.jpg"loading="lazy" decoding="async" width="1080" height="426" /></picture></figure></div><h2>Fix Your Mess</h2>
<p>这个插件可以检测出所有被解组的 instances 子组件，对它们进行清理或再次编辑等操作。严格来说这个其实不算 Lint 工具，只是对设计稿比较有洁癖的人来说很有用。比如还有的插件可以检查隐藏的图层，没有使用的图层等等。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673859_883853.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673859_883853.jpg" alt="EEDE2E167F88E21A37D8086A4B385CA5.jpg"loading="lazy" decoding="async" width="1080" height="540" /></picture></figure></div><h2>Modulz</h2>
<p>之前在 <a href="https://mp.weixin.qq.com/s?__biz=MzIyNTgyNzEyNQ==&amp;mid=2247484273&amp;idx=1&amp;sn=496b0d4f0c482982e8905ad8a6371531&amp;chksm=e878832bdf0f0a3dd3c2cb37bad2caecbd353a1b6dc9aa5d1d5b9548f315af0e7b239cdc14f6&amp;scene=21#wechat_redirect">未来的 UI 设计工具要来了吗？</a>介绍过的全新设计工具 Modulz，也提供了一个 Lint 功能，看如下截图是不是联想到了基于 Chrome 浏览器的网站性能测试工具 Lighthouse？其实如果把浏览器看作是一个设计工具的话，Lighthouse 确实是最完美的 Lint 工具了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673837_351997.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673837_351997.jpg" alt="1B3ABC8CF93642CFA9B18D5D543DD571.jpg"loading="lazy" decoding="async" width="1080" height="654" /></picture></figure></div><h2>Sketch Beta</h2>
<p>Sketch 官方最近也在做一个叫做 Sketch Assistants 的功能，提供类似 Lint 的功能，相信官方自己做这件事，功能特性和使用体验上应该好很多吧。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240673827_203503.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1080/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240673827_203503.jpg" alt="C0E53A4049F71DCF783CF4586BFBC4B7.jpg"loading="lazy" decoding="async" width="1080" height="730" /></picture></figure></div><p>总结
所以可以看到，现阶段的 Design Lint 工具，能做到的事情还非常有限，主要包括：</p>
<ul>
<li><p>颜色：检查图层的颜色是否使用了色板（或 design token ）中定义的颜色。</p>
</li>
<li><p>字体：检查字体大小、颜色、字体、行高等样式。</p>
</li>
<li><p>样式：包括圆角、描边、阴影等等。</p>
</li>
<li><p>拼写：检查文字图层中文字的拼写，只能通过苹果系统自带的字典 API 来做。</p>
</li>
<li><p>内边距（Padding）：只能检查图层和元素之间的垂直间距。</p>
</li>
<li><p>对比度（Contrast）：检查两个图层或文字图层的颜色对比度，显示 accessibility 评分。</p>
</li>
</ul>
<p>而暂时还不能做到，个人非常希望将来可以实现的事情大概有：</p>
<ul>
<li><p>图层和图层组的命名：想象一下这个应该不难做到，在插件的设置里定义好一份命名规范，如果不符合规范就会报错。</p>
</li>
<li><p>组件和样式的命名：检查颜色等样式的命名方式是否符合前端规范，组件的命名是否采用了 name / name 这样的命名方式，语义是否符合前端规范。</p>
</li>
<li><p>像素对齐：检查矢量图的像素对齐问题。</p>
</li>
<li><p>网格系统和元素位置：检查画布上的元素是否正确地使用了定义好的网格系统。还可以检查元素间的位置，比如基于 8pt 的原则，是否有元素不小心使用了奇数的间距等等。</p>
</li>
<li><p>图片处理：检查设计稿中载入的图片是否太大而影响文件性能，是否采用 Fill 的方式而不是其他方式载入的。</p>
</li>
<li><p>布局和响应式设计：对于需要做响应式设计的元素，你可能经常忘了设置 constraints 或使用 autolayout，如果可以智能地检查出来就很省心了。</p>
</li>
</ul>
<p>综上所述，目前 Design Lint 这件事，还处于不太成熟的早期阶段，没有大厂推动，设计软件本身也都没有做太多相关功能，也没有更开放的 API，即使个人开发者有一些小的尝试，也很难做到很完善。设计不是艺术，如今的互联网设计是高度工程化的一个环节，希望借助新的设计工具大潮的推进，会有更多的厂商意识到这件事并积极做出一些改变吧。</p>
<h2>References</h2>
<ul>
<li><p>如何在會議中有效表達設計: <a href="https://medium.com/uxeastmeetswest/%E5%A6%82%E4%BD%95%E5%9C%A8%E6%9C%83%E8%AD%B0%E4%B8%AD%E6%9C%89%E6%95%88%E8%A1%A8%E9%81%94%E8%A8%AD%E8%A8%88-how-to-present-your-design-in-design-review-6e192e5dd4b3">https://medium.com/uxeastmeetswest/如何在會議中有效表達設計-how-to-present-your-design-in-design-review-6e192e5dd4b3</a></p>
</li>
<li><p>Chromatic 2.0: <a href="https://www.chromatic.com/">https://www.chromatic.com/</a></p>
</li>
<li><p>Airtest: <a href="https://airtest.netease.com/">https://airtest.netease.com/</a></p>
</li>
<li><p>Cluse: <a href="https://cluse.cc/">https://cluse.cc/</a></p>
</li>
<li><p>Contrast: <a href="https://usecontrast.com/">https://usecontrast.com/</a></p>
</li>
<li><p>Contraste: <a href="https://contrasteapp.com/">https://contrasteapp.com/</a></p>
</li>
<li><p>Design Lint: <a href="https://lintyour.design/">https://lintyour.design/</a></p>
</li>
<li><p>Roller: <a href="https://www.trytoybox.com/roller">https://www.trytoybox.com/roller</a></p>
</li>
<li><p>Sketch-lint: <a href="https://github.com/saranshsolanki/sketch-lint">https://github.com/saranshsolanki/sketch-lint</a></p>
</li>
<li><p>Sleuth: <a href="https://sleuth.keap.design/">https://sleuth.keap.design/</a></p>
</li>
<li><p>Fix Your Mess: <a href="https://www.figma.com/community/plugin/817773666528183444">https://www.figma.com/community/plugin/817773666528183444</a></p>
</li>
<li><p>Modulz: <a href="https://www.modulz.app/">https://www.modulz.app/</a></p>
</li>
<li><p>Sketch Beta: <a href="https://www.sketch.com/beta/">https://www.sketch.com/beta/</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 04]]></title><guid>https://swiftsiqi.com/posts/justask004</guid><link>https://swiftsiqi.com/posts/justask004</link><description><![CDATA[BFF，SFF 到底是什么？]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 05:10:11 +0000</pubDate><content:encoded><![CDATA[<h1><strong>BFF，SFF 到底是什么？</strong></h1>
<h2>BFF，SFF 到底是什么？</h2>
<ul>
<li><p>BFF: Backend For Frontend</p>
</li>
<li><p>SFF: Serverless For Frontend</p>
</li>
</ul>
<p>关于 BFF 可以参考我的这篇笔记: 《微服务设计》中关于 BFF 内容的读后感</p>
<p>文章核心内容的总结：</p>
<p>至于 SFF，则是阿里 Egg 团队提出的一个新概念，那么为什么要提出 SFF 呢？这就要说说 SFF 的前任，也就是 BFF，它出了什么问题，Egg 团队认为 BFF 存在下面 3 个问题：</p>
<ul>
<li><p>专业人才储备</p>
<ul>
<li><p>远远低估了前端的缺人程度，一将难求，无人可招。</p>
</li>
<li><p>全栈人才的培养成本不低，包括前端需要学习后端 DevOps，后端需要学习前端的用户交互。</p>
</li>
</ul>
</li>
<li><p>基建墙，各种流程太重</p>
<ul>
<li><p>不同的基建服务需要去不同的后台走申请流程，N 多个工单需等待审批。</p>
</li>
<li><p>像 DRM 的配置、Mobilegw 的配置，需要在每种环境中单独配置一遍。</p>
</li>
</ul>
</li>
<li><p>资源浪费</p>
<ul>
<li><p>在 BFF 场景下，服务器水位较低（10% ~ 30%），基于微服务的高可用诉求导致了服务器资源的浪费。</p>
</li>
<li><p>譬如在蚂蚁容灾要求下，至少需要 11 台 4C8G 的容器。据此估算，支撑内部上千个中台应用，则就至少需要约 2000 台 32 核物理机！！！</p>
</li>
</ul>
</li>
</ul>
<p>而 SFF 的目标是 提升前端的研发效能，以一当百。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240704812_046223.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1492/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240704812_046223.jpg" alt="EDCDDC798E1763843244E9E490C15913.jpg"loading="lazy" decoding="async" width="1492" height="638" /></picture></figure></div><p>主要思路为：专业的人做专业的事，让业务开发者专注于业务本身的研发。</p>
<ul>
<li><p>场景化：根据不同业务场景做垂直领域定制，减少不必要的干扰，专注于业务逻辑的开发。</p>
</li>
<li><p>轻流程化：打破基建墙，一站式的接入三方服务，减少各种不必要的流程和工单，以代码为中心，声明即接入。</p>
</li>
<li><p>Serverless 化：让应用能利用云平台实现资源的按需分配和弹性伸缩，从而减少资源浪费。</p>
</li>
<li><p>自动化运维：DevOps → noops，减少研发对基础措施和运维的关注，交给我们这些专业的框架维护者。</p>
</li>
</ul>
<p>一句话阐述：让纯前端开发者，只需写几个 Function 即可使用到后端相关的能力。</p>
<p>研发的角色将变为</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240704804_394779.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1492/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240704804_394779.jpg" alt="589117058F81941C865685731F1B9311.jpg"loading="lazy" decoding="async" width="1492" height="784" /></picture></figure></div><p>一些思考与疑问：</p>
<p>回看 SFF 的整体概念，似乎 Egg 团队想表达的核心观点是： 将 BFF 这个层级做成 Serverless 的，这样做也有几个明显的好处：</p>
<ul>
<li><p>在SFF的模式下，原先 BFF 层里的前端开发在获取后端业务信息时，借助 Serverless 的能力，只需要写一些函数（FaaS）或者直接调用一些基础服务（SaaS）就 ok 了</p>
</li>
<li><p>原先的 BFF 层里的服务器维护工作还是相对较重的，一旦 BFF 层级变成 Serverless 的形式，维护将变得更加容易</p>
</li>
</ul>
<p>那么这些 SFF 这个概念对于 我们的 MBC 有一定指导意义么？</p>
<p>我觉得有，而且肯定有！</p>
<p>首先，SFF 提出的三个矛盾，我相信明眼人都可以看到，这就是未来 MBC 会遇到的问题，完全没毛病，但这些问题是现在还不是痛点或者痒点，但等体量大起来，这些问题就会暴露出来。</p>
<p>其次，试想一下在前端 Serverless 化的一个典型网站 CodePen ，如果说它是未来中台控制台的终极形态，也不为过，我们完全可以做到在 CodePen 上直接写几个简单函数，就实现了业务开发，而不再关注一切跟服务器相关的内容，那会多爽啊。这个模式没毛病，除非你非要折腾那些和业务没什么关系的代码。</p>
<p>最后，技术在发展，BFF 是 2015 年的概念了，这个架构关注的核心点是不同端上的差异问题（比如像 b 站 Android 和 iOS 的 app 设计体系是完全不同的两套，长得不一样，所以需要的 API 数据是极有可能不同的 ）和快速开发的问题（摆脱后端的发版依赖，将数据的控制权掌握在前端手里，后端只作为物料的供给方），而这些点已经不全是美团 app 所需要解决的核心问题了（例如美团 app 在  Android 和 iOS 上的样式是完全一致的，其实一套 API 就可以），MBC 在某种程度上将前端的主动权拿回到了自己的手里，也就是灵活性有了，所以研发效率会有一定的提升，那么未来到底怎么再提升研发效率，我想降低开发的门槛会是一个方向(在纯UI 方面，我们已经有了 MTFlexbox，好比你不会 iOS ，不会 Android 都可以，只要会 MTFlexbox 就行，从某种程度上就是降低了开发门槛), 也就是所谓的 Serveless，毕竟这样开发者不用懂太多的服务端知识，就可以马上开发。相信当我们的架构在灵活性和可操作性上都有了很好的平衡，整体的研发效率又会有一个提升。</p>
<div class="blockquote"><blockquote><p>参考资料</p>
<p><a href="https://www.yuque.com/egg/nodejs/sff-history">https://www.yuque.com/egg/nodejs/sff-history</a> by egg 团队</p>
</blockquote></div>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 03]]></title><guid>https://swiftsiqi.com/posts/justask003</guid><link>https://swiftsiqi.com/posts/justask003</link><description><![CDATA[GraphQL 到底是什么？]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 04:59:41 +0000</pubDate><content:encoded><![CDATA[<h1><strong>GraphQL 到底是什么？</strong></h1>
<h2>GraphQL 到底是什么？</h2>
<p>一句话来定义 GraphQL：GraphQL 是一种描述请求数据方法的语法，通常用于客户端从服务端加载数据。</p>
<p>它有 3 个显著的特征：</p>
<p>它允许客户端指定具体所需的数据。</p>
<p>它让从多个数据源汇总取数据变得更简单。</p>
<p>它使用了类型系统来描述数据。</p>
<h3>历史背景</h3>
<p>GraphQL 是由 Facebook 开发的，用于解决他们巨大、老旧的架构的数据请求问题。但是即使是比 Facebook 小很多的 app，也同样会碰上一些传统 REST API 的局限性问题。</p>
<p>例如，假设你要展示一个文章（posts）列表，在每篇文章的下面显示喜欢这篇文章的用户列表（likes），其中包括用户名和用户头像。这个需求很容易解决，你只需要调整你的 posts API 请求，在其中嵌入包括用户对象的 likes 列表，如下所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705354_3915615.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240705354_3915615.jpg" alt="19162DFCCC43BB3FC14D8FFDEAFDC39D.jpg"loading="lazy" decoding="async" width="800" height="736" /></picture></figure></div><p>但是现在你是在开发移动 app，加载所有的数据明显会降低 app 的速度。所以你得请求两个接口（API），一个包含了 likes 的信息，另一个不含这些信息（只含有文章信息）。</p>
<p>现在我们再掺入另一种情况：posts 数据是由 MySQL 数据库存储的，而 likes 数据却是由 Redis 存储的。现在你该怎么办？</p>
<p>按着这个剧本想一想 Facebook 的客户端有多少个数据源和 API 需要管理，你就知道为什么现在评价很好的 REST API 所体现出的局限性了。</p>
<h3>解决的方案</h3>
<p>Facebook 提出了一个概念很简单的解决方案：不再使用多个“愚蠢”的节点，而是换成用一个“聪明”的节点来进行复杂的查询，将数据按照客户端的要求传回。</p>
<p>实际上，GraphQL 层处于客户端与一个或多个数据源之间，它接收客户端的请求然后根据你的设定取出需要的数据。还是不明白吗？让我们打个比方吧！</p>
<p>之前的 REST 模型就好像你预定了一块披萨，然后又要叫便利店送一些日用品上门，接着打电话给干洗店去取衣服。这有三个商店，你就得打三次电话。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705342_9891.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240705342_9891.jpg" alt="0CB551D0229EF482D45A34C80BFC1110.jpg"loading="lazy" decoding="async" width="800" height="288" /></picture></figure></div><p>GraphQL 从某方面来说就像是一个私人助理：你只需要给它这三个店的地址，然后简单地告诉它你需要什么 （“把我放在干洗店的衣服拿来，然后带一块大号披萨，顺便带两个鸡蛋”），然后坐着等他回来就行了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705334_974835.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240705334_974835.jpg" alt="764CE7295D4BF870CF84A07EF6FE0822.jpg"loading="lazy" decoding="async" width="800" height="404" /></picture></figure></div><p>换句话说，为了让你能和这个神奇的私人助手沟通，GraphQL 建立了一套标准的语言。</p>
<p>一个例子</p>
<p>设想我们使用 GraphQL 查询数据，你只需要写出下面的代码即可</p>
<div class="block-code"><pre><code>query { 
client(id: 1) { 
    id
    name 
  } 
}</code></pre></div>
<p>你的第一印象：“这个不是JSON？”。</p>
<p>还真不是！就如我们之前说的，GraphQL设计的中心是为客户端服务。</p>
<p>GraphQL的设计者希望可以写一个和期待的返回数据schema差不多的查询。</p>
<p>那么后端会返回什么呢？</p>
<div class="block-code"><pre><code>{
  &quot;data&quot;: {
    &quot;client&quot;: {
      &quot;id&quot;: &quot;1&quot;,
      &quot;name&quot;: &quot;Uncle Charlie&quot;
    }
  }
}</code></pre></div>
<p>就如我们期望的，server会返回一个JSON串。这个JSON的schema和查询的基本一致。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705323_110124.gif" type="image/webp"><img src="https://i.typlog.com/siqi/8240705323_110124.gif" alt="EC838A77B773EC7D676FB8D3965A2482.gif"loading="lazy" decoding="async" width="800" height="394" /></picture></figure></div><div class="blockquote"><blockquote><p>参考资料：</p>
<p><a href="https://www.freecodecamp.org/news/so-whats-this-graphql-thing-i-keep-hearing-about-baf4d36c20cf/">https://www.freecodecamp.org/news/so-whats-this-graphql-thing-i-keep-hearing-about-baf4d36c20cf/</a> by Sacha Greif</p>
</blockquote></div>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 02]]></title><guid>https://swiftsiqi.com/posts/justask002</guid><link>https://swiftsiqi.com/posts/justask002</link><description><![CDATA[Serverless 到底是什么 ？FaaS 和 BaaS 又是什么？]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 04:55:33 +0000</pubDate><content:encoded><![CDATA[<h1><strong>Serverless 到底是什么 ？FaaS 和 BaaS 又是什么？</strong></h1>
<h2>Serverless 到底是什么 ？FaaS 和 BaaS 又是什么？</h2>
<p>一种比较通俗的说法：你部署代码的时候再也不用关心服务器之类的问题。</p>
<p>一段相对严谨的说法：无服务器架构（Serverless architectures）是指一个应用大量依赖第三方服务（后端即服务，Backend as a Service，简称“BaaS”），或者把代码交由托管的、短生命周期的容器中执行（函数即服务，Function as a Service，简称“FaaS”）。现在最知名的 FaaS 平台是 AWS Lambda。把这些技术和单页应用等相关概念相结合，这样的架构无需维护传统应用中永远保持在线的系统组件。Serverless 架构的长处是显著减少运维成本、复杂度、以及项目起步时间，劣势则在于更加依赖平台供应商和现阶段仍有待成熟的支持环境。</p>
<p>后一种说法里提到了 BaaS 和 FaaS，这两个名词到底是什么意思呢？</p>
<ul>
<li><p>BaaS: Backend as a Service，这里的Backend可以指代任何第三方提供的应用和服务，比如提供云数据库服务的Firebase和Parse，提供统一用户身份验证服务的Auth0和Amazon Cognito等。</p>
</li>
<li><p>FaaS: Functions as a Service，应用以函数的形式存在，并由第三方云平台托管运行，比如之前提到的AWS Lambda，Google Cloud Functions等。</p>
</li>
</ul>
<p>相比于 Baas ，FaaS 还是比较抽象，下面有一个例子来说明</p>
<p>函数，往大了说可以是一个应用的main函数，往小了说也可以是一个简单的加法函数，那到底该如何理解FaaS中的函数呢？先来看张图。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705589_960984.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8240705589_960984.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8240705589_960984.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8240705589_960984.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8240705589_960984.jpg" alt="75A24E962503B27165D5D343B547E13A.jpg"loading="lazy" decoding="async" width="1920" height="665" /></picture></figure></div><p>左侧的Monolith即我们常说的单体应用，中间是微服务，右侧就是FaaS中的函数。</p>
<p>如同一个单体应用可以按业务模块拆分成多个微服务，一个微服务也可以按使用场景拆分成多个函数。</p>
<p>比如一个广告微服务，至少可以拆分出实时竞价、展示计数、报表查询等多个函数。</p>
<p>也就是说，FaaS中的函数和微服务中的API是同一粒度的。</p>
<p>但不同于API，在Serverless架构下，每个函数都是独立部署，按需执行。</p>
<p>在解释了 BaaS 和 FaaS 后，我们回过头来在用一个例子来解释 Serverless</p>
<p>举一个实际的例子，假设我要部署一个小系统，它的作用是从一个数据库中检索一个列表，并以 JSON 格式呈现出来。（因为我自己比较熟悉 Node，我们来写一个具体的实现）</p>
<div class="block-code" data-language="js"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">var</span><span class="w"> </span><span class="nx">express</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s1">&#39;express&#39;</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="kd">var</span><span class="w"> </span><span class="nx">app</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">express</span><span class="p">();</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/cats&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span><span class="w"> </span><span class="nx">res</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="c1">//go get my cats</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">cats</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;Luna&quot;</span><span class="p">,</span><span class="s2">&quot;Robin&quot;</span><span class="p">,</span><span class="s2">&quot;Cracker&quot;</span><span class="p">,</span><span class="s2">&quot;Pig&quot;</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">cats</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">});</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="nx">app</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s1">&#39;port&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="mf">3000</span><span class="p">);</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;port&#39;</span><span class="p">),</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Express running on http://localhost:&#39;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;port&#39;</span><span class="p">));</span><span class="w"></span>
</div><div class="line"><span class="p">});</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>整个代码是基于 Express 框架来实现的，所以为了实现这个 API，我必须加载 Express，但重点在下面：</p>
<ul>
<li><p>首先，我需要一种设置和处理路由的方法，以便我知道何时显示路由 <code>cats</code>。换句话说，我要知道 <code>/cats</code> 是否被请求，而不是别的请求。</p>
</li>
<li><p>我必须启动一个 Web 服务器才能响应这些 API 调用。 虽然 Express 使这个变得很容易，但是我还是要做这一步。</p>
</li>
<li><p>我要处理一个请求和一个响应来处理客户端 API 调用。Express 再次简化了这个流程，但是现在我的逻辑和 HTTP 有着密切的联系。</p>
</li>
</ul>
<p>所以总的来说还不错。我可以在几分钟内运行起来。但是我需要部署它。</p>
<ul>
<li><p>首先我找到一个主机。例如美团云，哈哈，在这些主机上能够部署我的 Node 代码，并在几分钟内启动并运行。</p>
</li>
<li><p>作为这个过程的一部分，我必须考虑应用程序的使用情况，并对 CPU，内存等进行一些选择。</p>
</li>
</ul>
<p>那么云服务带来的改变是什么呢？我可以几秒钟内在云服务上的机器里部署好 Node 环境。这里还是涉及到了服务器。我们还是需要设置它（不管容易与否）。</p>
<p>所以在这些场景下，不管怎样你都需要设置服务器。</p>
<p>现在让我们回过头来看看 serverless。使用相同的用例，下面是我的代码：</p>
<div class="block-code" data-language="js"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">function</span><span class="w"> </span><span class="nx">main</span><span class="p">(</span><span class="nx">args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="c1">//go get my cats</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="nx">cats</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">&quot;Luna&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;Robin&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;Cracker&quot;</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;Pig&quot;</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">cats</span><span class="o">:</span><span class="w"> </span><span class="nx">cats</span><span class="p">};</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>就这样。实现业务逻辑的代码是我唯一需要关心的。</p>
<div class="blockquote"><blockquote><p>请注意，这个函数需要一个 <code>args</code> 参数，这是 OpenWhisk 处理参数的方式。在 Lambda 或谷歌云功能中可能会有所不同，所以有一些依赖平台的代码。</p>
</blockquote></div>
<p>部署这些只需要运行命令行将其添加到 OpenWhisk 中，类似的程序可以用于其他 serverless 平台。所以最后…</p>
<p>我只关心业务逻辑（获取 <code>/cats</code>）</p>
<p>我不关心路由，我的平台会告诉我 <code>cats</code> 是可用的</p>
<p>我没有绑定到 HTTP 模型了。我可以使用这个例子作为 serverless 平台每天运行的计划任务，甚至不考虑网络。</p>
<p>最后，我仍然在做同样的事情 — 使我的业务逻辑可用。但是我并不关心与网络访问相关的其它事情。</p>
<p>换句话说，我不关心服务器。</p>
<div class="blockquote"><blockquote><p>参考资料:</p>
<p><a href="https://www.telerik.com/blogs/why-serverless">https://www.telerik.com/blogs/why-serverless</a> by Raymond Camden</p>
<p><a href="https://martinfowler.com/articles/serverless.html">https://martinfowler.com/articles/serverless.html</a> by Mike Roberts</p>
</blockquote></div>
]]></content:encoded></item><item><title><![CDATA[不懂就要问系列 - 01]]></title><guid>https://swiftsiqi.com/posts/justask001</guid><link>https://swiftsiqi.com/posts/justask001</link><description><![CDATA[laaS，Paas，SaaS 到底是什么？]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 01 Oct 2025 04:48:16 +0000</pubDate><content:encoded><![CDATA[<h1><strong>laaS，Paas，SaaS 到底是什么？</strong></h1>
<h2>laaS，Paas，SaaS 到底是什么？</h2>
<p>在理解之前，先弄清这几个词儿的定义</p>
<ul>
<li><p>IaaS：基础设施服务，Infrastructure-as-a-service</p>
</li>
<li><p>PaaS：平台服务，Platform-as-a-service</p>
</li>
<li><p>SaaS：软件服务，Software-as-a-service</p>
</li>
</ul>
<p>老外有一个比较形象的解释来说明这个问题，假设你是一个餐饮从业者，打算做披萨生意</p>
<p>你可以从头到尾，自己生产披萨，但是这样比较麻烦，需要准备的东西多，因此你决定外包一部分工作，采用他人的服务。你有三个方案。</p>
<ul>
<li><p>方案一：IaaS：他人提供厨房、炉子、煤气，你使用这些基础设施，来烤你的披萨。</p>
</li>
<li><p>方案二：PaaS：除了基础设施，他人还提供披萨饼皮。你只要把自己的配料洒在饼皮上，让他帮你烤出来就行了。也就是说，你要做的就是设计披萨的味道（海鲜披萨或者鸡肉披萨），他人提供平台服务，让你把自己的设计实现。</p>
</li>
<li><p>方案三：SaaS：他人直接做好了披萨，不用你的介入，到手的就是一个成品。你要做的就是把它卖出去，最多再包装一下，印上你自己的 Logo。</p>
</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705985_359124.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_850/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240705985_359124.jpg" alt="4FBE679BAEE88EB4A8E48D710F9DAE8B.jpg"loading="lazy" decoding="async" width="850" height="668" /></picture></figure></div><p>从左到右，自己承担的工作量（上图蓝色部分）越来越少，IaaS &gt; PaaS &gt; SaaS。</p>
<p>对应软件开发，则是下面这张图（同理上一张图，蓝色部分为需要自己承担的工作量）</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8240705975_405358.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_550/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8240705975_405358.jpg" alt="0A46F71DDD476A09382C73F6B62BE764.jpg"loading="lazy" decoding="async" width="550" height="322" /></picture></figure></div><p>SaaS 是软件的开发、管理、部署都交给第三方，不需要关心技术问题，可以拿来即用。普通用户接触到的互联网服务，几乎都是 SaaS，下面是一些例子。</p>
<ul>
<li><p>客户管理服务 Salesforce</p>
</li>
<li><p>团队协同服务 Google Apps</p>
</li>
<li><p>储存服务 Box</p>
</li>
<li><p>储存服务 Dropbox</p>
</li>
<li><p>社交服务 Facebook / Twitter / Instagram</p>
</li>
</ul>
<p>PaaS 提供软件部署平台（runtime），抽象掉了硬件和操作系统细节，可以无缝地扩展（scaling）。开发者只需要关注自己的业务逻辑，不需要关注底层。下面这些都属于 PaaS。</p>
<ul>
<li><p>Heroku</p>
</li>
<li><p>Google App Engine</p>
</li>
<li><p>OpenShift</p>
</li>
</ul>
<p>IaaS 是云服务的最底层，主要提供一些基础资源。它与 PaaS 的区别是，用户需要自己控制底层，实现基础设施的使用逻辑。下面这些都属于 IaaS。</p>
<ul>
<li><p>Amazon EC2</p>
</li>
<li><p>Digital Ocean</p>
</li>
<li><p>RackSpace Cloud</p>
</li>
</ul>
<div class="blockquote"><blockquote><p>参考资料：</p>
<p><a href="http://www.ruanyifeng.com/blog/2017/07/iaas-paas-saas.html">http://www.ruanyifeng.com/blog/2017/07/iaas-paas-saas.html</a> by 阮一峰</p>
</blockquote></div>
]]></content:encoded></item><item><title><![CDATA[闲话 Apple 的 App Review 趣事]]></title><guid>https://swiftsiqi.com/posts/secret-of-app-review-team</guid><link>https://swiftsiqi.com/posts/secret-of-app-review-team</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 18 Jan 2024 10:39:22 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>起因是因为 Epic 和 Apple 的官司结案了，突然想起来之前看过的一些材料，就顺手整理了这份杂文，里面的很多内容在时效性上已经比较落后了，不一定与当下完全一致，不过当个奇闻轶事读读也好啦！</p>
</blockquote></div>
<h2>一份被忽视的资料</h2>
<p>对于外界而言，Apple 的 App Review 团队一直都显得十分神秘，直到 2019 年 CNBC 的一篇名为《<a href="https://www.cnbc.com/2019/06/21/how-apples-app-review-process-for-the-app-store-works.html">Inside Apple’s team that greenlights iPhone apps for the App Store</a>》的文章发布后，大家才对其有了一定的了解。</p>
<p>其中有不少特别有意思的冷知识，例如审核人员会通过 Mac 电脑访问一个叫 App Claim 的 Web 网站，批量审核应用，然后他们通常会在 iPad 上审核应用，即使这是一款 iPhone 应用。（Reviewers “claim” a batch of apps through a web portal on a Mac desktop, called App Claim. They often examine the app on an attached iPad, even if it’s an iPhone app.）</p>
<p>所以，我突然明白了为什么早年间，即使我们提交的是 iPhone 应用，但在拒审邮件的截图附件中，总是能看到 iPad 屏幕截图的身影，现在看来，原来如此~</p>
<p>不过，去年 <a href="https://en.wikipedia.org/wiki/Epic_Games_v._Apple">Epic 和 Apple 的官司</a> 又一次把 App Review 团队带到了大众的视野中。</p>
<p>关于这件事，IGN 上有一篇文章叫做 <a href="https://www.ign.com/articles/epic-vs-apple-shows-the-courts-were-not-prepared-for-the-games-industrys-obsessive-secrecy">Epic vs. Apple Shows the Courts Were Not Prepared for the Games Industry's Obsessive Secrecy</a>，里面提到了不少在法庭上辩论的细节，一开始我还好奇媒体是如何知道这些细节的，后来仔细阅读，才发现法庭里的很多内容其实是对外公开的，还好 IGN 的这篇文章里提供了信息的出处，好奇的我怎能错过！</p>
<ul>
<li>美国政府机构在公开网站上的存档 - <a href="https://cand.uscourts.gov/cases-e-filing/cases-of-interest/epic-games-inc-v-apple-inc/">Epic Games, Inc. v. Apple Inc.</a>，</li>
<li>热心网友在 box 上提供的存档 - <a href="https://app.box.com/s/6b9wmjvr582c95uzma1136exumk6p989/folder/135953042066">exhibits</a>，</li>
</ul>
<p>其中一篇文档成功的吸引到了我，那就是 App Store 审核团队高级总监 Trystan Kosmynka 在 Epic Games vs Apple 诉讼大战时提供的关于 App Store 审核团队的证词文件。</p>
<p>在这些文件中，我们可以发现许多有意思的信息，首先是在开庭当天（May 3 2021 Apple Opening），Apple 提供了一些关于审核团队的数据！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428504_016066.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428504_016066.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="725" /></picture></figure></div><p>来，我们看看这篇 slide 都讲了啥？</p>
<ul>
<li>在 2020 年，App Store 在全球有 180W 的 App</li>
<li>一共有 15W+ 的 App 因为违反隐私规定被拒审。</li>
<li>一共有 100W+ 的 App 因为不良或者非法内容被删除或者下架。</li>
<li>一共有 200W+ 的 App 因为没有及时更新或者适配新系统而下架。</li>
<li>一共有 10W+ 的 App 因为滥用或者不必要地使用用户信息而遭到拒审。</li>
<li>一共有 50W+ 的开发者账户因为欺诈和传播非法内容被吊销。</li>
<li>一共有 500个审核专家，它们每周平均要审核 100W+ 的 App</li>
</ul>
<p>说真，第一次看到这份数据的时候，大家还是蛮震惊的，一方面是每年拒审下架的 app 数量之多，一方面是审核团队的人数又是如此之少！</p>
<p>现在想想，我们被拒审好像也不足为奇了.......</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428491_301657.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428491_301657.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="924" /></picture></figure></div><p>上面的数据来自 May 6 2021 里 Trystan Kosmynka 的证词，这个图里我们可以看到 2019 年 16 号左右的 SLA 指数，SLA 是 Apple 内部用来衡量 App 审核效率的一个指标（如果一个 App 从审核开始发起到结束的耗时控制在 1天/2天，那么就认为达标）。</p>
<p>从公开的数据来看，11 月 16 日那一周，1 Day SLA 指标在 89.8%， 2 Day SLA 指标在 96.6%。</p>
<p>而从上面图表的下半部分来看，Apple 似乎对 1 Day SLA 的指标是要求在 50% 以上，而 2 Day SLA 的指标是控制在 90% 以上，而且你要注意到，这里是不区分工作日和周末的哦！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428484_171564.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_480/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428484_171564.png" alt="image.png"loading="lazy" decoding="async" width="480" height="325" /></picture></figure></div><p>上面的这张图来自 May 7 2021 里 Trystan Kosmynka 的证词，从图里可以看出，应用审核团队的人员分工有很多，常规审核、新发布功能审核等；有涉及到对内容的理解，比如说版权，法律等；涉及到用户利益，比如说付费，IAP，隐私；还有开发者账户等。</p>
<h2>关于 App Review 的审核流程</h2>
<p>先说说广义上的审核流程，结合 May 7 2021 里 Trystan Kosmynka 的证词，我们可以看到 App Review 的审核流程会有如下 5 个阶段：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428476_995298.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428476_995298.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="764" /></picture></figure></div><p>但对于开发者而言，第二部分的 Review 环节是大家更关心的部分，因为它就是大家所说的狭义上的审核流程，但是关于这个部分的细节，Apple 似乎从来没有公开过。</p>
<p>不过同样在 May 7 2021 里 Trystan Kosmynka 的证词中，我们终于对这个黑盒系统有了更多的了解，让我们先看一下 App Review Process 的相关 Slide。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428467_514091.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_767/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428467_514091.png" alt="image.png"loading="lazy" decoding="async" width="767" height="1280" /></picture></figure></div><p>所以从这些资料里，我们可以看到 App Store 的审核流程总共分为静态分析（Static Analysis）、动态分析（Dynamic Analysis）、MOZART 以及人工审核（Magellan）。</p>
<p>静态分析，包括了：</p>
<ul>
<li>DT App Analyer（DT应用分析器）</li>
<li>App Similarity（应用相似度）</li>
<li>Z Strings（Z 字符串）</li>
<li>App Transparency（应用透明度）</li>
</ul>
<p>动态分析，包括了：</p>
<ul>
<li>App Transparency（应用透明度）</li>
<li>Mercury</li>
</ul>
<p>至于这些东西的具体作用，在 Trystan Kosmynka 的证词里都做了一些简单的说明，感兴趣的朋友可以自行查看，我们在这里就不展开了。</p>
<p>除了这些，我们还可以从公开资料中了解到，Apple 还会使用一个名为 Columbus 的审核系统，通过它的介入，让 App 的审核流程变得更加高效。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428453_542242.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428453_542242.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="504" /></picture></figure></div><p>在这些资料中，还介绍了如何对新旧二进制文件进行审核，以及审核后台的样子。</p>
<p>简单来说，在每次的审核过程中，系统都会把二进制文件的静态检查签名和动态检查签名信息合并成一个 App 特定的版本签名信息，通过对比新旧 App 的版本签名信息，来判断 App 的状态是否符合预期。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428445_469091.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428445_469091.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="1073" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428439_433116.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1007/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428439_433116.png" alt="image.png"loading="lazy" decoding="async" width="1007" height="1280" /></picture></figure></div><h2>关于 App Review Guideline 的解读</h2>
<p>在这些公开资料中，最让我涨姿势的还是 Apple 对其 App Review Guideline 的许多解读和背景故事，其中一篇资料是他们内部对于用户隐私数据（地理定位）的讨论，非常有启发性，从这点来说，我认为 Apple 确实是在真正的关心用户体验。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428426_782843.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_880/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428426_782843.png" alt="image.png"loading="lazy" decoding="async" width="880" height="1280" /></picture></figure></div><p><a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guideline</a> 是 Apple 审核所有 App 的统一基准，在审核过程中的任何问题都可以在这里得到解释。它的重要性，我想不用多说，大家都是明白的。</p>
<p>在公开的资料中，有一份名为 《11/22/2019 iOS App Review（ERB）》特别值得反复阅读，为什么这么说？</p>
<p>因为这份材料是面向 ERB 的！可能有的读者还不了解 ERB 是什么意思，ERB 的全称是 Executive Review Board，即执行审核委员会，那可能又有读者在问，这个组织是干嘛的呢？</p>
<p>如果应用被拒绝审核，开发者可以向上申诉，申诉的审核委员会（ERB），也是对 App 是否可以上架 App Store 或被拒绝进行最后决定的机构。</p>
<p>如果你仔细阅读这份包含了 57 个 App 的审核资料（如下图所示），结合之前提到的 App Review Guideline，我想肯定会有不少新的启发和感悟。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428362_175766.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428362_175766.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="890" /></picture></figure></div><h2>拒审后的解决方案</h2>
<p>拒审对于大多数开发者而言，都不是什么陌生的事情，而且在公开的资料中（May 6, 2021 Trystan Kosmynka），我们可以看到了 2019 年 Nov 16 这一周的相关应用审核数据，其中包含了 iOS，watchOS，tvOS，macOS 和 Testflight 这五个渠道的 Top 10 拒审原因。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428341_064888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428341_064888.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="904" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428334_819087.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428334_819087.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="904" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428330_051586.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428330_051586.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="904" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428322_860121.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428322_860121.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="894" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428317_556294.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1280/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428317_556294.png" alt="image.png"loading="lazy" decoding="async" width="1280" height="894" /></picture></figure></div><p>除了 Apple 提供的这份数据外，我们还可以在第三方服务商的网站上查到类似的信息，比如 <a href="https://www.qimai.cn/trend/reviewNecessary">七麦数据</a> 提供的这个统计列表。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8294428306_153749.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_850/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8294428306_153749.png" alt="image.png"loading="lazy" decoding="async" width="850" height="551" /></picture></figure></div><p>总的来看，2.1 Performance: App Completeness, 2.3 Performance: Accurate Metadata，5.1.1 Legal: Data Collection &amp; Stroge ，4.3 Design: Spam 等几个条款是开发者在审核过程中最常被拒的原因。</p>
<h2>总结</h2>
<p>这篇文章虽然揭开了审核团队的冰山一角，但可能也没什么重量级的干货，就当个奇闻趣事看看吧。</p>
<p>总之，关于应用审核，就不要想什么歪门邪道了，答应我，还是做遵纪守法的好公民吧！</p>
]]></content:encoded></item><item><title><![CDATA[第 17 章 图像和 UI：示例]]></title><guid>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-17</guid><link>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-17</link><description><![CDATA[节选自《iOS和macOS性能优化》]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 04 Aug 2022 06:29:34 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这一部分的文章是我早年间参与<a href="https://book.douban.com/subject/30269356/">iOS和macOS性能优化</a>这本书翻译时的原稿, 当时的水平有限, 翻译的可能会有一些纰漏, 还请多多指教.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<p>本章将通过两个具体示例来详述如何优化大型应用程序：一款以图像为核心的天气应用和 Wunderlist 3 任务管理器（译者注：一款生活实用类软件）。</p>
<h2>优美的天气应用</h2>
<p>几年之前，我与一家刚刚成立的柏林创业公司进行了接触，迄今为止主要业务是开发益智类应用程序，并且取得了相当优秀的成绩。他们的目标是构建一款最优美的天气应用，现在已经基本完成了 UI 设计（参见图 17.1），但是同时遇到了一些阻碍，延缓了项目的交付。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895104_625616.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_864/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895104_625616.png" alt="image.png"loading="lazy" decoding="async" width="864" height="530" /></picture></figure></div><p>这款应用程序依赖大量图片资源，以致于经常因为内存错误而引发崩溃。此外性能问题也令人堪忧。我已经在前面几章中讲述了一些经验教训，但是最明显的一点仍然是内存警告与线程之间恼人的交互：若主线程运行内存消耗大的程序，进程会收到内存警告并处理，这将阻塞当前进程，甚至可能会被销毁，即使原则上你可以做些事情来挽救它。另一方面，如果后台线程上运行内存消耗大的程序，那么即使主线程尝试处理内存警告，但是后台线程可能仍会继续占用内存，从而导致进程被销毁。</p>
<p>解决方案如下：通过间断性发送消息的方式，后台线程向主线程“登记”申请内存，然后等待主线程返回的结果，特别是在分配大量内存之前。这样做的结果就是让主线程有时间对所有内存警告作出回应，并且在必要的时候停止后台线程。</p>
<h3>更新</h3>
<p>然而，本章所述的主要任务是对应用进行相应的更新。截至目前，iOS 设备的屏幕尺寸和分辨率数目又有所增加，并且为了同时兼容旧设备与新设备，我们需要处理新设备与旧设备之间存在的巨大功能差异。当然，设计团队可能还需要更多的动画、更逼真的图像以及更强的视差效果。</p>
<p>当我介入这个项目的时候，可能需要花费几分钟的时间才能够启动应用，单单高分辨率版本的图像资源本身就占用了 491 MB，并且这还不是全部的资源。为每个设备添加优化过的资源以及所缺少的图像，很容易就能让应用大小超过 1 GB，但是 Apple 仅允许 100 MB 以下的应用程序使用空中下载技术进行购买与更新（译者注：OTA Over-The-Aire 的简称，是通过移动通信（GSM或CDMA）的空中接口对SIM卡数据及应用进行远程管理的技术，可以简单理解为2G 3G 4G网络）。</p>
<p>所以当务之急是将资源的容量减少五分之四以上，并且还要在不增加资源的基础上支持当前所有 iOS 设备，此外还得大大缩短加载时间。我不得不承认，这似乎是个不可能完成的任务。</p>
<h3>探索 PNG</h3>
<p>应用初始版本使用的是 PNG 图像，尽管这些图像有着媲美照片的真实画质。我当时对这个选择已经有所质疑，但是在版本发布的前一夜再来做任何事情就真的太晚了，而且这个团队似乎也知道他们在做什么。</p>
<p>此外，虽然这些图像看起来与真实照片无异，但实际上是通过合成得到，而且 PNG 通常认为比 JPEG 更适合进行图像合成。因为 PNG 进行了充分的优化，它使用了 8 位 / 256 色的调色板图像来进行有损压缩，甚至可以将调色板颜色减少到 8 位以下，以为了空间分辨率而牺牲色彩分辨率。</p>
<p>原则上来说，牺牲色彩分辨率是一个好主意，因为人类视觉对于亮度变化的分辨要远远强于对色彩变化的分辨，但是这种做法可能会有所偏差：使用某种交互工具将原始尺寸的图像进行多次压缩，并调整相应的参数，直到图像达到最小尺寸，也就是实现一种“看起来还行”的效果。这种方法的问题在于，由于图像将会被放大显示，空间上的缺陷将会超过色彩上的缺陷。解决方案是同时“增加”颜色分辨率，但是我不知道该如何实现。</p>
<p>原型应用试图避免为每个设备配置多个版本的资源，这种方式也是有问题的——它对某些特定出现问题的设备保存了优化过的资源版本（实际上，这是为了将其降低到适当的分辨率上）。为了不影响现有的渲染代码，这种二次采样和保存操作是在正常加载代码之前完成的（参见图 17.2），这导致第一次启动的用户体验非常糟糕：应用会在加载屏幕停留好几分钟，不停的转圈圈，无法进行交互，并且设备会变得很烫手。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895093_133334.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_652/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895093_133334.png" alt="image.png"loading="lazy" decoding="async" width="652" height="918" /></picture></figure></div><p>此外，iOS 的 PNG 写入代码不像外部工具那样具备相应的优化机制，因此图像将被保存为 32 位的 RGBA，显然会比原始文件要大很多，尽管图像的分辨率很低。</p>
<h3>头脑风暴</h3>
<p>就我而言，很显然，“至少” PNG 格式需要重新考虑是否继续使用。我的第一个想法是围绕类似金字塔 (pyramidal) 编码方案进行，或者直接使用诸如 JPEG 2000 之类的小波 (wavelet) 编码。金字塔编码方案的优点是，它通过不停提取图像的较低分辨率版本来压缩图像，并且只存储这些版本之间的差距。这意味着解压缩操作将会自动提取较低分辨率的版本，因此可以仅仅只提供一个图像文件，就可以实现多个分辨率。</p>
<p>不过，iOS 和 Mac OS X 对 JPEG 2000 的支持相当慢，所以使用 JPEG 2000 并不是一个好选择。还有相当多的证据表明，这种性能上的缺陷并不是没有人尝试去修复，而是被现有的格式和技术手段所制约住了。所以这种方法似乎相当令人生畏，尽管我们的需求允许使用较少层次的金字塔简单实现，也可以使用其他的压缩机制来编码这些基本图像。</p>
<p>我们还研究了 PNG 当中所使用的 flate 压缩的替代方法。flate 压缩是一个非常好用的通用无损压缩器，但是我们的需求并不是很广泛，而是非常具体的。例如，我们只需要压缩图像，并且比起压缩速度而言，我们更关注解压缩速度（flate 有时会平衡两者的速度）。我们看到的一个替代方案是 LZ4，它的解压缩速度比起最差压缩比的 flate 速度至少要快 10 倍。</p>
<p>此外，我们其实还有其他神奇的选择，比如说 Apple 的移动芯片组直接支持的预压缩 PVRTC 结构格式。该数据格式不需要 CPU 解码；它可以直接馈送到 GPU，从而得到最佳的性能。另一方面，它的压缩率和质量都很一般。我还尝试使用 MPEG 影片格式，它也有一个硬件解码器，但是在 iOS 上一次只能存在一个 MPEG，而且它很难与场景中的其他对象组合在一起。</p>
<h3>JPEG 数据点</h3>
<p>直至最后，我们的面前仍然摆放了很多选择，而且依旧不清楚什么是正确的选择。我们需要相关的数据，所以我开始进行试验，从最普通的 JPEG 和 PNG 图像格式开始。</p>
<p>毫无疑问，JPEG 格式的文件在尺寸大小方面遥遥领先，对于 491 MB 的资源来说，使用非常保守的 0.7 质量设置就能够压缩到 87 MB，并且图片质量没有明显的损失。更令人惊讶的是，Apple 推荐将 Xcode “优化过的” PNG 作为默认格式，但是 JPEG 压缩文件的解码速度明显更快，至少在我运行测试的 Mac 上是这样的。</p>
<p>我们决定更加深入研究 JPEG 解码，得到了更多的好消息——使用 TurboJPEG 库能够将速度额外提升 20% 到 200%。更重要的是，对于我们在第 16 章“为何绝对不要绘制缩略图”一节中提到的需求而言，<code>CGImageSourceCreateThumbnailAtIndex()</code> 函数变得完全无用，也就是从 JPEG 2000 当中快速提取低分辨率图像！</p>
<h3>测量时的小错误</h3>
<p>当然，这里我犯了一个严重错误，没有实际在设备上运行这些测试，真正运行测试的时候就出现了一个巨大的断层：性能变得更加糟糕，特别是相对于 PNG 而言，而 PNG 格式现在竟然变得更加迅速了！这种性能差异对我来说没有任何意义，因为 CPU 是非常相似的，即便设备上的速度要稍微慢一些，但是两个解编码器之间的性能差异也不应该像我们测量的那么大才对。随后我对 Independent JPEG Group 的 libjpeg 软件的其中一个版本，也就是 JPEG 的官方参考实现，发现得到了比 Apple 标准库更好的结果，事情变得棘手起来。</p>
<p>其原因在于，Apple 实际上在 iPhone 系统集成芯片 (SOC) 中包含了一个 JPEG 解码硬件。这个解码硬件的工作效率实际上可能比软件还要慢，但是所使用的功率要更少一些，所以即便它的工作效率较慢，Apple 仍然更加偏好它。此外使用 Mach IPC 接口与硬件通信也会产生一些性能开销。</p>
<p>幸运的是，事实证明这些性能开销是固定的，因为我对一些小图像进行了测量。图 17.3 至 17.5 展示了一组更具代表性的测量结果，我们对不同的尺寸和不同的二次采样设置的图像进行了测量。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895083_083526.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_752/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895083_083526.png" alt="image.png"loading="lazy" decoding="async" width="752" height="598" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895074_86278.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_706/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895074_86278.png" alt="image.png"loading="lazy" decoding="async" width="706" height="578" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895067_787457.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_704/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895067_787457.png" alt="image.png"loading="lazy" decoding="async" width="704" height="594" /></picture></figure></div><p>对于较大的图像而言，在对更多数据进行解码的时候，固定的性能消耗将会被摊销，此时 Apple JPEG 轻松地击败了 TurboJPEG，这个替代品的性能位于第二位，而 PNG 的速度依旧慢得让人难以接受，因为 JPEG 解码器确实能够只进行部分解码来获得性能的大幅度提升。</p>
<p>最后，尽管小图像相对性能的下降非常显著，但是由于图像很小，绝对性能的下降并没有那么大。一个中等大小的 JPEG 所“耗费”的性能抵得上将近一百个小图像，并且如果有一个非常大的图像，那么结果就显而易见了。此外，硬件性能的下降因模型而异，我所测试的是最慢的设备之一。</p>
<p>所以实际上，这个结果比我们一开始用设备测试后得出的结果要好，如果其他的方案都失败了，那么我们就可以回到 <code>libjpeg</code> 或者使用 TurboJPEG。因此，看起来我们似乎不需要那种高精尖的技术，JPEG 能够满足我们所需的一切。</p>
<h3>JPNG 与 JPJP</h3>
<p>现在只剩最后一道障碍了：我们需要将许多资源组合在一起，从而形成最终的场景，并且还是用了大量的透明度设置，然而 JPEG 不支持透明度设置。幸运之神再次眷顾了我们，有人曾经遇到并解决了这个特殊问题：Nick Lockwood 提出了 JPNG 文件格式，也就是将 JPEG 和 PNG 组合成一个单独的文件，JPEG 提供颜色信息，PNG 提供 alpha 遮罩。</p>
<p>首先，使用 PNG 来提供 alpha 通道似乎听起来有些矛盾。没错，PNG 支持 alpha，但是我们并不会使用 alpha 值来编码图像，我们只需要编码出一个简单的灰度图，这样就与对另一个图像应用 alpha 通道的表现类似。另一方面，alpha 通道通常比图像更具备块状特性，因此 flate 压缩的效果应该也会很不错。尽管如此，实际上并没有一个很好的理由让 alpha 通道以 PNG 的形式提供，对于我们来说这同样是一个严重的限制，因为这意味着对于最高分辨率而言，必须要砍掉四分之一的数据。</p>
<p>相反，我们决定更新 JPNG 格式，以便它也可以使用（灰度）JPEG 图像来作为 Alpha 通道。此外，我们还会修改库的 API，以允许指定图像的分辨率/大小，并且还可以使用 <code>CGImageSourceCreateThumbnailAtIndex()</code> 来提取较低分辨率的图像。</p>
<h3>优美的启动</h3>
<p>最后，我们实现了这个看似不可能完成的任务。让这个应用保持在 100 MB 的限制之内，并且还可以支持所有的新设备，设计师对于他们可以添加新图形和动画感到十分满意。用户呢？他们非常喜欢，应用在美国和德国的 App Store 上都获得了 4.5+ 的评分，并且很多评论表示早上打开这个应用还能够驱赶瞌睡。</p>
<p>使用 JPEG 子集化机制的一个好处就是：即便是在非常老旧的设备上，我们仍然可以非常快速地加载这些分辨率显著降低的图像（分辨率只有之前的四分之一甚至八分之一），以便在高分辨率图片正在加载的过程中，用户仍然能够看到最终的场景。</p>
<p>我们能做的还有很多。首先我们并没有集成 JPEG 软解码，所有的解码都要通过硬件解码器完成。显然，从 CPU Profile 中可以看到，CPU 的利用率显著低于 100%。虽然硬件通常要更快一些，但是添加两个软件解码器似乎可以让解码吞吐量至少增加一倍，特别是如果我们还设法对图像进行排序，以便让硬件解码器优先解码较大的图像，软件解码器优先解码较小的图像。我们还可以添加一些 PVRTC 格式的图像，使用这种压缩还能够进一步提升性能，此外或许还可以从 MPEG 视频中解码某些动画序列。这种想法是尽可能多地利用可用的硬件资源，只要它们不会互相干扰即可。</p>
<p>但是这是之后的优化目标了。</p>
<h2>Wunderlist 3</h2>
<p>2013 年底，Wunderkinder 团队请我去帮忙推出 Wunderlist 3，在这期间，我们一起推出了名为 Objectice-C 客户端：Mac 和 iOS 的架构（译者注：没搞明白），它的表现格外突出，至今仍然无与伦比。这个团队以及参与的这款产品给我留下了深刻印象，。</p>
<p>一年半后，微软对这个团队和他们所构建的产品表现出了浓厚的兴趣，与此同时收购了这个公司，这意味着忠实的 Apple 程序员现在已被邪恶的 Redmond 帝国所雇佣了。并且还深深喜爱上了它！</p>
<h3>Wunderlist 2</h3>
<p>Wunderlist 2.0 版本就许多方面而言是一款非常优异的产品，大部分用户都非常喜欢，但是它的性能和稳定性仍然亟待改善。当我首次下载并启动应用的时候竟然直接闪崩了，直至最后版本稳定下来之前，依旧连续崩溃了好几次。</p>
<p>Mac 和 iOS 客户端的数据模型使用 Core Data 来构建，此外也用来关联 UI 组件。正如我们在第 12 章所看到的那样，对于少量数据和简单用例而言，Core Data 的表现还行，毕竟性能的要求并不高。但是在处理中等或者大量数据的时候，保持高性能就变成了极大的挑战，开发团队发现他们得创建更复杂、同时也更加脆弱的权衡措施，才能够保证 Core Data 不会由于 I/O 而阻塞主线程。</p>
<h3>整体架构</h3>
<p>Wunderlist 3 Objective-C 客户端的整体架构如图 17.6 所示。这个架构并没有什么特别的地方。其中包含了一个内存模型 (in-memory model)，它会在启动的时候从持久化（硬盘）存储中进行初始化。内存模型将会与 UI （双向）和后端（也是双向）保持同步。我们还会让磁盘存储与内存模型保持同步，但是由于磁盘存储对于应用而言是无法直接看见的，因此这个同步是单向访问。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895054_507877.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_480/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895054_507877.png" alt="image.png"loading="lazy" decoding="async" width="480" height="356" /></picture></figure></div><p>然而，正是这种简单的架构才可能成就优秀的性能：通过明确界定不同子系统之间的界限，使得责任分工清晰明了。例如，模型对象和内存中数据库都不需要知道存储或者网络 I/O 中的任何内容，这样就不会有出人意料的数据交互发生。它们最多知道如何将自身转换成字典，也就是让外部代码可以将其序列化为某种持久化的数据格式。</p>
<p>我们用另一种方法来表示该架构，如示例 17.1 所示，这次是用代码的形式来表示的。<code>|=</code> 和 <code>=|=</code> 运算符（类似图 17.6 当中的实线箭头）表示<em>数据流约束 (dataflow constraints)</em>，其行为与 Excel 公式非常类似，并且可以视作永久分配 (permanent assignment)，因此它的行为与正常分配类似，只不过系统会维护它们之间的关系。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">memory-model :<span class="o">=</span> persistence.
</div><div class="line">persistence  <span class="p">|</span><span class="o">=</span> memory-model.
</div><div class="line"><span class="nv">ui</span>          <span class="o">=</span><span class="p">|</span><span class="o">=</span> memory-model.
</div><div class="line"><span class="nv">backend</span>     <span class="o">=</span><span class="p">|</span><span class="o">=</span> memory-model.
</div></code></pre></div>
</div>
<h3>URI 与进程中 REST</h3>
<p>内存模型和持久化存储的基础架构模型是 <em>In-Process REST</em>，这是一种适用于应用当中的 REST 架构风格。所有的实体都由标识符对象所引用，这些标识符对象则是发挥 URI (通用资源标识符，Uniform Resource Identifier) 的作用；在 Wunderlist 中，指的是 <code>WLObjectReference</code> 类的实例，它会将<em>实体类型 (entity type)</em>、<em>容器 ID (container id)</em> 和<em>对象 ID (object id)</em> 进行编码。容器 ID 是封闭对象 (enclosing object) 的 ID，比如说任务所属列表的列表 ID。并非所有对象都有明确的容器；例如，列表或者已登入用户是直接位于 URI 根结构下面的。示例 17.2 展示了用字符串 URI 表示的 <code>WLObjectReferences</code> 实例：</p>
<div class="blockquote"><blockquote><p>示例 17.2 内部 URI</p>
</blockquote></div>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">task://container/2/id/3
</div><div class="line">list://id/2/
</div><div class="line">task://container/2/
</div><div class="line">task://id/3
</div></code></pre></div>
</div>
<p>URI 是结构化的。例如，示例 17.2 中的第一个 URI 引用了 id 2 列表中的 id 3 对象所表示的任务。第二个 URI 表示 id 2 列表对象。第三个 URI 是一个数组，表示包含在 id 2 列表当中的所有任务。最后一个 URI 指示表示 id 为 3 的一个任务，并没有提供任何列表 id。在我们目前的实现中，这会在所有列表中搜索满足此条件的任务。</p>
<p>数据存储被组织成一系列对象，其行为类似于 Web 服务器，只是它们不会使用 HTTP 协议来进行通信，不过这与示例 17.3 所示的标准 Objective-C 消息协议类似。如你所见，消息会一一与 GET、PUT 和 DELETE 这几个动词相对应，唯一的区别是，我们通常传入对象数组，而非单个对象。</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">@protocol</span> <span class="nc">WLStorage</span> <span class="o">&lt;</span><span class="bp">NSObject</span><span class="o">&gt;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="o">-</span><span class="w"> </span><span class="p">(</span><span class="bp">NSArray</span><span class="o">*</span><span class="p">)</span><span class="n">objectsForReference</span><span class="o">:</span><span class="p">(</span><span class="n">WLObjectReference</span><span class="o">*</span><span class="p">)</span><span class="n">ref</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">removeObjectsForReference:</span><span class="p">(</span><span class="n">WLObjectReference</span><span class="o">*</span><span class="p">)</span><span class="nv">ref</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">setObjects:</span><span class="p">(</span><span class="bp">NSArray</span><span class="o">*</span><span class="p">)</span><span class="nv">new</span><span class="w"> </span><span class="nf">forReference:</span><span class="p">(</span><span class="n">WLObjectReference</span><span class="o">*</span><span class="p">)</span><span class="nv">ref</span><span class="p">;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>内存存储、硬盘存储和表示 REST 后端的对象都遵循相同的协议，因此大部分的存储操作都是可以互相替换的。为了进行测试，我们可以将第二个内存存储替换为磁盘存储，或者替换为后端同步，也可以同时实现两者的功能，这样就加快了测试的速度。事实上，这个协议非常简单，这同样意味着可以相互组合。例如，我们将计算实体 (computed entities) 的匹配器 (filter) 添加到存储层次结构中，这样就可以使用相同的方式来对其进行访问了，或者也可以针对某个特定的实体，将其放到多种持久化存储当中。</p>
<p>我们可以独立于 <code>WLObjectReferences</code> 所引用的对象来执行相关的计算。例如，我们可以确定磁盘路径和后端 URL。正如我们在示例 17.2 当中所看到的那样，我们还可以剔除 URI 的最后一个部分来确定对象所属的组。</p>
<h3>最终一致的异步数据存储</h3>
<p>回想我们之前使用 Core Data 的经验，让数据存储保持简洁、快速是初期设计中的优先考虑事项之一。我认为我们成功做到了这一点：我们的 CTO 很喜欢在讲座中震惊听众，他这样说：我们将数据以单独的 JSON 文件形式存储在磁盘上。这种做法其实非常有效——如果您回想一下第 12 章的内容，使用 Foundation 方法来对 JSON 格式进行编码和解码是最快的，恰好 JSON 也是我们与后端通信的数据格式，因此让存储格式和后端通讯格式保持相同，已经证明对于调试而言是非常不错的方法。</p>
<p>我们已经证明，这种简单的机制所带来的性能提升是非常惊人的。我们的其它客户端使用了数据库，或者其他复杂的序列化格式，但是 Objective-C 客户端在性能上始终遥遥领先，特别是在处理压力测试的时候，考虑到一个理智的——呃，不，一个不理智的用户很有可能会创建很多个列表和任务。尽管从事实上来说，使用这种格式就很多方面而言存在很多<em>不足</em>。例如，我们可能会写入太多的小文件。然而，每当我以为某个问题需要换用更复杂的方法才能解决时（否则会很伤脑筋），但实际上最后我会发现这个问题是由一个简单的错误所引起的，并且解决起来非常简单。</p>
<p>数据存储之所以如此简单，完全要归功于我们的后端，它由一个松散的微服务集合所组成，可以保证不同的实体之间最终能够保持一致。这意味着我们的一致性需求并不只满足于存储的规范，因此将 YES 传递给 <code>NSData</code> 的 <code>writeToFile:atomically:</code> 方法来保证独立文件的一致性，这个做法非常有效。</p>
<p>所有写入到磁盘的操作都是与主线程异步的；不过，这些操作都是在一个负责写入磁盘的后台线程中同步循环执行的。主线程只会将需要保存到后台写入线程的 对象 URI，通过队列发送出去。当后台写入线程在队列中获取到特定的 URI 后，就会从内存存储中获取当前的最新条目，然后将其序列化到磁盘当中。</p>
<p>由于磁盘写入器总是保存当前最新版本，所以可以简单地剔除队列中重复的写入请求 URI，以合并多个写入请求。这有助于减少磁盘子系统的负载。</p>
<h3>RESTOperation 队列</h3>
<p>在上一节中，我提到写入请求将通过队列发送到文件写入模块当中。这个队列就是 <code>WLRESTOperationQueue</code>，这个实例我们在整个系统中用来异步连接代理实体 (acting entities)。可以这么说，这是让 Wunderlist 在处理网络交互和管理持久化的同时，仍然可以响应用户操作的秘密武器。</p>
<p>顾名思义，<code>WLRESTOperationQueue</code> 由一个 REST 操作队列组成，而每个操作又由一个 <code>WLObjectReference</code> 和一个 REST 动词（GET、PUT、DELETE）组成，该动词用于告知目标应该对引用执行何种操作。操作的含义取决于具体的目标。对于磁盘存储而言，如果收到了 PUT 就意味着要将这个由 URI 指定的对象存储到磁盘；对于 Web 接口而言，则是意味着发送 HTTP PUT 请求给后端。</p>
<p>可以从任意线程中来添加队列，并且队列可以维护子集的工作线程，以便为条目进行服务。队列可以选择将结果传递给指定的目标线程，这个线程与服务线程不同；比如说主线程。与 GCD 相比，让每个队列都具备单独的工作线程，可以大大减少线程的数量，以及相应的资源消耗。</p>
<p><code>WLRESTOperationQueue</code> 对象通过自动拒绝重复条目 (entry)，来支持合并操作。要实现这个功能，最关键的一点取决于：队列当中的条目只能是引用。我们花了很多时间来证明这一点，因此 <code>WLRESTOperationQueue</code> 目前的版本历经了大约一年左右的完善才得以出现。</p>
<p>如果我们尝试使用实际对象指针的任意一种变体，都会出现不合意的结果（比如说以这个写入磁盘的示例应用为例）。</p>
<ul>
<li>将可变对象写入到队列当中，并在写入之后对其进行修改，因此这种对象在保存的时候仍有可能会被修改。这是一个很糟糕的想法，如果要解决这个问题，那么就需要添加数不胜数的锁，即便如此仍可能允许进行有冲突的修改。</li>
<li>将副本发在队列当中可能意味着：每当对其进行修改，那么所有相同对象都会执行一次写入。这在高负载的情况下会导致性能严重下降，特别是您需要保证性能良好的时候。</li>
<li>清除最新添加的对象（通过 URI 的方式）意味着：只有第一条更新操作会被写入；后面执行的所有更新都会丢失，直到对象再次执行更改。</li>
<li>清除最老添加的对象，很容易导致所修改的对象永远无法写入到磁盘的情况出现。</li>
</ul>
<p>借助 URI 队列，即便是在高负载的情况下也仅仅只意味着会有多余的更改操作累积下来，但是磁盘子系统仍会尽可能保持最高的吞吐量。到目前为止，我们已经很少遇见磁盘写入速率跟不上系统写入速率的情况，而这些由小错误引起的问题也很容易进行修复。</p>
<h3>流畅、反应灵敏的 UI</h3>
<p>对于 UI 而言（图 17.7），实际上我们使用了一种经典的 MVC 方法，在 Wunderlist 的架构中可以表示为 <code>ui =|= model</code>。在经典的 MVC 中，当 UI 准备好进行更新并需要数据时，UI 便会去模型中拉取数据，对应到 Apple 的 MVC，其特征是控制器负责将数据从模型推送到 UI 中。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895039_777566.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_864/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895039_777566.png" alt="image.png"loading="lazy" decoding="async" width="864" height="560" /></picture></figure></div><p>让 UI 在准备就绪后，就自行完成更新其实才是 MVC 的基本准则，而这在目前流行的大部分 ViewController 编程实践中往往会被忽略，但是当在动画运行的时候，需要通过异步操作来为其添加更多数据，从而协调动画的运行，因此这个准则变得至关重要，因为尝试依赖模型推送来协调几乎是不可能实现这一点的，并且还可能会导致各种复杂解决方案的出现，例如说 FRP 和 React，但是传统的 MVC 对此就有解决方案：我们只需要通知用户界面（无需推送数据），并让<em>其</em>决定何时该自行更新即可。</p>
<p>在我们的示例中，UI 元素将由 URI 进行参数化，URI 当中包含了 UI 元素所应该展示的对象。之后，可以使用这个 URI 来发送 <code>objectsForReference:</code> 消息，从内存存储中获取最新的对象版本。</p>
<p>这个 URI 同样也可以用在更新通知当中。我们可以使用 Cocoa 最基本的 <code>NSNotificationCenter</code> 方法，将对应的 URI 与其一同进行参数化。随后，UI 元素可以将此 URI 与其内部维护的 URI 进行比较，以确定是否需要更新自身。</p>
<p>如前所述，URI 还会互相关联，因此假设有一个 URI 为 <code>task://container/2/id/3</code> 的任务被修改了，那么展示该列表 <code>task://container/2</code> 的列表视图同样也会进行更新。</p>
<p>我们可以使用 <code>WLRESTOperationQueue</code> 对象分离 UI 线程和任何可能会发生模型修改的线程。当模型对某个特定对象进行修改后，它会将该对象的 URI 发布到队列当中，并将配置为表示“模型已更改”的 <code>NSNotification</code> 传递到 UI 线程上的默认 <code>NSNotificationCenter</code> 当中。</p>
<p>队列的合并行为巧妙地解决了这样一个问题：我们尽可能降低每个单独更新的等待时间，同时避免在发生大量连续更改时频繁重载 UI 。此外，这种行为同样也不会丢失任何更新。</p>
<p>对于 UI 而言，我们实际上还需要添加一项功能：自动组合。在正常操作下，我们希望每个单独元素会立即独立进行更新。然而，随着负载的增加，这种做法变得越来越无意义。当您将数百条新列表任务发送给设备时，让每个列表都执行一遍动画效果不仅毫无意义，并且还会让用户感到非常烦人、感觉非常混乱。</p>
<p>自动合并的工作方法就是监视队列的深度。对于进入到队列当中的 URI 而言，随着队列越来越长，合并级别也将逐步增加，URI 后面的元素移除得也越来越多。如果将合并级别设置为默认值，那么诸如 <code>task://container/2/id/3</code> 之类的 URI 将原样进入到队列当中，并且合并只会影响到特定任务的更改。</p>
<p>如果将自动合并的合并级别设置为 1，那么就会从 URI 后面移除一个元素，仅仅只留下前面的容器：<code>task://container/2</code>。这会造成两种影响：一方面，当前 UI 中的整个列表会全部刷新，而不是针对某个特定任务进行更新；另一方面，这还会将列表当中所有独立的项目更新给合并在一起。因此，这就不会对列表当中的项目进行多次更新，因为我们对整个列表执行了更新。</p>
<p>最后，如果更新的频率仍然超出了用户界面展示的能力，那么合并级别 2 将会移除 URI 末尾的所有内容，只留下一个通用的 “UI 需要更新”的消息，并且还会将所有的 UI 更新请求合并成一个，以让 UI 自行进行刷新。</p>
<p>通过这种机制，我们就再也不用担心 UI 在大量更新的过程中跟不上变化，或者出现无响应的情况。除非我们引入了新的 BUG。</p>
<h3>简评 Wunderlist</h3>
<p>这里所展现的架构元素显然不是 Wunderlist 3 之所以性能如此强劲的全部原因。我们还有一个庞大的后端团队，为我们提供快速的 HTTP 和 WebSocket 接口，此外整个团队还进行了深入、详细的性能调查，并根据需求适当进行调整。不过，架构元素会确保审查的次数并不会很多，并且目的也十分明确，因此所做的调整都是非常微小、简单的，而不是时刻都在奋战性能问题。</p>
<p>我的意思是，这并不是实现高性能的唯一途径，换句话说：那些我们没有使用的技术并不是不能实现高性能。我认为，应用这些技术和基本原则，不仅可以在使用其他技术的同时实现高性能，并且还可以任意使用我们所提供的工具来直接获得出人意料的性能。优秀的性能是应用每月获取 500 万活跃用户，并且在 App Store 中获取 4.5 到 5 星评价的关键因素。</p>
<h2>总结</h2>
<p>在本章中，我们讨论了两个示例，通过将所有调优手段“结合在一起”，从而开发出优秀的、高性能的应用。优美天气应用是一个很极端的例子，因为它将调优目标推动到了一个非常具体的调优方向（即加载并显示大型图像集），此外还要实现一些看起来完全不可能的功能，最后还得为应用预留充足的空间。我们需要仔细分析硬件和软件的功能，并恰当调整需求，并根据需求作出适当调整，此外还需要一些来自外界的支持……我们自定义一种图像文件格式，它是另一种自定义图像格式的改版。</p>
<p>Wunderlist 是当代移动应用的一个典型示例，其中混杂了数据存储、实时网络访问以及 UI 的频繁更新等功能。我们将之前章节中所学到经验教训结合在了一起——例如，都要尽可能在内存中处理大部分工作，避免使用数据库引擎，无论性能是否与之相关，都尽可能使用简单、快速的存储机制。我们将第 16 章当中所述的更新机制进行了概括，将 UI 更新限制在某个架构元素当中，从而用于协调和简化应用的所有部分：网络层、数据存储、内存模型和 UI。</p>
<p>这两个示例都展示了如今手机的无限可能，并且最后，我们还是需要将硬件的能力推向极致，从而才能实现非凡的性能。</p>
]]></content:encoded></item><item><title><![CDATA[第 16 章 图像和 UI：陷阱和技巧]]></title><guid>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-16</guid><link>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-16</link><description><![CDATA[节选自《iOS和macOS性能优化》]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 03 Aug 2022 06:32:47 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这一部分的文章是我早年间参与<a href="https://book.douban.com/subject/30269356/">iOS和macOS性能优化</a>这本书翻译时的原稿, 当时的水平有限, 翻译的可能会有一些纰漏, 还请多多指教.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<p>实际上，图形程序设计本身就是一个完整成熟的领域，有许多值得一读的书籍，网上资料也非常丰富，所以本书不可能涵盖并详述所有知识点。比如，我们会粗略了解下 OpenGL，学习非游戏、用户端应用程序中常见的图形技巧，仅此而已。</p>
<h3>陷阱</h3>
<p>响应性方面，最大的陷阱之一是在主线程中执行较长或不可预测的操作。所有 I/O 都存在该问题，我们从之前章节中了解到，即使是最小的 I/O 操作也可能耗费很长时间。另一方面，单纯将这些操作放到后台线程执行，而不想办法提升 I/O 响应性，这只会更加糟糕，关于这点我们之前也见过不少了：用户可以操作所有界面，但没有任何反应，并且也没有显示旋转光标（译者：系统卡主时出现的进度指示器，俗称“风火轮”）。在这种情况下，操作放置在主线程执行更合适一些，至少主线程操作会触发系统繁忙的旋转光标，告知用户发生了什么。</p>
<p>对于图形来说，预先渲染所有图形资源并以位图格式进行传递是提供图形最普遍的技术之一，同时这也是最大和最明显的错误。最极端的例子就是 iPad 上一些杂志类应用程序，用 Adobe 发布工具直接发布到应用里，杂志的每一页都是预先渲染好的整页位图，同时适配横屏和竖屏。由于 I/O 是现代计算机体验中最慢的一部分，不管是使用（蜂窝数据）无线数据还是直接从硬盘读取，哪怕是固态硬盘，用户体验都差强人意。视网膜高清屏出来以后，预先渲染的位图在高清屏上看起来就会非常模糊，效果更差了。</p>
<p>随着高分辨率视网膜显示屏的出现，让一切都看起来像是艺术品，任何数字生成的东西都应该可以直接作为矢量图使用，除去那些直接用代码绘制的部分。而位图问题显而易见：本身太大了而且无法缩放，必须适配每个分辨率版本，不过 “像素完美” 的理念依然会经久不衰。</p>
<p>这个理念将一直延续下去。</p>
<p>iPhone 6 Plus 有 1,920×1,080 个面板，但是实际渲染是分辨率的 3 倍，即 2,208×1,242 像素，由于两个分辨率不匹配，因此像素对显示的分辨率下采样 1.15 倍。无论是通过下采样的手法（Quartz 和合适的设备会自动处理）还是单独一个步骤专门对整个渲染帧缓冲区进行下采样并不（很）重要，不管采取哪种方式，都不符合 “像素完美” 的预渲染设计理念。其中这也不重要 —— 400 dpi 的屏幕分辨率，也没人会在单个像素点上较真，用户十分喜爱 6 Plus 的屏幕。</p>
<p>如果必须要使用位图格式的图形，也存在多种优化技术供我们选择。即使是 Xcode 生成的 “优化过” 的 PNG 文件，也能借助这些优化技术减少文件大小以及文件加载的时间。我们将在第 17 章中详细讨论这些。</p>
<p>在绘图方面，最大的问题往往是过度绘制（多次绘制相同的像素）和重绘没有变化的屏幕区域。幸运的是，第 15 章介绍的那些工具将助你诊断和解决这些问题，请使用这些工具！</p>
<h3>技巧</h3>
<p>要获得良好的图形性能，通常意味着要从需要展示的屏幕和像素入手，进行逆向操作，而不是从更改模型作为开始并将其展示在屏幕上。</p>
<p>一方面，绘制过程涉及到 AppKit、UIKit 提供的脏矩形，另一方面，每当 UI 层收到改动通知都需要设置这些矩形。“过多通信导致安装缓慢” 一节包含了一个庞大的示例，展示为了实现较好的效果，在项目中需要克服的一些障碍，以及如何将性能从难以忍受的缓慢提升到几乎无法测量的快速。</p>
<div class="blockquote"><blockquote><p>译者注：脏矩形就是每一帧绘制图形界面的时候，需要重新绘制、有变化的区域。</p>
</blockquote></div>
<p>大型复杂的路径一直是 Quartz 的巨大难题，由于需要计算自相交以实现抗锯齿效果，所使用的几何图形算法是分段数的二次项（检查每段上的相交点，对比其他的段）。底层算法已经改进了不少，所以这也不再是一个大问题了，但是还是要考虑路径的长度，最好是使用中等长度的路径。</p>
<p>不再重复绘制形状，使用 Quartz 图样设备。对渐变来说都是一样的：使用内置的内容。在绘制性能方面，<code>CGLayer</code> 并没有太大的帮助。自 NextStep 起图像绘制问题就没有得到改善，而 <code>CGImage</code> 重写了基本图形绘制部分，尽可能加快了绘制速度。不过，<code>CGLayer</code> 在生成 PDF 时仍具有优势，得益于保留了重复元素的向量信息。如果你打算打印或者生成 PDF 里有重复的元素，那么 <code>CGLayer</code> 是个好东西。</p>
<p>考虑到 15 章里介绍的 OpenGL 潜在的性能优势，以及现在的游戏在高帧率下呈现的超级复杂场景，使用 OpenGL 加速绘制绝对是无脑行为。虽然概念上讲得通，实际上实现类似加速绘制的行为绝对是无脑行为。苹果公司曾多次尝试在系统级上将 OpenGL 的加速机制融入 Quartz 中，据我所知，所有的尝试都以失败告终。额外的执行操作将会吃掉图形加速所获的所有好处。还有一个问题非常明显，Quartz API 的调用/返回性质，能很好的映射到 OpenGL API 上，但是不能很好的映射到实际的硬件接口上（参见第 14 章 Metal 章节里的 OpenGL 和 Metal 的讨论。）</p>
<p>另外一个原因是图形原语不同：Quartz 使用填充和描边的贝塞尔路径，这些路径必须经过昂贵的细分，将其转换成阴影三角形供图形硬件使用，同时开放应用程序接口暴露给外部调用。 如果形状被重复使用，那么转换成本可以被其他使用者均摊，不过接口并未真正提供该功能。</p>
<h3>过多通信导致安装缓慢</h3>
<p>一项调查结果出乎意料：Mac OS X 新版本下，某个安装程序按照预期的步骤执行安装数千个小文件，竟然出现了进度减慢的神秘现象。在新版本下，安装程序速度减缓了几倍，几乎降了一个数量级。I/O 速率比磁盘子系统慢了很多，CPU 的使用率可以忽略不计，所以看起来好像没什么瓶颈可以用来解释这种性能下降的情形。</p>
<h4>节流显示</h4>
<p>在使用了大量工具和抓耳挠腮之后，终于发现安装器提供显示了状态更新信息。它正在显示每个安装文件名称，为了确保用户有机会看到每一个文件名，每次都会刷新屏幕上的内容。这样能满足用户了解过程进度的渴望，却与 Mac OS X 图形子系统的节流机制相冲突，限制图形的更新频率，现在设置成了大约 60 Hz。试着更频繁的刷新屏幕，就能轻而易举地阻塞你的程序，正如示例 16.1 所示。</p>
<p>示例 16.1 在 60 Hz 下运行</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">Cocoa</span>
</div><div class="line"> <span class="kd">class</span> <span class="nc">AppController</span><span class="p">:</span> <span class="bp">NSObject</span><span class="p">,</span> <span class="n">NSApplicationDelegate</span> <span class="p">{</span>
</div><div class="line">   <span class="kd">var</span> <span class="nv">mainWindow</span><span class="p">:</span> <span class="n">NSWindow</span><span class="p">?</span>
</div><div class="line">   <span class="kd">func</span> <span class="nf">applicationDidFinishLaunching</span><span class="p">(</span><span class="n">n</span><span class="p">:</span> <span class="bp">NSNotification</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">      <span class="kd">let</span> <span class="nv">window</span> <span class="p">=</span> <span class="n">NSWindow</span><span class="p">(</span><span class="n">contentRect</span><span class="p">:</span> <span class="n">NSMakeRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">320</span><span class="p">,</span> <span class="mi">200</span><span class="p">),</span>
</div><div class="line">                             <span class="n">styleMask</span><span class="p">:</span> <span class="n">NSTitledWindowMask</span><span class="p">,</span>
</div><div class="line">                             <span class="n">backing</span><span class="p">:</span> <span class="n">NSBackingStoreType</span><span class="p">.</span><span class="n">Buffered</span><span class="p">,</span>
</div><div class="line">                             <span class="k">defer</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span>
</div><div class="line">      <span class="n">window</span><span class="p">.</span><span class="n">orderFrontRegardless</span><span class="p">()</span>
</div><div class="line">      <span class="kc">self</span><span class="p">.</span><span class="n">mainWindow</span> <span class="p">=</span> <span class="n">window</span>
</div><div class="line">      <span class="n">NSApp</span><span class="p">.</span><span class="n">activateIgnoringOtherApps</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</div><div class="line">      <span class="n">dispatch_async</span><span class="p">(</span><span class="n">dispatch_get_main_queue</span><span class="p">())</span> <span class="p">{</span>
</div><div class="line">         <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">60</span> <span class="p">{</span>
</div><div class="line">           <span class="kc">self</span><span class="p">.</span><span class="n">drawSomething</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
</div><div class="line">         <span class="p">}</span>
</div><div class="line">       <span class="n">NSApp</span><span class="p">.</span><span class="n">terminate</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"> <span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">func</span> <span class="nf">drawSomething</span><span class="p">(</span> <span class="n">i</span><span class="p">:</span><span class="nb">Int</span> <span class="p">)</span> <span class="p">{</span>
</div><div class="line">      <span class="kd">let</span> <span class="nv">window</span><span class="p">=</span><span class="kc">self</span><span class="p">.</span><span class="n">mainWindow</span><span class="p">!</span>
</div><div class="line">      <span class="n">window</span><span class="p">.</span><span class="n">contentView</span><span class="p">?.</span><span class="n">lockFocus</span><span class="p">()</span>
</div><div class="line">      <span class="n">NSColor</span><span class="p">.</span><span class="n">redColor</span><span class="p">().</span><span class="kr">set</span><span class="p">()</span>
</div><div class="line">      <span class="n">NSBezierPath</span><span class="p">.</span><span class="n">fillRect</span><span class="p">(</span> <span class="n">NSMakeRect</span><span class="p">(</span> <span class="mi">10</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mi">200</span><span class="p">,</span><span class="mi">120</span> <span class="p">))</span>
</div><div class="line">      <span class="kd">let</span> <span class="nv">labels</span><span class="p">=</span><span class="nb">String</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
</div><div class="line">      <span class="kd">let</span> <span class="nv">label</span><span class="p">:</span><span class="bp">NSString</span><span class="p">=</span><span class="n">labels</span>
</div><div class="line">      <span class="n">NSColor</span><span class="p">.</span><span class="n">blackColor</span><span class="p">().</span><span class="kr">set</span><span class="p">()</span>
</div><div class="line">      <span class="n">label</span><span class="p">.</span><span class="n">drawAtPoint</span><span class="p">(</span> <span class="n">CGPoint</span><span class="p">(</span><span class="n">x</span><span class="p">:</span><span class="mi">20</span><span class="p">,</span><span class="n">y</span><span class="p">:</span><span class="mi">20</span><span class="p">),</span>  <span class="n">withAttributes</span><span class="p">:</span><span class="kc">nil</span><span class="p">)</span>
</div><div class="line">      <span class="n">window</span><span class="p">.</span><span class="n">contentView</span><span class="p">?.</span><span class="n">unlockFocus</span><span class="p">()</span>
</div><div class="line">      <span class="n">window</span><span class="p">.</span><span class="n">flushWindow</span><span class="p">()</span>
</div><div class="line"><span class="p">}</span> <span class="p">}</span>
</div><div class="line"><span class="n">NSApplication</span><span class="p">.</span><span class="n">sharedApplication</span><span class="p">()</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">controller</span> <span class="p">=</span> <span class="n">AppController</span><span class="p">()</span>
</div><div class="line"><span class="n">NSApp</span><span class="p">.</span><span class="n">delegate</span> <span class="p">=</span> <span class="n">controller</span>
</div><div class="line"><span class="n">NSApp</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</div></code></pre></div>
</div>
<p>在 8 核 的 Mac Pro 上执行这段代码 600 次只花费了 10.0 秒，MacBook Pro 同样如此 ，两个系统 CPU 的空闲超过了 90%。</p>
<p>注意一下，Quartz 足够聪明可以分辨是否有任何绘图行为，因此只要在循环中调用 <code>flushWindow()</code> 就会立即得到返回结果，因为在这种情况下实际并未产生刷新行为。我们需要实际地绘制一些东西，但绘制内容并非总是不同。</p>
<h4>使用节流显示</h4>
<p>虽然节流显示的使用方式有很多，例如游戏里使用 OpenGL 来获得更高的刷新频率，请牢记以下要点：实际状态更新的频次可能比用户知悉的要多得多。比如，我通过 NSURLSession DataTask 的代理方法进行测试，在家用 6-MBit/s 的 DSL 光缆条件下，显示状态更新次数为每秒 273 次。</p>
<p>我相信用户并不关心，也不想知道每秒进行了 273 次状态更新 —— 换做我也是如此 —— 实际上我们也无法读取刷新如此频繁的字节数。就字节计数文本而言，每秒一次足矣，当然如果加快更新速率会使得进度条更加平滑，例如每秒钟刷新10次。</p>
<p>至此，我已经使用了三种技术来避免过高的 UI 刷新频率，其中有两种是有效的。第一种技术适合用在连续的进度监视上，比如下载或磁盘进度。使用了大约 10 Hz 的计时器来查询进度并更新显示。除了解决进度显示的平滑问题以及一些限制，该技术避免了模型→视图通信，从架构角度而言是可取的。 定时器技术的缺点是它必须独立于底层操作启动和停止。</p>
<p>第二种技术是批量处理更新请求，避免引入显示计时器，但不能避免 模型→视图 通信。实现批量处理更新请求的一种方式是预先缓存，然后发送更新消息，在某个点比如 0.1 秒之后及时地传递该更新消息，如在下章节示例 16.4 中展示了这种操作。</p>
<p>第三种技术我本以为会有效的，结果还是图样图森破了，<code>performSelector:afterDelay:</code> 取消了之前的请求，实际上我曾多次见过该项技术，哎，实际上没起作用，至少如果有足够的负载使间隔时间短于延迟，这样的话，能一直取消更新信息的发送直到更新停止为止。</p>
<h4>今日安装程序和进度报告</h4>
<p>考虑到这个特定问题是如此糟糕，你可能认为该情况很少见，甚至根本不可能发生。那你就错了—— 在 2016 年 1 月，据报道，当呈现进度条时，节点包管理器 npm 速度减慢了 50% 到 200%，微软的自动更新仍然占用了 150% 的 CPU 资源（这是在双核机器上使用 <code>top</code> 命令测量得到的结果），归结原因是在更新过程中开启了进度显示条。</p>
<p>在 OS X 10.9 和 iOS 7 中，苹果引入了 <code>NSProgress</code> 类和 <code>NSProgressReporting</code> 协议，明确支持长时间运行任务的报告进度。基本实现思路如下：让对象汇报每个活动的进度，然后将这些单独的进度汇总在一起，组成总的进度。在上世纪 90 年代末期就实现了相似的系统，干得好！</p>
<p>哎，谈及进度报告，苹果实际上完全失误了，或许就是熟视无睹。指示 UI 进度的推荐方法实际上就是监听 <code>NSProgress</code> 对象的 <code>percentCompleted</code> 属性。这并不能解决我们遇到的问题，还会引入 KVO 通知的问题，KVO 传递通知的线程和改变进度条状态使用的线程是同一个。</p>
<p>实际上，苹果公司警告过：“不要在紧密循环中更新 <code>completedUnitCount</code>”，看来苹果公司知悉该问题，意识到了实际上并没有解决问题。</p>
<h3>iPhone 无法承受之重(Overwhelming an iPhone)</h3>
<p>几年前，有人请我帮忙开发一款新闻类应用程序，管理一些类似 RSS 里的 Feed 条目，使用常见的 UITableView 进行展示。每个条目对其所处的不同状态都显示不同的 UI：尚未下载，下载了一些元数据，缩略图已接收，有无音频。另外，在下载音频数据时需要展示下载进度，这些文件可能要很久才能下载完毕。</p>
<p>只要我们使用一个 feed 且条目数量小于 10，就不会出现什么问题。但是一旦超过 10 个 feed 或者 30 个条目，应用程序就会遇到明显的性能问题。我们尝试了很多常见的优化措施，比如，将长时间运行的操作（如生成缩略图）放到后台线程上，避免不必要的工作，例如为每个条目的状态更改都同步到数据库中，这些措施都无济于事。</p>
<p>具体的症状表现为：UI 界面可能会很长一段时间僵住不动，然后才会恢复使用。应用程序第一次启动时最容易发生，因为此时正在读取、更新 feed 下的所有条目。之后启动应用程序就不太会遇到该问题了，因为数据已经缓存在应用里了。即便如此，也给用户的第一印象造成了不好的影响。</p>
<p>分析表明，UI 界面僵住主要是 UIKit 花时间在执行绘制代码（绘制文本、table 布局），Cell 的复用机制如期运行，创建 Cell 没有什么额外的消耗。这里也没有什么可优化的对吧？</p>
<p>不对！实际上问题出现在示例 16.2 的这段代码里。</p>
<p>示例 16.2 模型改变时通知视图代码</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">notifyChanged</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">[[</span><span class="bp">NSNotificationCenter</span><span class="w"> </span><span class="n">defaultCenter</span><span class="p">]</span><span class="w"></span>
</div><div class="line"><span class="w">               </span><span class="nl">postNotificationName</span><span class="p">:</span><span class="w"> </span><span class="s">@&quot;UserStatusChanged&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">               </span><span class="nl">object</span><span class="p">:</span><span class="nb">nil</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>尽管这段代码看起来没什么问题，和很多 <code>NSNotificationCenter</code> 示例代码都相似，实际上会产生两个问题。首先，缺少之前章节讨论的更新节流（update-throttling），其次未给出具体细节：只说有些东西改变了，却没有明确指出是什么。缺少这么重要的上下文信息，UI 元素（这个例子中的 table view）收到通知后，无奈只能更新所有的 UI 元素。不仅要更新可见的元素，还要更新不可见的元素，有些甚至不在当前的界面里！</p>
<p>有个非常简单的方法可以解决该问题，就是在通知中指明当前的对象，幸运的是 <code>NSNotification</code> 可以实现该需求。示例 16.3 显示了实现的代码：</p>
<p>示例 16.3 模型改变时通知视图，并传递上下文</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">notifyChanged</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">   </span><span class="p">[[</span><span class="bp">NSNotificationCenter</span><span class="w"> </span><span class="n">defaultCenter</span><span class="p">]</span><span class="w"></span>
</div><div class="line"><span class="w">               </span><span class="nl">postNotificationName</span><span class="p">:</span><span class="w"> </span><span class="s">@&quot;UserStatusChanged&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">               </span><span class="nl">object</span><span class="p">:</span><span class="nb">self</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>接收通知的代码能够从通知中获取有问题的对象，看一下是否相关（属于当前的 table），接着只更新这一行。示例 16.4 的代码在之前章节所说的批量更新中，将根据上下文内容进行了单独的更新。假设 table view 只能显示一屏的 “条目”，客户端调用 <code>-refreshItemsFromBackground:</code>，例如，通过 <code>NSNotification</code> 确定该条目的索引值，然后使用该值。</p>
<p>示例 16.4 批量更新 table view 条目</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">@property</span><span class="w"> </span><span class="p">(</span><span class="k">retain</span><span class="p">)</span><span class="w">  </span><span class="bp">NSMutableSet</span><span class="w">  </span><span class="o">*</span><span class="n">indexesToRefresh</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">refreshAccumulatedItems</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="bp">NSSet</span><span class="w"> </span><span class="o">*</span><span class="n">items</span><span class="o">=</span><span class="nb">nil</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">@synchronized</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="n">itemIndexes</span><span class="o">=</span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">indexesToRefresh</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">setIndexesToRefresh</span><span class="o">:</span><span class="nb">nil</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">[</span><span class="n">tableview</span><span class="w"> </span><span class="n">reloadRowsAtIndexPaths</span><span class="o">:</span><span class="p">[</span><span class="n">itemIndexes</span><span class="w"> </span><span class="n">allObjects</span><span class="p">]</span><span class="w"></span>
</div><div class="line"><span class="w">                        </span><span class="nl">withRowAnimation</span><span class="p">:</span><span class="n">UITableViewRowAnimationNone</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">triggerRefresh</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">performSelector</span><span class="o">:</span><span class="k">@selector</span><span class="p">(</span><span class="n">refreshAccumulatedItems</span><span class="p">)</span><span class="w"></span>
</div><div class="line"><span class="w">           </span><span class="nl">withObject</span><span class="p">:</span><span class="nb">nil</span><span class="w"> </span><span class="n">afterDelay</span><span class="o">:</span><span class="mf">0.2</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">refreshItemFromBackground:</span><span class="n">item</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="bp">NSIndexPath</span><span class="o">*</span><span class="w"> </span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">indexPathForItem</span><span class="o">:</span><span class="n">item</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">@synchronized</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="o">!</span><span class="n">indexesToRefresh</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">setIndexesToRefresh</span><span class="o">:</span><span class="p">[</span><span class="bp">NSMutableSet</span><span class="w"> </span><span class="n">setWithObject</span><span class="o">:</span><span class="n">index</span><span class="p">]];</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">performSelectorOnMainThread</span><span class="o">:</span><span class="k">@selector</span><span class="p">(</span><span class="n">triggerRefresh</span><span class="p">)</span><span class="w"></span>
</div><div class="line"><span class="w">                   </span><span class="nl">withObject</span><span class="p">:</span><span class="nb">nil</span><span class="w"> </span><span class="n">waitUntilDone</span><span class="o">:</span><span class="nb">NO</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">[</span><span class="n">indexesToRefresh</span><span class="w"> </span><span class="n">addObject</span><span class="o">:</span><span class="n">index</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">}</span><span class="w"> </span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">   </span><span class="p">}</span><span class="w"> </span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>批量更新代码基本上按照如下步骤执行：如果没有可供刷新的批量集合，则新建一个然后再安排刷新；如果存在，则只需要将结果添加到批量操作的队列中。启动过程也很简单 —— 获得需要更新的批量集合，清除并刷新 table view。请注意，如果发生任何事情导致这些索引失效，你将要清除当前更新批次。</p>
<h3>一切都是假象</h3>
<p>关于处理 UI 性能问题，我学到最重要的技巧之一就是伪造，如果你要做的事情执行速度实在太慢，你可以向用户展示一个旋转图标，告知用户你正在努力完成剩下的工作，提示用户完成进度。或者用动画效果表达。</p>
<p>使用动画效果掩饰延迟是非常聪明的办法，让 iPhone 看起来运行速度非常快，反应迅速。尽管硬件性能相对有限，且竞争对手推出了更高性能的硬件，更好的硬件加速处理，动画效果仍让 iPhone 保持了领先地位。</p>
<p>例如，打开 PDF 文件渲染第一页（或头两页）会花一些时间，然而，借助动画效果将文件从缩略图大小切换成全屏的这个过程，用户的注意力被动画效果所吸引，同时系统忙于处理打开 PDF 的工作。动画可以由 GPU 处理，所以不会占据 CPU 的资源。</p>
<h4>图像的缩放和剪切</h4>
<p>在上世纪 90 年代，我开发了多款 NeXT 软件，以输出设备的驱动为主，从 NeXT 彩色打印机到成本高达数万美元的高端彩色激光复印机，都可以使用我开发的软件。其中一个软件是 eXTRASLIDE（见图 16.1），可驱动宝丽来 CI-5000S 数字调色板录像机。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894895_117196.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_791/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894895_117196.png" alt="image.png"loading="lazy" decoding="async" width="791" height="660" /></picture></figure></div><p>从工程师的角度来说，程序的核心部分是底层驱动，使用 SCSI 端口将由 Display PostScript 渲染的高清分辨率位图传送到设备上，设备往往使用自定义且缺乏文档说明的协议。</p>
<p>良好的性能对上面这些软件来说至关重要。就拿宝丽来录像机来说，有 4000 行分辨率，图像大小 48 M，对传送图像有实时性要求。1 GB 内存的手机看起来貌似有点不够用，但是当时我们最高端的盒子是佳能 object.station 41，32 MB 的 DRAM，100 MHz 的 486 处理器。频率钟显示此设备的 CPU 和现代 CPU 之间的差异是 10 到 20 倍，基准测试显示是 100 倍，简而言之，该设备比当今中等配置的手机要慢得多。那时候，我们还认为它“超级快”呢。</p>
<p>就 eXTRASLIDE 而言，还有另外一个问题：需要前端界面对原材料进行定位、缩放、剪切（见图 16.1）。小菜一碟，除了需要硬件需要花费大量的时间重绘原素材，实时重绘是无法实现的。于是，标准做法是绘制矩形轮廓辅助定位，定位后重新一次性绘制整张图片。</p>
<p>这时候 <code>NSImage</code> 就派上用场了。不管你的原素材是什么，它都能为不同的屏幕分辨率自动创建并缓存预览界面以及渲染。这样会导致一些问题，尽管类名称已经暗示和图片有关（<code>NSBitmapImageRep</code> 是处理位图的类），有时候人们依旧会忘记正在处理一个包装器，而不是一张图片，但在这种情况下正是我们所需要的。eXTRASLIDE 的预览图片足够小一遍满足界面交互的所需性能。不管原始图片有多大，从截图中也可能看出，预览图片非常小。</p>
<p>最后一个问题就是 <code>NSImage</code> 只会考虑屏幕的有效缓存，如果分辨率和屏幕正好匹配的话。否则，就会从原始的展示中生成缓存。哎，在缩放的时候的确会发生这样的事情，每次缩放都会导致分辨率不再匹配屏幕，因而触发重绘机制。这本是 “正确” 行为，然而反复采样缩略图会引发严重的质量问题，也意味着不可能实时缩放。</p>
<p>关键的技巧如图 16.2 所示，只用最初的 <code>NSImage</code> 缓存创建新的 <code>NSImage</code> 实例，在这里，<code>NSImage</code> 没得选，只能缩放低分辨率位图，因此可以实现实时缩放。缩放低质量缩略图没什么问题，图片在移动的时候，人眼无法分辨细节，一旦实施缩放结束（按钮松开），就切回原来的 <code>NSImage</code>，重新缓存新的屏幕位图。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894884_552981.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_580/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894884_552981.png" alt="image.png"loading="lazy" decoding="async" width="580" height="243" /></picture></figure></div><p>我得到的经验教训就是，凡是交互形式的程序，只要不被抓住，就可以作弊。在这里的例子中，只要能缩放就行，哪怕是只能缩放低分辨率的位图。虽然不如缩放源文件效果好，图片在运动过程中，这两者之间的区别不是很大，也不会引人注意，客户还是很喜欢这个特性的。</p>
<h4>缩略图绘制</h4>
<p>在开发获奖软件 Livescribe Desktop 时，图片，特别是缩略图，再次使用了作弊手段未被发现，结果证明这样的处理是合理的。Livescribe Smartpen 使用红外摄像机精准地捕捉位置，正如你在纸上书写不规则的点图案一样。桌面应用展示并整理这些捕捉到的手写或绘制页面。</p>
<p>每个笔记的预览模式，应该展示缩略图，展示捕捉到的纸张背景的向量笔触，很明显这不是笔捕捉的图像。这些背景有两个问题，意识高分辨率的 PNG 图像渲染速度非常慢，包含笔触的文件格式在读取时需要执行很多初始化的工作。</p>
<h4>如何不绘制缩略图</h4>
<p>绘制缩略图的第一个方案，如图 16.3 所示，以 “缩略图” 为主。一张缩略图实际上就是一张从源文件中生成的小图片，我们的 Windows 客户端直接为每个页面创建了缩略图图片，保存在硬盘里。</p>
<p>Mac 团队认为他们应该也这样做，不过效果更好，因为 OS X 支持高质量的 PDF，苹果也引入了 <code>CGImageSource CreateThumbnailAtIndex()</code> 方法，专门用于从硬盘中加载缩略图。那么最可能出什么纰漏呢？</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894875_738718.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_890/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894875_738718.png" alt="image.png"loading="lazy" decoding="async" width="890" height="219" /></picture></figure></div><p>额怎么说呢，使用该方法后所有东西都出错了：因为 PDF 是一种与分辨率无关的格式，每个 PDF “缩略图” 包含了全分辨率的 PNG，绘制到 PDF 需要解压 PNG，生成 PDF 后再重新压缩。这些 PDF 由于包含了太多细节信息，因此 “缩略图” 渲染速度也很慢，为每个 PDF 生成新的线程解决进程自身执行缓慢的问题。</p>
<p>要尝试这个办法，需要重启机器，因为 200 个线程对 CPU 和内存的消耗会产生大量持续的交换。</p>
<h4>如何真的不画缩略图</h4>
<p>把 PDF 写到硬盘后再去渲染明显就是一个典型的 <em>烂注意</em> TM，我们仍然坚持认为一张缩略图就是一个特定的图像，我们刚刚接受了位图在低分辨率下够用的想法。苹果公司的 <em>ImageKit</em> （新出的）和 <code>IKImageBrowser View</code> 提供了答案：快速（OpenGL 加速！）、可用的图片视图。很完美吧？</p>
<p>应用程序接口也非常简单，我们需要做的就是提供数据源，然后 API 返回给我们任何图片格式的图片。</p>
<p>哎！可惜结果和理想效果差距甚远，如图 16.4 所示：缩略图生成后，速度就会变的超级快。但是初始的加载却非常缓慢，一张一张渲染缩略图的过程简直辣眼睛。更糟糕的是，打开过程中发生的延迟导致我们无法绘制任何东西。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894867_336459.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_877/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894867_336459.png" alt="image.png"loading="lazy" decoding="async" width="877" height="505" /></picture></figure></div><p>这也导致我们收到了亚马逊客户无爱的评论：</p>
<div class="blockquote"><blockquote><p>如果笔记有数百页之多，每次打开软件的时候都要重新加载缩略图，这也太奇怪了。如果你愿意等那么长的时间，只要软件仍处于打开状态，这些缩略图总会存下来的。但是等的时间也太长了吧！如果这时候你关闭软件，然后重新打开，猜猜会发生什么？你又要重新等待缩略图加载！我不知道这个问题是否出现在 Windows 版本的软件中，不过在 Mac 版里确实出现了。</p>
</blockquote></div>
<p>基本上和之前遇到的问题一样：无法获得 <code>IKImageBrowserView</code> 想要展示的缩略图。我们使用的是大尺寸、共享且渲染缓慢的背景图和每个数据源里的向量数据，必须要将这两者用 <code>IKImageBrowserView</code> 结合在一起，这自然拖慢了进程速度。</p>
<h4>如何绘制非缩略图</h4>
<p>庆幸的是，导致问题发生的架构给出了解决方案。不再作为原子单元和实际图片传递每张缩略图，而是使用古老的 Quartz/AppKit 绘制所有的屏幕元素，使用 <code>ThumbView</code> 类里的 <code>-drawRect:</code> 方法。</p>
<p>与 eXTRASLIDE 图片缩放相似，<code>NSImage</code> 可用于缓存已优化的、屏幕大小分辨率的 PNG 格式背景图片，每台笔记本仅需缓存一次，之后为每张缩略图绘制 <code>NSImage</code> 背景图。这使得绘制背景操作瞬间完成成为可能。</p>
<p>然而，我们还遗留一个问题需要解决：从文件格式中提取笔触数据略微有些耗时。解决方案还是有点取巧嫌疑：不再像过去那样等到所有的笔触数据都可用时才操作，而是在渲染完背景图后立即绘制所有的缩略图，就可以看见需要绘制的<em>一些东西</em>，即便不是最终的图片。</p>
<p>结果如图 16.5 所示，首先绘制所有背景图，同时检索笔触数据。在下一步中，所有的笔触数据都会在同一时间出现。</p>
<p>在屏幕的效果非常动态：缩略图似乎是立即出现了，感觉像是活的可以触摸的对象。原因就是缩略图立即出现然后缓慢的改变，比一个接连一个出现最终完整的缩略图的效果要好得多。该解决方案给用户一种立即响应的感觉，感觉像是正在直接操作屏幕上的项目，而不是等待计算机响应操作。</p>
<p>这种技术类似于苹果要求 iOS 应用程序的启动页快速响应，看似响应及时，“实则因为它出现在界面后，立即被第一个屏幕置换掉了“。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894857_609381.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_877/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894857_609381.png" alt="image.png"loading="lazy" decoding="async" width="877" height="399" /></picture></figure></div><p>想要达到这种效果的关键在于不要将缩略图当做一个独立的单位，活用缩略图（背景图+笔触数据）的结构优势：首先，考虑到背景图绘制缓慢，所以我们一次只绘制一页，不要每个页面都绘制，然后，在等待解码笔触数据的时候可以显示这些背景图。效果对比如图 16.6 所示。</p>
<p>实际上我们不需要更快的图形程序或借助 OpenGL 图形程序接口。事实证明，使用 OpenGL 内部的方法（IKImageBrowserView）实现该需求时速度不增反降，不如使用 Quartz 和 AppKit 进行绘制。通常情况下，数据结构优势远远超过了 API 的消耗。</p>
<h3>iPhone 上绘制直线</h3>
<p>本章 “一切都是假象”一节中讨论的 Livescribe 软件还使用了另外一个方案：称之为“Paper Replay”：可以用笔记录音频和笔画，播放这段音频时，笔画的动画效果会和音频同步浮现。更具体地说，所谓的<em>未来墨迹</em> (即尚未书写的笔记) 呈现灰色，随着音频播放书写过的墨迹显示绿色。该效果可以从手写文本（或图片）处找到，看起来就好像是第一次书写时那样。</p>
<p>对于 Mac 客户端来说，这真的是小菜一碟。直接使用 Quartz 代码绘制笔触，调用 <code>PageView’s drawRect:</code> 方法。对于 Paper Replay 功能，只需添加 <code>time-from</code> 和 <code>time-to</code> 参数（每一笔都有自己的时间戳），然后绘制笔触两遍：设置笔触颜色为灰色，设置 <code>time-to</code> 为录音中的当前时间后，绘制一遍；设置笔触颜色为绿色，设置 <code>time-from</code> 为录音中的当前时间后，再绘制一遍。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894847_314525.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_527/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894847_314525.png" alt="image.png"loading="lazy" decoding="async" width="527" height="681" /></picture></figure></div><p>当我们把这段代码移植到 iPhone 上后，我们决定使用 Core Animation，也是苹果推荐使用的<strong>高效</strong>应用程序接口，毕竟我们用了动画效果。具体来说，用 <code>CATiledLayer</code> 支持缩放功能，<code>CATiledLayer</code> 内部甚至支持多线程，所以速度应该会更快。只可惜凡事都有意外：在复杂的图形上，Instruments 显示每秒只有 3-4 帧，CPU 使用率已达 100%。在动画工程中还要实现重绘，重绘命令也会占用 CPU，使得应用程序彻底瘫痪。Paper Replay 功能没法用了，更别提还会有更复杂的页面内容。</p>
<p>到底做错了什么？我们并非忽略了性能问题，实际上，我们所做的一切都是为了提升性能。为了提高速度，我们甚至研究了笔触的最佳长度，可是不管长度如何，性能上只提升了 10% - 20% ，实在是微不足道。于是我们查看 OpenGL，问题依然萦绕我的思绪，这个问题本不应该这么难解决的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894839_37752.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_893/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894839_37752.png" alt="image.png"loading="lazy" decoding="async" width="893" height="347" /></picture></figure></div><p>当然了，问题的确不难解决，我们只是被技术细节蒙蔽了双眼，却没有真正地思考问题。图 16.7 显示了 Paper Replay 的两帧，学习飞行课程的笔记——两种不同的方式输入空白区。该区域实际上需要重绘，才能让第一张图的矩形所示区域变成第二张图的样子。</p>
<p>笔是由手控制的实体物品，在一定的时间内移动速度是有限的，六十分之一秒内，一帧所移动的距离不可能很远。所以只有屏幕的一小部分需要在两帧之间做出改变，也只有这一小部分需要重绘。</p>
<p>之前一味地关注 Core Animation，从而忽视了这点。Core Animation 只允许一次替换整个 layer 的位图。AppKit 和 UIkit 的视图机制，换句话说，都允许使用 <code>drawRect:</code> 方法只绘制一部分区域（或多个矩形子区域），用 <code>setNeedsDisplayInRect:</code> 让视图中一部分矩形无效。在 iOS 中，这些矩形最终绘制写入作为整个 layer 的位图。</p>
<p>在了解到应该使用 UIKit 而不是 Core Animation 后，我们立即修改了代码。添加了 <code>PageView</code>，放到绘制代码这里，在设置时间的代码里，添加了一些程序，从一组时间中获取更改后的矩形，接着针对特定的几帧让一部分矩形无效。效果非常明显：之前只能每秒处理 3-4 帧，就到达了 CPU 的极限，现在每秒可以处理 60 帧，而 CPU 的使用率只有 2% - 3%，很大程度上独立于页面内容的复杂度。更棒的是，UI 的动画直接从 UI 界面程序，没有了被音频推出来的感觉，所以永远都不会出现音频和动画不同步的情况了。</p>
<h3>总结</h3>
<p>在本章，我们了解了图形性能和响应性。尽管底层绘制性能更容易测量，一直是开发者热衷讨论的话题，我觉得架构模式和特定领域的优化有更深远的影响。实际上，阻止高级优化的底层的技术限制，常常比单纯的低级优化有更深远的影响，在 model-view-controller 通信机制中更为明显，也和我们网络连接的设备更息息相关，因为我们可以快速更改 model 无需用户输入。我们会在下一章中寻找一种更容易理解的解决方案。</p>
]]></content:encoded></item><item><title><![CDATA[第 15 章 图像和 UI：测量和工具]]></title><guid>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-15</guid><link>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-15</link><description><![CDATA[节选自《iOS和macOS性能优化》]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Tue, 02 Aug 2022 06:36:36 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这一部分的文章是我早年间参与<a href="https://book.douban.com/subject/30269356/">iOS和macOS性能优化</a>这本书翻译时的原稿, 当时的水平有限, 翻译的可能会有一些纰漏, 还请多多指教.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<p>正如其他领域的性能一样，如果不清楚导致缓慢的原因，优化图形通常是毫无意义的，更为重要的是你不知道刚才所做的优化对性能是有益还是有害。</p>
<p>在各方面而言，测量图形性能和响应性会比测量其他类型的性能更加困难。一般来说，相关的操作会涉及到一些完全封闭的系统库，未必有权限接触的进程，甚至可能是完全无法感知的硬件。</p>
<p>更重要的是，涉及到图像性能，你需要真正的关心单个事件的时间，而这个时间的数量级会降低到几十毫秒的水平上。在之前的章节中，我们通过测量大量的单个事件，然后进行分割以获得单个事件的的“时长”。这其实并不完全正确，因为获取的实际上是 <em>平均</em> 时长。</p>
<p>正如在第 14 章中所见的，平均数在测量图像时长中的作用非常有限：例如现在有两组数据，第一组数据中，在 1 秒的时间内，每一帧都能在保证在 16.6 ms 后刷新，第二组数据中，同样是在 1 秒的时间内，前 50 帧在 1ms 内完成刷新，而后 10 帧以每帧 90ms 的速度刷新，虽然第二组数据的平均数比第一组数据要好，但是在视觉上很难接受这样的效果，还是第一种方法在视觉上更平滑一些。</p>
<p>幸运的是，我们还是有解决办法的：系统和相应的工具给出了更好的解决方案，可以让开发者直观地观察到是否高效达成了目标。例如，如果你正用同样的值重绘一个像素，这个无效的操作指令就会被标注。</p>
<p>本章将会介绍这些特定的工具以及它们报告出来的信息在整个图形管线（graphics pipeline）中的意义，我们还会说说如何将这些工具和之前介绍的通用工具结合起来使用。</p>
<h3>CPU 分析仪</h3>
<p>在之前的章节中，主要介绍了基于 CPU 绘制图像的 Quartz 框架和基于硬件绘制图像 OpenGL 框架，以及这两个框架在性能上的区别。图 15.1 展示了时间分析仪（time profile）在 Quartz 示例下表现。</p>
<p>时间分析仪里的前 11 个条目包含了开发者自己编写的代码，但可以看到，这些代码只占用了总时间的 3.3%，最后一个开发者编写的代码条目是个闭包，这个闭包在 <code>-[GLBenchView drawOn:inRect:]</code> 中定义，被 <code>-[MPWAbstractContext ingsave:]</code> 方法调用。剩下 96.7% 的运行时间都花在了 Quartz 的函数 <code>CGContextDrawPath()</code> 上。这个看起来很有意思，其中 50% 都花费在了 <code>CGSColorMaskSover ARGB8888()</code> 里，而且很显然支持 SSE 的函数 <code>CGSColor MaskSoverARGB8888_sse()</code> 在这里并没起到什么作用。</p>
<p>事实上，你花了大量的时间绘制路径，也许算是有用的信息，但是它并没有告诉你为什么会这样。是路径太复杂了吗？还是进行了多余的绘制？或者你正在尝试的操作对系统来说太复杂了？如何是后者的话，那我们可以肯定这是不应该发生的，因为即便是 Quartz 也能在理论上以动画帧速率（animation frame rate）填充屏幕上的每个像素点。</p>
<p>当使用硬件加速时，问题更严重，因为现在 CPU 几乎处于闲置状态，只是在等 GPU 的结果，图 15.2 显示了基准程序在同样时间下的数据图，不过这次用的是 OpenGL 代码，用 CPU 进行绘制。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894705_191787.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1077/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894705_191787.png" alt="image.png"loading="lazy" decoding="async" width="1077" height="574" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894695_241357.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1078/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894695_241357.png" alt="image.png"loading="lazy" decoding="async" width="1078" height="616" /></picture></figure></div><p>鉴于 Quartz 示例下，CPU 花费了 2176 ms，而在 OpenGL 示例下，CPU 仅仅花费了6 ms，而这6 ms 中真正用于绘图的代码只占用了 16% 的时间。</p>
<p>当使用硬件协助绘图时，CPU 的分析结果不会告诉你的应用程序瓶颈在哪里。</p>
<h3>Quartz 调试</h3>
<p>在 Mac OS X 上，有个专门用来调试图形性能的工具，叫做 <em>Quartz Debug</em>。图 15.3 显示了其主要菜单选项和帧率表盘。Quartz Debug 可以调试 Mac OS X 图形堆栈中的全局元素。它不仅会检测你的程序，还会检测所有正在运行的程序，包括 Quartz Debug 本身，最好在测试之前关闭或者隐藏其他正在运行的程序。</p>
<p>我个人觉得最有用的选项是 <em>Flash identical screen updates</em>，差不多在菜单的中间位置，开启该选项后，Quartz 会用红色矩形块标注屏幕中重复刷新相同内容的区域，这表示红色区块的绘制操作是多余的，应该被删除。很显然我们没有必要绘制同样的内容。</p>
<p>下一步是 <em>Flash screen updates</em> 设置，这个设置和前面说的选项的很像，不过它会在有更新的界面上闪动一个黄色矩形框。该选项能让你区分出那些刷新次数过于频繁的地方。打开该选项可能会产生一些干扰数据。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894687_466514.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_864/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894687_466514.png" alt="image.png"loading="lazy" decoding="async" width="864" height="740" /></picture></figure></div><p><em>Autoflush drewing</em> 选项会关闭合并内存的访问模式（coalescing），所以每个绘制操作都会直接展示在屏幕上（如果开启前面所说的两个选项，可能会造成闪烁），这会产生更多干扰信息，但是这样会将绘制过程划分的更细，展示的矩形越多，就越能让开发者了解是系统是如何绘制的、以及绘制过程发生了哪些改变。</p>
<p>最后，应该注意随着 Quartz Debug 的运行，应用程序与平常的运行状态有一点区别，绘制这些额外的矩形会造成不小的开销，你甚至可以感觉到屏幕刷新的过程中有一些延迟。开启一个 Flash 选项，然后尝试拖拽窗口，这时不仅能看到有很多的闪烁效果，拖拽的过程也会变得迟钝。关闭延迟能让性能恢复正常，但闪烁会导致肉眼难以识别屏幕上的情况。</p>
<h3>Core Animation 工具</h3>
<p>iOS 有一个更高级的调试工具，这也许是因为 iOS 上的图像架构更复杂，但手机的硬件性能不够强大，所以这就要求调试更加严格和精准。总之，在 iOS 上你需要这些高级调试工具！</p>
<p>最主要的调试工具是 Core Animation，它属于 Instruments 一部分，并非像 Quartz Debug 那样的独立工具。它和 Instruments 集成在一起非常有用，你可以将多种工具结合起来调试，并由此寻找问题的根源。</p>
<p>图 15.4 显示了在开发 Wunderlist 3 时，我们遇到的一个动画性能问题。</p>
<p>测试针对的 iPhone 5s，但在 iPhone 4s 上动画性能下降地更为明显。通过关注性能下降区域，然后切换到 CUP 调试工具上，我们可以弄清楚发生了什么 —— 一段程序反复调用 <code>valueForKeyPath:</code>：进行计算。回顾下第 3 章提到的，使用键值访问比直接访问或发送信息要慢得多。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894677_9831505.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1042/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894677_9831505.png" alt="image.png"loading="lazy" decoding="async" width="1042" height="871" /></picture></figure></div><p>简单的解决方案是日常工作不再使用 <code>valueForKeyPath:</code>方法，而使用循环和发送消息，这样计算速度更快。如果此路不通，还有一个办法，就是延迟计算操作，稍等片刻再计算，在后台线程上执行，而不是主线程上，或者逐步计算。</p>
<h3>当 CPU 不再是问题</h3>
<p>在之前的几个例子中，我们足够幸运，发现问题都出现在 CPU 上，当然这是使用了第 2 章所说的分析工具确定了问题所在。不过要是问题不在 CPU 上怎么办？iOS 的 Core Animation instrument 有一组和 Quartz Debug 工具类似的选项，除了应用广泛，也适用于一些特殊的 iPhone / iPad 环境。</p>
<p>正如在第 14 章中解释的那样，iOS 将最为笨重且低效的位图作为标准，虽然这样通过充分利用 GPU 来抵消性能上的文档，但这也意味着，在遇到大量数据时，数据读取效率低的缺点会格外明显。图 15.5 Core Animation 的选项表里列举出了一些虽然看起来不显眼，但有可能造成性能问题的情况，这些选项的显示结果和 Quartz Debug 的显示结果相似。</p>
<p>具体来说，这个调试工具支持以下功能：</p>
<ul>
<li><strong>Color Blended Layers</strong>——将目标色和来源色混合意味着需要同时读取目标色值和源色值，不混合就意味着能少读取一次数据，工具会将进行混合的图层层用红色标注，不需要混合的图层用绿色标注。</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894665_601733.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_570/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894665_601733.png" alt="image.png"loading="lazy" decoding="async" width="570" height="534" /></picture></figure></div><ul>
<li><strong>Color Hits Green and Misses Red</strong>——这里指的是支持 <code>shouldRasterize</code> 标识的系统缓存。通常状况下 Core Animation 会在每次更新后复制/混合整个视图层级树，当视图设置了 <code>shouldRasterize</code> 标识时，这就意味着它告诉 Core Animation 去缓存该整个视图的栅格图像。然而，系统并不会保留所有视图的栅格图，它是一个全局缓存，在有限的时间内缓存有限数量的位图。</li>
<li><strong>Color Copied Images</strong>——用于标注图像是否能直接被 GPU 使用，或者图像是否需要通过 CPU进行一次 复制/转换（开启选项后，系统会为需要转换的情况标注颜色）。</li>
<li><strong>Color Misaligned Image</strong>——该标记用于优化内存的访问模式。当图像边界以字（word）的方式对齐时，所有的内存只需要访问一个完整的字。当图像没有对齐时，需要读取多个字里的内容进行计算，之后 GPU 会根据计算结果对原始数据进行一些必要的处理。根据 GPU 的智能程度，额外的内存访问可能发生在每个字或者图像的边缘上。不管哪种方式，最好避免不对齐的图像，开启这个选项会标注出没有对齐的图像。</li>
<li><strong>Flash Updated Regions</strong>——这个功能与名为 Quartz Debug 的功能相似：当屏幕上的某个区域改变后，该区域会以独特的颜色进行闪烁。目前我还没有在 iOS 的调试工具中找到与 Quartz Debug 里 “flash identical regions” 功能相似的选项。</li>
<li><strong>Color OpenGL Fast Path Blue</strong>——开启改功能后，调试工具会标注出屏幕上哪些区域需要避免合成，应该直接使用 OpenGL 进行渲染。</li>
<li><strong>Color Offscreen Rendered Yellow</strong>——开启该功能后，调试工具会显示哪些区域是先进行离屏渲染，再将渲染内容复制到屏幕上的，很明显额外的复制操作会存在潜在的性能问题。</li>
</ul>
<p>和 Quartz Debug 需要注意的一样，这些选项会全局影响设备的渲染，而不是仅仅影响正在使用 Instruments 的应用。</p>
<p>为了更形象的描述这些选项，让我们看一下 iPad 的 PostScript/PDF 预览界面的缩略图。图 15.6 显示了看起来正常的缩略图，没有开启颜色标识。滚动的时候会有一点延迟的感觉，所以在这里我们使用刚才所说 debug 选项来找找里面的问题。</p>
<p>图 15.7 显示了开启了 <em>Color Blended Layers</em> 选项后的缩略图。每个单独的缩略图都被 Instruments 工具标红了（受纸质书籍的打印问题，这些红色区域在书中会显示为深灰色），意味着这里使用了混合功能。由于这里完全不需要显示缩略图后的背景，所以这里不应该有 blending 才对。</p>
<p>首当其冲的想法就是 <code>UIImageView</code> 没有被设置成不透明色，所以才有了阴影，阴影需要混合。然而，将视图设置成不透明色、并移除阴影后，结果还是没什么变化，由于使用了混合，所有的缩略图仍然是红色的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894657_12228.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_750/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894657_12228.png" alt="image.png"loading="lazy" decoding="async" width="750" height="1031" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894648_459747.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_745/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894648_459747.png" alt="image.png"loading="lazy" decoding="async" width="745" height="1018" /></picture></figure></div><p>经过检查，问题的原因是缩略图本身包含一个 alpha 通道。 从视觉角度上来说，这个 alpha 通道是不需要的，因为所有的像素都是不透明的，但这并不是 GPU 或者 API 可以理解的，所以它们还是会进行混合（blending）操作，这样就会重新计算目标对象的像素。</p>
<p>这些缩略图是参考 PDF 文件并通过 Core Graphics 位图上下文创建的，所以解决方案是在 <code>CGBitmapContextCreate()</code> 方法中，使用 <code>kCGImageAlphaNoneSkipLast</code>，而不是 <code>kCGImageAlphaPremultiplied Last</code>。使用 <code>kCGImageAlphaNone</code> 似乎是一个显而易见的去掉透明图层的办法，但是它并会不起任何作用，如果这样设置的话，函数在运行时返回了一个错误信息。</p>
<p>图 15.8 显示了解决方案的结果：现在屏幕上所有的东西都成了绿色（这些颜色在纸质书中显示为浅灰色），现在没有图层混合的问题了。但是你会惊奇的发现，性能几乎没有什么提升，不过这就是争取到的最好结果了，毕竟你也没费多少精力，但是优化仍然是值得的。</p>
<p>最后，我还查看了没有对齐的图像（如图 15.9），在图 15.9 显示 app 中没有对齐的缩略图。由于这些图片应该居中且宽度没法控制，所以不太好解决图像对齐的问题，所以就目前来讲，该问题仍未得到优化解决。</p>
<p>如果图像未对齐真的会造成明显的性能问题，那么我们可以修改缩略图的生成过程，让图片的宽度取整，让其接近一个几乎 “对齐安全” 的数值，接着居中绘制实际文档的大号缩略图。然而，这需要让图片的边缘透明才能保证精准还原图片，但这又会导致图层混合问题，从而增大计算开销。当然另外一种选择是在图片还原度上妥协，用小的白色边框绘制缩略图或将其缩放到略微不同的尺寸。</p>
<h3>我在测量什么？</h3>
<p>我们在考虑是使用静态的图像资源，还是使用代码绘制图像。其中一个考量因素就是不同技术的性能表现如何。示例 15.10 是绘制的渐变色效果。</p>
<p>在示例 15.1 的代码中，想绘制渐变效果，要么用 CoreGraphics，要么加载一张预渲染渐变效果的 PNG 或 JPEG 图片文件，并测量一下每种方法的时间。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894638_575657.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_730/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894638_575657.png" alt="image.png"loading="lazy" decoding="async" width="730" height="1012" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894630_912254.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_725/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894630_912254.png" alt="image.png"loading="lazy" decoding="async" width="725" height="1013" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894622_507933.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_321/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894622_507933.png" alt="image.png"loading="lazy" decoding="async" width="321" height="366" /></picture></figure></div><p>示例 15.1 尝试记录加载图像的时间和生成图像的时间</p>
<div class="block-code" data-language="ojjc"><pre><code>-(void)timeImageDrawingAndLoading {
    CGFloat drawnTime, PNGTime, JPGTime;
    CFTimeInterval startTime, endTime;
    startTime = CACurrentMediaTime();
    self.drawnImageView.image = [self drawnImage];
    endTime = CACurrentMediaTime();
    drawnTime += 1000*(endTime - startTime);

    //What Am I Measuring? 321
    startTime = CACurrentMediaTime();
    self.PNGImageView.image = [UIImage imageNamed:@&quot;Image.png&quot;];
    endTime = CACurrentMediaTime();

    PNGTime += 1000*(endTime - startTime);
    SEL flusher = NSSelectorFromString(@&quot;_flushSharedImageCache&quot;);
    [[UIImage class] performSelector:flusher];

    startTime = CACurrentMediaTime();
    self.JPGImageView.image = [UIImage imageNamed:@&quot;Image.jpg&quot;];
    endTime = CACurrentMediaTime();

    JPGTime += 1000*(endTime - startTime);
    [[UIImage class] performSelector:flusher];
    NSLog(@&quot;Drawing %f, PNG %f, JPG %f&quot;, drawnTime, PNGTime, JPGTime);
}</code></pre></div>
<p>测量出来的时间是：绘制用了 11.1 毫秒，PNG 3.89 毫秒，JPG 0.51 毫秒，所以这是一个能够说明问题的例子，不是么？我们可以粗略地解释这些数字：正如在第 14 章中见到的，Core Animation 基于位图，所以绘制是一个额外步骤，然而图片的加载或存储可以简单的理解为是直接从后备存储器（backing storage）中获取的。</p>
<p>先别急着下结论！</p>
<p>因为这些时长是模拟器的测量结果（数值可疑），另外，JPG 的读取时间比 PNG 快了 8 倍？这有点奇怪啊。</p>
<p>首先，打开 Instruments，了解一下大体发生了什么，<code>timeImageDrawing AndLoading</code> 方法中没有图片解码，相反稍后在实际绘制这些视图时，会看到一些 PNG 的解码操作。我没有看到 JPEG 的解码过程，整个过程发生的太快了。这验证了我们对 iOS 中图片加载和解码的理解：它是一种懒加载的模式，只在万不得已才会加载/解码，比如绘图时。</p>
<p>一些书籍声称，当为 <code>UIImageView</code> 的 <code>image</code> 属性赋值或者 <code>CALayer</code> 的 <code>image</code> 属性赋值时，解码是会被强制执行的，不过我在实践中并没有发现这点。</p>
<p>在真机上运行代码得到以下结果：绘制 3.26 毫秒，PMG 67.2 毫秒，JPG 49.1 毫秒。这次，结果反转了，绘制比 PNG 解码快 20 倍，比 JPG 解码快 15 倍。然而，对该结果也令人困惑——因为在这里 iOS 实际上并没有解码图片（通过 Instrument 工具可以确认这点），那么，到底发生了什么？</p>
<p>如果仔细观察一下 Instruments，可以在当前的例子中，JPG、PNG 的“解码”时长是包含了各自解码器的初始时间成本，所以“加载”图片的副本可能会快很多倍：绘制 3.50 毫秒，PNG 2.54 毫秒，JPG 1.91 毫秒。但是这些并不是解码图片的真正时长，现在得到的时长只是从硬盘中读取图片元数据并用于将来解码用。</p>
<p>为了将在视图中绘制图像与解码过程区分出来（当然这很难区分），我们需要在位图上下文中“绘制”图像，使用此方法获得以下时长：绘制 3.41 毫秒，PNG 7.91 毫秒，JPG 8.39 毫秒。这是目前最接近的真实时间，尽管很有点小误差，我们不得不把解码和绘制时间合在了一起。尽管苹果公司声称该操作与纯解码基本上是一样的，但我们还是不能百分之百确定。</p>
<p>另一个异常现象是，尽管现在可以清楚地在 CPU 调试工具中追踪到 PNG 的解码时间，但还是没法追踪 JPEG 的解码时间。运行 CPU Profile Instrument 工具，打开 “Record Waiting Threads” 选项，我们可以看到 JPEG 解码位于 <code>mach_msg_trap()</code> 中，这意味着它在等待一个不会被 CPU 调试工具显示任何信息的进程完成任务。答案很显然是 iPhone 有一个 JPEG 的硬件解码器，使用该解码器时，不会在 CPU 上显示任何信息。</p>
<p>硬件解码器在处理大图片时非常快，但对于小图片而言，这个开销就比较大了，即便针对 PNG 使用相对缓慢的 zlib/flate 解压器，总体速度依然很快，而且类似 TurboJPEG 这样专门的 JPEG 库甚至可以快好几倍。</p>
<h3>总结</h3>
<p>本章展示了在衡量图形性能过程中会遇到各种错综复杂的情况，除了要考虑其他子系统的影响外，实际操作中的延迟也会影响测量结果。获取这些操作的平均值是无法得到有意义的结果。</p>
<p>然而，还是有一些测量的途径的——比如，Mark 1 eyeball 是个非常好的测量装置，特别是当它配备一个性能良好的电子秒表（带有机械按钮的那种大秒表，不是 iPhone 上的那种）时，效果更佳。当我们使用手动测量结果作为性能评估的关键指标是，我们只花费了十分之一秒就诊断出问题所在。</p>
<p>在下一章中，我们将关注如何解决发现的问题。</p>
]]></content:encoded></item><item><title><![CDATA[第 14 章 图像和 UI：原理]]></title><guid>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-14</guid><link>https://swiftsiqi.com/posts/iOS-and-macOS-Performance-Tuning-chapter-14</link><description><![CDATA[节选自《iOS和macOS性能优化》]]></description><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Mon, 01 Aug 2022 06:39:43 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这一部分的文章是我早年间参与<a href="https://book.douban.com/subject/30269356/">iOS和macOS性能优化</a>这本书翻译时的原稿, 当时的水平有限, 翻译的可能会有一些纰漏, 还请多多指教.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<p>在之前的章节中，我们已经学习了不少底层系统性能方面的知识，例如 CPU、内存和 I/O ，现在我们可以将学到的知识应用到搭建一个高性能的用户界面中。这意味着应用程序既能快速绘制用户界面，又能高效响应用户请求。</p>
<h3>响应能力</h3>
<p>当提及高性能的用户界面（high-performance user interface）时，通常也意味着它是一个<em>响应式</em>的界面，也就是说它能快速响应用户的操作。这里的快速响应到底得多快呢？一般说来，就是能多快就多快，不过我们还是设定了一些阈值来区分用户的感知程度。具体的数值请见表 14.1。</p>
<p>下表中最值得关注的两个阈值分别为响应用户操作的 100 毫秒和保证动画流畅所需的 60 Hz/16（2/3）毫秒，如果你不想让用户在向计算机发出指令后等待一段时间才获得反馈，而是希望让用户有一种在屏幕直接操控物体的感觉，那么这两个阈值就显得及其重要。下表提及的 25 Hz（也有说是 30 Hz）也是一个常见的阈值，举个例子来说，它可以应用在模拟电影（analog film，用模拟胶片拍摄的电影，需要进行数字化后才能在电子设备上播放）的重制过程中，如果想让整个电影画面平稳过渡的话，它还依赖模拟媒介提供的集成效果，例如运动模糊等。而对于数字屏幕上的动画，60 Hz 将成为我们的目标。</p>
<p>表 14.1 响应时间阈值表</p>
<div class="block-table"><table><thead>
<tr>
  <th style="text-align:right">响应阈值</th>
  <th style="text-align:left">效果</th>
</tr>
</thead>
<tbody>
<tr>
  <td style="text-align:right">10 秒</td>
  <td style="text-align:left">能够引起用户的注意</td>
</tr>
<tr>
  <td style="text-align:right">1 秒</td>
  <td style="text-align:left">能够保证用户持续关注</td>
</tr>
<tr>
  <td style="text-align:right">100 毫秒</td>
  <td style="text-align:left">能够造成直接操作对象的感觉</td>
</tr>
<tr>
  <td style="text-align:right">40 毫秒（25 Hz）</td>
  <td style="text-align:left">能够将多个帧合成动画</td>
</tr>
<tr>
  <td style="text-align:right">16（2/3）毫秒（60 Hz）</td>
  <td style="text-align:left">能够让动画变得平缓细腻</td>
</tr>
<tr>
  <td style="text-align:right">1 毫秒</td>
  <td style="text-align:left">能够追踪到快速的手势操作</td>
</tr>
</tbody>
</table></div><p>请见<a href="http://www.youtube.com/watch?v=vOvQCPLkPt4">http://www.youtube.com/watch?v=vOvQCPLkPt4</a>. 。</p>
<p>相比之前看到的阈值而言，60-Hz，即 16.67 毫秒，可不是一个多么长的时间。对于一些基本的计算密集型（CPU-bound）操作来说，这个时间是没问题的，但如果你还需要在磁盘搜索上花费 7ms，那你就不得不考虑下花费如何使用剩下的时间来做点其他的事儿，更何况图形系统还要占用一些剩余的时间。</p>
<p>所以 60 HZ 是一个极限值了么？尽管表 14.1 的最后一项不是 60 Hz，但受限于当前的软硬件水平，60 HZ 的确是极限了。虽然 60 HZ 能够保证动画非常流畅，但实际上仍然有提升的可能性。我们举个例子，当你用光笔（pen or stylus）在屏幕上进行绘制或者写作的时候，假设笔尖移动的速度大约是 5 cm/s（大约 2 英寸/每秒），而屏幕在 60 HZ 的刷新率下，暂且认为它会以每 16.7 ms 移动 1 mm 的速度延伸，屏幕渲染出来的“印记”和光笔的笔尖之间有着明显的距离差，这会产生一种“滞后”的感觉。当你在屏幕上拖拽物体的时候，也会产生同样的效果。假如你快速地移动指针设备（手指，鼠标等设备），系统实际上是无法做到精准追踪的效果。</p>
<p>幸运的是，对于鼠标的光标这一类物体实在是太小了，小到以至于它快速移动时，人眼根本无法精准定位，因此它带来的滞后感不易被察觉。但是要拖拽一个比较大的物体时，人眼就能察觉到这种滞后感。在当下科技水平无法从本质上解决这一实际问题时，充分利用人类感知的极限是一种保持响应能力的常见技术手段。</p>
<h3>软件和 API</h3>
<p>在 Mac OS X 和 iOS 中，大部分的人机交互都由高层级的框架完成，如 iOS 上的 UIKit，Mac OS X 上的 AppKit。这些框架读取用户的输入并将其转换成事件传递到程序中，同时它们还负责绘制用户界面。图 14.1 从一个较高的视角对图形相关的 API 进行了分层。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894479_303616.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_510/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894479_303616.png" alt="image.png"loading="lazy" decoding="async" width="510" height="517" /></picture></figure></div><p>这些高层级框架提供了大量现成的控件，如按钮（buttons）、复选框(checkboxes)、表格(tables)和文本编辑器(text editors)。除个别控件外，它们中的绝大多数不光完备性强，还拥有快速响应的能力，所以它们的性能并没有什么问题。</p>
<p>这些控件基于 OS X 的 NSView 类和 iOS 的 UIView，它允许用户扩展视图的层次结构。一个视图（view）表示屏幕上的一个矩形区域，可用drawRect:方法渲染，可处理用户的输入事件。这些高层级框架主要通过 Core Graphic 的 API 进行绘制方面的工作，Core Graphic 有时候也被叫做 Quartz。</p>
<p>另外一个与图形相关的主要 API 是 OpenGL，它主要用于 3D 图形应用中，例如一些 3D 游戏，由于这套 API 拥有对图形加速硬件的访问权限，也常常被其他的 API 所使用。除此之外，还有一些处理图片或者视频的 API，如 Core Image 和 Core Video，以及视频播放相关的 AVFoundation 或 Quick Time X，最新的框架还包括了用于 2D 游戏开发的框架 SpriteKit 和用于处理 3D 图形的高级框架 SceneKit。</p>
<p>在显示列表（display list）被图像系统持有且直接驱动显像管（CRT）成像的时代，<em>保留模式（retained-mode）</em>和<em>即时模式（immediate-mode）</em>的意义在于它们区分了两种不同风格的图形 API。在即时模式中，程序通过主动调用绘图 API 的方式进行渲染。而在保留模式中，程序不会主动调用绘图相关的 API ，而是通过更新 API 创建的对象这种方式来实现渲染的效果。</p>
<p>图 14.2 以三个几何图形为例来阐述二者之间的不同。图表上方是保留模式风格的 API，程序首先生成三个图形，这些图形会以某种数据结构或着数据库的形式被 API 所持有。如果想要改变粉色矩形的坐标，程序必须要记住该矩形，并且告诉 API 要移动该矩形。接着 API 就会担负起刷新屏幕并渲染该矩形到新的位置上的工作。图表下方是即时模式风格的 API，它没有保留模式那么复杂，它只需绘制两遍场景（scence）即可，粉色矩形一次是在旧的坐标上，一次是新的坐标上。</p>
<p>Quartz 和 OpenGL 这两个重要的图形 API 都是即时模式风格的 API，绘图命令可以直接发起并立即执行。虽然 OpenGL 将自己内部的 API 划分为了即时模式和保留模式，但从广义上来说，OpenGL 的 API 应该都是即时模式风格的 API。SceneKit 和 SpriteKit 属于保留模式风格的 API；你需要创建 nodes（节点）并将其添加到场景中，之后的过程中，我们只需要对节点进行操作即可。</p>
<p>乍一看，保留模式风格的 API 似乎更简单一些，尤其在图形对象的层级关系满足应用需求的时候。因此在许多场景中，一个简易的图形编辑器可以是这些 API 轻度封装后的产物。然而，现实中的大多数应用都有特定的应用场景和特定的数据模型，这也就是说通过业务对象模型（domain-model）和相关算法得到的图元也会缺乏共性。在这种情况下，我们可以选择继续抽象相关算法以便它能够处理不同类型的数据模型，也可以选择放弃保留模式，而投入到即时模式的怀抱中。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894470_932163.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1163/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894470_932163.png" alt="image.png"loading="lazy" decoding="async" width="1163" height="853" /></picture></figure></div><p>UIKIt/AppKit 是个混合体：视图本身是保留模式风格的 API，但是绘制的时候使用的是即时模式风格的 API， 想想 <code>drawRect:</code> 方法。在这种混合模式下，你可以定义视图的相关特性并在发出重新绘制视图的命令后，把视图的移动，缩放和相关变换的工作交给系统来完成，但你同样保留了灵活修改视图内容的能力。图 14.2 中的三个几何图形，既可以用三个独立的视图对象表示（保留模式），也可以用 <code>drawRect:</code> 方法统一绘制（即时模式）。</p>
<p>Core Animation 和上面的情况有点相似，我们会在后面用另外一种方式来讨论它。</p>
<h3>Quartz 和 PostScript 图像模型</h3>
<p>Quartz 看起来很像当今计算机环境下的产物，但它的前身可以追溯到施乐公司 Palo Alto 研究中心（PARC）的 Bravo 系统，后来面向打字机的 PostScript 页面描述语言（page description language）和 Quartz 走的很近，借助 NeXT 公司开发的视窗和绘图系统( windowing and drawing system) ，Adobe 公司的 PDF，以及阉割掉编程功能的 PostScript，Quartz 最终演变成了 Mac OS X 的 Quartz 框架。</p>
<p>PDF 和 Quartz 是基于 PostScript 的产物，而 PostScript 这门页面描述语言的一个特性就是它拥有精确定义的图像模型。PostScript 语言操作的对象会被定义成栅格图像（raster image），我们根据其来源将这些被操作的对象分成三种类型的图元：</p>
<ol>
<li>栅格图像</li>
<li>路径（path），filled 类型或 stroked 类型</li>
<li>文本</li>
</ol>
<p>这三种图元可以进行仿射变换，任意裁剪以及着色，在之后的版本中，还可以使用渐变色或其他非常量类型的着色方式。早期图像模型的构成方式决定了在栅格化之后用画家算法（Painter’s Algorithm）着色的结果，这意味着对于某个具体的像素点而言，后添加的图像模型会最终决定其着色方式，不论这个像素上之前的图像模型是什么样子的。Quartz 和当前版本下的 PDF 使用了新的图像构成方式，这种方式具备了 alpha 混合（alpha-blending）的能力，这意味着先添加的图像模型和后添加的图像模型会共同影响最终的生成结果。抗锯齿（anti-aliasing）技术可以看做是 alpha 混合的一种变体，它是将原始图像模型的边缘和其相对应的背景色进行了混合，从而产生抗锯齿的效果。</p>
<p>最重要的是在这种设计模式下，每个像素的信息都可以通过图像模型获取，而不依赖具体的实现方式，苹果公司非常认同并坚持这种方式。</p>
<p>在图像模型中，所有的对象都会被转换为 filled 类型的路径。文本字符通过编码映射转换成字形（glyph），然后再通过字体程序变成 filled 类型的路径。stroked 类型的路径可以理解为由自身轮廓线围成的 filled 类型的路径；路径的顶点样式（cap）和拐点样式（line）也会以几何图形的形式添加进路径中。所有曲线在高分辨率下会被转换成一个个连接着的直线段，之后就是栅格处理了。</p>
<p>平整的栅格图像是由矩形网格组成的，所以栅格图像本身也可以像其他矩形一样进行缩放和旋转。举个例子，我们可以绘制一个编号从 0 到 255 号， 256 像素宽、1 像素高的栅格图形，它的效果与我们绘制一个灰度值介于 0 到 1 之间，共有 256 个矩形的效果一样。事实上，将图像进行栅格化就是一个渐变过程，这种渐变和我们常说的渐变有一些不同：用一个路径（clip path）描绘出需要绘制的形状，将对应的图像绘制到这个形状中。</p>
<p>图 14.3 用 <em>Times Roman</em> 字体的小写字母 a 来演示上面的内容。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894458_931339.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_942/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894458_931339.png" alt="image.png"loading="lazy" decoding="async" width="942" height="457" /></picture></figure></div><p>注意这个转换步骤的每一步都会导致数据量的增加，有时候增长的非常显著。字母本身可以用一个字节表示，例如 ASCII 码中 97 就代表小写字母 a。字形也可以是表明某个特定字体的数字或名称。然而，字母 a 的轮廓有 3 个直线段和 34 个曲线段组成，总计 210 个浮点坐标，将曲线拟合成直线会成倍地增加坐标数量，当然这主要取决于分辨率（在当前的测试环境下会生成 484 个坐标点）。最终，网格化后的字形有 24 point 的尺寸，在 retina 屏幕下会产生大约 2 KB 的黑白栅格数据，或者带有 alpha 信息的 8 KB 的全彩色栅格数据。如果你还想使用 super-sampling 技术进行抗锯齿处理，那么栅格属性还会继续增大。</p>
<p>在现实生活中我们完全可以不这样做，也不应该这样做，否则文本和光栅图形的处理会非常慢。虽然精准的图像模型能够更好的还原图像本身，但这也意味着我们需要能够保存更多信息的图像模型。</p>
<h3>OpenGL</h3>
<p>Mac OS X 平台上的第二个基础图形 API 就是 3D OpenGL API（iOS 上也叫 OpenGL ES）。OpenGL 最早是由 Silicon Graphics 开发出来的，它是一种与语言无关的、跨平台的 3D 图形 API，但是在后来它变成了一个开源标准。远远超越了现在的 PHIGS 开源标准，而其中的原因就是 OpenGL 与 Postscript，Quartz 一样，都是即时模式风格的图形框架，相比较与 PHIGS 这种保留模式风格的图形框架，OpenGL 的使用更加简单。</p>
<p>OpenGL 和 Quartz 支持的图形模式和图元大不相同，OpenGL 可以通过顶点数组（vertex array）实现对多边形(polygon meshe)、折线(polyline)或点云（point cloud）使用。在 OpenGL 中，不能直接使用图像（image）进行渲染，但可以通过在物体表面覆盖纹理贴图（texture）的方式达到同样的效果。另外 OpenGL 不支持文本类型，所以它会将文本转换成多边形或者位图。</p>
<p>尽管绘制图像本身是即时模式的，但 OpenGL 也可以将纹理贴图或顶点数组上传到图形硬件中，以便图形硬件在之后可以多次使用。在 OpenGL 的领域内，这就是保留模式。这将保留模式与另外一种数据被逐步指定的模式区分开来。</p>
<p>OpenGL 有这样一个问题，它自身提供的是一种过程式（procedural）/返回式（call return）的 API，这使得 OpenGL 与更加面向批量处理的现代化图形硬件格格不入。这种格格不入体现在，看起来非常“自然”的 API 并不高效，而高效的 API 看起来非常别扭且容易出错。</p>
<h3>Metal</h3>
<p>针对 OpenGL 与现代化图形硬件格格不入的情况，苹果公司给出了自己的解决方案。那就是 <em>Metal</em> API。与 Khronos 组织的 <em>Vulcan</em> API 非常相似， Metal 是一个面向底层的 API。</p>
<p>在 Metal 的世界中，应用程序不仅管理着命令缓冲区（comman buffer），也负责将缓冲区中的指令发送到 GPU 中，这与让程序直接调用 API 中去更新状态或者绘制视图的方式不太一样。</p>
<h3>图形硬件加速</h3>
<p>尽管计算机图形学中的一些术语，如 “Display List（显示列表）”会让我们回想曾经还使用过向量显示器（vector display）（光束在 CRT 的作用下可以绘制出特定的线段），但事实上，现在所有的显示器都是栅格显示器。栅格显示器定义由图像元素（像素）组成的矩形网格，这就像栅格图像和原始图像的关系。</p>
<p>虽然现代固态显示器经历了诸如 LCD、OLED、等离子屏幕等技术的发展，但最终显示的像素矩阵仍然是由硬件预设：这也就是说在硬件上的每一个独立元素会对应着帧缓冲区（frame buffer）的每一个元素。</p>
<p>针对前面所说的情况，即使考虑到视网膜（retina）屏幕的存在。我们需要提供的像素数量也可以说是相对稳定的，iPhone 屏幕的像素数量在七十万到二百万之间，iPad 屏幕需要的像素数量接近三百万，iPad Pro 或者笔记本电脑的像素数量接近五百万。因此无论应用程序多么复杂，只要在每次刷新的过程中能够处理这么多的像素数，就代表你可以改变整个屏幕的内容。在大 O 表示法中，它的复杂度是 O(k)。</p>
<p>虽然像素的数量是一个常量，我们从算法复杂度的角度上来说可以忽略它的存在，但由于像素的数量非常大，在实际的计算过程中我们还是无法完全忽视它。大多数情况下，绘制图形是当今非服务器型（non-server）计算机中计算量最大的任务。</p>
<p>随着时间的推移，市面上出现了许多能够增强图形计算的硬件架构，最常见的几种如图 14.4 所示。架构（1）中没有任何额外的硬件支持，CPU 在内存中进行绘制，而且内存还会被当做用于刷新屏幕的帧缓冲器。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894448_5669775.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_780/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894448_5669775.png" alt="image.png"loading="lazy" decoding="async" width="780" height="739" /></picture></figure></div><p>CPU 绘图模型（CPU-drawing model）的优点是简单通用。从 Xerox Alto 到 Apple II，以及早期的 Macintosh 和 NeXT 机器，都使用了这个模型。Quartz 就是使用 CPU 绘图模型的一个例子 ，即 Quartz 操作 CPU 在内存中进行绘制。</p>
<p>从几何计算到绘制像素中,图形处理器（graphics processing units, GPUs）可以在绘图的各个过程中发挥作用。在架构（2）中，独立出来的 GPU 和 CPU 一样，都可以直接访问内存，这种架构在二十世纪八九十年代的家用电脑中非常普遍，不过现在已被弃用。</p>
<p>如今，图形硬件的架构大体可以分为两类，一种是架构（3）中的独立显卡模式，GPU 配有独立的显存（VRAM）；另一种是架构（4）中的集成显卡模式，GPU 和 CPU 在同一个芯片上，通过公用总接线口访问同样的 RAM。</p>
<p>特定硬件协助通用硬件更好的完成工作，这种模式似乎看起来有点像历史的重演，但随着这种特定硬件的普及，现在的 GPU 还可以用在 OpenCL 的计算中。例如英特尔的 “Larrabee” 显卡由大量的通用 x86 内核组成。最终，集成显卡的架构会越来越像最初的 CPU 配置，可以看做是前者的一种变体。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894440_6946335.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_793/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894440_6946335.png" alt="image.png"loading="lazy" decoding="async" width="793" height="605" /></picture></figure></div><p>尽管这些架构看起来大同小异，但在性能上还是存在不小的差异。举个例子，如图 14.5 所示，我在 2D 平面下绘制了许多三角形进行测试，测试设备是一台 13 英寸、拥有视网膜显示屏、英特尔 HD Graphics 4000 集成显卡的 MacBook Pro。</p>
<p>首先我尝试用 OpenGL 进行实验，显而易见，我们将用到图形硬件，详情见示例 14.1</p>
<p>示例 14.1 用 OpenGL 进行三角形绘制的基准测试代码</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">drawRect:</span><span class="p">(</span><span class="n">NSRect</span><span class="p">)</span><span class="nv">dirtyRect</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">iterations</span><span class="o">=</span><span class="mi">10000</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glClearColor</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span><span class="w"> </span><span class="mf">0.5</span><span class="p">,</span><span class="w"> </span><span class="mf">0.5</span><span class="p">,</span><span class="w"> </span><span class="mf">0.5</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glEnable</span><span class="w"> </span><span class="p">(</span><span class="n">GL_BLEND</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glBlendFunc</span><span class="w"> </span><span class="p">(</span><span class="n">GL_SRC_ALPHA</span><span class="p">,</span><span class="w"> </span><span class="n">GL_ONE_MINUS_SRC_ALPHA</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glPushMatrix</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glScalef</span><span class="p">(</span><span class="mf">0.4</span><span class="p">,</span><span class="w"> </span><span class="mf">0.4</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="n">i</span><span class="o">&lt;</span><span class="n">iterations</span><span class="p">;</span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="kt">float</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">-0.5</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">i</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">iterations</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="n">glColor4f</span><span class="p">(</span><span class="mf">1.0f</span><span class="p">,</span><span class="w"> </span><span class="mf">0.85f</span><span class="p">,</span><span class="w"> </span><span class="mf">0.35f</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="mf">0.4</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="n">glBegin</span><span class="p">(</span><span class="n">GL_POLYGON</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="n">a</span><span class="o">*=</span><span class="mi">2</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="n">glVertex3f</span><span class="p">(</span><span class="w">  </span><span class="mf">0.0</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w">  </span><span class="mf">0.6</span><span class="p">,</span><span class="w"> </span><span class="mf">0.0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="n">glVertex3f</span><span class="p">(</span><span class="w"> </span><span class="mf">-0.2</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="mf">-0.3</span><span class="p">,</span><span class="w"> </span><span class="mf">0.0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="n">glVertex3f</span><span class="p">(</span><span class="w">  </span><span class="mf">0.2</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="mf">-0.3</span><span class="w"> </span><span class="p">,</span><span class="mf">0.0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="n">glVertex3f</span><span class="p">(</span><span class="w">  </span><span class="mf">0.0</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w">  </span><span class="mf">0.6</span><span class="p">,</span><span class="w"> </span><span class="mf">0.0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="n">glEnd</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glPopMatrix</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">glFinish</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>在执行效率上，示例 14.1 里 OpenGL 的代码比示例 14.2 里 Quartz 的代码快了十多倍，我在一台 2007 年产，拥有独立显卡的 Mac Pro 上看到了明显的对比结果。</p>
<p>示例 14.2 用 Quartz 进行三角形绘制的基准测试代码</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">-(</span><span class="kt">void</span><span class="p">)</span><span class="nf">drawRect:</span><span class="p">(</span><span class="n">NSRect</span><span class="p">)</span><span class="nv">dirtyRect</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="kt">int</span><span class="w"> </span><span class="n">iterations</span><span class="o">=</span><span class="mi">10000</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextRef</span><span class="w"> </span><span class="n">context</span><span class="o">=</span><span class="p">[[</span><span class="n">NSGraphicsContext</span><span class="w"> </span><span class="n">currentContext</span><span class="p">]</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">graphicsPort</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextSetRGBFillColor</span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">,</span><span class="mf">0.5</span><span class="p">,</span><span class="mf">0.5</span><span class="p">,</span><span class="mf">1.0</span><span class="w"> </span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextAddRect</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="n">dirtyRect</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextFillPath</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextScaleCTM</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">frame</span><span class="p">].</span><span class="n">size</span><span class="p">.</span><span class="n">width</span><span class="p">,</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p">[</span><span class="nb">self</span><span class="w"> </span><span class="n">frame</span><span class="p">].</span><span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="w"> </span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextTranslateCTM</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">0.25</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="mf">0.5</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="n">CGContextScaleCTM</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="mf">0.2</span><span class="p">,</span><span class="w"> </span><span class="mf">0.2</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">  </span>
</div><div class="line"><span class="w">  </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="n">i</span><span class="o">&lt;</span><span class="n">iterations</span><span class="p">;</span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kt">float</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w">  </span><span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">i</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">iterations</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextSetRGBFillColor</span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0f</span><span class="p">,</span><span class="w"> </span><span class="mf">0.85f</span><span class="p">,</span><span class="w"> </span><span class="mf">0.35f</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="mf">0.4</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">a</span><span class="o">*=</span><span class="mi">2</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextMoveToPoint</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">0.0</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w">  </span><span class="mf">0.6</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextAddLineToPoint</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">-0.2</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="mf">-0.3</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextAddLineToPoint</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="mf">0.2</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="mf">-0.3</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextClosePath</span><span class="p">(</span><span class="n">context</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">CGContextFillPath</span><span class="p">(</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"> </span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>在使用 Quartz 时，CPU 已经达到了百分之百的利用率，而在使用 OpenGL 时，将对 GPU 操作的指令传递到命令缓冲器（command buffer）后，CPU 就空闲下来，等待这些命令执行完毕。上面两种情况都符合低功耗 CPU 的标准。</p>
<p>GPU 不仅在架构上拥有一定的优势，它还可以在图形计算中充分发挥自身并行计算的能力，这是以芯片上有大量的晶体管为前提做保障的。虽然 CPU 也有相似的晶体管数量，但是 CPU 却并不具备相同的并行能力。</p>
<p>所以，相比于 CPU 设计者需要采取更加精密的方案才能让 CPU 在执行大量串性指令的时候得到小幅的性能提升，而利用额外的硬件资源就会让整体性能的提升近似于线性增长，例如让 GPU 分担一部分的计算量。GPU 和 CPU 在处理串行指令上的区别是造成它两性能差距的主要原因之一（在适当的计算负载下），但是 GPU 的性能提升曲线更为陡峭，意味着 CPU 和 GPU 的性能差距每年都会扩大。</p>
<h3>从 Quartz 到 Core Animation</h3>
<p>Mac OS X 和 iOS 的图像 API 最开始只有基于 CPU 渲染的 Quartz，随后 Quartz Extreme，Quartz GL，以及 Core Animation 等图像框架逐渐加入到这个大家庭中。驱动这些 API 演变的不只是基于 OpenGL 的游戏，还有充分利用现代 GPU 潜能进行系统级别图形渲染的目标。</p>
<p>让 Quartz 利用 GPU 进行绘制的最直接方式就是将 Quartz 的图元映射为 GPU 的命令。这个方案的问题在于图形硬件实现了与 OpenGL 兼容的图元，而 OpenGL 的图元与 Quartz 的图元并不兼容。OpenGL 并没有 filled path 类型的图元；它只有三角形或四边形组成的多边形网络，并没有曲线的概念，与 stroke path 类型的图元相对应的是折线。尽管这种映射是可行的，例如我们将 filled path 转换为多边形，但这种转换的代价相当大。另外，OpenGL 对渲染模型的定义非常松散（loosely），也就是说，这意味着硬件在解释命令方面有很大的回旋余地。这种松散的特性不仅与 Quartz 严格定义的成像模型冲突，也和苹果公司对图形质量的要求相冲突。</p>
<p>然而，有一个图元可以很容易的进行转换，那就是栅格图像（raster image）。有一个名为窗口管理者（Window Mnanager）的隐藏子系统会处理栅格图像，它允许不同进程所拥有的窗口对象可以与单个物理屏幕进行映射，这样每个进程可以只关注自己的窗口对象，无需关注其他进程的操作。</p>
<p>由于窗口管理者的存在，不管是即时模式还是保留模式，Mac OS X 上的每个图形 API 最后都变成了保留模式的 API，具有窗口位图和保留状态，具备保留模式 API 的所有优势：窗口可以随意移动，移动的过程可视化，完全由 Window Manager 控制。这和之前的视窗系统（windowing systems）不同，也不像是 AppKit 或 UIKit 中的视图层级结构，无需客户端代码来重绘显示的部分，因此即使进程不响应，窗口的操作总是非常平滑的。</p>
<p>在 Mac OS X 10.2 引入了 Quartz Extreme 库，这个库为系统添加了一些图形硬件加速的能力。系统中的每个窗口都变成了 OpenGL 里的矩形，窗口的内容（由 Quartz 或其他图形 API 提供）都变成了 OpenGL 的纹理，这些纹理被映射到了相应的矩形中。每个窗口的内容都由合适的 API 进行绘制，然后再使用 OpenGL 和图形硬件将内容整合到位图窗口（bitmap window）中。</p>
<p>这种改变不仅更好地利用了显卡的性能，对于许多操作而言，也完全消除了 CPU 的负载：为了移动窗口，视窗管理者（window server）只需改变矩形的坐标，想要将窗口置前或置后，只需调整矩形的 z 值。</p>
<p>Core Animation 将这种架构从 Window Server 引入到客户端程序中，每个 CALayer 不再是绘制到一个共享的后台存储(shared backing store)里，而是维护自己的栅格图像，然后通过硬件支持的单独进程（iOS 上的渲染服务器）将这些图像组合在一起。其实，每个 CALayer 就像是 Quartz Extreme 中的窗口，我们可以改变 CALayer 的位置、透明度和旋转等属性，通过操作 CALayer 映射的 OpenGL 图元的几何形状，我们也可以将实际的位图组合加载到图形硬件中。</p>
<p>图 14.6 展示了从 Window Server，扩展了 Quartz Extreme 的 Window Server，到 Core Animation 的发展过程。如您所见，硬件加速的增长和图形架构中后置缓冲区（backing buffer）的增长相对应。在图形资源极端的情况下，除了解码这些资源外，根本就没有软件渲染；整个管道的其余部分都是硬件加速的。所谓的开销就是内存，资源的分辨率，以及图片资源解码所需的时间，这可能也是最重要的。</p>
<p>就像独立 Window Server 进程能够让窗口的操作平滑过渡一样，这种架构也可以在任意的客户端达到动画运行平滑的效果。一旦设置完所有的图层，动画就会独立于调用程序而运行，它会在一个单独的进程中执行，并且能够得到硬件的支持。</p>
<p>这是一种极其不明显的性能优化方法，因为该方法的基础是处理最宝贵的图元，即栅格图像。栅格图像使用了大量的内存和内存带宽，并用于最后的图像合成。在其他条件相同的情况下，Core Animation 的架构会导致速度变慢，但是实际上其他的条件不可能相同，因为这些操作可以从图形硬件中获得不少好处。</p>
<p>另外，我们在简单性和相应的可预测性上获得了收益，使得操作可以单独执行，只需给图形硬件发送指令流即可。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304894422_177248.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1039/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304894422_177248.png" alt="image.png"loading="lazy" decoding="async" width="1039" height="873" /></picture></figure></div><p>大大减少了 CPU 的工作，将不同的层复合在一起，为这些组合创建动画空间。实际上动画由独立的进程控制，在高性能的 GPU 上执行，这意味着比起客户端程序，该系统可以保证更高的平滑度和性能。</p>
<p>性能保证了动画可以成为用户体验的核心，除了能够让用户体验更加“立体”和真实之外，动画还能极大地提高感知响应性。</p>
<p>如果用户的操作触发了一个动画，即使系统没有给出最终的结果，用户也会认为这个应用是具有响应性的。只要找到合适的动画效果，这个动画所花费的时间都可用于处理用户的操作。</p>
<h3>总结</h3>
<p>在本章中，我们主要了解了应用程序响应用户操作的基本心理学，学习了图形编程的特点和需要权衡的问题，接着我们还了解了图形硬件和 Mac OS X 以及之后 iOS 的图像 API 的协同进化史。</p>
<p>这种协同进化给我们留下了许多的关于图形编程的方法和需要权衡的地方，我们将在后面的章节进行详细的探讨。</p>
]]></content:encoded></item><item><title><![CDATA[从预编译的角度理解 Swift 与 Objective-C 及混编机制]]></title><guid>https://swiftsiqi.com/posts/objective-c-and-swift-from-precompile</guid><link>https://swiftsiqi.com/posts/objective-c-and-swift-from-precompile</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 19 Jun 2022 06:16:58 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这篇文章是我早年在美团技术博客上发布的一篇文章, 部分内容可能已经过时, 请开发者注意.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<h2>TL;DR</h2>
<p>文章涉及面广，篇幅长，阅读完需要耗费一定的时间与精力，如果你带有较为明确的阅读目的，可以参考以下建议完成阅读：</p>
<ul>
<li>如果你对预编译的理论知识已经了解，可以直接从【原来它是这样的】的章节开始进行阅读，这会让你对预编译有一个更直观的了解。</li>
<li>如果你对 search path 的工作机制感兴趣，可以直接【关于第一个问题】的章节阅读，这会让你更深刻，更全面的了解到它们的运作机制，</li>
<li>如果您对 Xcode Phases 里的 Header 的设置感到迷惑，可以直接从【揭开 Public，Private，Project 的真实面目】阅读，这会让你理解为什么说 Private 并不是真正的私有头文件</li>
<li>如果你想了解如何通过 hmap 技术提升编译速度，可以从关于【基于 hmap 优化 Search Path 的策略】的章节开始阅读，这会给你提供一种新的编译加速思路。</li>
<li>如果你想了解如何通过 VFS 技术进行 Swift 产物的构建，可以从 【关于第二个问题】开始阅读，这会让你理解如何用另外一种提升构建 Swift 产物的效率。</li>
<li>如果你想了解 Swift 和 Objective-C 是如何找寻方法声明的，可以从 【Swift 来了】的章节阅读，这会让你从原理上理解混编的核心思路和解决方案。</li>
</ul>
<h2>概述</h2>
<p>随着 Swift 的发展，国内的技术社区出现了一些关于如何实现 Swift 与 Objective-C 混编的文章，这些文章的主要内容还是围绕着指导开发者进行各种操作来实现混编的效果，例如在 Build Setting 中开启某个选项，在 podspec 中增加某个字段，鲜有文章对这些操作背后的工作机制做剖析，大部分核心概念也都是一笔带过。</p>
<p>正是因为这种现状，很多开发者在面对与预期不符的行为时，又或者各种奇怪报错时，会无从下手，而这也是由于对其工作原理不够了解所导致的。</p>
<p>笔者自身在美团平台负责 CI/CD 相关的工作，这其中也包含了 Objective-C 与 Swift 混编的内容，出于让更多开发者能够进一步理解混编工作机制的目的，笔者编写了这篇技术文章。</p>
<p>该文章从预编译的基础知识入手，由浅至深的介绍了 Objective-C 和 Swift 的工作机制，并通过这些机制来解释混编项目中使用到的技术和各种参数的作用，由此来指导开发者如何进行混编。</p>
<p>好了废话不多说，我们开始吧！</p>
<h2>预编译知识指北</h2>
<h3><code>#import</code> 的机制和缺点</h3>
<p>在我们使用某些系统组件的时候，我们通常会写出如下形式的代码：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;UIKit/UIKit.h&gt;</span>
</div></code></pre></div>
</div>
<p><code>#import</code> 其实是 <code>#include</code> 语法的微小创新，它们在本质上还是十分接近的。<code>#include</code> 做的事情其实就是简单的复制粘贴，将目标 <code>.h</code> 文件中的内容一字不落地拷贝到当前文件中，并替换掉这句 <code>#include</code>，而 <code>#import</code> 实质上做的事情和 <code>#include</code> 是一样的，只不过它还多了一个能够避免头文件重复引用的能力而已。</p>
<p>为了更好的理解后面的内容，我们这里需要展开说一下它到底是如何运行的？</p>
<p>从最直观的角度来看：</p>
<p>假设在 <code>MyApp.m</code> 文件中，我们 <code>#import</code> 了 <code>iAd.h</code> 文件，编译器解析此文件后，开始寻找 iAd 包含的内容（<code>ADInterstitialAd.h</code>，<code>ADBannerView.h</code>），及这些内容包含的子内容（<code>UIKit.h</code>，<code>UIController.h</code>，<code>UIView.h</code>，<code>UIResponder.h</code>），并依次递归下去，最后，你会发现 <code>#import &lt;iAd/iAd.h&gt;</code> 这段代码变成了对不同 SDK 的头文件依赖。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898268_28497.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898268_28497.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898268_28497.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898268_28497.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898268_28497.png" alt="image.png"loading="lazy" decoding="async" width="1972" height="864" /></picture></figure></div><p>如果你觉得听起来有点费劲，或者似懂非懂，我们这里可以举一个更加详细的例子，不过请记住，对于 C 语言的预处理器而言， <code>#import</code> 就是一种特殊的复制粘贴。</p>
<p>结合前面提到的内容，在 AppDelegate 中添加 <code>iAd.h</code>：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;iAd/iAd.h&gt;</span>
</div><div class="line"><span class="k">@implementation</span> <span class="nc">AppDelegate</span><span class="w"></span>
</div><div class="line"><span class="c1">//...</span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>然后编译器会开始查找 <code>iAd/iAd.h</code> 到底是哪个文件且包含何种内容，假设它的内容如下：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cm">/* iAd/iAd.h */</span><span class="w"></span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADBannerView.h&gt;</span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADBannerView_Deprecated.h&gt;</span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADInterstitialAd.h&gt;</span>
</div></code></pre></div>
</div>
<p>在找到上面的内容后，编译器将其复制粘贴到 AppDelegate 中：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;iAd/ADBannerView.h&gt;</span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADBannerView_Deprecated.h&gt;</span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADInterstitialAd.h&gt;</span>
</div><div class="line">
</div><div class="line"><span class="k">@implementation</span> <span class="nc">AppDelegate</span><span class="w"></span>
</div><div class="line"><span class="c1">//...</span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>现在，编译器发现文件里有 3 个 <code>#import</code> 语句 了，那么就需要继续寻找这些文件及其相应的内容，假设 <code>ADBannerView.h</code> 的内容如下：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cm">/* iAd/ADBannerView.h */</span><span class="w"></span>
</div><div class="line"><span class="k">@interface</span> <span class="bp">ADBannerView</span> : <span class="bp">UIView</span><span class="w"></span>
</div><div class="line"><span class="k">@property</span><span class="w"> </span><span class="p">(</span><span class="k">nonatomic</span><span class="p">,</span><span class="w"> </span><span class="k">readonly</span><span class="p">)</span><span class="w"> </span><span class="n">ADAdType</span><span class="w"> </span><span class="n">adType</span><span class="p">;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">id</span><span class="p">)</span><span class="nf">initWithAdType:</span><span class="p">(</span><span class="n">ADAdType</span><span class="p">)</span><span class="nv">type</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="cm">/* ... */</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>那么编译器会继续将其内容复制粘贴到 AppDelegate 中，最终变成如下的样子：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">@interface</span> <span class="bp">ADBannerView</span> : <span class="bp">UIView</span><span class="w"></span>
</div><div class="line"><span class="k">@property</span><span class="w"> </span><span class="p">(</span><span class="k">nonatomic</span><span class="p">,</span><span class="w"> </span><span class="k">readonly</span><span class="p">)</span><span class="w"> </span><span class="n">ADAdType</span><span class="w"> </span><span class="n">adType</span><span class="p">;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">id</span><span class="p">)</span><span class="nf">initWithAdType:</span><span class="p">(</span><span class="n">ADAdType</span><span class="p">)</span><span class="nv">type</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="cm">/* ... */</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADBannerView_Deprecated.h&gt;</span>
</div><div class="line"><span class="cp">#import &lt;iAd/ADInterstitialAd.h&gt;</span>
</div><div class="line">
</div><div class="line"><span class="k">@implementation</span> <span class="nc">AppDelegate</span><span class="w"></span>
</div><div class="line"><span class="c1">//...</span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>这样的操作会一直持续到整个文件中所有 <code>#import</code> 指向的内容被替换掉，这也意味着 <code>.m</code> 文件最终将变得极其的冗长。</p>
<p>虽然这种机制看起来是可行的，但它有两个比较明显的问题：健壮性和拓展性。</p>
<h4>健壮性</h4>
<p>首先这种编译模型会导致代码的健壮性变差！</p>
<p>这里我们继续采用之前的例子，在 AppDelegate 中定义 <code>readonly</code> 为 <code>0x01</code>，而且这个定义的声明在 <code>#import</code> 语句之前，那么此时又会发生什么事情呢？</p>
<p>编译器同样会进行刚才的那些复制粘贴操作，但可怕的是，你会发现那些在属性声明中的 <code>readonly</code> 也变成了 <code>0x01</code>，而这会触发编译器报错！</p>
<div class="block-code" data-language="objkc"><pre><code>@interface ADBannerView : UIView
@property (nonatomic, 0x01) ADAdType adType;

- (id)initWithAdType:(ADAdType)type

/* ... */
@end

@implementation AppDelegate
//...
@end</code></pre></div>
<p>面对这种错误，你可能会说它是开发者自己的问题。</p>
<p>确实，通常我们都会在声明宏的时候带上固定的前缀来进行区分。但生活里总是有一些意外，不是么？</p>
<p>假设某个人没有遵守这种规则，那么在不同的引入顺序下，你可能会得到不同的结果，对于这种错误的排查，还是挺闹心的，不过这还不是最闹心的，因为还有动态宏的存在，心塞 ing。</p>
<p>所以这种靠遵守约定来规避问题的解决方案，并不能从根本上解决问题，这也从侧面反应了编译模型的健壮性是相对较差的。</p>
<h4>拓展性</h4>
<p>说完了健壮性的问题，我们来看看拓展性的问题。</p>
<p>Apple 公司对它们的 Mail App 做过一个分析，下图是 Mail 这个项目里所有 <code>.m</code> 文件的排序，横轴是文件编号排序，纵轴是文件大小。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896781_096739.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896781_096739.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896781_096739.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896781_096739.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896781_096739.png" alt="image.png"loading="lazy" decoding="async" width="2468" height="1598" /></picture></figure></div><p>可以看到这些由业务代码构成的文件大小的分布区间很广泛，最小可能有几 kb，最大的能有 200+ kb，但总的来说，可能 90% 的代码都在 50kb 这个数量级之下，甚至更少。</p>
<p>如果我们往该项目的某个核心文件（核心文件是指其他文件可能都需要依赖的文件）里添加了一个对 <code>iAd.h</code> 文件的引用，对其他文件意味着什么呢？</p>
<div class="blockquote"><blockquote><p>这里的核心文件是指其他文件可能都需要依赖的文件</p>
</blockquote></div>
<p>这意味着其他文件也会把 <code>iAd.h</code> 里包含的东西纳入进来，当然，好消息是，iAd 这个 SDK 自身只有 25KB 左右的大小。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898237_560746.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898237_560746.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898237_560746.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898237_560746.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898237_560746.png" alt="image.png"loading="lazy" decoding="async" width="2468" height="1598" /></picture></figure></div><p>但你得知道 iAd 还会依赖 UIKit 这样的组件，这可是个 400KB+ 的大家伙</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898226_305031.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898226_305031.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898226_305031.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898226_305031.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898226_305031.png" alt="image.png"loading="lazy" decoding="async" width="2468" height="1598" /></picture></figure></div><p>所以，怎么说呢？</p>
<p>在 Mail App 里的所有代码都需要先涵盖这将近 425KB 的头文件内容，即使你的代码只有一行 <code>hello world</code>。</p>
<p>如果你认为这已经让人很沮丧的话，那还有更打击你的消息，因为 UIKit 相比于 macOS 上的 Cocoa 系列大礼包，真的小太多了，Cocoa 系列大礼包可是 UIKit 的 29 倍……</p>
<p>所以如果将这个数据放到上面的图表中，你会发现真正的业务代码在 file size 轴上的比重真的太微不足道了。</p>
<p>所以这就是拓展性差带来的问题之一！</p>
<p>很明显，我们不可能用这样的方式引入代码，假设你有 M 个源文件且每个文件会引入 N 个头文件，按照刚才的解释，编译它们的时间就会是 M * N，这是非常可怕的！</p>
<div class="blockquote"><blockquote><p>备注：文章里提到的 iAd 组件为 25KB，UIKit 组件约为 400KB， macOS 的 Cocoa 组件是 UIKit 的 29 倍等数据，是 WWDC 2013 Session 404 Advances in Objective-C 里公布的数据，随着功能的不断迭代，以现在的眼光来看，这些数据可能已经偏小，在 WWDC 2018 Session 415 Behind the Scenes of the Xcode Build Process 中提到了 Foundation 组件，它包含的头文件数量大于 800 个，大小已经超过 9MB。</p>
</blockquote></div>
<h3>PCH(PreCompiled Header)是一把双刃剑</h3>
<p>为了优化前面提到的问题，一种折中的技术方案诞生了，它就是 PreCompiled Header。</p>
<p>我们经常可以看到某些组件的头文件会频繁的出现，例如 UIKit，而这很容易让人联想到一个优化点，我们是不是可以通过某种手段，避免重复编译相同的内容呢？</p>
<p>而这就是 PCH 为预编译流程带来的改进点！</p>
<p>它的大体原理就是，在我们编译任意 <code>.m</code> 文件前, 编译器会先对 PCH 里的内容进行预编译，将其变为一种二进制的中间格式缓存起来，便于后续的使用。当开始编译 <code>.m</code> 文件时，如果需要 PCH 里已经编译过的内容，直接读取即可，无须再次编译。</p>
<p>虽然这种技术有一定的优势，但实际应用起来，还存在不少的问题。</p>
<p>首先，它的维护是有一定的成本的，对于大部分历史包袱沉重的组件来说，将项目中的引用关系梳理清楚就十分麻烦，而要在此基础上梳理出合理的 PCH 内容就更加麻烦，同时随着版本的不断迭代，哪些头文件需要移出 PCH，哪些头文件需要移进 PCH 将会变得越来越麻烦。</p>
<p>其次，PCH 会引发命名空间被污染的问题，因为 PCH 引入的头文件会出现在你代码中的每一处，而这可能会是多于的操作，比如 iAd 应当出现在一些与广告相关的代码中，它完全没必要出现在帮助相关的代码中（也就是与广告无关的逻辑），可是当你把它放到 PCH 中，就意味组件里的所有地方都会引入 iAd 的代码，包括帮助页面，这可能并不是我们想要的结果！</p>
<div class="blockquote"><blockquote><p>如果你想更深入的了解 PCH 的黑暗面，建议阅读 <a href="https://qualitycoding.org/precompiled-header/">4 Ways Precompiled Headers Cripple Your Code</a> ，里面已经说得相当全面和透彻。</p>
</blockquote></div>
<p>所以 PCH 并不是一个完美的解决方案，它能在某些场景下提升编译速度，但也有缺陷！</p>
<h3>Clang Module 的来临</h3>
<p>为了解决前面提到的问题，Clang 提出了 module 的概念，关于它的介绍可以在 <a href="https://clang.llvm.org/docs/Modules.html">Clang 官网</a> 上找到。</p>
<p>简单来说，你可以把它理解为一种对组件的描述，包含了对接口（API）和实现（dylib/a）的描述，同时 module 的产物是被独立编译出来的，不同的 module 之间是不会影响的。</p>
<p>在实际编译之时，编译器会创建一个全新的空间，用它来存放已经编译过的 module 产物。如果在编译的文件中引用到某个 module 的话，系统将优先在这个列表内查找是否存在对应的中间产物，如果能找到，则说明该文件已经被编译过，则直接使用该中间产物，如果没找到，则把引用到的头文件进行编译，并将产物添加到相应的空间中以备重复使用。</p>
<p>在这种编译模型下，被引用到的 module 只会被编译一次，且在运行过程中不会相互影响，这从根本上解决了健壮性和拓展性的问题。</p>
<p>module 的使用并不麻烦，同样是引用 iAd 这个组件，你只需要这样写即可。</p>
<div class="block-code"><pre><code>@import iAd;</code></pre></div>
<p>在使用层面上，这将等价于以前的 <code>#import &lt;iAd/iAd.h&gt;</code> 语句，但是会使用 clang module 的特性加载整个 iAd 组件。如果只想引入特定文件（比如 <code>ADBannerView.h</code>），原先的写法是 <code>#import &lt;iAd/ADBannerView.h.h&gt;</code>，现在可以写成：</p>
<div class="block-code"><pre><code>@import iAd.ADBannerView;</code></pre></div>
<p>通过这种写法会将 iAd 这个组件的 API 导入到我们的应用中，同时这种写法也更符合语义化（semanitc import）。</p>
<p>虽然这种引入方式和之前的写法区别不大，但它们在本质上还是有很大程度的不同，Module 不会“复制粘贴”头文件里的内容，也不会让 <code>@import</code> 所暴露的 API 被开发者本地的上下文篡改，例如前面提到的 <code>#define readonly 0x01</code>。</p>
<p>此时，如果你觉得前面关于 clang module 的描述还是太抽象，我们可以再进一步去探究它工作原理， 而这就会引入一个新的概念 – modulemap。</p>
<p>不论怎样，module 只是一个对组件的抽象描述罢了，而 modulemap 则是这个描述的具体呈现，它对框架内的所有文件进行了结构化的描述，下面是 UIKit 的 modulemap 文件</p>
<div class="block-code"><pre><code>framework module UIKit {
  umbrella header &quot;UIKit.h&quot;
  module * {export *}
  link framework &quot;UIKit&quot;
}</code></pre></div>
<p>这个 module 定义了组件的 umbrella header 文件（UIKit.h），需要导出的子 module（所有），以及需要 link 的框架名称（UIKit），正是通过这个文件，让编译器了解到 Module 的逻辑结构与头文件结构的关联方式！</p>
<p>可能又有人会好奇，为什么我从来没看到过 <code>@import</code> 的写法呢?</p>
<p>这是因为 Xcode 的编译器能够将符合某种格式的 <code>#import</code> 语句自动转换成 module 识别的 <code>@import</code> 语句，从而避免了开发者的手动修改。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898203_259308.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1360/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898203_259308.png" alt="image.png"loading="lazy" decoding="async" width="1360" height="260" /></picture></figure></div><p>唯一需要开发者完成的就是开启相关的编译选项。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898193_405789.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1572/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898193_405789.png" alt="image.png"loading="lazy" decoding="async" width="1572" height="570" /></picture></figure></div><p>对于上面的编译选项，需要开发者注意的是:</p>
<p><code>Apple Clang - Language - Modules</code> 里 <code>Enable Module</code> 选项是指引用系统库的的时候，是否采用 module 的形式。</p>
<p>而 <code>Packaing</code> 里的 <code>Defines Module</code> 是指开发者编写的组件是否采用 module 的形式。</p>
<p>说了这么多，我想你应该对 <code>#import</code>， <code>pch</code>， <code>@import</code> 有了一定的概念。当然，如果我们深究下去，可能还会有如下的疑问：</p>
<ul>
<li>对于未开启 clang module 特性的组件，clang 是通过怎样的机制查找到头文件的呢？在查找系统头文件和非系统头文件的过程中，有什么区别么？</li>
<li>对于已开启 clang module 特性的组件，clang 是如何决定编译当下组件的 module 呢？另外构建的细节又是怎样的，以及如何查找这些 module 的？还有查找系统的 module 和非系统的 module 有什么区别么？</li>
</ul>
<p>为了解答这些问题，我们不妨先动手实践一下，看看上面的理论知识在现实中的样子。</p>
<h2>原来它是这样的</h2>
<p>在前面的章节中，我们将重点放在了原理上的介绍，而在在这个章节中，我们将动手看看这些预编译环节的实际样子。</p>
<h3><code>#import</code> 的样子</h3>
<p>假设我们的源码样式如下：</p>
<div class="block-code"><pre><code>#import &quot;SQViewController.h&quot;
#import &lt;SQPod/ClassA.h&gt;

@interface SQViewController ()
@end

@implementation SQViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [ClassA new];
    NSLog(@&quot;%@&quot;, a);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end</code></pre></div>
<p>想要查看代码预编译后的样子，我们可以在 <code>Navigate to Related Items</code> 按钮中找到 <code>Preprocess</code> 选项</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898178_983732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898178_983732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898178_983732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898178_983732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898178_983732.png" alt="image.png"loading="lazy" decoding="async" width="1918" height="1320" /></picture></figure></div><p>既然知道了如何查看预编译后的样子，我们不妨看看代码在使用 <code>#import</code>, PCH 和 <code>@import</code> 后，到底会变成什么样子？</p>
<p>这里我们假设被引入的头文件，即 ClassA 中的内如如下：</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">@interface</span> <span class="nc">ClassA</span> : <span class="bp">NSObject</span><span class="w"></span>
</div><div class="line"><span class="k">@property</span><span class="w"> </span><span class="p">(</span><span class="k">nonatomic</span><span class="p">,</span><span class="w"> </span><span class="k">strong</span><span class="p">)</span><span class="w"> </span><span class="bp">NSString</span><span class="w"> </span><span class="o">*</span><span class="n">name</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">sayHello</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>通过 preprocess 可以看到代码大致如下，这里为了方便展示，将无用代码进行了删除。这里记得要将 Build Setting 中 Packaging 的 <code>Define Module</code> 设置为 NO，因为其默认值为 YES，而这会导致我们开启 clang module 特性。</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">@import</span><span class="w"> </span><span class="n">UIKit</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="k">@interface</span> <span class="nc">SQViewController</span> : <span class="bp">UIViewController</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="k">@interface</span> <span class="nc">ClassA</span> : <span class="bp">NSObject</span><span class="w"></span>
</div><div class="line"><span class="k">@property</span><span class="w"> </span><span class="p">(</span><span class="k">nonatomic</span><span class="p">,</span><span class="w"> </span><span class="k">strong</span><span class="p">)</span><span class="w"> </span><span class="bp">NSString</span><span class="w"> </span><span class="o">*</span><span class="n">name</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">sayHello</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="k">@interface</span> <span class="nc">SQViewController</span><span class="w"> </span><span class="p">()</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="k">@implementation</span> <span class="nc">SQViewController</span><span class="w"></span>
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">viewDidLoad</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">[</span><span class="nb">super</span><span class="w"> </span><span class="n">viewDidLoad</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">ClassA</span><span class="w"> </span><span class="o">*</span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">ClassA</span><span class="w"> </span><span class="n">new</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">NSLog</span><span class="p">(</span><span class="s">@&quot;%@&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">a</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">didReceiveMemoryWarning</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">[</span><span class="nb">super</span><span class="w"> </span><span class="n">didReceiveMemoryWarning</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="k">@end</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>这么一看，<code>#import</code> 的作用还就真的是个 copy &amp; write。</p>
<h3><code>pch</code> 的真容</h3>
<p>对于 CocoaPods 默认创建的组件，一般都会关闭 PCH 的相关功能，例如笔者创建的 SQPod 组件，它的 <code>Precompile Prefix Header</code> 功能默认值为 NO。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898163_947482.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1332/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898163_947482.png" alt="image.png"loading="lazy" decoding="async" width="1332" height="214" /></picture></figure></div><p>为了查看预编译的效果，我们将 <code>Precompile Prefix Header</code> 的值改为 YES，并编译整个项目，通过查看 build log，我们可以发现相比于 NO 的状态，在编译的过程中，增加了一个步骤，即 <code>Precompile SQPod-Prefix.pch</code> 的步骤。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898156_270108.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1596/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898156_270108.png" alt="image.png"loading="lazy" decoding="async" width="1596" height="1904" /></picture></figure></div><p>通过查看这个命令的 <code>-o</code> 参数，我们可以知道其产物是名为 <code>SQPod-Prefix.pch.gch</code> 的文件</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898147_731665.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898147_731665.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898147_731665.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898147_731665.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898147_731665.png" alt="image.png"loading="lazy" decoding="async" width="1698" height="466" /></picture></figure></div><p>这个文件就是 PCH 预编译后的产物，同时在编译真正的代码时，会通过 <code>-include</code> 参数将其引入</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898140_810722.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1596/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898140_810722.png" alt="image.png"loading="lazy" decoding="async" width="1596" height="1512" /></picture></figure></div><h3>又见 clang module</h3>
<p>在开启 Define Module 后，系统会为我们自动创建相应的 modulemap 文件，这一点可以在 Build Log 中查找到</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898130_837304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898130_837304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898130_837304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898130_837304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898130_837304.png" alt="image.png"loading="lazy" decoding="async" width="1618" height="206" /></picture></figure></div><p>它的内容如下：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">framework module SQPod <span class="o">{</span>
</div><div class="line">  umbrella header <span class="s2">&quot;SQPod-umbrella.h&quot;</span>
</div><div class="line">
</div><div class="line">  <span class="nb">export</span> *
</div><div class="line">  module * <span class="o">{</span> <span class="nb">export</span> * <span class="o">}</span>
</div><div class="line"><span class="o">}</span>
</div></code></pre></div>
</div>
<p>当然，如果系统自动生成的 modulemap 并不能满足你的诉求，我们也可以使用自己创建的文件，此时只需要在 Build Setting 的 Module Map File 选项中填写好文件路径，相应的 clang 命令参数是 <code>-fmodule-map-file</code>。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898120_051663.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1314/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898120_051663.png" alt="image.png"loading="lazy" decoding="async" width="1314" height="242" /></picture></figure></div><p>最后让我们看看 module 编译后的产物形态。</p>
<p>这里我们构建一个名为 SQPod 的 module ，将它提供给名为 Example 的工程使用，通过查看 <code>-fmodule-cache-path</code> 的参数，我们可以找到 module 的缓存路径</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898087_357327.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1572/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898087_357327.png" alt="image.png"loading="lazy" decoding="async" width="1572" height="1640" /></picture></figure></div><p>进入对应的路径后，我们可以看到如下的文件</p>
<p><a class="img-link" href="https://www.sketchk.xyz/2022/08/29/precompile-for-objective-c-and-swift/15.jpg"><img src="https://www.sketchk.xyz/2022/08/29/precompile-for-objective-c-and-swift/15.jpg" alt="IMAGE" /></a></p>
<p>其中后缀名为 <code>pcm</code> 的文件就是构建出来的二进制中间产物。</p>
<p>现在，我们不仅知道了预编译的基础理论知识，也动手查看了预编译环节在真实环境下的产物，现在我们要开始解答之前提到的两个问题了！</p>
<h2>打破砂锅问到底</h2>
<h3>关于第一个问题</h3>
<div class="blockquote"><blockquote><p>对于未开启 clang module 特性的组件，clang 是通过怎样的机制查找到头文件的呢？在查找系统头文件和非系统头文件的过程中，有什么区别么？</p>
</blockquote></div>
<p>在早期的 clang 编译过程中，头文件的查找机制还是基于 header search path 的，这也是大多数人所熟知的工作机制，所以我们不做赘述，只做一个简单的回顾。</p>
<p>header seach path 是构建系统提供给编译器的一个重要参数，它的作用是在编译代码的时候，为编译器提供了查找相应头文件路径的信息，通过查阅 Xcode 的 Build System 信息，我们可以知道相关的设置有三处 header search path，system header search path，user header search path。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896475_719088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896475_719088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896475_719088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896475_719088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896475_719088.png" alt="image.png"loading="lazy" decoding="async" width="2232" height="504" /></picture></figure></div><p>它们的区别也很简单，system header search path 是针对系统头文件的设置，通常代指 <code>&lt;&gt;</code> 方式引入的文件，user header search path 则是针对非系统头文件的设置，通常代指 <code>&quot;&quot;</code> 方式引入的文件，而 header search path 并不会有任何限制，它普适于任何方式的头文件引用。</p>
<p>听起来好像很复杂，但关于引入的方式，无非是以下四种形式:</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;A/A.h&gt;</span>
</div><div class="line"><span class="cp">#import &quot;A/A.h&quot;</span>
</div><div class="line"><span class="cp">#import &lt;A.h&gt;</span>
</div><div class="line"><span class="cp">#import &quot;A.h&quot;</span>
</div></code></pre></div>
</div>
<p>我们可以两个维度去理解这个问题，一个是引入的符号形式，另一个是引入的内容形式</p>
<ul>
<li>引入的符号形式：<a href="https://stackoverflow.com/questions/1044360/import-using-angle-brackets-and-quote-marks">通常来说</a>，双引号的引入方式(<code>“A.h”</code> 或者 <code>&quot;A/A.h&quot;</code>)是用于查找本地的头文件，需要指定相对路径，尖括号的引入方式(<code>&lt;A.h&gt;</code> 或者 <code>&lt;A/A.h&gt;</code>)是全局的引用，其路径由编译器提供，如引用系统的库，但随着 header search path 的加入，让这种区别已经被淡化了。</li>
<li>引入的内容形式：对于 <code>X/X.h</code> 和 <code>X.h</code> 这两种引入的内容形式，前者是说在对应的 search path 中，找到目录 A 并在 A 目录下查找 <code>A.h</code>，而后者是说在 search path 下查找 <code>A.h</code> 文件，而不一定局限在 A 目录中，至于是否递归的寻找则取决于对目录的选项是否开启了 <code>recursive</code> 模式</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896446_704745.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1173/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896446_704745.png" alt="image.png"loading="lazy" decoding="async" width="1173" height="402" /></picture></figure></div><p>在很多工程中，尤其是基于 CocoaPods 开发的项目，我们已经不会区分 system header search path 和 user header search path，而是一股脑的将所有头文件路径添加到 header search path 中，这就导致我们在引用某个头文件时，不会再局限于前面提到的约定，甚至在某些情况下，前面提到的四种方式都可以做到引入某个指定头文件。</p>
<h4>header maps</h4>
<p>随着项目的迭代和发展，原有的头文件索引机制还是受到了一些挑战，为此，Clang 官方也提出了自己的解决方案。</p>
<p>为了理解这个东西，我们首先要在 build setting 中开启 Use Header Map 选项。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896390_212669.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896390_212669.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896390_212669.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896390_212669.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896390_212669.png" alt="image.png"loading="lazy" decoding="async" width="1818" height="136" /></picture></figure></div><p>然后在 build log 里获取相应组件里对应文件的编译命令，并在最后加上 <code>-v</code> 参数，来查看其运行的秘密：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">clang &lt;list of arguments&gt; -c SQViewController.m -o SQViewcontroller.o -v
</div></code></pre></div>
</div>
<p>在 console 的输出内容中，我们会发现一段有意思的内容：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896372_345499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896372_345499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896372_345499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896372_345499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896372_345499.png" alt="image.png"loading="lazy" decoding="async" width="1748" height="808" /></picture></figure></div><p>通过上面的图，我们可以看到编译器将寻找头文件的顺序和对应路径展示出来了，而在这些路径中，我们看到了一些陌生的东西，即后缀名为 <code>.hmap</code> 的文件。</p>
<p>那 hmap 到底这是个什么东西呢？</p>
<p>当我们开启 Build Setting 中的 Use Header Map 选项后，会自动生成的一份头文件名和头文件路径的映射表，而这个映射表就是 hmap 文件，不过它是一种二进制格式的文件，也有人叫它为 header map，总之，它的核心功能就是让编译器能够找到相应头文件的位置。</p>
<p>为了更好的理解它，我们可以通过 milend 编写的小工具 <a href="https://github.com/milend/hmap">hmap</a> 来查其内容。</p>
<p>在执行相关命令（即 <code>hmap print</code>）后，我们可以发现这些 hmap 里保存的信息结构大致如下：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896327_22396.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896327_22396.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896327_22396.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896327_22396.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896327_22396.png" alt="image.png"loading="lazy" decoding="async" width="1884" height="384" /></picture></figure></div><p>需要注意，映射表的键值并不是简单的文件名和绝对路径，它的内容会随着使用场景产生不同的变化，例如头文件引用是在 <code>&quot;...&quot;</code> 的形式，还是 <code>&lt;...&gt;</code> 的形式，又或是在 Build Phase 里 Header 的配置情况。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896315_135617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896315_135617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896315_135617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896315_135617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896315_135617.png" alt="image.png"loading="lazy" decoding="async" width="1900" height="456" /></picture></figure></div><p>至此我想你应该明白了，一旦开启 Use Header Map 选项后，Xcode 会优先去 hmap 映射表里寻找头文件的路径，只有在找不到的情况下，才会去 header search path 中提供的路径遍历搜索。</p>
<p>当然这种技术也不是一个什么新鲜事儿，在 Facebook 的 <a href="https://buck.build/">buck</a> 工具中也提供了类似的东西，只不过文件类型变成了 <code>HeaderMap.java</code> 的样子。</p>
<h4>查找系统库的头文件</h4>
<p>上面的过程让我们理解了在 header map 技术下，编译器是如何寻找相应的头文件的，那针对系统库的文件又是如何索引的呢？例如 <code>#import &lt;Foundation/Foundation.h&gt;</code></p>
<p>回想一下上一节 console 的输出内容，它的形式大概如下：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">#include &quot;...&quot; search starts here:</span>
</div><div class="line">XXX-generated-files.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">XXX-project-headers.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">
</div><div class="line"><span class="c1">#include &lt;...&gt; search starts here:</span>
</div><div class="line">XXX-own-target-headers.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">XXX-all-target-headers.hmap <span class="o">(</span>headermap<span class="o">)</span> 
</div><div class="line">Header Search Path 
</div><div class="line">DerivedSources
</div><div class="line">Build/Products/Debug <span class="o">(</span>framework directory<span class="o">)</span>
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/usr/include 
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/System/Library/Frameworks<span class="o">(</span>framework directory<span class="o">)</span>
</div></code></pre></div>
</div>
<p>我们会发现，这些路径大部分是用于查找非系统库文件的，也就是开发者自己引入的头文件，而与系统库相关的路径只有以下两个：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">#include &lt;...&gt; search starts here:</span>
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/usr/include 
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/System/Library/Frameworks.<span class="o">(</span>framework directory<span class="o">)</span>
</div></code></pre></div>
</div>
<p>当我们查找 <code>Foundation/Foundation.h</code> 这个文件的时候，我们会首先判断是否存在 Foundation 这个 framework。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="nv">$SDKROOT</span>/System/Library/Frameworks/Foundation.framework
</div></code></pre></div>
</div>
<p>接着，我们会进入 framework 的 Headers 文件夹里寻找对应的头文件</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="nv">$SDKROOT</span>/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h
</div></code></pre></div>
</div>
<p>如果没有找到对应的文件，索引过程会在此中断，并结束查找。</p>
<p>以上便是系统库的头文件搜索逻辑。</p>
<h4>framework search path</h4>
<p>到底为止，我们已经解释了如何依赖 header search path，hmap 等技术寻找头文件的工作机制，也介绍了寻找系统库（system framework）头文件的工作机制。</p>
<p>那这是全部头文件的搜索机制么？答案是否定的，其实我们还有一种头文件搜索机制，它是基于 Framework 这种文件结构进行的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896291_514952.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896291_514952.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896291_514952.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896291_514952.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896291_514952.png" alt="image.png"loading="lazy" decoding="async" width="1900" height="232" /></picture></figure></div><p>对于开发者自己的 Framework，可能会存在 “private” 头文件，例如在 podspec 里用 <code>private_header_files</code> 的描述文件，这些文件在构建的时候，会被放在 Framework 文件结构中的 PrivateHeaders 目录。</p>
<p>所以针对有 PrivateHeaders 目录的 Framework 而言，clang 在检查 Headers 目录后，会去 PrivateHeaders 目录中寻找是否存在匹配的头文件，如果这两个目录都没有，才会结束查找。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="nv">$SDKROOT</span>/System/Library/Frameworks/Foundation.framework/PrivateHeaders/SecretClass.h
</div></code></pre></div>
</div>
<p>不过也正是因为这个工作机制，会产生一个特别有意思的问题，那就是当我们使用 Framework 的方式引入某个带有 “private” 头文件的组件时，我们总是可以以下面的方式引入这个头文件！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896278_373702.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896278_373702.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896278_373702.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896278_373702.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896278_373702.png" alt="image.png"loading="lazy" decoding="async" width="2618" height="644" /></picture></figure></div><p>怎么样，是不是很神奇，这个被描述为 “private” 的头文件怎么就不私有了？</p>
<p>究其原因，还是由于 clang 的工作机制，那为什么 clang 要设计出来这种看似很奇怪的工作机制呢？</p>
<h4>揭开 Public，Private，Project 的真实面目</h4>
<p>其实你也看到我在上一段的写作中，将所有 private 单词标上了双引号，其实就是在暗示，我们曲解了 private 的含义。</p>
<p>那么这个 “private” 到底是什么意思呢？</p>
<p>在 Apple 官方的 <a href="https://help.apple.com/xcode/mac/current/#/dev50bab713d">Xcode Help - What are build phases?</a> 文档中，我们可以看到如下的一段解释：</p>
<div class="blockquote"><blockquote><p>Associates public, private, or project header files with the target. Public and private headers define API intended for use by other clients, and are copied into a product for installation. For example, public and private headers in a framework target are copied into Headers and PrivateHeaders subfolders within a product. Project headers define API used and built by a target, but not copied into a product. This phase can be used once per target.</p>
</blockquote></div>
<p>总的来说，我们可以知道一点，就是 Build Phases - Headers 中提到 Public 和 Private 是指可以供外界使用的头文件，且分别放在最终产物的 Headers 和 PrivateHeaders 目录中，而 Project 中的头文件是不对外使用的，也不会放在最终的产物中。</p>
<p>如果你继续翻阅一些资料，例如 <a href="https://stackoverflow.com/questions/7439192/xcode-copy-headers-public-vs-private-vs-project">StackOverflow - Xcode: Copy Headers: Public vs. Private vs. Project?</a> 和 <a href="https://stackoverflow.com/questions/10584936/understanding-xcodes-copy-headers-phase/18910393#18910393">StackOverflow - Understanding Xcode’s Copy Headers phase</a>，你会发现在早期 Xcode Help 的 Project Editor 章节里，有一段名为 Setting the Role of a Header File 的段落，里面详细记载了三个类型的区别。</p>
<div class="blockquote"><blockquote><p><strong>Public</strong>: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
<strong>Private</strong>: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked “private”. Thus the symbols are visible to all clients, but clients should understand that they’re not supposed to use them.
<strong>Project</strong>: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.</p>
</blockquote></div>
<p>至此我们应该彻底了解了 Public，Private，Project 的区别，简而言之，Public 还是通常意义上的 Public，Private 则代表 In Progress 的含义，至于 Project 才是通常意义上的 Private 含义。</p>
<p>那么 CocoaPods 中 Podspec 的 Syntax 里还有 <code>public_header_files</code> 和 <code>private_header_files</code> 两个字段，它们的真实含义是否和 Xcode 里的概念冲突呢？</p>
<p>这里我们仔细阅读一下<a href="https://guides.cocoapods.org/syntax/podspec.html">官方文档的解释</a>，尤其是 <code>private_header_files</code> 字段。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896258_605384.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1382/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896258_605384.png" alt="image.png"loading="lazy" decoding="async" width="1382" height="996" /></picture></figure></div><p>我们可以看到，<code>private_header_files</code> 在这里的含义是说，它本身是相对于 public 而言的，这些头文件本义是不希望暴露给用户使用的，而且也不会产生相关文档，但是在构建的时候，会出现在最终产物中，只有既没有被 public 和 private 标注的头文件，才会被认为是真正的私有头文件，且不出现在最终的产物里。</p>
<p>其实这么看来，CocoaPods 对于 public 和 private 的理解是和 Xcode 中的描述一致的，两处的 Private 并非我们通常理解的 Private，它的本意更应该是开发者准备对外开放，但又没完全 ready 的头文件，更像一个 In Progress 的含义。</p>
<p>所以，如果你真的不想对外暴露某些头文件，请不要再使用 Headers 里的 Private 或者 podspec 里的 <code>private_header_files</code> 了。</p>
<p>至此，我想你应该彻底理解了 Search Path 的搜索机制和略显奇怪的 Public，Private，Project 设定了！</p>
<h4>基于 hmap 优化 Search Path 的策略</h4>
<p>在查找系统库的头文件的章节中，我们通过 <code>-v</code> 参数看到了寻找头文件的搜索顺序：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">#include &quot;...&quot; search starts here:</span>
</div><div class="line">XXX-generated-files.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">XXX-project-headers.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">
</div><div class="line"><span class="c1">#include &lt;...&gt; search starts here:</span>
</div><div class="line">XXX-own-target-headers.hmap <span class="o">(</span>headermap<span class="o">)</span>
</div><div class="line">XXX-all-target-headers.hmap <span class="o">(</span>headermap<span class="o">)</span> 
</div><div class="line">Header Search Path 
</div><div class="line">DerivedSources
</div><div class="line">Build/Products/Debug <span class="o">(</span>framework directory<span class="o">)</span>
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/usr/include 
</div><div class="line"><span class="k">$(</span>SDKROOT<span class="k">)</span>/System/Library/Frameworks<span class="o">(</span>framework directory<span class="o">)</span>
</div></code></pre></div>
</div>
<p>假设，我们没有开启 hmap 的话，所有的搜索都会依赖 header search path 或者 framework search path，那这就会出现 3 种问题：</p>
<ul>
<li>第一个问题，在一些巨型项目中，假设依赖的组件有 400+，那此时的索引路径就会达到 800+ 个（一份 public 路径，一份 private 路径），同时搜索操作可以看做是一种 IO 操作，而我们知道 IO 操作通常也是一种耗时操作，那么，这种大量的耗时操作必然会导致编译耗时增加。</li>
<li>第二个问题，在打包的过程中，如果 header search path 过多过长，会触发命令行过长的错误，进而导致命令执行失败的情况。</li>
<li>第三个问题，在引入系统库的头文件时，clang 会将前面提到的目录遍历完才进入搜索系统库的路径，也就是 <code>$(SDKROOT)/System/Library/Frameworks(framework directory)</code>，即前面的 header search 路径越多，耗时也会越长，这是相当不划算的。</li>
</ul>
<p>那如果我们开启 hmap 后，是否就能解决掉所有的问题呢？</p>
<p>实际上并不能，而且在基于 CocoaPods 管理项目的状况下，又会带来新的问题。下面是一个基于 CocoaPods 构建的全源码工程项目，它的整体结构如下：</p>
<p>首先，Host 和 Pod 是我们的两个 Project，Pods 下的 target 的产物类型为 static library。</p>
<p>其次，Host 底下会有一个同名的 Target，而 Pods 目录下会有 n+1 个 target，其中 n 取决于你依赖的组件数量，而 1 是一个名为 Pods-XXX 的 target，最后，Pods-XXX 这个 target 的产物会被 Host 里的 target 所依赖。</p>
<p>整个结构看起来如下所示。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896226_594006.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896226_594006.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896226_594006.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896226_594006.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896226_594006.png" alt="image.png"loading="lazy" decoding="async" width="2078" height="1278" /></picture></figure></div><p>此时我们将 PodA 里的文件全部放在 Header 的 Project 类型中。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896215_181143.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896215_181143.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896215_181143.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896215_181143.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896215_181143.png" alt="image.png"loading="lazy" decoding="async" width="1608" height="632" /></picture></figure></div><p>在基于 Framework 的搜索机制下，我们是无法以任何方式引入到 ClassB 的，因为它既不在 Headers 目录，也不在 PrivateHeader 目录中。</p>
<p>可是如果我们开启了 Use Header Map 后，由于 PodA 和 PodB 都在 Pods 这个 Project 下，满足了 Header 的 Project 定义，通过 Xcode 自动生成的 hmap 文件会带上这个路径，所以我们还可以在 PodB 中以 <code>#import &quot;ClassB.h&quot;</code> 的方式引入。</p>
<p>而这种行为，我想应该是大多数人并不想要的结果，所以一旦开启了 Use Header Map，再结合 CocoaPods 管理工程项目的模式，我们极有可能会产生一些误用私有头文件的情况，而这个问题的本质是 Xcode 和 CocoaPods 在工程和头文件上的理念冲突造成的。</p>
<p>除此之外，CocoaPods 在处理头文件的问题上还有一些让人迷惑的地方，它在创建头文件产物这块的逻辑大致如下：</p>
<ul>
<li>在构建产物为 Framework 的情况下:<ul>
<li>根据 podspec 里的 <code>public_header_files</code> 字段的内容，将相应头文件设置为 Public 类型，并放在 Headers 中</li>
<li>根据 podspec 里的 <code>private_header_files</code> 字段的内容，将相应文件设置为 Private 类型，并放在 PrivateHeader 中</li>
<li>将其余未描述的头文件设置为 Project 类型，且不放入最终的产物中</li>
<li>如果 podspec 里未标注 public 和 private 的时候，会将所有文件设置为 public 类型，并放在 Header 中</li>
</ul>
</li>
<li>在构建产物为 Static Library 的情况下:<ul>
<li>不论 podspec 里如何设置 <code>public_header_files</code> 和 <code>private_header_files</code>，相应的头文件都会被设置为 Project 类型</li>
<li>在 <code>Pods/Headers/Public</code> 中会保存所有被声明为 <code>public_header_files</code> 的头文件</li>
<li>在 <code>Pods/Headers/Private</code> 中会保存所有头文件，不论是 <code>public_header_files</code> 或者 <code>private_header_files</code> 描述到，还是那些未被描述的，这个目录下是当前组件的所有头文件全集</li>
<li>如果 podspec 里未标注 public 和 private 的时候，<code>Pods/Headers/Public</code> 和 <code>Pods/Headers/Private</code> 的内容一样且会包含所有头文件。</li>
</ul>
</li>
</ul>
<p>正是由于这种机制，还导致了另外一种有意思的问题。</p>
<p>在 Static Library 的状况下，一旦我们开启了 Use Header Map，结合组件里所有头文件的类型为 Project 的情况，这个 hmap 里只会包含 <code>#import &quot;A.h&quot;</code> 的键值引用，也就是说只有 <code>#import &quot;A.h&quot;</code> 的方式才会命中 hmap 的策略，否则都将通过 header search path 寻找其相关路径。</p>
<p>而我们也知道，在引用其他组件的时候，通常都会采用 <code>#import &lt;A/A.h&gt;</code> 的方式引入。至于为什么会用这种方式，一方面是这种写法会明确头文件的由来，避免问题，另一方面也是这种方式可以让我们在是否开启 clang module 中随意切换，当然还有一点就是，Apple 在 WWDC 里曾经不止一次的建议开发者使用这种方式引入头文件。</p>
<p>接着上面的话题来说，所以说在 Static Library 的情况下且以 <code>#import &lt;A/A.h&gt;</code> 这种标准方式引入头文件时，开启 Use Header Map 并不会提升编译速度，而这同样是 Xcode 和 CocoaPods 在工程和头文件上的理念冲突造成的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896184_423189.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1417/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896184_423189.png" alt="27.png"loading="lazy" decoding="async" width="1417" height="496" /></picture></figure></div><p>这样来看的话，虽然 hmap 有种种优势，但是在 CocoaPods 的世界里显得格格不入，也无法发挥自身的优势。</p>
<p>那这就真的没有办法解决了么？</p>
<p>当然，问题是有办法解决的，我们完全可以自己动手做一个基于 CocoaPods 规则下的 hmap 文件。</p>
<p>举一个简单的例子，通过遍历 PODS 目录里的内容去构建索引表内容，借助 <a href="https://github.com/milend/hmap">hmap</a> 工具生成 header map 文件，然后将 Cocoapods 在 header search path 中生成的路径删除，只添加一条指向我们自己生成的 hmap 文件路径，最后关闭 Xcode 的 Ues Header Map 功能，也就是 Xcode 自动生成 hmap 的功能，如此这般，我们就实现了一个简单的，基于 CocoaPods 的 header map 功能。</p>
<p>同时在这个基础上，我们还可以借助这个功能实现不少管控手段，例如</p>
<ul>
<li>从根本上杜绝私有文件被暴露的可能性。</li>
<li>统一头文件的引用形式</li>
<li>…</li>
</ul>
<p>目前我们已经自研了一套基于上述原理的 cocoapods 插件，它的名字叫做 cocoapods-hmap-prebuilt，是由笔者与同事 @宋旭陶 共同开发的，</p>
<p>说了这么多，让我们看看它在实际工程中的使用效果！</p>
<p>经过全源码编译的测试，我们可以看到该技术在提速上的收益较为明显，以美团和点评 App 为例，全链路时长能够提升 45% 以上，其中 Xcode 打包时间能提升 50%。</p>
<h3>关于第二个问题</h3>
<div class="blockquote"><blockquote><p>对于已开启 clang module 特性的组件，clang 是如何决定编译当下组件的 module 呢？另外构建的细节又是怎样的，以及如何查找这些 module 的？还有查找系统的 module 和非系统的 module 有什么区别么？</p>
</blockquote></div>
<p>首先，我们来明确一个问题， clang 是如何决定编译当下组件的 module 呢</p>
<p>以 <code>#import &lt;Foundation/NSString.h&gt;</code> 为例，当我们遇到这个头文件的时候：</p>
<p>首先会去 Framework 的 Headers 目录下寻找相应的头文件是否存在，然后就会到 Modules 目录下查找 modulemap 文件</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896160_187061.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_942/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896160_187061.png" alt="image.png"loading="lazy" decoding="async" width="942" height="516" /></picture></figure></div><p>此时，Clang 会去查阅 modulemap 里的内容，看看 NSString 是否为 Foundation 这个 Module 里的一部分，</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">// Module Map - Foundation.framework/Modules/module.modulemap
</div><div class="line">framework module Foundation <span class="o">[</span>extern_c<span class="o">]</span> <span class="o">[</span>system<span class="o">]</span> <span class="o">{</span>
</div><div class="line">    umbrella header <span class="s2">&quot;Foundation.h&quot;</span>
</div><div class="line">    <span class="nb">export</span> *
</div><div class="line">    module * <span class="o">{</span>
</div><div class="line">        <span class="nb">export</span> *
</div><div class="line">    <span class="o">}</span>
</div><div class="line">
</div><div class="line">    explicit module NSDebug <span class="o">{</span>
</div><div class="line">        header <span class="s2">&quot;NSDebug.h&quot;</span>
</div><div class="line">        <span class="nb">export</span> *
</div><div class="line">    <span class="o">}</span>
</div><div class="line"><span class="o">}</span>
</div></code></pre></div>
</div>
<p>很显然，这里通过 umbrella header，我们是可以在 <code>Foundation.h</code> 中找到 <code>NSString.h</code> 的。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">// Foundation.h
</div><div class="line">…
</div><div class="line"><span class="c1">#import &lt;Foundation/NSStream.h&gt;</span>
</div><div class="line"><span class="c1">#import &lt;Foundation/NSString.h&gt;</span>
</div><div class="line"><span class="c1">#import &lt;Foundation/NSTextCheckingResult.h&gt;</span>
</div><div class="line">…
</div></code></pre></div>
</div>
<p>至此，clang 会判定 <code>NSString.h</code> 是 Foundation 这个 module 的一部分并进行相应的编译工作，此时也就意味着 <code>#import &lt;Foundation/NSString.h&gt;</code> 会从之前的 textual import 变为 module import</p>
<h4>Module 的构建细节</h4>
<p>上面的内容解决了是否构建 module，而这一块我们会详细阐述构建 module 的过程！</p>
<p>在构建开始前，clang 会创建一个完全独立的空间来构建 module，在这个空间里会包含 module 涉及的所有文件，除此之外不会带入其他任何文件的信息，而这也是 module 健壮性好的关键因素之一。</p>
<p>不过，这并不意味着我们无法影响到 module 的唯一性，真正能影响到其唯一性的是其构建的参数，也就是 clang 命令后面的内容，关于这一点后面还会继续展开，这里我们先点到为止。</p>
<p>当我们在构建 Foundation 的时候，我们会发现 Foundation 自身要依赖一些组件，这意味着我们也需要构建被依赖组件的 module</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896146_958668.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896146_958668.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896146_958668.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896146_958668.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896146_958668.png" alt="image.png"loading="lazy" decoding="async" width="2962" height="746" /></picture></figure></div><p>但很明显的是，我们会发现这些被依赖组件也有自己的依赖关系，在它们的这些依赖关系中，极有可能会存在重复的引用。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896136_628284.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896136_628284.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896136_628284.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896136_628284.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896136_628284.png" alt="image.png"loading="lazy" decoding="async" width="2962" height="1220" /></picture></figure></div><p>此时 module 的复用机制就体现出来优势了，我们可以复用先前构建出来的 module，而不必一次次的创建或者引用，例如 Drawin 组件，而保存这些缓存文件的位置就是前面章节里提到的保存 <code>pcm</code> 类型文件的地方。</p>
<p>先前我们提到了 clang 命令的参数会真正影响到 module 的唯一性，那具体的原理又是怎样的？</p>
<p>clang 会将相应的编译参数进行一次 hash，将获得的 hash 值作为 module 缓存文件夹的名称，这里需要注意的是，不同的参数和值会导致文件夹不同，所以想要尽可能的利用 module 缓存，就必须保证参数不发生变化。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ clang -fmodules —DENABLE_FEATURE<span class="o">=</span><span class="m">1</span> …
</div><div class="line"><span class="c1">## 生成的目录如下</span>
</div><div class="line">98XN8P5QH5OQ/
</div><div class="line">  CoreFoundation-2A5I5R2968COJ.pcm
</div><div class="line">  Security-1A229VWPAK67R.pcm
</div><div class="line">  Foundation-1RDF848B47PF4.pcm
</div><div class="line">  
</div><div class="line">$ clang -fmodules —DENABLE_FEATURE<span class="o">=</span><span class="m">2</span> …
</div><div class="line"><span class="c1">## 生成的目录如下</span>
</div><div class="line">1GYDULU5XJRF/
</div><div class="line">  CoreFoundation-2A5I5R2968COJ.pcm
</div><div class="line">  Security-1A229VWPAK67R.pcm
</div><div class="line">  Foundation-1RDF848B47PF4.pcm
</div></code></pre></div>
</div>
<p>这里我们大概了解了系统组件的 module 构建机制，这也是开启 <code>Enable Modules(C and Objective-C)</code> 的核心工作原理。</p>
<h4>神秘的 Virtual File System（VFS）</h4>
<p>对于系统组件，我们可以在 <code>/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/System/Library/Frameworks</code> 目录里找到它的身影，它的目录结构大概是这样的</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896119_557861.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896119_557861.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896119_557861.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896119_557861.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896119_557861.png" alt="image.png"loading="lazy" decoding="async" width="1694" height="358" /></picture></figure></div><p>也就是说，对于系统组件而言，构建 module 的整个过程是建立在这样一个完备的文件结构上，即在 Framework 的 Modules 目录中查找 modulemap，在 Headers 目录中加载头文件。</p>
<p>那对于用户自己创建的组件，clang 又是如何构建 module 的呢？</p>
<p>通常我们的开发目录大概是下面的样子，它并没有 modules 目录，也没有 headers 目录，更没有 modulemap 文件，看起来和 framework 的文件结构也有着极大的区别。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896109_723752.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1242/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896109_723752.png" alt="image.png"loading="lazy" decoding="async" width="1242" height="466" /></picture></figure></div><p>在这种情况下，clang 是没法按照前面所说的机制去构建 module 的，因为在这种文件结构中，压根就没有 Modules 和 Headers 目录。</p>
<p>为了解决这个问题，clang 又提出了一个新的解决方案，叫做 Virtual File System（VFS）。</p>
<p>简单来说，通过这个技术，clang 可以在现有的文件结构上虚拟出来一个 Framework 文件结构，进而让 clang 遵守前面提到的构建准则，顺利完成 module 的编译，同时 VFS 也会记录文件的真实位置，以便在出现问题的时候，将文件的真实信息暴露给用户。</p>
<p>为了进一步了解 VFS，我们还是从 Build Log 中查找一些细节！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896101_425121.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896101_425121.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896101_425121.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896101_425121.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896101_425121.png" alt="image.png"loading="lazy" decoding="async" width="2380" height="994" /></picture></figure></div><p>在上面的编译参数里，我们可以找到一个 <code>-ivfsoverlay</code> 的参数，查看 help 说明，可以知道其作用就是向编译器传递一个 VFS 描述文件并覆盖掉真实的文件结构信息。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">-ivfsoverlay &lt;value&gt;    Overlay the virtual filesystem described by file over the real file system
</div></code></pre></div>
</div>
<p>顺着这个线索，我们去看看这个参数指向的文件，它是一个 yaml 格式的文件，在将内容进行了一些裁剪后，它的核心内容如下，：</p>
<div class="block-code" data-language="yaml"><div class="highlight"><pre><span></span><code><div class="line"><span class="p p-Indicator">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="s">&quot;case-sensitive&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;false&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="s">&quot;version&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="nv">0</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="s">&quot;roots&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">{</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="s">&quot;name&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;XXX/Debug-iphonesimulator/PodA/PodA.framework/Headers&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="s">&quot;type&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;directory&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="s">&quot;contents&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&quot;name&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;ClassA.h&quot;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&quot;type&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;file&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">          </span><span class="s">&quot;external-contents&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;XXX/PodA/PodA/Classes/ClassA.h&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">},</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="nv">......</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&quot;name&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;PodA-umbrella.h&quot;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&quot;type&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;file&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">          </span><span class="s">&quot;external-contents&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;XXX/Target</span><span class="nv"> </span><span class="s">Support</span><span class="nv"> </span><span class="s">Files/PodA/PodA-umbrella.h&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">}</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="p p-Indicator">]</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">},</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">{</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="s">&quot;contents&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="s">&quot;name&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;XXX/Products/Debug-iphonesimulator/PodA/PodA.framework/Modules&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="s">&quot;type&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;directory&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&quot;name&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;module.modulemap&quot;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&quot;type&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;file&quot;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">          </span><span class="s">&quot;external-contents&quot;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&quot;XXX/Debug-iphonesimulator/PodA.build/module.modulemap&quot;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">}</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="p p-Indicator">]</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">}</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p p-Indicator">]</span><span class="w"></span>
</div><div class="line"><span class="p p-Indicator">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>结合前面提到的内容，我们不难看出它在描述这样一个文件结构：</p>
<p>借用一个真实存在的文件夹来模拟 framework 里的 Headers 文件夹，在这个 Headers 文件夹里有名为 <code>PodA-umbrella.h</code> 和 <code>ClassA.h</code> 等的文件，不过这几个虚拟文件与 <code>external-contents</code> 指向的真实文件相关联，同理还有 Modules 文件夹和它里面的 <code>module.modulemap</code> 文件。</p>
<p>通过这样的形式，一个虚拟的 framework 目录结构诞生了！此时 clang 终于能按照前面的构建机制为用户创建 module 了！</p>
<h2>Swift 来了</h2>
<h3>没有头文件的 Swift</h3>
<p>前面的章节我们聊了很多 C 语言系的预编译知识，在这个体系下，文件的编译是分开的，当我们想引用其他文件里的内容时，就必须引入相应的头文件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896085_721084.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896085_721084.png" alt="image.png"loading="lazy" decoding="async" width="3698" height="1818" /></picture></figure></div><p>而对于 Swift 这门语言来说，它并没有头文件的概念，对于开发者而言，这确实省去了写头文件的重复工作，但这也意味着，编译器会进行额外的操作来查找接口定义并需要持续关注接口的变化！</p>
<p>为了更好的解释 Swift 和 Objective-C 是如何寻找到彼此的方法声明的，我们这里引入一个例子，在这个例子由三个部分组成：</p>
<ul>
<li>第一部分是一个 ViewController 的代码，它里面包含了一个 view，其中 PetViewController 和 PetView 都是 Swift 代码。</li>
<li>第二部分是一个 App 的代理，它是 Objective-C 代码。</li>
<li>第三个部分是一段单测代码，用来测试第一个部分中的 ViewController，它是 Swift 代码。</li>
</ul>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">UIKit</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">PetViewController</span><span class="p">:</span> <span class="bp">UIViewController</span> <span class="p">{</span>
</div><div class="line">  <span class="kd">var</span> <span class="nv">view</span> <span class="p">=</span> <span class="n">PetView</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="s">&quot;Fido&quot;</span><span class="p">,</span> <span class="n">frame</span><span class="p">:</span> <span class="n">frame</span><span class="p">)</span>
</div><div class="line">  <span class="err">…</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="p">#</span><span class="kd">import</span> <span class="s">&quot;PetWall-Swift.h&quot;</span>
</div><div class="line"><span class="p">@</span><span class="n">implementation</span> <span class="n">AppDelegate</span>
</div><div class="line"><span class="err">…</span>
</div><div class="line"><span class="p">@</span><span class="n">end</span>
</div><div class="line"><span class="p">@</span><span class="n">testable</span> <span class="kd">import</span> <span class="nc">PetWall</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">TestPetViewController</span><span class="p">:</span> <span class="n">XCTestCase</span> <span class="p">{</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>它们的关系大致如下所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896074_149732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896074_149732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896074_149732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896074_149732.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896074_149732.png" alt="image.png"loading="lazy" decoding="async" width="2038" height="672" /></picture></figure></div><p>为了能让这些代码编译成功，编译器会面对如下 4 个场景：</p>
<p>首先是寻找声明，这包括寻找当前 target 内的方法声明（PetView），也包括来自 Objective-C 组件里的声明（UIViewController 或者 PetKit）。</p>
<p>然后是生成接口，这包括被 Objective—C 使用的接口，也包括被其他 target (Unit Test）使用的 Swift 接口。</p>
<h3>第一步 - 如何寻找 Target 内部的 Swift 方法声明</h3>
<p>在编译 <code>PetViewController.swift</code> 时，编译器需要知道 PetView 的初始化构造器的类型，才能检查调用是否正确。</p>
<p>此时编译器会加载 <code>PetView.swift</code> 文件并解析其中的内容, 这么做的目的就是确保初始化构造器真的存在，并拿到相关的类型信息，以便 <code>PetViewController.swift</code> 进行验证。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896060_966308.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1326/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896060_966308.png" alt="image.png"loading="lazy" decoding="async" width="1326" height="741" /></picture></figure></div><p>编译器并不会对初始化构造器的内部做检查，但它仍然会进行一些额外的操作，这是什么意思呢？</p>
<p>与 clang 编译器不同的是，swiftc 编译的时候，会将相同 target 里的其他 swift 文件进行一次解析，用来检查其中与被编译文件关联的接口部分是否符合预期。</p>
<p>同时我们也知道，每个文件的编译是独立的，且不同文件的编译是可以并行开展的，所以这就意味着每编译一个文件，就需要将当前 target 里的其余文件当做接口，重新编译一次。 等于任意一个文件，在整个编译过程中，只有 1 次被作为生产 <code>.o</code> 产物的输入，其余时间会被作为接口文件反复解析。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896048_867424.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896048_867424.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896048_867424.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896048_867424.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896048_867424.png" alt="image.png"loading="lazy" decoding="async" width="2462" height="2066" /></picture></figure></div><p>不过在 Xcode 10 以后，Apple 对这种编译流程进行了优化！</p>
<p>在尽可能保证并行的同时，将文件进行了分组编译，这样就避免了 group 内的文件重复解析，只有不同 group 之间的文件会有重复解析文件的情况。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896031_444714.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896031_444714.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896031_444714.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896031_444714.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896031_444714.png" alt="image.png"loading="lazy" decoding="async" width="2730" height="2066" /></picture></figure></div><p>而这个分组操作的逻辑，就是刚才提到的一些额外操作。</p>
<p>至此，我们应该了解了 Target 内部是如何寻找 Swift 方法声明的了。</p>
<h3>第二步 - 如何找到 Objective-C 组件里的方法声明</h3>
<p>回到第一段代码中，我们可以看到 PetViewController 是继承自 UIViewController，而这也意味着我们的代码会与 Objective-C 代码进行交互，因为大部分系统库，例如 UIKit 等，还是使用 Objective-C 编写的。</p>
<p>在这个问题上，Swift 采用了和其他语言不一样的方案！</p>
<p>通常来说，两种不同的语言在混编时需要提供一个接口映射表，例如 JavaScript 和 TypeScript 混编时候的 <code>.d.ts</code> 文件，这样 TypeScript 就能够知道 JavaScript 方法在 TS 世界中的样子。</p>
<p>然而，Swift 不需要提供这样的接口映射表, 免去了开发者为每个 Objective-C API 声明其在 Swift 世界里样子，那它是怎么做到的呢?</p>
<p>很简单，Swift 编译器将 clang 的大部分功能包含在其自身的代码中，这就使得我们能够以 module 的形式，直接引用 Objective-C 的代码。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896011_782378.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1562/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304896011_782378.png" alt="image.png"loading="lazy" decoding="async" width="1562" height="974" /></picture></figure></div><p>既然是通过 module 的形式引入 Objective-C，那么 framework 的文件结构则是最好的选择，此时编译器寻找方法声明的方式就会有下面三种场景：</p>
<ul>
<li>对于大部分的 target 而言，当导入的是一个 Objective-C 类型的 framework 时，编译器会通过 modulemap 里的 header 信息寻找方法声明</li>
<li>对于一个既有 Objective-C，又有 Swift 代码的 framework 而言，编译器会从当前 framework 的 umbrella header 中寻找方法声明，从而解决自身的编译问题，这是因为通常情况下 modulemap 会将 umbrella header 作为自身的 header 值。</li>
<li>对于 App 或者 Unit Test 类型的 target，开发者可以通过为 target 创建 briding header 来导入需要的 Objective-C 头文件，进而找到需要的方法声明。</li>
</ul>
<p>不过我们应该知道 Swift 编译器在获取 Objective-C 代码过程中，并不是原原本本的将 Objective—C 的 API 暴露给 Swift，而是会做一些 “Swift 化” 的改动，例如下面的 Objective-C API 就会被转换成更简约的形式。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304896004_311834.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304896004_311834.png" alt="image.png"loading="lazy" decoding="async" width="4128" height="944" /></picture></figure></div><p>这个转换过程并不是什么高深的技术，它只是在编译器上的硬编码，如果感兴趣，可以在 Swift 的开源库中的找到相应的代码 - <a href="https://github.com/apple/swift/blob/main/lib/Basic/PartsOfSpeech.def">PartsOfSpeech.def</a></p>
<p>当然，编译器也给与了开发者自行定义 “API 外貌” 的权利，如果你对这一块感兴趣，不妨阅读我的另一篇文章 - <a href="https://sketchk.xyz/2020/07/02/WWDC20-10680-Refine-Objective-C-frameworks-for-Swift/">WWDC20 10680 - Refine Objective-C frameworks for Swift</a>，那里面包含了很多重塑 Objective-C API 的技巧。</p>
<p>不过这里还是要提一句，如果你对生成的接口有困惑，可以通过下面的方式查看编译器为 Objective-C 生成的 Swift 接口。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895995_556898.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1242/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895995_556898.png" alt="image.png"loading="lazy" decoding="async" width="1242" height="1060" /></picture></figure></div><h3>第三步 - Target 内的 Swift 代码是如何为 Objective-C 提供接口的</h3>
<p>前面讲了 Swift 代码是如何引用 Objective-C 的 API，那么 Objective-C 又是如何引用 Swift 的 API 呢？</p>
<p>从使用层面来说，我们都知道 Swift 编译器会帮我们自动生成一个头文件，以便 Objective-C 引入相应的代码，就像第二段代码里引入的 <code>PetWall-Swift.h</code> 文件，这种头文件通常是编译器自动生成的，名字的构成是 <code>组件名-Swift</code> 的形式。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895971_422817.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895971_422817.png" alt="image.png"loading="lazy" decoding="async" width="3596" height="1068" /></picture></figure></div><p>但它到底是怎么产生的呢？</p>
<p>在 Swift 中，如果某个类继承了 NSObject 类且 API 被 <code>@objc</code> 关键字标注，就意味着它将暴露给 Objective-C 代码使用。</p>
<p>不过对于 App 和 Unit Test 类型的 target 而言，这个自动生成的 header 会包含访问级别为 public 和 internal 的 API，这使得同一 target 内的 Objective-C 代码也能访问 Swift 里 internal 类型的 API，这也是所有 Swift 代码的默认访问级别。</p>
<p>但对于 framework 类型的 target 而言，Swift 自动生成的头文件只会包含 public 类型的 API，因为这个头文件会被作为构建产物对外使用，所以像 internal 类型的 API 是不会包含在这个文件中。</p>
<div class="blockquote"><blockquote><p>注意，这种机制会导致在 framework 类型的 target 中，如果 Swift 想暴露一些 API 给内部的 Objective-C 代码使用，就意味着这些 API 也必须暴露给外界使用，即必须将其访问级别设置为 public 。</p>
</blockquote></div>
<p>那么编译器自动生成的 API 到底是什么样子，有什么特点呢？</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895960_792681.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895960_792681.png" alt="image.png"loading="lazy" decoding="async" width="3718" height="1068" /></picture></figure></div><p>上面是截取了一段自动生成的头文件代码，左侧是原始的 Swift 代码，右侧是自动生成的 Objective-C 代码，我们可以看到在 Objective-C 的类中，有一个名为 <code>SWIFT_CLASS</code> 的宏，将 Swift 与 Objective-C 中的两个类进行了关联。</p>
<p>如果你稍加注意，就会发现关联的一段乱码中还绑定了当前的组件名（PetWall），这样做的目的是避免两个组件的同名类在运行时发生冲突。</p>
<p>当然，你也可以通过向 <code>@objc(Name)</code> 关键字传递一个标识符，借由这个标识符来控制其在 Objective-C 中的名称，如果这样做的话，需要开发者确保转换后的类名不与其他类名出现冲突。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895952_616758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895952_616758.png" alt="image.png"loading="lazy" decoding="async" width="3824" height="1068" /></picture></figure></div><p>这大体上就是 Swift 如何像 Objective-C 暴露接口的机理了，如果你想更深入的了解这个文件的由来，就需要看看第四步。</p>
<h3>第四步 - Swift Target 如何生成供外部 Swift 使用的接口</h3>
<p>Swift 采用了 Clang Module 的理念，并结合自身的语言特性进行了一系列的改进。</p>
<p>在 Swift 中，module 是方法声明的分发单位，如果你想引用相应的方法，就必须引入对应的 module，之前我们也提到了 swift 的编译器包含了 clang 的大部分内容，所以它也是兼容 clang module 的。</p>
<p>所以我们可以引入 Objective-C 的 module，例如 XCTest，也可以引入 Swift Target 生成的 module，例如 PetWall</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">XCTest</span>
</div><div class="line"><span class="p">@</span><span class="n">testable</span> <span class="kd">import</span> <span class="nc">PetWall</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">TestPetViewController</span><span class="p">:</span> <span class="n">XCTestCase</span> <span class="p">{</span>
</div><div class="line">  <span class="kd">func</span> <span class="nf">testInitialPet</span><span class="p">()</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">controller</span> <span class="p">=</span> <span class="n">PetViewController</span><span class="p">()</span>
</div><div class="line">    <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">controller</span><span class="p">.</span><span class="n">view</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="s">&quot;Fido&quot;</span><span class="p">)</span>
</div><div class="line">  <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>在引入 swift 的 module 后，编译器会反序列化一个后缀名为 <code>.swiftmodule</code> 的文件，并通过这种文件里的内容来了解相关接口的信息。</p>
<p>例如，以下图为例，在这个单元测试中，编译器会加载 PetWall 的 module，并在其中找寻 PetViewController 的方法声明，由此确保其创建行为是符合预期的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895936_354871.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895936_354871.png" alt="image.png"loading="lazy" decoding="async" width="3824" height="1234" /></picture></figure></div><p>这看起来很像第一步中 target 寻找内部 Swift 方法声明的样子，只不过这里将解析 swift 文件的步骤，换成了解析 swiftmodule 文件而已。</p>
<p>不过需要注意的是，这个 swfitmodule 文件并不是文本文件，它是一个二进制格式的内容，通常我们可以在构建产物的 Modules 文件夹里寻找到它的身影。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895925_265797.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1486/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895925_265797.png" alt="image.png"loading="lazy" decoding="async" width="1486" height="284" /></picture></figure></div><p>在 target 的编译的过程中，面向整个 target 的 swiftmodule 文件并不是一下产生的，每一个 swift 文件都会生成一个 swiftmodule 文件，编译器会将这些文件进行汇总，最后再生成一个完整的，代表整个 target 的 swiftmodule，也正是基于这个文件，编译器构造出了用于给外部使用的 Objective-C 头文件，也就是第三步里提到的头文件</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895909_860888.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895909_860888.png" alt="image.png"loading="lazy" decoding="async" width="3840" height="2368" /></picture></figure></div><p>不过随着 Swift 的发展，这一部分的工作机制也发生了些许变化。</p>
<p>我们前面提到的 swiftmodule 文件是一种二进制格式的文件，而这个文件格式会包含一些编译器内部的数据结构，不同编译器产生的 swiftmodule 文件是互相不兼容的，这也就导致了不同 Xcode 构建出的产物是无法通用的，如果对这方面的细节感兴趣，可以阅读 Swift 社区里的两篇官方 Blog：<a href="https://swift.org/blog/abi-stability-and-apple/">Evolving Swift On Apple Platforms After ABI Stability</a> 和 <a href="https://swift.org/blog/abi-stability-and-more/">ABI Stability and More</a>，这里就不展开讨论了。</p>
<p>为了解决这一问题，Apple 在 Xcode 11 的 Build Setting 中提供了一个新的编译参数 Build Libraries for Distribution，正如这个编译参数的名称一样，当我们开启它后，构建出来的产物不会再受编译器版本的影响，那它是怎么做到这一点的呢？</p>
<p>为了解决这种对编译器的版本依赖，Xcode 在构建产物上提供了一个新的产物，swiftinterface 文件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895884_934446.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895884_934446.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895884_934446.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895884_934446.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895884_934446.png" alt="image.png"loading="lazy" decoding="async" width="1960" height="372" /></picture></figure></div><p>这个文件里的内容和 swiftmodule 很相似，都是当前 module 里的 API 信息，不过 swiftinterface 是以文本的方式记录，而非 swiftmodule 的二进制方式。</p>
<p>这就使得 swiftinterface 的行为和源代码一样，后续版本的 swift 编译器也能导入之前编译器创建的 swiftinterface 文件，像使用源码的方式一样使用它。</p>
<p>为了更进一步了解它，我们来看看 swiftinterface 的真实样子，下面是一个 <code>.swift</code> 文件和 <code>.swiftinterface</code> 文件的比对图。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895876_071409.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895876_071409.png" alt="image.png"loading="lazy" decoding="async" width="4236" height="3292" /></picture></figure></div><p>在 swiftinterface 文件中，有以下点需要注意</p>
<ul>
<li>文件会包含一些元信息，例如文件格式版本，编译器信息，和 Swift 编译器将其作为模块导入所需的命令行子集。</li>
<li>文件只会包含 public 的接口，而不会包含 private 的接口，例如 currentLocation</li>
<li>文件只会包含方法声明，而不会包含方法实现，例如 Spacesship 的 init，fly 等方法</li>
<li>文件会包含所有隐式声明的方法，例如 Spacesship 的 deinit 方法 ，Speed 的 Hashable 协议</li>
</ul>
<p>总的来说，swiftinterface 文件会在编译器的各个版本中保持稳定，主要原因就是这个接口文件会包含接口层面的一切信息，不需要编译器再做任何的推断或者假设。</p>
<p>好了，至此我们应该了解了 Swift Target 是如何生成供外部 Swift 使用的接口了。</p>
<h3>这四步意味着什么？</h3>
<h4>此 module 非彼 module</h4>
<p>通过上面的例子，我想大家应该能清楚的感受到 swift module 和 clang module 不完全是一个东西，虽然它们有很多相似的地方。</p>
<p>clang module 是面向 C 语言家族的一种技术，通过 modulemap 文件来组织 <code>.h</code> 文件中的接口信息，中间产物是二进制格式的 pcm 文件。</p>
<p>swift module 是面向 Swift 语言的一种技术，通过 swiftinterface 文件来组织 <code>.swift</code> 文件中的接口信息，中间产物二进制格式的 swiftmodule 文件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895859_700914.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895859_700914.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895859_700914.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895859_700914.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895859_700914.png" alt="image.png"loading="lazy" decoding="async" width="2020" height="1340" /></picture></figure></div><p>所以说理清楚这些概念和关系后，我们在构建 Swift 组件的产物时，就会知道哪些文件和参数不是必须的了。</p>
<p>例如当你的 Swift 组件不想暴露自身的 API 给外部的 Objective-C 代码使用的话，可以将 Build Setting 中 Swift Compiler - General 里的 Install Objective-C Compatiblity Header 参数设置为 NO，其编译参数为 <code>SWIFT_INSTALL_OBJC_HEADER</code>，此时不会生成 <code>&lt;ProductModuleName&gt;-Swift.h</code> 类型的文件，也就意味着外部组件无法以 Objective-C 的方式引用组件内 Swift 代码的 API。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895849_971549.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1340/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895849_971549.png" alt="image.png"loading="lazy" decoding="async" width="1340" height="156" /></picture></figure></div><p>而当你的组件里如果压根就没有 Objective-C 代码的时候，你可以将 Build Setting 中 Packaging 里 Defines Module 参数设置为 NO，其编译参数为 <code>DEFINES_MODULE</code>, 此时不会生成 <code>&lt;ProductModuleName&gt;.modulemap</code> 类型的文件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895843_244457.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1340/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895843_244457.png" alt="image.png"loading="lazy" decoding="async" width="1340" height="132" /></picture></figure></div><h4>Swift 和 Objective-C 混编的三个“套路”</h4>
<p>基于刚才的例子，我们应该理解了 swift 在编译时是如何找到其他 API 的，以及它又是如何暴露自身 API 的，而这些知识就是解决混编过程中的基础知识，为了加深影响，我们可以将其绘制成 3 个流程图</p>
<p>当 Swift 和 Objective-C 文件同时在一个 App 或者 Unit Test 类型的 target 中，不同类型文件的 API 寻找机制如下</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895824_549886.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895824_549886.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895824_549886.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895824_549886.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895824_549886.png" alt="image.png"loading="lazy" decoding="async" width="2300" height="1366" /></picture></figure></div><p>当 Swift 和 Objective-C 文件在不同 target 中，例如不同 Framework 中，不同类型文件的 API 寻找机制如下</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895817_242132.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895817_242132.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895817_242132.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895817_242132.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895817_242132.png" alt="image.png"loading="lazy" decoding="async" width="2300" height="1366" /></picture></figure></div><p>当 Swift 和 Objective-C 文件同时在一个target 中，例如同一 Framework 中，不同类型文件的 API 寻找机制如下</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895808_708975.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304895808_708975.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304895808_708975.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304895808_708975.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304895808_708975.png" alt="image.png"loading="lazy" decoding="async" width="2396" height="1366" /></picture></figure></div><p>对于第三个流程图，需要做以下补充说明</p>
<ul>
<li>由于 swiftc，也就是 swift 的编译器，包含了大部分的 clang 功能，其中就包含了 clang module，借由组件内已有的 modulemap 文件，swift 编译器就可以轻松找到相应的 Objective-C 代码。</li>
<li>相比于第二个流程而言，第三个流程中的 modulemap 是组件内部的，而第二个流程中，如果想引用其他组件里的 Objective-C 代码，需要引入其他组件里的 modulemap 文件才可以</li>
<li>所以基于这个考虑，并未在流程 3 中标注 modulemap。</li>
</ul>
<h4>构建 Swift 产物的新思路</h4>
<p>在前面的章节里，我们提到了 Swift 找寻 Objective-C 的方式，其中提到了，除了 App 或者 Unit Test 类型的 target 外，其余的情况下都是通过 framework 的 module map 来寻找 Objective-C 的 API，那么如果我们不想使用 framework 的形式呢？</p>
<p>目前来看，这个在 Xcode 中是无法直接实现的，原因很简单，Build Setting 中 Search Path 选项里并没有 modulemap 的 search path 配置参数。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895799_567669.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1422/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895799_567669.png" alt="image.png"loading="lazy" decoding="async" width="1422" height="510" /></picture></figure></div><p>为什么一定需要 modulemap 的 search path 呢？</p>
<p>基于前面了解到的内容，swiftc 包含了 clang 的大部分逻辑，在预编译方面，swiftc 只包含了 clang module 的模式，而没有其他模式，所以 Objective-C 想要暴露自己的 API 就必须通过 modulemap 来完成。</p>
<p>而对于 Framework 这种标准的文件夹结构，modulemap 文件的相对路径是固定的，它就在 Modules 目录中，所以 Xcode 基于这种标准结构，直接内置了相关的逻辑，而不需要将这些配置再暴露出来。</p>
<p>从组件的开发者角度来看，他只需要关心 modulemap 的内容是否符合预期，以及路径是否符合规范。</p>
<p>从组件的使用者角度来看，他只需要正确的引入相应的 Framework 就可以使用到相应的 API。</p>
<p>这种只需要配置 Framework 的方式，避免了配置 header search path，也避免了配置 static library path，可以说是一种很友好的方式，如果再将 modulemap 的配置开放出来，反而显得多此一举。</p>
<p>那如果我们抛开 Xcode，抛开 Framework 的限制，还有别的办法构建 Swift 产物么？</p>
<p>答案是肯定有的，这就需要借助前面所说的 VFS 技术！</p>
<p>假设我们的文件结构如下所示：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">├── LaunchPoint.swift
</div><div class="line">├── README.md
</div><div class="line">├── build
</div><div class="line">├── repo
</div><div class="line">│   └── MyObjcPod
</div><div class="line">│       └── UsefulClass.h
</div><div class="line">└── tmp
</div><div class="line">    ├── module.modulemap
</div><div class="line">    └── vfs-overlay.yaml
</div></code></pre></div>
</div>
<p>其中 <code>LaunchPoint.swift</code> 引用了 <code>UsefulClass.h</code> 中的一个公开 API，并产生了依赖关系。</p>
<p>另外，<code>vfs-overlay.yaml</code> 文件重新映射了现有的文件目录结构，其内容如下：</p>
<div class="block-code" data-language="yaml"><div class="highlight"><pre><span></span><code><div class="line"><span class="p p-Indicator">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="s">&#39;version&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="nv">0</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="s">&#39;roots&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&#39;name&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;/MyObjcPod&#39;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&#39;type&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;directory&#39;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="s">&#39;contents&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&#39;name&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;module.modulemap&#39;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&#39;type&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;file&#39;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">          </span><span class="s">&#39;external-contents&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;tmp/module.modulemap&#39;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">},</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">{</span><span class="w"> </span><span class="s">&#39;name&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;UsefulClass.h&#39;</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">&#39;type&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;file&#39;</span><span class="p p-Indicator">,</span><span class="w"></span>
</div><div class="line"><span class="w">          </span><span class="s">&#39;external-contents&#39;</span><span class="p p-Indicator">:</span><span class="w"> </span><span class="s">&#39;repo/MyObjcPod/UsefulClass.h&#39;</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p p-Indicator">}</span><span class="w"></span>
</div><div class="line"><span class="w">      </span><span class="p p-Indicator">]</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p p-Indicator">}</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p p-Indicator">]</span><span class="w"></span>
</div><div class="line"><span class="p p-Indicator">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>至此，我们通过如下的命令，便可以获得 LaunchPoint 的 swiftmodule，swiftinterface 等文件，具体的示例可以查看我在 github 上的链接 - <a href="https://github.com/SketchK/manually-expose-objective-c-API-to-swift-example">manually-expose-objective-c-API-to-swift-example</a></p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">swiftc -c LaunchPoint.swift -emit-module -emit-module-path build/LaunchPoint.swiftmodule -module-name index -whole-module-optimization -parse-as-library -o build/LaunchPoint.o -Xcc -ivfsoverlay -Xcc tmp/vfs-overlay.yaml -I /MyObjcPod
</div></code></pre></div>
</div>
<p>那这意味着什么呢？</p>
<p>这就意味着，只提供相应的 <code>.h</code> 文件和 <code>.modulemap</code> 文件就可以完成 Swift 二进制产物的构建，而不再依赖 Framework 的实体。同时，对于 CI 系统来说，在构建产物时，可以避免下载无用的二进制产物（<code>.a</code> 文件），这从某种程度上会提升编译效率。</p>
<p>如果你没太理解上面的意思，我们可以展开说说。</p>
<p>例如，对于 PodA 组件而言，它自身依赖 PodB 组件，在使用原先的构建方式时，我们需要拉取 PodB 组件的完整 Framework 产物，这会包含 Headers 目录，Modules 目录里的必要内容，当然还会包含一个二进制文件（PodB），但在实际编译 PodA 组件的过程中，我们并不需要 B 组件里的二进制文件，而这让拉取完整的 Framework 文件显得多余了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304895786_708712.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_870/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304895786_708712.png" alt="image.png"loading="lazy" decoding="async" width="870" height="306" /></picture></figure></div><p>而借助 VFS 技术，我们就能避免拉取多余的二进制文件，进一步提升 CI 系统的编译效率。</p>
<h2>总结</h2>
<p>感谢你的耐心阅读，至此，整篇文章终于结束了，通过这篇文章，我想你应该：</p>
<ul>
<li>理解 Objective-C 的三种预编译的工作机制，其中 clang module 做到了真正意义上的语义引入，提升了编译的健壮性和扩展性。</li>
<li>在 Xcode 的 search path 的各种技术细节使用到了 hmap 技术，通过加载映射表的方式避免了大量重复的 IO 操作，可以提升编译效率。</li>
<li>在处理 Framework 的头文件索引时，总是会先搜索 Headers 目录，再搜索 PrivateHeader 目录</li>
<li>理解 Xcode Phases 构建系统中，Public 代表公开头文件，Private 代表不需要使用者感知，但物理存在的文件， 而 Project 代表不应让使用者感知，且物理不存在的文件。</li>
<li>不使用 Framework 的情况下且以 <code>#import &lt;A/A.h&gt;</code> 这种标准方式引入头文件时，在 CocoaPods 上使用 hmap 并不会提升编译速度。</li>
<li>通过 <code>cocoapods-hmap-built</code> 插件，可以将大型项目的全链路时长节省 45% 以上，Xcode 打包环节的时长节省 50% 以上。</li>
<li>clang module 的构建机制确保了其不受上下文影响（独立编译空间），复用效率高（依赖决议），唯一性（参数哈希化）</li>
<li>系统组件通过已有的 Framework 文件结构实现了构建 module 的基本条件 ，而非系统组件通过 VFS 虚拟出相似的 Framework 文件 结构，进而具备了编译的条件。</li>
<li>可以粗浅的将 Clang Module 里的 <code>.h/m</code>，<code>.moduelmap</code>，<code>.pch</code> 的概念对应为 Swift Module 里的 <code>.swift</code>，<code>.swiftinterface</code>，<code>.swiftmodule</code> 的概念</li>
<li>理解三种具有普适性的 Swift 与 Objective-C 混编方法<ul>
<li>同一 target 内（App 或者 Unit 类型），基于 <code>&lt;PorductModuleName&gt;-Swift.h</code> 和 <code>&lt;PorductModuleName&gt;-Bridging-Swift.h</code></li>
<li>同一 target 内，基于 <code>&lt;PorductModuleName&gt;-Swift.h</code> 和 clang 自身的能力</li>
<li>不同 target 内，基于 <code>&lt;PorductModuleName&gt;-Swift.h</code> 和 <code>module.modulemap</code></li>
</ul>
</li>
<li>利用 VFS 机制构建，可以在构建 Swift 产物的过程中避免下载无用的二进制产物，进一步提升编译效率</li>
</ul>
<p>最后，在编写这篇文章的过程中，我的同事 @叶樉 和 @宋旭陶 给与了我许多指导与帮助，也正是在大家的共同努力下，才有了这篇文章，希望它能对亲爱的读者您，有所帮助！</p>
<h2>参考文档</h2>
<ul>
<li><a href="https://developer.apple.com/videos/play/wwdc2013/404/">Apple - WWDC 2013 Advances in Objective-C</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2018/415/">Apple - WWDC 2018 Behind the Scenes of the Xcode Build Process</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2019/416/">Apple - WWDC 2019 Binary Frameworks in Swift</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2020/10147">Apple - WWDC 2020 Distribute binary frameworks as Swift packages</a></li>
<li><a href="https://swift.org/blog/abi-stability-and-apple/">Swift org - Evolving Swift On Apple Platforms After ABI Stability</a></li>
<li><a href="https://swift.org/blog/abi-stability-and-more/">Swift org - ABI Stability and More</a></li>
<li><a href="https://stackoverflow.com/questions/1044360/import-using-angle-brackets-and-quote-marks">StackOverflow - <code>#import</code> using angle brackets &lt; &gt; and quote marks “ ”</a></li>
<li><a href="https://stackoverflow.com/questions/7439192/xcode-copy-headers-public-vs-private-vs-project">StackOverflow - Xcode: Copy Headers: Public vs. Private vs. Project?</a></li>
<li><a href="https://stackoverflow.com/questions/10584936/understanding-xcodes-copy-headers-phase/18910393#18910393">StackOverflow - Understanding Xcode’s Copy Headers phase</a></li>
<li><a href="https://help.apple.com/xcode/mac/current/#/dev50bab713d">Xcode Help - What are build phases?</a></li>
<li><a href="https://xcodebuildsettings.com/">Xcode Build Settings</a></li>
<li><a href="https://nerdranchighq.wpengine.com/blog/manual-swift-understanding-the-swift-objective-c-build-pipeline/">Big Nerd Ranch - Manual Swift: Understanding the Swift/Objective-C Build Pipeline</a></li>
<li><a href="https://www.bignerdranch.com/blog/build-log-groveling-for-fun-and-profit-manual-swift-continued/">Big Nerd Ranch - Build Log Groveling for Fun and Profit: Manual Swift Continued</a></li>
<li><a href="https://www.bignerdranch.com/blog/build-log-groveling-for-fun-and-profit-part-2-even-more-manual-swift/">Big Nerd Ranch - Build Log Groveling for Fun and Profit, Part 2: Even More Manual Swift</a></li>
<li><a href="https://qualitycoding.org/precompiled-header/">Quality Coding - 4 Ways Precompiled Headers Cripple Your Code</a></li>
<li><a href="https://www.youtube.com/watch?v=o3HG0Z3yc5c">try! Swift Tokyo 2018 - Exploring Clang Modules</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[cocoapods-hmap-prebuilt - 一款可以让大型 iOS 工程编译速度提升 50% 的工具]]></title><guid>https://swiftsiqi.com/posts/a-solution-of-speed-up-your-iOS-project</guid><link>https://swiftsiqi.com/posts/a-solution-of-speed-up-your-iOS-project</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 19 Mar 2022 05:28:03 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这篇文章是我早年在美团技术博客上发布的一篇文章, 部分内容可能已经过时, 请开发者注意.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<h2>cocoapods-hmap-prebuilt 是什么？</h2>
<p>cocoapods-hmap-prebuilt 是美团平台迭代组自研的一款 cocoapods 插件，以 <a href="https://clang.llvm.org/doxygen/classclang_1_1HeaderMap.html">Header Map 技术</a> 为基础，进一步提升代码的编译速度，完善头文件的搜索机制。</p>
<p>虽然以二进制组件的方式构建 App 是 HPX (公司移动端统一持续集成/交付平台)的主流解决方案，但在某些场景下（Profile、Address/Thread/UB/Coverage Sanitizer、App 级别静态检查、ObjC 方法调用兼容性检查等等等等），我们的构建工作还是需要以全源码编译的方式进行；再结合实际开发过程中，大多是以源码的方式开发，所以我们将实验对象设置为基于全源码编译的流程。</p>
<p>废话不多说，我们来看看它的实际使用效果！</p>
<p>总的来说，以美团和大众点评的全源码编译流程为实验对象的前提下，cocoapods-hmap-prebuilt 插件能将总链路提升 45% 以上的速度，在 Xcode 打包环节上能提升 50% 以上的速度，是不是有点动心了？</p>
<p>为了更好的理解这个插件的价值和功能，我们不妨先理解一下当前的工程中存在的问题！</p>
<h2>为什么现有的项目不够好？</h2>
<p>目前公司内的 App 都是基于 CocoaPods 做包管理方面的工作，所以在实际的开发过程中，CocoaPods 会在 <code>Pods/Header/</code> 目录下添加组件名目录和头文件软链，类似于下面的形式:</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">/Users/sketchk/Desktop/MyApp/Pods
</div><div class="line">└── Headers
</div><div class="line">    ├── Private
</div><div class="line">    │   └── AFNetworking
</div><div class="line">    │       ├── AFHTTPRequestOperation.h -&gt; ./XXX/AFHTTPRequestOperation.h
</div><div class="line">    │       ├── AFHTTPRequestOperationManager.h -&gt; ./XXX/AFHTTPRequestOperationManager.h
</div><div class="line">    │       ├── ...
</div><div class="line">    │       └── UIRefreshControl+AFNetworking.h -&gt; ./XXX/UIRefreshControl+AFNetworking.h
</div><div class="line">    └── Public
</div><div class="line">        └── AFNetworking
</div><div class="line">            ├── AFHTTPRequestOperation.h -&gt; ./XXX/AFHTTPRequestOperation.h
</div><div class="line">            ├── AFHTTPRequestOperationManager.h -&gt; ./XXX/AFHTTPRequestOperationManager.h
</div><div class="line">            ├── ...
</div><div class="line">            └── UIRefreshControl+AFNetworking.h -&gt; ./XXX/UIRefreshControl+AFNetworking.h
</div></code></pre></div>
</div>
<p>也正是通过这样的目录结构和软链，CocoaPods 得以在 Header Search Path 中添加如下的参数，使得预编译环节顺利进行。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">$(</span>inherited<span class="k">)</span>
</div><div class="line"><span class="si">${</span><span class="nv">PODS_ROOT</span><span class="si">}</span>/Headers/Private
</div><div class="line"><span class="si">${</span><span class="nv">PODS_ROOT</span><span class="si">}</span>/Headers/Private/AFNetworking
</div><div class="line"><span class="si">${</span><span class="nv">PODS_ROOT</span><span class="si">}</span>/Headers/Public
</div><div class="line"><span class="si">${</span><span class="nv">PODS_ROOT</span><span class="si">}</span>/Headers/Public/AFNetworking
</div></code></pre></div>
</div>
<p>虽然这种构建 search path 的方式解决了预编译的问题，但在某些项目中，例如多达 400+ 组件的巨型项目中，会造成以下几点问题：</p>
<ol>
<li>大量的 header search path 路径，会造成编译参数中的 <code>-I</code> 选项极速膨胀，在达到一定长度后，甚至会造成无法编译的情况</li>
<li>目前美团的工程中，已经有近 5W 个头文件，这意味着不论是头文件的搜索过程，还是软链的创建过程，都会引起大量的文件 IO 操作，进而会产生一些耗时操作。</li>
<li>编译时间会随着组件数量急剧增长，以美团和大众点评有 400+ 个组件的体量为参考，全源码打包耗时均为 1 小时以上。</li>
<li>基于路径顺序查找头文件的方式有潜在的风险，例如重名头文件的情况，排在后面的头文件永远无法参与编译</li>
<li>由于 <code>${PODS_ROOT}/Headers/Private</code> 路径的存在，让引用其他组件的私有头文件变为了可能。</li>
</ol>
<p>上面的问题，好一点的不过是浪费了 1 个小时而已，而不好的情况则是让有风险的代码上线了，你说开发者头疼不头疼？</p>
<h2>Header Map 是个啥？</h2>
<p>还好 cocoapods-hmap-prebuilt 的出现，让这些问题变成了历史，不过要想理解它为什么能解决这些问题，我们得先理解一下什么是 Header Map！</p>
<p><strong>Header Map 其实是一组头文件信息映射表！</strong></p>
<p>为了更直观的理解 Header Map，我们可以在 build setting 中开启 Use Header Map 选项，真实的体验一下它。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898803_2241535.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898803_2241535.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898803_2241535.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898803_2241535.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898803_2241535.png" alt="image.png"loading="lazy" decoding="async" width="1818" height="136" /></picture></figure></div><p>然后在 build log 里获取相应组件里对应文件的编译命令，并在最后加上 <code>-v</code> 参数，来查看其运行的秘密：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">clang &lt;list of arguments&gt; -c some-file.m -o some-file.o -v
</div></code></pre></div>
</div>
<p>在 console 的输出内容中，我们会发现一段有意思的内容：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898791_053902.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898791_053902.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898791_053902.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898791_053902.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898791_053902.png" alt="image.png"loading="lazy" decoding="async" width="1748" height="808" /></picture></figure></div><p>通过上面的图，我们可以看到编译器将寻找头文件的顺序和对应路径展示出来了，而在这些路径中，我们看到了一些陌生的东西，即后缀名为 <code>.hmap</code> 的文件，后面还有个括号写着 headermap。</p>
<p>没错！它就是 Header Map 的实体。</p>
<p>此时 clang 已经在刚才提到的 hmap 文件里塞入了一份头文件名和头文件路径的映射表，不过它是一种二进制格式的文件，为了验证这个的说法，我们可以通过 milend 编写的<a href="https://github.com/milend/hmap">hmap 工具</a>来查其内容。</p>
<p>在执行相关命令（即 <code>hmap print</code>）后，我们可以发现这些 hmap 里保存的信息结构大致如下, 类似于一个 key-value 的形式，key 值是头文件的名称，value 是头文件的实际物理路径：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898779_327541.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898779_327541.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898779_327541.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898779_327541.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898779_327541.png" alt="image.png"loading="lazy" decoding="async" width="1884" height="384" /></picture></figure></div><p>需要注意，映射表的键值内容会随着使用场景产生不同的变化，例如头文件引用是在 <code>&quot;...&quot;</code> 的形式下，还是 <code>&lt;...&gt;</code> 的形式下，又或是在 Build Phase 里 Header 的配置情况。例如，你将头文件设置为 public 的时候，在某些 hmap 中，它的 key 值就为 <code>PodA/ClassA</code>，而将其设置为 project 的时候，它的 key 值可能就是 <code>ClassA</code>，而配置这些信息的地方，如下图所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898771_465502.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898771_465502.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898771_465502.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898771_465502.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898771_465502.png" alt="image.png"loading="lazy" decoding="async" width="1900" height="456" /></picture></figure></div><p>至此我想你应该了解到 Header Map 到底是个什么东西了。</p>
<p>当然这种技术也不是一个什么新鲜事儿，在 Facebook 的 <a href="https://buck.build/">buck</a> 工具中也提供了类似的东西，只不过文件类型变成了 <code>HeaderMap.java</code> 的样子。</p>
<p>此时，我估计你可能并不会对 buck 产生太多的兴趣，而是开始思考上一张图中 Headers 的 public，private，project 到底代表着什么意思，好像我从来没怎么关注过，以及为什么它会影响 hmap 里的内容？</p>
<h2>Public，Private，Project 是个啥？</h2>
<p>在 Apple 官方的 <a href="https://help.apple.com/xcode/mac/current/#/dev50bab713d">Xcode Help - What are build phases?</a> 文档中，我们可以看到如下的一段解释：</p>
<div class="blockquote"><blockquote><p>Associates public, private, or project header files with the target. Public and private headers define API intended for use by other clients, and are copied into a product for installation. For example, public and private headers in a framework target are copied into Headers and PrivateHeaders subfolders within a product. Project headers define API used and built by a target, but not copied into a product. This phase can be used once per target.</p>
</blockquote></div>
<p>总的来说，我们可以知道一点，就是 Build Phases - Headers 中提到 Public 和 Private 是指可以供外界使用的头文件，而 Project 中的头文件是不对外使用的，也不会放在最终的产物中。</p>
<p>如果你继续翻阅一些资料，例如 <a href="https://stackoverflow.com/questions/7439192/xcode-copy-headers-public-vs-private-vs-project">StackOverflow - Xcode: Copy Headers: Public vs. Private vs. Project?</a> 和 <a href="https://stackoverflow.com/questions/10584936/understanding-xcodes-copy-headers-phase/18910393#18910393">StackOverflow - Understanding Xcode’s Copy Headers phase</a>，你会发现在早期 Xcode Help 的 Project Editor 章节里，有一段名为 Setting the Role of a Header File 的段落，里面详细记载了三个类型的区别。</p>
<div class="blockquote"><blockquote><p><strong>Public</strong>: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
<strong>Private</strong>: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked “private”. Thus the symbols are visible to all clients, but clients should understand that they’re not supposed to use them.
<strong>Project</strong>: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.</p>
</blockquote></div>
<p>至此我们应该彻底了解了 Public，Private，Project 的区别，简而言之，Public 还是通常意义上的 Public，Private 则代表 In Progress 的含义，至于 Project 才是通常意义上的 Private 含义。</p>
<p>此时，你会不会联想到 CocoaPods 中 Podspec 的 Syntax 里还有 <code>public_header_files</code> 和 <code>private_header_files</code> 两个字段，它们的真实含义是否和 Xcode 里的概念冲突呢？</p>
<p>这里我们仔细阅读一下<a href="https://guides.cocoapods.org/syntax/podspec.html">官方文档的解释</a>，尤其是 <code>private_header_files</code> 字段。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898759_112975.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1523/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898759_112975.png" alt="image.png"loading="lazy" decoding="async" width="1523" height="580" /></picture></figure></div><p>我们可以看到，<code>private_header_files</code> 在这里的含义是说，它本身是相对于 public 而言的，这些头文件本义是不希望暴露给用户使用的，而且也不会产生相关文档，但是在构建的时候，会出现在最终产物中，只有既没有被 public 和 private 标注的头文件，才会被认为是真正的私有头文件，且不出现在最终的产物里。</p>
<p>其实看起来，CocoaPods 对于 public 和 private 的官方解释是和 Xcode 中的描述一致的，两处的 Private 并非我们通常理解的 Private，它的本意更应该是开发者准备对外开放，但又没完全 Ready 的头文件，更像一个 In Progress 的含义。</p>
<p>这一块是不是有点让人大跌眼镜，那么，在现实世界中，我们是否正确的使用了它们呢？</p>
<h2>为什么用原生的 hmap 不能改善编译速度？</h2>
<p>前面我们介绍了 hmap 是什么，以及怎么开启它（启用 Build Setting 中的 Use Header Map 选项），也介绍了一些影响生成 hmap 的因素（Public，Private，Project）</p>
<p>那是不是我只要开启 Xcode 提供的 Use Header Map 就可以提升编译速度了呢?</p>
<p>很可惜，答案是不行的！</p>
<p>至于原因，我们就从下面的例子开始说起，假设我们有一个基于 CocoaPods 构建的全源码工程项目，它的整体结构如下：</p>
<ul>
<li>首先，Host 和 Pod 是我们的两个 Project，Pods 下的 target 的产物类型为 static library。</li>
<li>其次，Host 底下会有一个同名的 Target，而 Pods 目录下会有 n+1 个 target，其中 n 取决于你依赖的组件数量，而 1 是一个名为 Pods-XXX 的 target，最后，Pods-XXX 这个 target 的产物会被 Host 里的 target 所依赖。</li>
</ul>
<p>整个结构看起来如下所示。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898749_160522.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898749_160522.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898749_160522.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898749_160522.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898749_160522.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="1181" /></picture></figure></div><p>当构建的产物类型为 Static Library 的时候，CocoaPods 在创建头文件产物过程中，它的逻辑大致如下:</p>
<ul>
<li>不论 podspec 里如何设置 <code>public_header_files</code> 和 <code>private_header_files</code>，相应的头文件都会被设置为 Project 类型</li>
<li>在 <code>Pods/Headers/Public</code> 中会保存所有被声明为 <code>public_header_files</code> 的头文件</li>
<li>在 <code>Pods/Headers/Private</code> 中会保存所有头文件，不论是 <code>public_header_files</code> 或者 <code>private_header_files</code> 描述到，还是那些未被描述的，这个目录下是当前组件的所有头文件全集</li>
<li>如果 podspec 里未标注 public 和 private 的时候，<code>Pods/Headers/Public</code> 和 <code>Pods/Headers/Private</code> 的内容一样且会包含所有头文件。</li>
</ul>
<p>正是由于这种机制，会导致一些有意思的问题发生。</p>
<ul>
<li>首先，由于所有头文件都被当做最终产物保留下来，在结合 header search path 里 <code>Pods/Headers/Private</code> 路径的存在，我们完全可以引用到其他组件里的私有头文件，例如我只要使用 <code>#import &lt;SomePod/Private_Header.h&gt;</code> 的方式，就会命中私有文件的匹配路径。</li>
<li>其次，就是在 Static Library 的状况下，一旦我们开启了 Use Header Map，结合组件里所有头文件的类型为 Project 的情况，这个 hmap 里只会包含 <code>#import &quot;ClassA.h&quot;</code> 的键值引用，也就是说只有 <code>#import &quot;ClassA.h&quot;</code> 的方式才会命中 hmap 的策略，否则都将通过 header search path 寻找其相关路径，例如下图中的 PodB，在其 build 的过程中，Xcode 会为 PodB 生成 5 个 hmap 文件，也就是说这 5 个文件只会在编译 PodB 中使用，其中 PodB 会依赖 PodA 的一些头文件，但由于 PodA 中的头文件都是 Project 类型的，所以其在 hmap 里的 key 全部为 <code>ClassA.h</code> ，也就是说我们只能以 <code>#import &quot;ClassA.h&quot;</code> 的方式引入。</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898736_214389.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304898736_214389.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304898736_214389.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304898736_214389.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304898736_214389.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="672" /></picture></figure></div><p>而我们也知道，在引用其他组件的时候，通常都会采用 <code>#import &lt;A/A.h&gt;</code> 的方式引入。至于为什么会用这种方式，一方面是这种写法会明确头文件的由来，避免问题，另一方面也是这种方式可以让我们在是否开启 clang module 中随意切换，当然还有一点就是，Apple 在 WWDC 里曾经不止一次的建议开发者使用这种方式引入头文件。</p>
<p>接着上面的话题来说，所以说在 Static Library 的情况下且以 <code>#import &lt;A/A.h&gt;</code> 这种标准方式引入头文件时，开启 Use Header Map 选项并不会帮我们提升编译速度。</p>
<p>但真的就没有办法使用 Header Map 了么？</p>
<h2>cocoapods-hmap-prebuilt 诞生了</h2>
<p>当然，总是有办法解决的，我们完全可以自己动手做一个基于 CocoaPods 规则下的 hmap 文件，正是基于这个想法，美团自研的 cocoapods-hmap-prebuilt 插件诞生了！</p>
<p>它的核心功能并不多，大概有以下几点：</p>
<ul>
<li>借助 CocodPods 处理 Header Search Path 和创建头文件 soft link 的时机，构建了头文件索引表并以此生成 n+1 个 hmap 文件（n 是每个组件自己的 private header 信息，1 是所有组件公共的 public header 信息）</li>
<li>重写 xcconfig 文件里的 header search path 到对应的 hmap 文件上，一条指向组件自己的 private hmap，一条指向所有组件共用的 public hmap。</li>
<li>针对 public hmap 里的重名头文件进行了特殊处理，只允许保存<code>组件名/头文件名</code>方式的 key-value，排查重名头文件带来的异常行为。</li>
<li>将组件自身的 Ues Header Map 功能关闭，减少不必要的文件创建和读取</li>
</ul>
<p>听起来可能有点绕，内容也有点多，不过这些你都不用关心，你只需要通过以下 2 个步骤就能将其使用起来：</p>
<ol>
<li>在 Gemfile 里声明插件</li>
<li>在 Podfile 里使用插件</li>
</ol>
<div class="block-code" data-language="ruby"><div class="highlight"><pre><span></span><code><div class="line"><span class="sr">//</span> <span class="n">this</span> <span class="n">is</span> <span class="n">part</span> <span class="n">of</span> <span class="no">Gemfile</span>
</div><div class="line"><span class="n">source</span> <span class="s1">&#39;http://sakgems.sankuai.com/&#39;</span> <span class="k">do</span>
</div><div class="line">  <span class="n">gem</span> <span class="s1">&#39;cocoapods-hmap-prebuilt&#39;</span>
</div><div class="line">  <span class="n">gem</span> <span class="s1">&#39;XXX&#39;</span>
</div><div class="line">  <span class="o">...</span>
</div><div class="line"><span class="k">end</span>
</div><div class="line">
</div><div class="line"><span class="sr">//</span> <span class="n">this</span> <span class="n">is</span> <span class="n">part</span> <span class="n">of</span> <span class="no">Podfile</span>
</div><div class="line"><span class="n">target</span> <span class="s1">&#39;XXX&#39;</span> <span class="k">do</span>
</div><div class="line">  <span class="n">plugin</span> <span class="s1">&#39;cocoapods-hmap-prebuilt&#39;</span>
</div><div class="line">  <span class="n">pod</span> <span class="s1">&#39;XXX&#39;</span>
</div><div class="line">  <span class="o">...</span>
</div><div class="line"><span class="k">end</span>
</div></code></pre></div>
</div>
<p>除此之外，为了拓展其实用性，我们还提供了头文件补丁（解决重名头文件的定向选取）和环境变量注入（无侵入的在其他系统中使用）的能力，便于其在不同场景下的使用。</p>
<h2>总结</h2>
<p>至此，关于 cocoapods-hmap-prebuilt 的介绍就要结束了。</p>
<p>回看整个故事的开始，Header Map 是我在研究 Swift 和 Objective-C 混编过程中发现的一个很小的知识点，而且 Xcode 自身就实现了一套基于 Header Map 的功能，在实际的使用过程中，它的表现并不理想。</p>
<p>但幸运的是，在后续的探索的过程中，我们发现了为什么 Xcode 的 Header Map 没有生效，以及为什么它与 CocoaPods 出现了不兼容的情况，虽然它的原理并不复杂，核心点就是将文件查找和读取等 IO 操作编变成了内存读取操作，但结合实际的业务场景，我们发现它的收益是十分可观的。</p>
<p>或许这是在提醒我们，要永远对技术保持一颗好奇的心！</p>
<p>最后，非常感谢 @宋旭陶 同学在工作之余，和我一起完成了 cocoapods-hmap-prebuilt 插件的开发工作，也非常感谢 @叶樉 同学，在我困惑的时候给出很多富有建设性的指导和意见。</p>
<div class="blockquote"><blockquote><p>其实利用 clang module 技术也可以解决本文一开始提到的几个问题，但它并不在这篇文章的讨论范围中，如果你对 clang module 或者对 Swift 与 Objective-C 混编感兴趣，欢迎阅读参考文档中的 《从预编译的角度理解 Swift 与 Objective-C 及混编机制》一文，以了解更多的详细信息。</p>
</blockquote></div>
<h2>参考文档</h2>
<ul>
<li><a href="https://developer.apple.com/videos/play/wwdc2018/415/">Apple - WWDC 2018 Behind the Scenes of the Xcode Build Process</a></li>
<li><a href="https://opensource.apple.com/source/lldb/lldb-167.2/llvm/tools/clang/lib/Lex/HeaderMap.cpp.auto.html">Apple 的 HeaderMap.cpp 源码</a></li>
<li>美团技术学院- 从预编译的角度理解 Swift 与 Objective-C 及混编机制</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[iOS系统中导航栏的转场解决方案与最佳实践]]></title><guid>https://swiftsiqi.com/posts/best-practice-of-navigation-controller-in-meituan-ios-app</guid><link>https://swiftsiqi.com/posts/best-practice-of-navigation-controller-in-meituan-ios-app</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 19 Feb 2022 05:23:58 +0000</pubDate><content:encoded><![CDATA[<div class="blockquote"><blockquote><p>这篇文章是我早年在美团技术博客上发布的一篇文章, 部分内容可能已经过时, 请开发者注意.
将文章同步到个人博客主要是为了同步和备份.</p>
</blockquote></div>
<h2>背景</h2>
<p>目前，开源社区和业界内已经存在一些 iOS 导航栏转场的解决方案，但对于历史包袱沉重的美团 App 而言，这些解决方案并不完美。有的方案不能满足复杂的页面跳转场景，有的方案迁移成本较大，为此我们提出了一套解决方案并开发了相应的转场库，目前该转场库已经成为美团点评多个 App 的基础组件之一。</p>
<p>在美团 App 开发的早期，涉及到导航栏样式改变的需求时，经常会遇到转场效果不佳或者与预期样式不符的“小问题”。在业务体量较小的情况下，为了满足快速的业务迭代，通常会使用硬编码的方式来解决这一类“小问题”。但随着美团 App 业务的高速发展，这种硬编码的方式遇到了以下的挑战：</p>
<ol>
<li>业务模块的不断增加，导致使用硬编码方式编写的代码维护成本增加，代码质量迅速下降。</li>
<li>大型 App 的路由系统使得页面间的跳转变得更加自由和灵活，也使得导航栏相关的问题激增，不但增加了问题的排查难度，还降低了整体的开发效率。</li>
<li>App 中的导航栏属于各个业务方的公用资源，由于缺乏相应的约束机制和最佳实践，导致业务方之间的代码耦合程度不断增加。</li>
</ol>
<p>从各个角度来看，硬编码的方式已经不能很好的解决此类问题，美团 App 需要一个更加合理、更加持久、更加简单易行的解决方案来处理导航栏转场问题。</p>
<p>本文将从导航栏的概念入手，通过讲解转场过程中的状态管理、转换时机和样式变化等内容，引出了在大型应用中导航栏转场的三种常见解决方案，并对美团点评的解决方案进行剖析。</p>
<h2>重新认识导航栏</h2>
<h3>导航栏里的 MVC</h3>
<p>在 iOS 系统中， 苹果公司不仅建议开发者遵循 MVC 开发框架，在它们的代码里也可以看到 MVC 的影子，导航栏组件的构成就是一个类似 MVC 的结构，让我们先看看下面这张图：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899611_735809.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1378/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899611_735809.png" alt="image.png"loading="lazy" decoding="async" width="1378" height="1000" /></picture></figure></div><p>在这张图里，我们可以将 UINavigationController 看做是 C，UINavigationBar 看做是 V，而 UIViewController 和 UINavigationItem 组成的 Stack 可以看做是 M。这里要说明的是，每个 UIViewController 都有一个属于自己的 UINavigationItem，也就是说它们是一一对应的。</p>
<p>UINavigationController 通过驱动 Stack 中的 UIViewController 的变化来实现 View 层级的变化，也就是 UINavigationBar 的改变。而 UINavigationBar 样式的数据就存储在 UIViewController 的 UINavigationItem 中。这也就是为什么我们在代码里只要设置 <code>self.navigationItem</code> 的相关属性就可以改变 UINavigationBar 的样式。</p>
<p>很多时候，国内的开发者会将 UINavigationBar 和 UINavigationController 混在一起叫导航栏，这样的做法不仅增加了开发者之间的沟通成本，也容易导致误解。毕竟它们是两个完全不一样的东西。</p>
<p>所以本文为了更好的阐明问题，会采用英文区分不同的概念，当需要描述笼统的导航栏概念时，会使用导航栏组件一词。</p>
<p>通过这一节的回顾，我们应该明确了 NavigationItem、ViewController、NavigationBar 和 NavigationController 在 MVC 框架下的角色。下面我们会重新梳理一下导航栏的生命周期和各个相关方法的调用顺序。</p>
<h3>导航栏组件的生命周期</h3>
<p>大家可以通过下图获得更为直观的感受，进而了解到导航栏组件在 push 过程中各个方法的调用顺序。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899601_530563.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899601_530563.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899601_530563.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899601_530563.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899601_530563.png" alt="image.png"loading="lazy" decoding="async" width="1798" height="1275" /></picture></figure></div><p>值得注意的地方有两点：</p>
<p>第一个是 UINavigationController 作为 UINavigationBar 的代理，在没有特殊需求的情况下，不应该修改其代理方法，这里是通过符号断点获取它们的调用顺序。如果我们创建了一个自定义的导航栏组件系统，它的调用顺序可能会与此不同。</p>
<p>第二个是用虚线圈起来的方法，它们也有可能不被调用，这与 ViewController 里的布局代码相关，假设跳转到新页面后，新旧页面中的控件位置会发生变化，或者由于数据改变驱动了控件之间的约束关系发生变化，这就会带来新一轮的布局，进而触发 <code>viewWillLayoutSubview</code> 和 <code>viewDidLayoutSubview</code> 这两个方法。当然，具体的调用顺序会与业务代码紧密相关，如果我们发现顺序有所不同，也不必惊慌。</p>
<p>下面这张图展示了导航栏在 pop 过程中各个方法的调用顺序：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899588_942024.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899588_942024.png" alt="image.png"loading="lazy" decoding="async" width="3550" height="2550" /></picture></figure></div><p>除了上面说到的两点，pop 过程中还需要注意一点，那就是从 B 返回到 A 的过程中，A 视图控制器的 <code>viewDidLoad</code> 方法并不会被调用。关于这个问题，只要提醒一下，大多数人都会反应过来是为什么。不过在实际开发过程中，总会有人忘记这一点。</p>
<p>通过这两个图，我们已经基本了解了导航栏组件的生命周期和相关方法的调用顺序，这也是后面章节的理论基础。</p>
<h3>导航栏组件的改变与革新</h3>
<p>导航栏组件在 iOS 11 发布时，获得了重大更新，这个更新可不是增加了一个大标题样式（Large Title Display Mode）那么简单，需要注意的地方大概有两点：</p>
<ol>
<li>导航栏全面支持 Auto Layout 且 NavigationBar 的层级发生了明显的改变，关于这一点可以阅读 <a href="http://sketchk.xyz/2018/02/23/How-to-make-your-UIBarButtonItem-perfect-match-in-iOS/">UIBarButtonItem 在 iOS 11 上的改变及应对方案</a> 。</li>
<li>由于引进了 Safe Area 等概念，<code>topLayoutGuide</code> 和 <code>bottomLayoutGuide</code> 等属性会逐渐废弃，虽然变化不大，但如果我们的导航栏在转场过程中总是出现视图上下移动的现象，不妨从这个方面思考一下，如果想深究可以查看 <a href="https://developer.apple.com/videos/play/wwdc2017/412/">WWDC 2017 Session 412</a>。</li>
</ol>
<h2>导航栏组件到底怎么了？</h2>
<p>经常有人说 iOS 的原生导航栏组件不好使用，抱怨主要集中在导航栏组件的状态管理和控件的布局问题上。</p>
<p>控件的布局问题随着 iOS 11 的到来已经变得相对容易处理了不少，但导航栏组件的状态管理仍然让开发者头疼不已。</p>
<p>可能已经有朋友在思考导航栏组件的状态管理到底是什么东西？不要着急，下面的章节就会做相关的介绍。</p>
<h3>导航栏的状态管理</h3>
<p>虽然导航栏组件的 push 和 pop 动画给人一种每次操作后都会创建一遍导航栏组件的错觉，但实际上这些 ViewController 都是由一个 NavigationController 所管理，所以你看到的 NavigationBar 是唯一的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899558_540112.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1477/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899558_540112.png" alt="image.png"loading="lazy" decoding="async" width="1477" height="824" /></picture></figure></div><p>在 NavigationController 的 Stack 存储结构下，每当 Stack 中的 ViewController 修改了导航栏，势必会影响其他 ViewController 展示的效果。</p>
<p>例如下图所示的场景，如果 NavigationBar 原先的颜色是绿色，但之后进入 Stack 里的 ViewController 将 NavigationBar 颜色修改为紫色后，在此之后 push 的 ViewController 会从默认的绿色变为紫色，直到有新的 ViewController 修改导航栏颜色才会发生变化。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899551_377897.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899551_377897.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899551_377897.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899551_377897.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899551_377897.png" alt="image.png"loading="lazy" decoding="async" width="2519" height="677" /></picture></figure></div><p>虽然在 push 过程中，NavigationBar 的变化听起来合情合理，但如果你在 NavigationBar 为绿色的 ViewController 里设置不当的话，那么当你 pop 回这个 ViewController 时，NavigationBar 可就不一定是绿色了，它还会保持为紫色的状态。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899544_852449.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899544_852449.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899544_852449.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899544_852449.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899544_852449.png" alt="image.png"loading="lazy" decoding="async" width="2519" height="677" /></picture></figure></div><p>通过这个例子，我们大概会意识到在导航栏里的 Stack 中，每个 ViewController 都可以永久的影响导航栏样式，这种全局性的变化要求我们在实际开发中必须坚持“谁修改，谁复原”的原则，否则就会造成导航栏状态的混乱。这不仅仅是样式上的混乱，在一些极端状况下，还有可能会引起 Stack 混乱，进而造成 Crash 的情况。</p>
<h3>导航栏样式转换的时机</h3>
<p>我们刚才提到了“谁修改，谁复原”的原则，但何时修改，何时复原呢？</p>
<p>对于那些存储在 Stack 中的 ViewController 而言，它其实就是在不断的经历 appear 和 disappear 的过程，结合 ViewController 的生命周期来看，<code>viewWillAppear:</code> 和 <code>viewWillDisappear:</code> 是两个完美的时间节点，但很多人却对这两个方法的调用存在疑惑。</p>
<p>苹果公司在它的 API 文档中专门用了一段文字来解答大家的疑惑，这段文字的标题为《Handling View-Related Notifications》，在这里我们直接引用原文：</p>
<div class="blockquote"><blockquote><p>When the visibility of its views changes, a view controller automatically calls its own methods so that subclasses can respond to the change. Use a method like viewWillAppear: to prepare your views to appear onscreen, and use the viewWillDisappear: to save changes or other state information. Use other methods to make appropriate changes.
Figure 1 shows the possible visible states for a view controller’s views and the state transitions that can occur. Not all ‘will’ callback methods are paired with only a ‘did’ callback method. You need to ensure that if you start a process in a ‘will’ callback method, you end the process in both the corresponding ‘did’ and the opposite ‘will’ callback method.</p>
</blockquote></div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899536_706758.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_925/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899536_706758.png" alt="image.png"loading="lazy" decoding="async" width="925" height="876" /></picture></figure></div><p>这里很好的解释了所有的 will 系列方法和 did 系列方法的对应关系，同时也给我们吃了一个定心丸，那就是在 appearing 和 disappearing 状态之间会由 will 系列方法进行衔接，避免了状态中断。这对于连续 push 或者连续 pop 的情况是及其重要的，否则我们无法做到 “谁修改，谁复原”的原则。</p>
<p>通常来说，如果只是一个简单的导航栏样式变化，我们的代码结构大体会如下所示：</p>
<div class="block-code"><pre><code>- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    // MARK: change the navigationbar style 
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    // MARK: restore the navigationbar style
}</code></pre></div>
<p>现在，我们明确了修改时机，接下来要明确的就是导航栏的样式会进行怎样的变化。</p>
<h3>导航栏的样式变化</h3>
<p>对于不同 ViewController 之间的导航栏样式变化，大多可以总结为两种情况：</p>
<ol>
<li>导航栏的显示与否</li>
<li>导航栏的颜色变化</li>
</ol>
<h4>导航栏的显示与否</h4>
<p>对于显示与否的问题，可以在上一节提到的两个方法里调用 <code>setNavigationBarHidden:animated:</code> 方法，这里需要提醒的有两点：</p>
<ol>
<li>在导航栏转场的过程中，不要天真的以为 <code>setNavigationBarHidden:</code> 和 <code>setNavigationBarHidden:animated:</code> 的效果是一样的，直接使用 <code>setNavigationBarHidden:</code> 会造成导航栏转场过程中的闪现、背景错乱等问题，这一现象在使用手势驱动转场的场景中十分常见，所以正确的方式是使用带有 animated 参数的 API。</li>
<li>在 push 和 pop 的方法里也会带有 animated 参数，尽量保证与 <code>setNavigationBarHidden:animated:</code> 中的 animated 参数一致。</li>
</ol>
<h4>导航栏的颜色变化</h4>
<p>颜色变化的问题就稍微复杂一些，在 iOS 7 后，导航栏增加了 <code>translucent</code> 效果，这使得导航栏背景色的变化出现了两种情况：</p>
<ol>
<li><code>translucent</code> 属性值为 YES 的前提下，更改导航栏的背景色。</li>
<li><code>translucent</code> 属性值为 NO 的前提下，更改导航栏的背景色。</li>
</ol>
<p>对于第一种情况，我们需要调用 UINavigationBar 的 <code>setBackgroundColor:</code> 方法。</p>
<p>对于第二种情况我们需要调用 UINavigationBar 的 <code>setBackgroundImage:forBarMetrics:</code> 方法。</p>
<p>对于第二种情况，这里有三点需要提示：</p>
<ol>
<li>在设置透明效果时，我们通常可以直接设置一个 <code>[UIImage new]</code> 创建的对象，无须创建一个颜色为透明色的图片。</li>
<li>在使用 <code>setBackgroundImage:forBarMetrics:</code> 方法的过程中，如果图像里存在 <code>alpha</code> 值小于 1.0 的像素点，则 <code>translucent</code> 的值为 YES，反之为 NO。也就是说，如果我们真的想让导航栏变成纯色且没有 <code>translucent</code> 效果，请保证所有像素点的 <code>alpha</code> 值等于 1。</li>
<li>如果设置了一个完全不透明的图片且强行将 NavigationBar 的 <code>translucent</code> 属性设置为 YES 的话，系统会自动修正这个图片并为它添加一个透明度，用于模拟 <code>translucent</code> 效果。</li>
<li>如果我们使用了一个带有透明效果的图片且导航栏的 <code>translucent</code> 效果为 NO 的话，那么系统会在这个带有透明效果的图片背后，添加一个不透明的纯色图片用于整体效果的合成。这个纯色图片的颜色取决于 <code>barStyle</code> 属性，当属性为 <code>UIBarStyleBlack</code> 时为黑色，当属性为 <code>UIBarStyleDefault</code> 时为白色，如果我们设置了 <code>barTintColor</code>，则以设置的颜色为基准。</li>
</ol>
<h4>分清楚 <code>transparent</code>，<code>translucent</code>，<code>opaque</code>，<code>alpha</code> 和 <code>opacity</code> 也挺重要</h4>
<p>在刚接触导航栏 API 时，许多人经常会把文档里的这些英文词搞混，也不太明白带有这些词的变量为什么有的是布尔型，有的是浮点型，总之一切都让人很困惑。</p>
<p>在这里将做了一个总结，这对于理解 Apple 的 API 设计原则十分有帮助。</p>
<p><code>transparent</code>， <code>translucent</code>， <code>opaque</code> 三个词经常会用在一起，它用于描述物体的透光强度，为了让大家更好的理解这三个词，这里做了三个比喻：</p>
<ul>
<li><code>transparent</code> 是指透明，就好比我们可以透过一面干净的玻璃清楚的看到外面的风景。</li>
<li><code>translucent</code> 是指半透明，就好比我们可以透过一面有点磨砂效果的塑料墙看外面的风景，不能说看不见，但我们肯定看不清。</li>
<li><code>opaque</code> 是指不透明，就好比我们透过一个堵石墙是看不见任何外面的东西，眼前看到的只有这面墙。</li>
</ul>
<p>这三个词更多的是用来表述一种状态，不需要量化，所以这与这三个词相关的属性，一般都是 BOOL 类型。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899521_79933.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899521_79933.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899521_79933.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899521_79933.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899521_79933.png" alt="image.png"loading="lazy" decoding="async" width="2093" height="550" /></picture></figure></div><p><code>alpha</code> 和 <code>opacity</code> 经常会在一起使用，它要表示的就是透明度，在 Web 端这两个属性有着明显的区别。</p>
<p>在 Web 端里，<code>opacity</code> 是设定整个元素的透明值，而 <code>alpha</code> 一般是放在颜色设置里面，所以我们可以做到对特定对元素的某个属性设定 <code>alpha</code>，比如背景、边框、文字等。</p>
<div class="block-code"><pre><code>div {
  width: 100px;
  height: 100px;
  background: rgba(0,0,0,0.5);
  border: 1px solid #000000;
  opacity: 0.5;
}</code></pre></div>
<p>这一概念同样适用于 iOS 里的概念，比如我们可以通过 <code>alpha</code> 通道单独的去设置 <code>backgroudColor</code>、<code>borderColor</code>，它们互不影响，且有着独立的 <code>alpha</code> 通道，我们也可以通过 <code>opacity</code> 统一设置整个 view 的透明度。</p>
<p>但与 Web 端不一致的是，iOS 里面的 view 不光拥有独立的 <code>alpha</code> 属性，同时也是基于 CALayer，所以我们可以看到任意 UIView 对象下面都会有一个 layer 的属性，用于表明 CALayer 对象。view 的 <code>alpha</code> 属性与 layer 里面的 <code>opacity</code> 属性是一个相等的关系，需要注意的是 view 上的 <code>alpha</code> 属性是 Web 端并不具备的一个能力，所以笔者认为：在 iOS 中去说 <code>alpha</code> 时，要区分是在说 view 上的属性，还是在说颜色通道里的 <code>alpha</code>。</p>
<p>由于这两个词都是在描述程度，所以我们看到它们都是 CGFloat 类型：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899509_771319.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304899509_771319.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304899509_771319.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304899509_771319.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304899509_771319.png" alt="image.png"loading="lazy" decoding="async" width="1903" height="580" /></picture></figure></div><h3>转场过程中需要注意的问题和细节</h3>
<p>说完了导航栏的转场时机和转场方式，其实大体上你已经能处理好不同样式间的转换，但还有一些细节需要你去考虑，下面我们来说说其中需要你关注的两点。</p>
<h4>translucent 属性带来的布局改变</h4>
<p>translucent 会影响导航栏组件里 ViewController 的 View 布局，这里需要大家理清 5 个 API 的使用场景：</p>
<ol>
<li><code>edgesForExtendedLayout</code></li>
<li><code>extendedLayoutIncluedsOpaqueBars</code></li>
<li><code>automaticallyAdjustScrollViewInsets</code></li>
<li><code>contentInsetAdjustmentBehavior</code></li>
<li><code>additionalSafeAreaInsets</code></li>
</ol>
<p>前三个 API 是 iOS 11 之前的 API，它们之间的区别和联系在 Stack Overflow 上有一个比较精彩的回答 - <a href="https://stackoverflow.com/questions/18798792/explaining-difference-between-automaticallyadjustsscrollviewinsets-extendedlayo">Explaining difference between automaticallyAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout in iOS7</a>，我在这里就不做详细阐述，总结一下它的观点就是:</p>
<p>如果我们先定义一个 UINavigationController，它里面包含了多个 UIViewController，每个 UIViewController 里面包含一个 UIView 对象：</p>
<ul>
<li>那么 <code>edgesForExtendedLayout</code> 是为了解决 UIViewController 与 UINavigationController 的对齐问题，它会影响 UIViewController 的实际大小，例如 <code>edgesForExtendedLayout</code> 的值为 <code>UIRectEdgeAll</code> 时，UIViewController 会占据整个屏幕的大小。</li>
<li>当 UIView 是一个 UIScrollView 类或者子类时，<code>automaticallyAdjustsScrollViewInsets</code> 是为了调整这个 UIScrollView 与 UINavigationController 的对齐问题，这个属性并不会调整 UIViewController 的大小。</li>
<li>对于 UIView 是一个 UIScrollView 类或者子类且导航栏的背景色是不透明的状态时，我们会发现使用 <code>edgesForExtendedLayout</code> 来调整 UIViewController 的大小是无效的，这时候你必须使用 <code>extendedLayoutIncludesOpaqueBars</code> 来调整 UIViewController 的大小，可以认为 <code>extendedLayoutIncludesOpaqueBars</code> 是基于 <code>automaticallyAdjustsScrollViewInsets</code> 诞生的，这也是为什么经常会看到这两个 API 会同时使用。</li>
</ul>
<p>这些调整布局的 API 背后是一套基于 <code>topLayoutGuide</code> 和 <code>bottomLayoutGuide</code> 的计算而已，在 iOS 11 后，Apple 提出了 Safe Area 的概念，将原先分裂开来的 <code>topLayoutGuide</code> 和 <code>bottomLayoutGuide</code> 整合到一个统一的 LayoutGuide 中，也就是所谓的 Safe Area，这个改变看起来似乎不是很大，但它的出现确实方便了开发者。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899498_569709.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1156/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899498_569709.png" alt="image.png"loading="lazy" decoding="async" width="1156" height="798" /></picture></figure></div><p>如果想对 Safe Area 带来的改变有更全面的认识，十分推荐阅读 Rosberry 的工程师 Evgeny Mikhaylov 在 Medium 上的文章 <a href="https://medium.com/rosberryapps/ios-safe-area-ca10e919526f">iOS Safe Area</a>，这篇文章基本涵盖了 iOS 11 中所有与 Safe Area 相关的 API 并给出了真正合理的解释。</p>
<p>这里只说一下 <code>contentInsetAdjustmentBehavior</code> 和 <code>additionalSafeAreaInsets</code> 两个 API。</p>
<p>对于 <code>contentInsetAdjustmentBehavior</code> 属性而言，它的诞生也意味着 <code>automaticallyAdjustsScrollViewInsets</code> 属性的失效，所以我们在那些已经适配了 iOS 11 的工程里能看到如下类似的代码：</p>
<div class="block-code"><pre><code>if (@available(iOS 11.0, *)) {
    self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
    self.automaticallyAdjustsScrollViewInsets = NO;
}</code></pre></div>
<p>此处的代码片段只是一个示例，并不适用所有的业务场景，这里需要着重说明几个问题：</p>
<ol>
<li>关于 <code>contentInsetAdjustmentBehavior</code> 中的 <code>UIScrollViewContentInsetAdjustmentAutomatic</code> 的说明一直很“模糊”，通过 Evgeny Mikhaylov 的文章，我们可以了解到他在大多数情况下会与 <code>UIScrollViewContentInsetAdjustmentScrollableAxes</code> 一致，当且仅当满足以下所有条件时才会与 <code>UIScrollViewContentInsetAdjustmentAlways</code> 相似：<ul>
<li>UIScrollView 类型的视图在水平轴方向是可滚动的，垂直轴是不可滚动的。</li>
<li>ViewController 视图里的第一个子控件是 UIScrollView 类型的视图。</li>
<li>ViewController 是 navigation 或者 tab 类型控制器的子视图控制器。</li>
<li>启用 <code>automaticallyAdjustsScrollViewInsets</code>。</li>
</ul>
</li>
<li>iOS 11 后，通过 <code>contentInset</code> 属性获取的偏移量与 iOS 10 之前的表现形式并不一致，需要获取 <code>adjustedContentInset</code> 属性才能保证与之前的 <code>contentInset</code> 属性一致，这样的改变需要我们在代码里对不同的版本进行适配。</li>
</ol>
<p>对于 <code>additionalSafeAreaInsets</code> 而言，如果系统提供的这几种行为并不能满足我们的布局要求，开发者还可以考虑使用 <code>additionalSafeAreaInsets</code> 属性做调整，这样的设定使得开发者可以更加灵活，更加自由的调整视图的布局。</p>
<h4>backIndicator 上的动画</h4>
<p>苹果提供了许多修改导航栏组件样式的 API，有关于布局的，有关于样式的，也有关于动画的。<code>backIndicatorImage</code> 和 <code>backIndicatorTransitionMaskImage</code> 就是其中的两个 API。</p>
<p><code>backIndicatorImage</code> 和 <code>backIndicatorTransitionMaskImage</code> 操作的是 NavigationBar 里返回按钮的图片，也就是下图红色圆圈所标注的区域。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899471_40872.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_724/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899471_40872.png" alt="image.png"loading="lazy" decoding="async" width="724" height="500" /></picture></figure></div><p>想要成功的自定义返回按钮的图标样式，我们需要同时设置这两个 API ，从字面上来看，它们一个是返回图片本身，另一个是返回图片在转场时用到的 mask 图片，看起来不怎么难，我们写一段代码试试效果：</p>
<div class="block-code"><pre><code>self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@&quot;backArrow&quot;];
self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@&quot;backArrowMask&quot;];</code></pre></div>
<p>代码里的图片如下所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899462_991864.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_320/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899462_991864.png" alt="image.png"loading="lazy" decoding="async" width="320" height="123" /></picture></figure></div><p>也许大多数人在这里会都认为，mask 图片会遮挡住文字使其在遇到返回按钮右边缘的时候就消失。但实际的运行效果是怎么样子的呢？我们来看一下：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899192_097512.gif" type="image/webp"><img src="https://i.typlog.com/siqi/8304899192_097512.gif" alt="13.gif"loading="lazy" decoding="async" width="100" height="100" /></picture></figure></div><p>在上面的图片中，我们可以看到返回按钮的文字从返回按钮的图片下面穿过并且文字被图片所遮挡，这种动画看起来十分奇怪，这是无法接受的。我们需要做点修改：</p>
<div class="block-code"><pre><code>self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@&quot;backArrow&quot;];
self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@&quot;backArrow&quot;];</code></pre></div>
<p>这一次我们将 <code>backIndicatorTransitionMaskImage</code> 改为 indicatorImage 所用的图片。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899128_857895.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_122/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899128_857895.png" alt="image.png"loading="lazy" decoding="async" width="122" height="123" /></picture></figure></div><p>到这里，可能大多数人都会好奇，这代码也能行？让我们看下它实际的效果：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899093_198959.gif" type="image/webp"><img src="https://i.typlog.com/siqi/8304899093_198959.gif" alt="mask2.gif"loading="lazy" decoding="async" width="100" height="100" /></picture></figure></div><p>在上面的图中，我们看到文字在到达图片的右边缘时就从下方穿过并被完全遮盖住了，这种动画效果虽然比上面好一些，但仍然有改进的空间，不过这里我们先不继续优化了，我们先来讨论一下它们背后的运作原理。</p>
<p>iOS 系统会将 indicatorImage 中不透明的颜色绘制成返回按钮的图标， indicatorTransitionMaskImage 与 indicatorImage 的作用不同。indicatorTransitionMaskImage 将自身不透明的区域像 mask 一样作用在 indicatorImage 上，这样就保证了返回按钮中的文字像左移动时，文字只出现在被 mask 的区域，也就是 indicatorTransitionMaskImage 中不透明的区域。</p>
<p>掌握了原理，我们来解释下刚才的两种现象：</p>
<p>在第一种实现中，我们提供的 indicatorTransitionMaskImage 覆盖了整个返回按钮的图标，所以我们在转场过程中可以清晰的看到返回按钮的文字。</p>
<p>在第二种实现中，我们使用 indicatorImage 作为 indicatorTransitionMaskImage，记住文字是只能出现在 indicatorTransitionMaskImage 里不透明的区域，所以显然返回按钮中的文字会在图标的最右边就已经被遮挡住了，因为那片区域是透明的。</p>
<p>那么前面提到的进一步优化指的是什么呢？</p>
<p>让我们来看一下下面这个示例图，为了更好的区分，我们将 indicatorTransitionMaskImage 用红色进行标注。黑色仍然是 indicatorImage。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899075_260412.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_105/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899075_260412.png" alt="image.png"loading="lazy" decoding="async" width="105" height="105" /></picture></figure></div><p>按照刚才介绍的原理，我们应该可以理解，现在文字只会出现在红色区域，那么它的实际效果是什么样子的呢，我们可以看下图：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899058_525408.gif" type="image/webp"><img src="https://i.typlog.com/siqi/8304899058_525408.gif" alt="mask3.gif"loading="lazy" decoding="async" width="100" height="100" /></picture></figure></div><p>现在，一个完美的返回动画，诞生啦！</p>
<div class="blockquote"><blockquote><p>此节所用的部分效果图出自 Ray Wenderlich 的文章 <a href="https://www.raywenderlich.com/1625-uiappearance-tutorial-getting-started">UIAppearance Tutorial: Getting Started</a></p>
</blockquote></div>
<h2>导航栏的跳转或许可以这么玩儿</h2>
<p>前两章的铺垫就是为了这一章的内容，所以现在让我们开始今天的大餐吧。</p>
<h3>这样真的好么？</h3>
<p>刚才我们说了两个页面间 NavigationBar 的样式变化需要在各自的 <code>viewWillAppear:</code> 和 <code>viewWillDisappear:</code> 中进行设置。那么问题就来了：这样的设置会带来什么问题呢？</p>
<p>试想一下，当我们的页面会跳到不同的地方时，我们是不是要在 <code>viewWillAppear:</code> 和 <code>viewWillDisappear:</code> 方法里面写上一堆的判断呢？如果应用里还有 router 系统的话，那么页面间的跳转将变得更加不可预知，这时候又该如何在 <code>viewWillAppear:</code> 和 <code>viewWillDisappear:</code> 里做判断呢？</p>
<p>现在我们的问题就来了，如何让导航栏的转场更加灵活且相互独立呢？</p>
<p>常见的解决方案如下所示：</p>
<ol>
<li>重新实现一个类似 UINavigationController 的容器类视图管理器，这个容器类视图管理器做好不同 ViewController 间的导航栏样式转换工作，而每个 ViewController 只需要关心自身的样式即可。</li>
</ol>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899034_571123.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_528/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899034_571123.png" alt="image.png"loading="lazy" decoding="async" width="528" height="500" /></picture></figure></div><ol start="2">
<li>将系统原有导航栏的背景设置为透明色，同时在每个 ViewController 上添加一个 View 或者 NavigationBar 来充当我们实际看到的导航栏，每个 ViewController 同样只需要关心自身的样式即可。</li>
</ol>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899023_867296.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_814/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899023_867296.png" alt="image.png"loading="lazy" decoding="async" width="814" height="500" /></picture></figure></div><ol start="3">
<li>在转场的过程中隐藏原有的导航栏并添加假的 NavigationBar，当转场结束后删除假的 NavigationBar 并恢复原有的导航栏，这一过程可以通过 Swizzle 的方式完成，而每个 ViewController 只需要关心自身的样式即可。</li>
</ol>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304899014_951161.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1554/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304899014_951161.png" alt="image.png"loading="lazy" decoding="async" width="1554" height="481" /></picture></figure></div><p>这三种方案各有优劣，我们在网上也可以看到很多关于它们的讨论。</p>
<p>例如方案一，虽然看起来工作量大且难度高，但是这个工作一旦完成，我们就会将处理导航栏转场的主动权牢牢抓在手里。但这个方案的一个弊端就是，如果苹果修改了导航栏的整体风格，就好比 iOS 11 的大标题特效，那么工作量就来了。</p>
<p>对于方案二而言，虽然看起来简单易用，但这需要一个良好的继承关系，如果整个工程里的继承关系混乱或者是历史包袱比较重，后续的维护就像“打补丁”一样，另外这个方案也需要良好的团队代码规范和完善的技术文档来做辅助。</p>
<p>对于方案三而言，它不需要所谓的继承关系，使用起来也相对简单，这对于那些继承关系和历史包袱比较重的工程而言，这一个不错的解决方案，但在解决 Bug 的时候，Swizzle 这种方式无疑会增加解决问题的时间成本和学习成本。</p>
<h3>我们的解决方案</h3>
<p>在美团 App 的早期，各个业务方都想充分利用导航栏的能力，但对于导航栏的状态维护缺乏理解与关注，随着业务方的增加和代码量的上升，与导航栏相关的问题逐渐暴露出来，此时我们才意识到这个问题的严重性。</p>
<p>大型 App 的导航栏问题就像一个典型的“公地悲剧”问题。在软件行业，公用代码的所有权可以被视作“公地”，因为不注重长期需求而容易遭到消耗。如果开发人员倾向于交付“价值”，而以可维护性和可理解性为代价，那么这个问题就特别普遍了。如果是这种情况，每次代码修改将大大减少其总体质量，最终导致软件的不可维护。</p>
<p>所以解决这个问题的核心在于：明确公用代码的所有权，并在开发期施加约束。</p>
<p>明确公用代码的所有权，可以理解为将导航栏相关的组件抽离成一个单独的组件，并交由特定的团队维护。而在开发期施加约束，则意味着我们要提供一套完整的解决方案让各个业务方遵守。</p>
<p>这一节我们会以美团内部的解决方案为例，讲解如何实现一个流畅的导航栏跳转过程和相关使用方法。</p>
<h4>设计理念</h4>
<p>使用者只用关心当前 ViewController 的 NavigationBar 样式，而不用在 push 或者 pop 的时候去处理 NavigationBar 样式。</p>
<p>举个例子来说，当从 A 页面 push 到 B 页面的时候，转场库会保存 A 页面的导航栏样式，当 pop 回去后就会还原成以前的样式，因此我们不用考虑 pop 后导航栏样式会改变的情况，同时我们也不必考虑 push 后的情况，因为这个是页面 B 本身需要考虑的。</p>
<h4>使用方法</h4>
<p>转场库的使用十分简单，我们不需要 import 任何头文件，因为它在底层通过 Method Swizzling 进行了处理，只需要在使用的时候遵循下面 4 点即可：</p>
<ul>
<li>当需要改变导航栏样式的时候，在视图控制器的 <code>viewDidLoad</code> 或者 <code>viewWillAppear:</code> 方法里去设置导航栏样式。</li>
<li>用 <code>setBackgroundImage:forBarMetrics:</code> 方法和 <code>shadowImage</code> 属性去修改导航栏的背景样式。</li>
<li>不要在 <code>viewWillDisappear:</code> 里添加针对导航栏样式修改的代码。</li>
<li>不要随意修改 translucent 属性，包括隐式的修改和显示的修改。</li>
</ul>
<div class="blockquote"><blockquote><p>隐式修改是指使用 <code>setBackgroundImage:forBarMetrics:</code> 方法时，如果 image 里的像素点没有 <code>alpha</code> 通道或者 <code>alpha</code> 全部等于 1 会使得 <code>translucent</code> 变为 NO 或者 nil。</p>
</blockquote></div>
<h3>基本原理</h3>
<p>以上，我们讲完了设计理念和使用方法，那么我们来看看美团的转场库到底做了什么？</p>
<p>从大方向上来看，美团使用的是前面所说的第三种方案，不过它也有一些自己独特的地方，为了更好的让大家理解整个过程，我们设计这样一个场景，从页面 A push 到页面 B，结合之前探讨过的方法调用顺序，我们可以知道几个核心方法的调用顺序大致如下：</p>
<ol>
<li>页面 A 的 <code>pushViewController:animated:</code></li>
<li>页面 B 的 <code>viewDidLoad</code> or <code>viewWillAppear:</code></li>
<li>页面 B 的 <code>viewWillLayoutSubviews</code></li>
<li>页面 B 的 <code>viewDidAppear:</code></li>
</ol>
<p>在 push 过程的开始，转场库会在页面 A 自身的 view 上添加一个与导航栏一模一样的 NavigationBar 并将真的导航栏隐藏。之后这个假的导航栏会一直存在页面 A 上，用于保留 A 离开时的导航栏样式。</p>
<p>等到页面 B 调用 <code>viewDidLoad</code> 或者 <code>viewWillAppear:</code> 的时候，开发者在这里自行设置真的导航栏样式。转场库在这里会对页面布局做一些修正和辅助操作，但不会影响导航栏的样式。</p>
<p>等到页面 B 调用 <code>viewWillLayoutSubviews</code> 的时候，转场库会在页面 B 自身的 view 上添加一个与真的导航栏一模一样的 NavigationBar，同时将真的导航栏隐藏。此时不论真的导航栏，还是假的导航栏都已经与 <code>viewDidLoad</code> 或者 <code>viewWillAppear:</code> 里设置的一样的。</p>
<div class="blockquote"><blockquote><p>当然，这一步也可以放在 <code>viewWillAppear:</code> 里并在 dispatch main queue 的下一个 runloop 中处理。</p>
</blockquote></div>
<p>等到页面 B 调用 <code>viewDidAppear:</code> 的时候，转场库会将假的导航栏样式设置到真的导航栏中，并将假的导航栏从视图层级中移除，最终将真的导航栏显示出来。</p>
<p>为了让大家更好地理解上面的内容，请参考下图：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898997_611315.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1212/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898997_611315.png" alt="image.png"loading="lazy" decoding="async" width="1212" height="500" /></picture></figure></div><p>说完了 push 过程，我们再来说一下从页面 B pop 回页面 A 的过程，几个核心方法的调用顺序如下：</p>
<ol>
<li>页面 B 的 <code>popViewControllerAnimated:</code></li>
<li>页面 A 的 <code>viewWillAppear:</code></li>
<li>页面 A 的 <code>viewDidAppear:</code></li>
</ol>
<p>在 pop 过程的开始，转场库会在页面 B 自身的 view 上添加一个与导航栏一模一样的 NavigationBar 并将真的导航栏隐藏，虽然这个假的导航栏会一直存在于页面 B 上，但它自身会随着页面 B 的 <code>dealloc</code> 而消亡。</p>
<p>等到页面 A 调用 <code>viewWillAppear:</code> 的时候，开发者在这里自行设置真的导航栏样式。当然我们也可以不设置，因为这时候页面 A 还持有一个假的导航栏，这里还保留着我们之前在 <code>viewDidLoad</code> 里写的导航栏样式。</p>
<p>等到页面 A 调用 <code>viewDidAppear:</code> 的时候，转场库会将假的导航栏样式设置到真的导航栏中，并将假的导航栏从视图层级中移除，最终将真的导航栏显示出来。</p>
<p>同样，我们可以参考下面的图来理解上面所说的内容：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898987_665255.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1212/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898987_665255.png" alt="image.png"loading="lazy" decoding="async" width="1212" height="500" /></picture></figure></div><p>现在，大家应该对我们美团的解决方案有了一定的认识，但在实际开发过程中，还需要考虑一些布局和适配的问题。</p>
<h2>最佳实践</h2>
<p>在维护这套转场方案的时间里，我们总结了一些此类方案的最佳实践。</p>
<h3>判断导航栏问题的基本准则</h3>
<p>如果发现导航栏在转场过程中出现了样式错乱，可以遵循以下几点基本原则：</p>
<ul>
<li>检查相应 ViewController 里是否有修改其他 ViewController 导航栏样式的行为，如果有，请做调整。</li>
<li>保证所有对导航栏样式变化的操作出现在 <code>viewDidLoad</code> 和 <code>viewWillAppear:</code> 中，如果在 <code>viewWillDisappear:</code> 等方法里出现了对导航栏的样式修改的操作，如果有，请做调整。</li>
<li>检查是否有改动 <code>translucent</code> 属性，包括显示修改和隐式修改，如果有，请做调整。</li>
</ul>
<h3>只关心当前页面的样式</h3>
<p>永远记住每个 ViewController 只用关心自己的样式，设置的时机点在 <code>viewWillAppear:</code> 或者 <code>viewDidLoad</code> 里。</p>
<h3>透明样式导航栏的正确设置方法</h3>
<p>如果需要一个透明效果的导航栏，可以使用如下代码实现：</p>
<div class="block-code"><pre><code>[self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
self.navigationController.navigationBar.shadowImage = [UIImage new];</code></pre></div>
<h3>导航栏的颜色渐变效果</h3>
<p>如果需要导航栏实现随滚动改变整体 <code>alpha</code> 值的效果，可以通过改变 <code>setBackgroundImage:forBarMetrics:</code> 方法里 image 的 <code>alpha</code> 值来达到目标，这里一般是使用监听 <code>scrollView.contentOffset</code> 的手段来做。请避免直接修改 NavigationBar 的 <code>alpha</code> 值。</p>
<p>还有一点需要注意的是，在页面转场的过程中，也会触发 <code>contentOffset</code> 的变化，所以请尽量在 disappear 的时候取消监听。否则会容易出现导航栏透明度的变化。</p>
<h3>导航栏背景图片的规范</h3>
<p>请避免背景图里的像素点没有 <code>alpha</code> 通道或者 <code>alpha</code> 全部等于 1，容易触发 <code>translucent</code> 的隐式改变。</p>
<h3>如果真的要隐藏导航栏</h3>
<p>如果我们需要隐藏导航栏，请保证所有的 ViewController 能坚持如下原则：</p>
<ol>
<li>每个 ViewController 只需要关心当前页面下的导航栏是否被隐藏。</li>
<li>在 <code>viewWillAppear:</code> 中，统一设置导航栏的隐藏状态。</li>
<li>使用 <code>setNavigationBarHidden:animated:</code> 方法，而不是 <code>setNavigationBarHidden:</code>。</li>
</ol>
<h3>转场动画与导航栏隐藏动画的一致性</h3>
<p>如果在转场的过程中还会显示或者隐藏导航栏的话，请保证两个方法的动画参数一致。</p>
<div class="block-code"><pre><code>- (void)viewWillAppear:(BOOL)animated{
    [self.navigationController setNavigationBarHidden:YES animated:animated];
}</code></pre></div>
<div class="blockquote"><blockquote><p><code>viewWillAppear:</code> 里的 animated 参数是受 push 和 pop 方法里 animated 参数影响。</p>
</blockquote></div>
<h3>导航栏固有的系统问题</h3>
<p>目前已知的有两个系统问题如下：</p>
<ol>
<li>当前后两个 ViewController 的导航栏都处于隐藏状态，然后在后一个 ViewController 中使用返回手势 pop 到一半时取消，再连续 push 多个页面时会造成导航栏的 Stack 混乱或者 Crash。</li>
<li>当页面的层级结构大体如下所示时，在红色导航栏的 Stack 中，返回手势会大概率的出现跨层级的跳转，多次后会导致整个导航栏的 Stack 错乱或者 Crash。</li>
</ol>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304898975_475236.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_712/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304898975_475236.png" alt="image.png"loading="lazy" decoding="async" width="712" height="580" /></picture></figure></div><h3>导航栏内置组件的布局规范</h3>
<p>导航栏里的组件布局在 iOS 11 后发生了改变，原有的一些解决方案已经失效，这些内容不在本篇文章的讨论范围之内，推荐阅读<a href="http://sketchk.xyz/2018/02/23/How-to-make-your-UIBarButtonItem-perfect-match-in-iOS/">UIBarButtonItem 在 iOS 11 上的改变及应对方案</a>，这篇文章详细的解释了 iOS 11 里的变化和可行的应对方案。</p>
<h2>总结</h2>
<p>本文涉及内容较多，从 iOS 系统下的导航栏概念到大型应用里的最佳实践，这里我们总结一下整篇文章的核心内容：</p>
<ul>
<li>理解导航栏组件的结构和相关方法的生命周期。<ul>
<li>导航栏组件的结构留有 MVC 架构的影子，在解决问题时，要去相应的层级处理。</li>
<li>转场问题的关键点是方法的调用顺序，所以了解生命周期是解决此类问题的基础。</li>
</ul>
</li>
<li>状态管理，转换时机和样式变化是导航栏里常见问题的三种表现形式，遇到实际问题时需要区分清楚。<ul>
<li>状态管理要坚持“谁修改，谁复原”的原则。</li>
<li>转换时机的设定要做到连续可执行。</li>
<li>样式变化的核心点是导航栏的显示与否与颜色变化。</li>
</ul>
</li>
<li>为了更好的配合大型应用里的路由系统，导航栏转场的常见解决方案有三种，各有利弊，需要根据自身的业务场景和历史包袱做取舍。<ul>
<li>解决方案1：自定义导航栏组件。</li>
<li>解决方案2：在原有导航栏组件里添加 Fake Bar。</li>
<li>解决方案3：在导航栏转场过程中添加 Fake Bar。</li>
</ul>
</li>
<li>美团在实际开发过程中采用了第三种方案，并给出了适合美团 App 的最佳实践。</li>
</ul>
<div class="blockquote"><blockquote><p>特别感谢<a href="https://github.com/MoZhouqi">莫洲骐</a>在此项目里的贡献与付出。</p>
</blockquote></div>
<h2>参考链接</h2>
<ul>
<li><a href="https://www.raywenderlich.com/1625-uiappearance-tutorial-getting-started">UIAppearance Tutorial: Getting Started</a></li>
<li><a href="https://github.com/MoZhouqi/KMNavigationBarTransition">KMNavigationBarTransition</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[关于 .d 文件一些思考与理解]]></title><guid>https://swiftsiqi.com/posts/something-about-d-file</guid><link>https://swiftsiqi.com/posts/something-about-d-file</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 07 Jul 2021 05:07:43 +0000</pubDate><content:encoded><![CDATA[<p>Reactive Cocoa 里的 <code>.d</code> 文件到底有什么用呢?</p>
<h2>问题的由来</h2>
<p>最近在开发过程中，遇到了一个自己还无法回答的问题，就是 Reactive Cocoa 里的两个 <code>.d</code> 文件到底有啥用，以及怎么用？</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900549_368253.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900549_368253.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900549_368253.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900549_368253.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900549_368253.png" alt="image.png"loading="lazy" decoding="async" width="2622" height="642" /></picture></figure></div><h2>DTrace</h2>
<p>DTrace 是一个动态追踪技术，说的可能更接地气一点，就是可以使用 DTrace 附加在一个已经运行的程序上，且不会打断当前程序的运行，也不需要重新编译或者启动此程序。</p>
<p>乍一听，感觉很不错，好像我们可以搞点事情了，但放到 macOS 和 iOS 的场景下就有了一些限制。</p>
<p>DTrace 只能在 macOS 上运行，Apple 也在 iOS 上使用 DTrace，用以支持像 Instruments 这样的工具，但对于第三方开发者，DTrace 只能运行于 macOS 或 iOS 模拟器。</p>
<h3>基本概念</h3>
<div class="blockquote"><blockquote><p>这篇文章本身的目的是为了解决文章开篇提到的问题，所以不会科普太多 DTrace 技术本身的基本概念。这里只是把下面用到的概念说明一下，方便读者理解。</p>
</blockquote></div>
<p>在 DTrace 里有两个比较重要的概念，它们分别是probe(探针)和 dtrace file(DTrace 脚本)。</p>
<p>探针是指我们利用在代码里埋的点，插的桩，它有一套标准的定义，本文在这里不展开了，感兴趣可以阅读这个资料：<a href="http://www.brendangregg.com/dtracebook/index.html">DTrace Book</a>。</p>
<p>DTrace 脚本，是用 D 语言编写的脚本，既可以用 DTrace 脚本声明 probe，也可以触发 probe。</p>
<p>声明 probe 的例子:</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// 声明 probe               </span>
</div><div class="line"><span class="n">provider</span><span class="w"> </span><span class="n">syncengine_sync</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">probe</span><span class="w"> </span><span class="nf">strategy_go_to_state</span><span class="p">(</span><span class="kt">int</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>或者调用 probe 的例子:</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// 调用 probe  </span>
</div><div class="line"><span class="n">syncengine_sync</span><span class="o">*:::</span><span class="n">strategy_go_to_state</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">printf</span><span class="p">(</span><span class="s">&quot;Transitioning to state %d</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">arg0</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>那么怎么启用 DTrace 呢？整体来说，有两种途径：</p>
<ul>
<li>使用 dtrace 脚本来触发，也就是 <code>.d</code> 文件</li>
<li>使用 dtrace 命令来触发，也就是命令行里的 <code>dtrace</code> 命令</li>
</ul>
<div class="blockquote"><blockquote><p>注意，如果想使用 dtrace 还需要关闭 System Integrity Protection，也就是常说的 Rootless，具体操作步骤是:</p>
<ul>
<li>重新启动你的macOS机器</li>
<li>当屏幕变成空白时，按住 <code>Command + R</code>，直到出现苹果的启动标志。这将使你的电脑进入 Recovery Mode</li>
<li>现在，从顶部找到 Utilities 菜单，然后选择 Terminal</li>
<li>在终端窗口打开后，输入 <code>csrutil disable &amp;&amp; reboot</code></li>
<li>只要 <code>csrutil disable</code> 命令成功，你的电脑就会在禁用 Rootless 后重新启动</li>
</ul>
</blockquote></div>
<h3>DTrace 的使用场景</h3>
<p>那么从使用者的角度来说，DTrace 只适用于两种场景：</p>
<ul>
<li>追踪系统内核代码：使用者只需要直接调用系统预埋的 probe 即可。</li>
<li>追踪 App 侧的自定义代码：使用者一方面需要在 App 侧埋 probe，另一方面也需要去调用自己埋的 probe。</li>
</ul>
<p>对于追踪系统内核的代码，其实有很多文章在说明，这里就不展开来说了，感兴趣可以看看网上的文章, 大多都是在将这种场景的使用方式.</p>
<p>今天的目标也是为了解释第二个场景，进而说明 Reactive Cocoa 里的 <code>.d</code> 文件的用途。</p>
<h2>在自己的代码里使用 DTrace 技术</h2>
<p>这里我们设计一个 CLI 工具，这个 CLI 工具不会停止，也不会做任何事儿，它的逻辑大概如下（其实就是一个无限循环）:</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;Foundation/Foundation.h&gt;</span>
</div><div class="line">
</div><div class="line"><span class="kt">int</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">argc</span><span class="p">,</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">argv</span><span class="p">[])</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">@autoreleasepool</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">  </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<h3>声明探针</h3>
<p>此时我们在工程里创建一个 provider.d 文件声明一个自定义的 probe</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">provider</span><span class="w"> </span><span class="n">zsq</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">probe</span><span class="w"> </span><span class="nf">go</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="p">};</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>此时的文件目录是如下</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">▶ tree
</div><div class="line">.
</div><div class="line">├── main.m
</div><div class="line">└── zsq.d
</div></code></pre></div>
</div>
<h3>预埋探针</h3>
<p>在 Xcode 里面的 build rule 会有这么一个自动化操作，如果判断出目标文件是 <code>.d</code> 文件，也就是 dtrace 文件，会生成对应的 <code>.h</code> 文件。</p>
<p>结合上面的例子，此时 Xcode 的 build system 就会生成一个 <code>zsq.h</code> 文件，在 build log 里我们可以查看到它。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900531_8031845.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900531_8031845.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900531_8031845.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900531_8031845.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900531_8031845.png" alt="image.png"loading="lazy" decoding="async" width="1894" height="1210" /></picture></figure></div><p>此时我们可以看一下 <code>zsq.h</code> 里的内容，对我们比较有用的是两个基于探针行为定义的宏 <code>ZSQ_GO_ENABLED</code>和 <code>ZSQ_GO</code>，感兴趣可以展开下面的代码来查看。</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cm">/*</span>
</div><div class="line"><span class="cm"> * Generated by dtrace(1M).</span>
</div><div class="line"><span class="cm"> */</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="cp">#ifndef_ZSQ_H</span>
</div><div class="line"><span class="cp">#define_ZSQ_H</span>
</div><div class="line">
</div><div class="line"><span class="cp">#if !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED</span>
</div><div class="line"><span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;unistd.h&gt;</span><span class="cp"></span>
</div><div class="line">
</div><div class="line"><span class="cp">#endif </span><span class="cm">/* !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED */</span><span class="cp"></span>
</div><div class="line">
</div><div class="line"><span class="cp">#ifdef__cplusplus</span>
</div><div class="line"><span class="k">extern</span><span class="w"> </span><span class="s">&quot;C&quot;</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="cp">#endif</span>
</div><div class="line">
</div><div class="line"><span class="cp">#define ZSQ_STABILITY &quot;___dtrace_stability$zsq$v1$1_1_0_1_1_0_1_1_0_1_1_0_1_1_0&quot;</span>
</div><div class="line">
</div><div class="line"><span class="cp">#define ZSQ_TYPEDEFS &quot;___dtrace_typedefs$zsq$v2&quot;</span>
</div><div class="line">
</div><div class="line"><span class="cp">#if !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED</span>
</div><div class="line">
</div><div class="line"><span class="cp">#defineZSQ_GO() \</span>
</div><div class="line"><span class="cp">do { \</span>
</div><div class="line"><span class="cp">__asm__ volatile(&quot;.reference &quot; ZSQ_TYPEDEFS); \</span>
</div><div class="line"><span class="cp">__dtrace_probe$zsq$go$v1(); \</span>
</div><div class="line"><span class="cp">__asm__ volatile(&quot;.reference &quot; ZSQ_STABILITY); \</span>
</div><div class="line"><span class="cp">} while (0)</span>
</div><div class="line"><span class="cp">#defineZSQ_GO_ENABLED() \</span>
</div><div class="line"><span class="cp">({ int _r = __dtrace_isenabled$zsq$go$v1(); \</span>
</div><div class="line"><span class="cp">__asm__ volatile(&quot;&quot;); \</span>
</div><div class="line"><span class="cp">_r; })</span>
</div><div class="line">
</div><div class="line">
</div><div class="line"><span class="k">extern</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">__dtrace_probe$zsq$go$v1</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="k">extern</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="nf">__dtrace_isenabled$zsq$go$v1</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="cp">#else</span>
</div><div class="line">
</div><div class="line"><span class="cp">#defineZSQ_GO() \</span>
</div><div class="line"><span class="cp">do { \</span>
</div><div class="line"><span class="cp">} while (0)</span>
</div><div class="line"><span class="cp">#defineZSQ_GO_ENABLED() (0)</span>
</div><div class="line">
</div><div class="line"><span class="cp">#endif </span><span class="cm">/* !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED */</span><span class="cp"></span>
</div><div class="line">
</div><div class="line">
</div><div class="line"><span class="cp">#ifdef__cplusplus</span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="cp">#endif</span>
</div><div class="line">
</div><div class="line"><span class="cp">#endif</span><span class="cm">/* _ZSQ_H */</span><span class="cp"></span>
</div></code></pre></div>
</div>
<p>通过这个自动生成的头文件，我们就可以在自己的代码中预埋自定义的 probe，大体的逻辑使用方式就是先判断 probe 是否 enable，如果 enable，再真的执行它。</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="cp">#import &lt;Foundation/Foundation.h&gt;</span>
</div><div class="line"><span class="cp">#import &quot;zsq.h&quot;</span>
</div><div class="line">
</div><div class="line"><span class="kt">int</span><span class="w"> </span><span class="nf">main</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">argc</span><span class="p">,</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">argv</span><span class="p">[])</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">@autoreleasepool</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="k">if</span><span class="p">(</span><span class="n">ZSQ_GO_ENABLED</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">                </span><span class="n">NSLog</span><span class="p">(</span><span class="s">@&quot;Hello&quot;</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="w">                </span><span class="n">ZSQ_GO</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="w">            </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<h3>触发探针</h3>
<p>首先正常启用 CLI 命令后</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">// 启用 cli 命令行工具
</div><div class="line">..../SQTool
</div></code></pre></div>
</div>
<p>在没有触发 DTrace 之前，终端不会有任何输出</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900513_960237.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900513_960237.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900513_960237.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900513_960237.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900513_960237.png" alt="image.png"loading="lazy" decoding="async" width="1782" height="606" /></picture></figure></div><p>此时我们在另一个 terminal 里去启用 dtrace 来追踪预埋的探针</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">// -s 参数的 zsq.d 指的是定义的 probe 文件， 
</div><div class="line">// -P 参数的 zsq30629 指的是 probe name + PID, probe name 在 .d 里查看， PID 命令可以通过 ps -A 查看
</div><div class="line">sudo dtrace -s zsq.d -P zsq30629
</div><div class="line">// 或者
</div><div class="line">sudo dtrace -P zsq30629 // 如果你的 .d 文件已经被 dtrace 加载了，就无须重复使用 -s 参数重复加载
</div></code></pre></div>
</div>
<p>此时，dtrace 服务被激活，CLI 里预埋的 probe 生效，我们就看到执行 CLI 里的 terminal 不断的在输出 Hello，也就是被 <code>ZSQ_GO_ENABLED()</code> 包裹的逻辑之一。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900501_003233.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900501_003233.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900501_003233.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900501_003233.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900501_003233.png" alt="image.png"loading="lazy" decoding="async" width="1782" height="1122" /></picture></figure></div><h3>总结</h3>
<p>至此，我们完成了自定义探针的定义，预埋和调用。</p>
<p>那基于前面的 demo，我们来理解下 Reactive Cocoa 里的 <code>.d</code> 文件到底干了什么？以及怎么用？</p>
<p>以 Reactive Cocoa 里的 RACCompoundDisposableProvider 为例，它只是定义了一个 provider 为 RACCompoundDisposable，probe 为 added 和 removed 的探针。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900485_160578.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900485_160578.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900485_160578.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900485_160578.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900485_160578.png" alt="image.png"loading="lazy" decoding="async" width="2462" height="640" /></picture></figure></div><p>而在 <code>RACCompoundDisposable.m</code> 中，会在 <code>addDisposable</code> 中预埋 <code>RACCOMPOUNDDISPOSABLE_ADDED</code> 的探针，感兴趣可以展开下面的代码来查看。</p>
<div class="block-code" data-language="objc"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">addDisposable:</span><span class="p">(</span><span class="n">RACDisposable</span><span class="w"> </span><span class="o">*</span><span class="p">)</span><span class="nv">disposable</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="n">NSCParameterAssert</span><span class="p">(</span><span class="n">disposable</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nb">self</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">disposable</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nb">nil</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">disposable</span><span class="p">.</span><span class="n">disposed</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="kt">BOOL</span><span class="w"> </span><span class="n">shouldDispose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">NO</span><span class="p">;</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="n">OSSpinLockLock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">_spinLock</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">_disposed</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="n">shouldDispose</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">YES</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="cp">#if RACCompoundDisposableInlineCount</span>
</div><div class="line"><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">unsigned</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">RACCompoundDisposableInlineCount</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">_inlineDisposables</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nb">nil</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="n">_inlineDisposables</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">disposable</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="k">goto</span><span class="w"> </span><span class="n">foundSlot</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="cp">#endif</span>
</div><div class="line">
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">_disposables</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nb">NULL</span><span class="p">)</span><span class="w"> </span><span class="n">_disposables</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">RACCreateDisposablesArray</span><span class="p">();</span><span class="w"></span>
</div><div class="line"><span class="n">CFArrayAppendValue</span><span class="p">(</span><span class="n">_disposables</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="k">__bridge</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="o">*</span><span class="p">)</span><span class="n">disposable</span><span class="p">);</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">RACCOMPOUNDDISPOSABLE_ADDED_ENABLED</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="n">RACCOMPOUNDDISPOSABLE_ADDED</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="n">description</span><span class="p">.</span><span class="n">UTF8String</span><span class="p">,</span><span class="w"> </span><span class="n">disposable</span><span class="p">.</span><span class="n">description</span><span class="p">.</span><span class="n">UTF8String</span><span class="p">,</span><span class="w"> </span><span class="n">CFArrayGetCount</span><span class="p">(</span><span class="n">_disposables</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">RACCompoundDisposableInlineCount</span><span class="p">);</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="cp">#if RACCompoundDisposableInlineCount</span>
</div><div class="line"><span class="nl">foundSlot</span><span class="p">:;</span><span class="w"></span>
</div><div class="line"><span class="cp">#endif</span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="n">OSSpinLockUnlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">_spinLock</span><span class="p">);</span><span class="w"></span>
</div><div class="line">
</div><div class="line"><span class="c1">// Performed outside of the lock in case the compound disposable is used</span>
</div><div class="line"><span class="c1">// recursively.</span>
</div><div class="line"><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">shouldDispose</span><span class="p">)</span><span class="w"> </span><span class="p">[</span><span class="n">disposable</span><span class="w"> </span><span class="n">dispose</span><span class="p">];</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>那么基于这个探针，我们就可以使用 dtrace 去追踪 add 的行为。</p>
<p>那这种功能有什么用呢？</p>
<p>如果你在开发前端的时候使用 redux，估计你八成会使用到这样的一个 debug tool - <a href="https://github.com/infinitered/reactotron">reaction</a>，它能将 saga 里的每个动作，展示在 debug tool 里面。</p>
<div class="photo"><figure><picture><source srcset="https://github.com/infinitered/reactotron/raw/master/docs/images/readme/reactotron-demo-app.gif" type="image/webp"><img src="https://github.com/infinitered/reactotron/raw/master/docs/images/readme/reactotron-demo-app.gif" alt="img"loading="lazy" decoding="async" width="981" height="695" /></picture></figure></div><p>那么同理到我们的 Reactive Cocoa 中，我们就可以搞一个 debug tool 实时监控代码里某些行为，方便我们调试。</p>
<h2>那 .d 文件对于我们意味着什么？</h2>
<p>从组件化角度看，分为两种业务场景，一种是源码形式，一种是二进制形式</p>
<ul>
<li>在源码形式下，需要保留 <code>.d</code> 文件：由于组件的内的 <code>.m</code> 文件还会依赖 由 <code>.d</code> 自动生成的 <code>.h</code> 文件，如果想让编译通过，就需要保留 <code>.d</code> 文件。例如 <code>RACCompoundDisposable.m</code> 会依赖 <code>RACCompoundDisposableProvider.d</code> 生成的 <code>RACCompoundDisposableProvider.h</code> 文件，</li>
<li>在二进制形式下，不需要保留 <code>.d</code> 文件：由于组件内的 <code>.m</code> 文件已经被编译成二进制，不再需要编译行为，<code>.d</code> 文件已经没有存在价值，也就不依赖 <code>RACCompoundDisposableProvider.d</code> 生成的 <code>RACCompoundDisposableProvider.h</code> 文件，</li>
</ul>
<p>那结合上面的两个视角，我们在 CI 上又应该干点什么呢？</p>
<ul>
<li>基于现在的状况（即没有人把 <code>.d</code> 生成的 <code>.h</code> 放到公开头文件里），我们就可以把把 <code>.d</code> 文件当做 <code>.m</code> 文件一样看待，即在二进制产物中删掉 <code>.d</code> 文件即可。</li>
</ul>
<p>那么可能就会有人，为啥不能把 <code>.d</code> 生成的 <code>.h</code> 放到公开头文件里呢，这个行为合理么？</p>
<ul>
<li>我认为是没有必要的，原因大致如下：<ul>
<li>首先 <code>.d</code> 文件生成的 <code>.h</code> 只是与 probe 相关的逻辑，为自己的组件提供了 dtrace 能力，方便自己的调试或者行为追踪。</li>
<li>这种埋点自查的能力应当只在自己的组件内（例如组件 A）使用，即使提供给外界（组件 B）使用，那么组件 B 也无法追踪组件 A 的行为，组件 B 只能追踪自己的行为，如果想追踪自己的行为，那又为什么要用 A 里的 probe 呢？对吧。自己追踪自己就好，不要用别人的探针，避免歧义。</li>
<li>所以这个 <code>.d</code> 文件从原理上是可以放到公开的 <code>.h</code> 文件中，但这并不是那么合理，所以从实际使用的角度上来说，是不应该将 <code>.d</code> 自动生成的 <code>.h</code> 文件放到公开的 <code>.h</code> 文件中。</li>
</ul>
</li>
</ul>
<p>好了，说到这里，我想你也大概明白了 <code>.d</code> 文件的作用和在组件化的时候要怎么对待它了，希望这篇文章能对你有所帮助！</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://bignerdranch.com/blog/hooked-on-dtrace-part-1/">Hooked on DTrace, part 1</a></li>
<li><a href="https://bignerdranch.com/blog/hooked-on-dtrace-part-2/">Hooked on DTrace, part 2</a></li>
<li><a href="https://bignerdranch.com/blog/hooked-on-dtrace-part-3/">Hooked on DTrace, part 3</a></li>
<li><a href="https://bignerdranch.com/blog/hooked-on-dtrace-part-4/">Hooked on DTrace, part 4</a></li>
<li><a href="https://www.objc.io/issues/19-debugging/dtrace/">DTrace</a></li>
<li><a href="https://www.brendangregg.com/dtracebook/index.html">DTrace Book</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Swift 2021 生态调研报告]]></title><guid>https://swiftsiqi.com/posts/Swift-in-China-2021</guid><link>https://swiftsiqi.com/posts/Swift-in-China-2021</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 21 Apr 2021 15:08:36 +0000</pubDate><content:encoded><![CDATA[<p>让我们一起看看 Swift 生态在 2021 年的现状吧！</p>
<h2>回顾 2020</h2>
<p>在去年的<a href="https://mp.weixin.qq.com/s/Ib48PWpKJNALlNoL4lm4-g">《一次关于 Swift 在 iOS 生态圈里的现状调研》</a>一文中, 我们分析了整个大环境的现状，在文章发表后得到了大家的广泛关注，时隔一年，我们再来看看有什么变化吧？</p>
<h2>Swift 语言</h2>
<h3>版本变化</h3>
<p>首先从去年的 5.2 版本，到现在即将发布 5.4 版本，Swift 经历了 2 个小的版本变化，分别是 5.3 和 5.4</p>
<p>其中 5.3 版本给出了以下几个语言特性：</p>
<ul>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0276-multi-pattern-catch-clauses.md">SE-0276</a>：catch 语句在捕获 error 的时候，可以更加灵活自由，例如一次捕获多个 error 或者对 error 的值进行绑定。</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0279-multiple-trailing-closures.md">SE-0279</a>：支持多个尾随闭包，这个特性主要是为 SwiftUI 准备的。</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0266-synthesized-comparable-for-enumerations.md">SE-0266</a>：enum 支持 comparable 协议，并根据顺序自行决定大小</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0269-implicit-self-explicit-capture.md">SE-0269</a>：在某些场景下可以避免 self 关键字的声明</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0281-main-attribute.md">SE-0281</a>：通过 <code>@main</code> 关键字定位程序入口</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0267-where-on-contextually-generic.md">SE-0267</a>：在函数的泛型和扩展中就可以使用包含 <code>where</code> 关键字的语句</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0280-enum-cases-as-protocol-witnesses.md">SE-0280</a>：protocol witness 匹配模型在枚举值中的加强</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0277-float16.md">SE-0277</a>：新增 Float16 的数据类型</li>
<li><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0268-didset-semantics.md">SE-0268</a>：didSet 方法优化和语义更新</li>
<li>首先在 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md">SE-0271</a> 中，Swift Package Manager 在资源文件的支持上有了进一步的提升，同时，在 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0278-package-manager-localized-resources.md">SE-0278</a> 中，SPM 对本地化资源的支持也有了改进，而且在 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0272-swiftpm-binary-dependencies.md">SE-0272</a> 中，SPM 终于支持了二进制形式的组件。在 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0273-swiftpm-conditional-target-dependencies.md">SE-0273</a>，SPM 允许我们对特定的 target 进行特殊的依赖配置。</li>
</ul>
<p>其中在未来的 5.4 版本又新增了以下几个语言特性：</p>
<ul>
<li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0287-implicit-member-chains.md">SE-0287</a>：提升了隐式成员表达式的类型推断能力。</li>
<li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0284-multiple-variadic-parameters.md">SE-0284</a>：在函数中可以定义多个可变参数。</li>
<li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md">SE-0289</a>：在 5.1 就公布的 Function Builder 功能正式命名为 Result Builder，并在原先的基础上进行了完善。</li>
<li><a href="https://bugs.swift.org/browse/SR-10069">SR-10069</a>：嵌套函数支持重载</li>
<li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0294-package-executable-targets.md">SE-0294</a>：新增 executable 类型的 target，使得 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0281-main-attribute.md">SE-0281</a> 新增的特性更易于使用。</li>
<li>property wrapper 除了可以作为属性外，还可以在函数里作为本地变量。</li>
</ul>
<h3>On the road to Swift 6</h3>
<p>在 2021 年的 1 月，Swift 社区的 <a href="https://twitter.com/tkremenek?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor">Ted Kremenek</a>，他的另一个身份是 Manager of the Languages and Runtime Team @Apple，在 <a href="https://forums.swift.org/categories">swift.org forum</a> 公布了一则名为<a href="https://forums.swift.org/t/on-the-road-to-swift-6/32862">《On the road to Swift 6》</a>的文章</p>
<p>在这篇文章里，提到了一些对 Swift 6 的规划，从大方向来说，Ted 提了三点：</p>
<ol>
<li>加速整个 Swift 软件生态的发展：包含兼容更多的开发平台，简化软件的安装部署和大力发展各类工具库。</li>
<li>打造极致的开发体验：包括更快的构建速度，更好用的调试工具，更灵敏的代码补全和更丰富的诊断信息。</li>
<li>结合开发者的反馈进一步发展语言特性：包括完善 API 的设计，拓展其在底层系统，服务器和机器学习方面的应用场景，同时对某些主流的语言特性提供支持，例如并发特性和内存相关的特性。</li>
</ol>
<p>同时 Swift 的核心团队也发生了一些变化，<a href="https://github.com/compnerd">Saleem Abdulrasool</a> 和 <a href="https://github.com/tomerd">Tom Doron</a> 作为新成员加入到核心团队，而 Dave Abrahams 则推出了核心团队。</p>
<p>这里稍微提一下的，Tom 是 SwiftNIO 的核心开发，同时在 SSWG(Swift Server Work Group) 项目中也是主要的发起者，而 Saleem 是 Swift to Windows 的核心发起者，这两个变动结合着最开始的三个大方向，可以看出整个核心团队是言行一致的。</p>
<p>另外关于 Swift 6 的公布时间，Ted 的原话是这样的：</p>
<div class="blockquote"><blockquote><p>Instead of announcing a specific timeline for “Swift 6”, the plan is for the community to be a part of seeing these efforts progress, with focused efforts and goals, and we release Swift 6 when those efforts culminate.</p>
</blockquote></div>
<p>所以这样看来，Swift 6 还是有一段时间才能与我们见面，毕竟人家说了 when those efforts culminate！</p>
<p>那反过来看，Swift 5 还将会是近期使用的主要版本。（PS：希望今年的 WWDC 21 不要被打脸）</p>
<h2>技术社区</h2>
<h3>语言排行榜</h3>
<p>同样我们来看一看编程语言排行榜 <a href="https://www.tiobe.com/tiobe-index/">TIOBE</a> 和 <a href="https://pypl.github.io/PYPL.html">PYPL</a> 的情况，在 TIOBE 的排行榜中，Swift 在今年的排名是第 15 名，而 Objective-C 已经彻底排在了 20 名之外了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864032_546946.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864032_546946.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864032_546946.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864032_546946.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864032_546946.png" alt="image.png"loading="lazy" decoding="async" width="1884" height="1530" /></picture></figure></div><p>而在 PYPL 的排行榜中，Swift 和 Objective-C 的热度还是较为接近的。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864023_498287.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1110/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304864023_498287.png" alt="image.png"loading="lazy" decoding="async" width="1110" height="1612" /></picture></figure></div><p>从社区的语言排行榜来看，虽然乍一看，感觉还是 Swift 和 Objective-C 共存的大环境，但其实背后也反映出，Swift 已经被大部分开发者所接受了。</p>
<h3>社区活跃度</h3>
<p>同样通过 <a href="https://madnight.github.io/githut/#/pull_requests/2021/1">GitHut 2.0</a> 这个工具对 GitHub 进行分析。</p>
<p>下面四张图的 Y 轴分别代表了 Pull Requests ，Pushes，Stars，Issues 的数量，蓝色的线代表 Objective-C ，浅橙色的线代表 Swift。</p>
<p>可以发现，在 Pull Request 方面，Swift 占比约 0.595%，而 Objective-C 占比约 0.335%</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864008_550169.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864008_550169.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864008_550169.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864008_550169.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864008_550169.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1262" /></picture></figure></div><p>同时 Push 方面，Swift 占比约 0.476%，而 Objective-C 占比约 0.310%</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864001_855238.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864001_855238.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864001_855238.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864001_855238.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864001_855238.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1262" /></picture></figure></div><p>在 Stars 方面，Swift 占比约 2.107%，而 Objective-C 占比约 1.067%</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863995_182545.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304863995_182545.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863995_182545.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304863995_182545.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304863995_182545.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1262" /></picture></figure></div><p>在 Issue 方面，Swift 占比约 0.767%，而 Objective-C 占比约 0.607%</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863988_833039.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304863988_833039.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863988_833039.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304863988_833039.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304863988_833039.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1262" /></picture></figure></div><p>总的来看，在 GitHub 的大环境中，社区中的开发者还是持续看好 Swift，也相对更加活跃，尤其在 Star 这项指标上可以明显看出，它高出了 Objective-C 近一倍！</p>
<h3>商用 SDK 的技术选型</h3>
<p>在今年我们还发现了一些有意思的现象，不少商用 SDK 也开始了 Swift 的迁移。</p>
<p>例如国外的 <a href="https://www.nordicsemi.com/">Nordic Semiconductor</a> 公司，它是北欧的一个半导体公司，主营蓝牙芯片，在业界属于领先地位，不少使用它家芯片的团队会涉及到固件升级问题，无线的升级方案需要进行固件传输、校验、升级管理等动作，而这些动作都得使用它们家提供的 SDK 来完成。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863977_629623.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_200/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863977_629623.png" alt="image.png"loading="lazy" decoding="async" width="200" height="200" /></picture></figure></div><p>在 Nordic Semiconductor 的 <a href="https://github.com/NordicSemiconductor">GitHub 页面</a>上，我们可以看到目前提供的商用 SDK 中，iOS 端只有 <a href="https://github.com/NordicSemiconductor/IOS-Pods-DFU-Library">Swift 版本</a>，而 Android 端只有 <a href="https://github.com/NordicSemiconductor/Android-DFU-Library">Java 版本</a>。</p>
<p>同时像 Google 的 Firebase 在其 <a href="https://github.com/firebase/firebase-ios-sdk/blob/master/ROADMAP.md">RoadMap</a> 里也明确指出了将更加关注 Swift 的使用体验并开始了部分改造。</p>
<p>相信不久的将来，会有越来越多的厂商加入到 Swift 的社区中，除了 <code>Swift 是未来</code> 这样人人都懂的道理以外，这两年新增的特性，例如 ABI 稳定，Module 稳定，以及 SPM 对 binary 组件的支持，都会导致厂商的态度改变，尤其是那些需要使用非源码形式发布组件的厂商，毕竟这些特性从根本上解决他们面临的工程问题。</p>
<h2>Apple 生态</h2>
<h3>SDK 能力</h3>
<p>同样，我们继续分析了 <a href="https://developer.apple.com/documentation/technologies">Apple Developer Documentation</a> 下的 239 个 主题，发现今年的 Swift 独占和 Objective-C 独占的 SDK 情况如下</p>
<div class="block-table"><table><thead>
<tr>
  <th>维度</th>
  <th style="text-align:center">个数</th>
  <th>SDK 名称</th>
</tr>
</thead>
<tbody>
<tr>
  <td>Swift 独占</td>
  <td style="text-align:center">13</td>
  <td>Swift(Swift Standard Library)，Combine，SwiftUI，RealityKit，CareKit，Create ML(Create ML， Create MLUI)，Playground Support，PlaygroundBluetooth，Apple CryptoKit，Swift Packages(Swift Package Manager)，Developer Tools Support，System，WidgetKit</td>
</tr>
<tr>
  <td>Objective-C 独占</td>
  <td style="text-align:center">12</td>
  <td>DarwinNotify，DriverKit（macOS 专属），EndpointSecurity（macOS 专属），HIDDriverKit（macOS 专属），Kernel（macOS 专属），NetworkingDriverKit（硬件驱动相关），PCIDriverKit（硬件驱动相关），SerialDriverKit（硬件驱动相关），USBDriverKit（硬件驱动相关），USBSerialDriverKit（硬件驱动相关），xcselect （macOS 专属），SCSIControllerDriverKit</td>
</tr>
</tbody>
</table></div><p>在 Swift 独占方面，新增了 3 个 SDK，分别是 Developer Tools Support，System，WidgetKit，其中 <a href="https://developer.apple.com/documentation/System">System</a> 是个用于进行底层文件操作（low-level file operation）的库，似乎这也是 Apple 的首个用 Swift 编写的系统底层库(PS：如果说的不对，还请各位读者指正)；另外一个想说的重点就是 <a href="https://developer.apple.com/documentation/WidgetKit">WidgetKit</a>，这也是首次 Apple 在推广系统新特性的时候强制要求开发者必须使用 Swift 技术，这个策略我认为还是十分高明的，它为 Swift 技术的推广和应用找到了新的出路。</p>
<p>同时 Objective-C 独占方面，新增了一个 <a href="https://developer.apple.com/documentation/scsicontrollerdriverkit">SCSIControllerDriverKit</a>，但相比于去年，<a href="https://developer.apple.com/documentation/professional_video_applications">Professional Video Applications</a> 和 <a href="https://developer.apple.com/documentation/iousbhost">IOUSBHost</a> 两个 Objective-C 独有的 SDK 被改造成了 Swift 和 Objective-C 都可以使用的情况，而 QTKit 被彻底废弃了。</p>
<p>至此，我们发现了，Swift 独占库的数量首次大于了 Objective-C 的独占库，是不是很有意思！</p>
<h3>原生 App 分析</h3>
<p>国外的开发者 <a href="https://github.com/Timac">Timac</a> 在其文章<a href="https://blog.timac.org/2020/0927-state-of-swift-ios14/">《Apple’s use of Swift and SwiftUI in iOS 14》</a>里对 iOS14 中的 Swift 和 SwiftUI 的使用情况进行了分析。</p>
<p>iOS 14.0 包含了 291 个使用 Swift 技术的二进制文件（PS: 还有一个统计口径是 351 个，不过这里面有很多程序对 Swift 的使用很初级，所以 Timac 就将其排除了），这个数量比 iOS 13 多了一倍以上，另外 Swift UI 也在 iOS 14 上被广泛使用，目前已经有 43 个了，其中去年新增的翻译应用是完全使用 Swift 和 SwiftUI 编写的 App。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863962_2405405.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304863962_2405405.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863962_2405405.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304863962_2405405.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304863962_2405405.png" alt="image.png"loading="lazy" decoding="async" width="1714" height="1090" /></picture></figure></div><h3>iOS 中不同编程语言的发展</h3>
<p>同样是 Timac 在其文章<a href="https://blog.timac.org/2020/1019-evolution-of-the-programming-languages-from-iphone-os-to-ios-14/">《Evolution of the programming languages from iPhone OS 1.0 to iOS 14》</a>给出了很多有意思的结论。</p>
<p>首先，在 iOS 14 中，总共有 4173 个二进制文件，具体的列表可以参考 <a href="https://blog.timac.org/2020/1019-evolution-of-the-programming-languages-from-iphone-os-to-ios-14/iOS14.txt">iOS 14.0 (18A373) 统计</a>，其中：</p>
<ul>
<li>88% 使用 Objective-C</li>
<li>17% 使用 C++</li>
<li>8% 使用 Swift</li>
<li>8% 全部使用 C</li>
<li>1% 使用 SwiftUI</li>
</ul>
<p>下面的图是 iPhone OS 1.0 到 iOS 14.0 中，各个二进制文件的情况，注意这里的二进制文件可以包含多个语言，所以下表的总数可能会大于二进制的总数，例如 iOS 14.0 里 <code>44 + 351 + 337 + 708 + 3667 &gt; 4173</code></p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863953_6278715.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304863953_6278715.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863953_6278715.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304863953_6278715.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304863953_6278715.png" alt="image.png"loading="lazy" decoding="async" width="2884" height="1618" /></picture></figure></div><p>从这个视角来看，也可以得出几个有意思的结论：</p>
<ul>
<li>首先，iOS 的每个版本都变得更加复杂</li>
<li>Swift 的使用在不断增多，而且至少目前来看，Swift 的使用已经超过了 C</li>
<li>Objective-C 的增长还是比较稳定的</li>
<li>C++ 的增长比较缓慢，或者说相当缓慢</li>
<li>C 的增加几乎没有变化</li>
</ul>
<p>如果上面的图看起来不明显，我们可以通过这个图来看趋势。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500/format,webp 1x, https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000/format,webp 2x" media="(min-width: 1500px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1500 1x, https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_3000 2x" media="(min-width: 1500px)"><source srcset="https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304863945_921041.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304863945_921041.png" alt="image.png"loading="lazy" decoding="async" width="3254" height="1472" /></picture></figure></div><p>当然上面的分析是基于数量来进行的，那么如果我们从体积上进行分析，也就是二进制大小的角度来看，又会得出怎样的结论呢？</p>
<p>Timac 在其文章<a href="https://blog.timac.org/2020/1122-comparing-iphone-os-with-ios-14-using-tree-maps/">《Comparing iPhone OS 1.0 with iOS 14 using tree maps》</a>里，也给出了一些自己的解读。</p>
<p>下面是 Timac 根据相关的数据和脚本绘制出来的 iOS 14 的 tree map<a href="https://www.wikiwand.com/zh/%E7%9F%A9%E5%BD%A2%E5%BC%8F%E6%A0%91%E7%8A%B6%E7%BB%93%E6%9E%84%E7%BB%98%E5%9B%BE%E6%B3%95">（矩形式树状结构绘图法）</a></p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863937_145416.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1000/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863937_145416.png" alt="image.png"loading="lazy" decoding="async" width="1000" height="1000" /></picture></figure></div><ul>
<li>其中 Preinstalled Assets 和 Linguistic Data 是与机器学习相关的预置资源</li>
<li>Health 相关的内容在 iOS 14 的占比不算小，可以看出其重视程度</li>
<li>在 iOS 3.1 之后，提供了 dyld shared cache 技术，红色区域就是支持这个特性的 framework。</li>
</ul>
<p>当然，Timac 对这个结果又进行了更细致的划分，它的结果如下</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863929_410721.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1000/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863929_410721.png" alt="image.png"loading="lazy" decoding="async" width="1000" height="1000" /></picture></figure></div><p>这里我们从二进制的大小，或者代码量的多少来考察某个系统功能的重要性，我们可以明显的看到，Apple 的人工智能推动了设备上的机器学习，如图像和视频中的物体检测、语言分析、声音分类和文本识别等技术。</p>
<p>所以如果未来想继续在 iOS 上开发的话，机器学习可能会是一个必备的基础知识了（PS：如果你开发过 <a href="https://developer.apple.com/documentation/widgetkit/intentconfiguration">IntentConfiguration</a> 类型的小组件，我想你大概就明白我在说什么了）。</p>
<h2>国内外客户端的使用现状</h2>
<h3>数据样本</h3>
<p>去年我们分析了国内外 App 使用 Swift 的情况，今年我们继续走起。</p>
<p>扫描的原理借鉴了<a href="https://mp.weixin.qq.com/s/vF_oOWFLimlyRi4mZpgpeQ">《如何检测 iOS 应用程序是否使用 Swift？》</a>，相应的工具可以参考 <a href="https://github.com/ZRTransmitter/SwiftAppAnalyzer">Swift App Analyzer</a>，这是我和好基友 <a href="https://github.com/OneeMe">OneeMe</a> 一起编写的。</p>
<p>App 排行榜的数据来源是 <a href="https://www.qimai.cn/">七麦数据</a> 提供的，日期为 2021 年 3 月 21 日，<a href="https://www.qimai.cn/rank/index/brand/free/device/iphone/country/cn/genre/5000/date/2021-03-21">国内免费应用 Top 100 榜单</a> 和 <a href="https://www.qimai.cn/rank/index/brand/free/device/iphone/country/us/genre/5000/date/2021-03-21">国外免费应用 Top 100 榜单</a></p>
<p>下面是扫描的结果：</p>
<div class="block-table"><table><thead>
<tr>
  <th>序号</th>
  <th>国内 App 版本</th>
  <th>是否使用 Swift</th>
  <th>国外 App 名称</th>
  <th>是否使用 Swift</th>
</tr>
</thead>
<tbody>
<tr>
  <td>01</td>
  <td>搜狗输入法</td>
  <td>NO</td>
  <td>Twitter</td>
  <td>YES</td>
</tr>
<tr>
  <td>02</td>
  <td>百度地图</td>
  <td>YES</td>
  <td>Uber</td>
  <td>YES</td>
</tr>
<tr>
  <td>03</td>
  <td>招商银行</td>
  <td>YES</td>
  <td>Fontise</td>
  <td>YES</td>
</tr>
<tr>
  <td>04</td>
  <td>优酷</td>
  <td>YES</td>
  <td>Prime Video</td>
  <td>YES</td>
</tr>
<tr>
  <td>05</td>
  <td>QQ浏览器</td>
  <td>NO</td>
  <td>Nike</td>
  <td>YES</td>
</tr>
<tr>
  <td>06</td>
  <td>QQ音乐</td>
  <td>YES</td>
  <td>Dasher</td>
  <td>YES</td>
</tr>
<tr>
  <td>07</td>
  <td>肯德基</td>
  <td>YES</td>
  <td>Capital One</td>
  <td>YES</td>
</tr>
<tr>
  <td>08</td>
  <td>抖音极速版</td>
  <td>NO</td>
  <td>PayPal</td>
  <td>YES</td>
</tr>
<tr>
  <td>09</td>
  <td>中国建设银行</td>
  <td>YES</td>
  <td>Twitch</td>
  <td>YES</td>
</tr>
<tr>
  <td>10</td>
  <td>饿了么</td>
  <td>NO</td>
  <td>Telegram</td>
  <td>YES</td>
</tr>
<tr>
  <td>11</td>
  <td>携程旅行</td>
  <td>YES</td>
  <td>Translate</td>
  <td>YES</td>
</tr>
<tr>
  <td>12</td>
  <td>闲鱼</td>
  <td>NO</td>
  <td>TV Remote</td>
  <td>YES</td>
</tr>
<tr>
  <td>13</td>
  <td>汽车之家</td>
  <td>NO</td>
  <td>Life360</td>
  <td>YES</td>
</tr>
<tr>
  <td>14</td>
  <td>WiFi 万能钥匙</td>
  <td>YES</td>
  <td>Google Photos</td>
  <td>YES</td>
</tr>
<tr>
  <td>15</td>
  <td>微视</td>
  <td>YES</td>
  <td>Walgreens</td>
  <td>YES</td>
</tr>
<tr>
  <td>16</td>
  <td>菜鸟</td>
  <td>YES</td>
  <td>Pinterest</td>
  <td>YES</td>
</tr>
<tr>
  <td>17</td>
  <td>高德地图</td>
  <td>NO</td>
  <td>Vrbo</td>
  <td>YES</td>
</tr>
<tr>
  <td>18</td>
  <td>知乎</td>
  <td>YES</td>
  <td>Chase</td>
  <td>YES</td>
</tr>
<tr>
  <td>19</td>
  <td>手机营业厅</td>
  <td>YES</td>
  <td>Starbucks</td>
  <td>YES</td>
</tr>
<tr>
  <td>20</td>
  <td>国家反诈中心</td>
  <td>NO</td>
  <td>Pandora</td>
  <td>YES</td>
</tr>
<tr>
  <td>21</td>
  <td>58 同城</td>
  <td>YES</td>
  <td>Google Docs</td>
  <td>NO</td>
</tr>
<tr>
  <td>22</td>
  <td>淘宝特价版</td>
  <td>NO</td>
  <td>Waze</td>
  <td>YES</td>
</tr>
<tr>
  <td>23</td>
  <td>UC 浏览器</td>
  <td>NO</td>
  <td>Credit Karma</td>
  <td>YES</td>
</tr>
<tr>
  <td>24</td>
  <td>小红书</td>
  <td>YES</td>
  <td>MM Live</td>
  <td>YES</td>
</tr>
<tr>
  <td>25</td>
  <td>微博</td>
  <td>NO</td>
  <td>Facebook</td>
  <td>YES</td>
</tr>
<tr>
  <td>26</td>
  <td>芒果TV</td>
  <td>NO</td>
  <td>Amazon Alexa</td>
  <td>YES</td>
</tr>
<tr>
  <td>27</td>
  <td>天眼查</td>
  <td>NO</td>
  <td>Snapchat</td>
  <td>YES</td>
</tr>
<tr>
  <td>28</td>
  <td>驾考宝典</td>
  <td>NO</td>
  <td>Coinbase</td>
  <td>YES</td>
</tr>
<tr>
  <td>29</td>
  <td>探探</td>
  <td>YES</td>
  <td>Xbox</td>
  <td>YES</td>
</tr>
<tr>
  <td>30</td>
  <td>个人所得税</td>
  <td>NO</td>
  <td>ClassDojo</td>
  <td>YES</td>
</tr>
<tr>
  <td>31</td>
  <td>腾讯地图</td>
  <td>NO</td>
  <td>Walmart</td>
  <td>YES</td>
</tr>
<tr>
  <td>32</td>
  <td>SOUL</td>
  <td>YES</td>
  <td>Google Maps</td>
  <td>NO</td>
</tr>
<tr>
  <td>33</td>
  <td>美柚</td>
  <td>YES</td>
  <td>PicsArt</td>
  <td>YES</td>
</tr>
<tr>
  <td>34</td>
  <td>轻颜相机</td>
  <td>YES</td>
  <td>Chrome</td>
  <td>NO</td>
</tr>
<tr>
  <td>35</td>
  <td>BOSS 直聘</td>
  <td>NO</td>
  <td>Hulu</td>
  <td>YES</td>
</tr>
<tr>
  <td>36</td>
  <td>快手极速版</td>
  <td>YES</td>
  <td>Outlook</td>
  <td>YES</td>
</tr>
<tr>
  <td>37</td>
  <td>作业帮</td>
  <td>YES</td>
  <td>Disney+</td>
  <td>YES</td>
</tr>
<tr>
  <td>38</td>
  <td>美团秀秀</td>
  <td>YES</td>
  <td>CapCut</td>
  <td>YES</td>
</tr>
<tr>
  <td>39</td>
  <td>Chrome</td>
  <td>NO</td>
  <td>Booking.com</td>
  <td>YES</td>
</tr>
<tr>
  <td>40</td>
  <td>迅雷</td>
  <td>YES</td>
  <td>Instagram</td>
  <td>YES</td>
</tr>
<tr>
  <td>41</td>
  <td>贝壳找房</td>
  <td>YES</td>
  <td>Zelle</td>
  <td>YES</td>
</tr>
<tr>
  <td>42</td>
  <td>WPS Office</td>
  <td>YES</td>
  <td>Messenger</td>
  <td>NO</td>
</tr>
<tr>
  <td>43</td>
  <td>百度网盘</td>
  <td>YES</td>
  <td>SHEIN</td>
  <td>YES</td>
</tr>
<tr>
  <td>44</td>
  <td>美团外卖</td>
  <td>NO</td>
  <td>Google Duo</td>
  <td>YES</td>
</tr>
<tr>
  <td>45</td>
  <td>番茄小说</td>
  <td>NO</td>
  <td>Zoom</td>
  <td>NO</td>
</tr>
<tr>
  <td>46</td>
  <td>中国工商银行</td>
  <td>YES</td>
  <td>Roku</td>
  <td>YES</td>
</tr>
<tr>
  <td>47</td>
  <td>快手</td>
  <td>YES</td>
  <td>Target</td>
  <td>YES</td>
</tr>
<tr>
  <td>48</td>
  <td>美颜相机</td>
  <td>YES</td>
  <td>WhatsApp</td>
  <td>YES</td>
</tr>
<tr>
  <td>49</td>
  <td>七猫小说</td>
  <td>YES</td>
  <td>Grubhub</td>
  <td>YES</td>
</tr>
<tr>
  <td>50</td>
  <td>滴滴出行</td>
  <td>YES</td>
  <td>Postmates</td>
  <td>YES</td>
</tr>
<tr>
  <td>51</td>
  <td>微信</td>
  <td>YES</td>
  <td>PS App</td>
  <td>YES</td>
</tr>
<tr>
  <td>52</td>
  <td>韩剧 TV</td>
  <td>YES</td>
  <td>Tinder</td>
  <td>YES</td>
</tr>
<tr>
  <td>53</td>
  <td>酷狗音乐</td>
  <td>NO</td>
  <td>Hopper</td>
  <td>YES</td>
</tr>
<tr>
  <td>54</td>
  <td>唯品会</td>
  <td>YES</td>
  <td>Shazam</td>
  <td>YES</td>
</tr>
<tr>
  <td>55</td>
  <td>爱奇艺</td>
  <td>YES</td>
  <td>Itsme</td>
  <td>YES</td>
</tr>
<tr>
  <td>56</td>
  <td>哔哩哔哩</td>
  <td>YES</td>
  <td>Bird</td>
  <td>YES</td>
</tr>
<tr>
  <td>57</td>
  <td>阿里巴巴</td>
  <td>NO</td>
  <td>Uber Eats</td>
  <td>YES</td>
</tr>
<tr>
  <td>58</td>
  <td>京东金融</td>
  <td>NO</td>
  <td>Netflix</td>
  <td>YES</td>
</tr>
<tr>
  <td>59</td>
  <td>醒图</td>
  <td>YES</td>
  <td>Domino’s</td>
  <td>YES</td>
</tr>
<tr>
  <td>60</td>
  <td>网易云音乐</td>
  <td>YES</td>
  <td>Arch-US</td>
  <td>YES</td>
</tr>
<tr>
  <td>61</td>
  <td>支付宝</td>
  <td>YES</td>
  <td>DoorDash</td>
  <td>YES</td>
</tr>
<tr>
  <td>62</td>
  <td>转转</td>
  <td>YES</td>
  <td>Fetch Rewards</td>
  <td>YES</td>
</tr>
<tr>
  <td>63</td>
  <td>叮咚买菜</td>
  <td>YES</td>
  <td>CBS Sports</td>
  <td>YES</td>
</tr>
<tr>
  <td>64</td>
  <td>今日头条</td>
  <td>YES</td>
  <td>Shop</td>
  <td>YES</td>
</tr>
<tr>
  <td>65</td>
  <td>邮储银行</td>
  <td>NO</td>
  <td>Spotify</td>
  <td>YES</td>
</tr>
<tr>
  <td>66</td>
  <td>懂车帝</td>
  <td>YES</td>
  <td>TikTok</td>
  <td>YES</td>
</tr>
<tr>
  <td>67</td>
  <td>夸克</td>
  <td>YES</td>
  <td>Lyft</td>
  <td>YES</td>
</tr>
<tr>
  <td>68</td>
  <td>美团</td>
  <td>NO</td>
  <td>SoundCloud</td>
  <td>YES</td>
</tr>
<tr>
  <td>69</td>
  <td>喜马拉雅</td>
  <td>YES</td>
  <td>WOMBO</td>
  <td>YES</td>
</tr>
<tr>
  <td>70</td>
  <td>得物(毒)</td>
  <td>YES</td>
  <td>Zillow</td>
  <td>YES</td>
</tr>
<tr>
  <td>71</td>
  <td>中国农业银行</td>
  <td>NO</td>
  <td>TextNow</td>
  <td>YES</td>
</tr>
<tr>
  <td>72</td>
  <td>QQ 邮箱</td>
  <td>YES</td>
  <td>HBO Max</td>
  <td>YES</td>
</tr>
<tr>
  <td>73</td>
  <td>钉钉</td>
  <td>NO</td>
  <td>Discord</td>
  <td>YES</td>
</tr>
<tr>
  <td>74</td>
  <td>百度</td>
  <td>YES</td>
  <td>Amazon Music</td>
  <td>YES</td>
</tr>
<tr>
  <td>75</td>
  <td>Top Widgets</td>
  <td>YES</td>
  <td>Google</td>
  <td>YES</td>
</tr>
<tr>
  <td>76</td>
  <td>Keep</td>
  <td>YES</td>
  <td>Google Drive</td>
  <td>YES</td>
</tr>
<tr>
  <td>77</td>
  <td>全民 K 歌</td>
  <td>NO</td>
  <td>Airbnb</td>
  <td>YES</td>
</tr>
<tr>
  <td>78</td>
  <td>哈罗出行</td>
  <td>NO</td>
  <td>Tubi</td>
  <td>YES</td>
</tr>
<tr>
  <td>79</td>
  <td>中国银行</td>
  <td>YES</td>
  <td>Etsy</td>
  <td>YES</td>
</tr>
<tr>
  <td>80</td>
  <td>Days Matter</td>
  <td>YES</td>
  <td>IRL</td>
  <td>YES</td>
</tr>
<tr>
  <td>81</td>
  <td>新氧医美</td>
  <td>NO</td>
  <td>Yelp</td>
  <td>YES</td>
</tr>
<tr>
  <td>82</td>
  <td>安居客</td>
  <td>YES</td>
  <td>Peacock</td>
  <td>YES</td>
</tr>
<tr>
  <td>83</td>
  <td>企业微信</td>
  <td>NO</td>
  <td>YouTube Music</td>
  <td>YES</td>
</tr>
<tr>
  <td>84</td>
  <td>中国移动</td>
  <td>YES</td>
  <td>Venmo</td>
  <td>YES</td>
</tr>
<tr>
  <td>85</td>
  <td>手机淘宝</td>
  <td>YES</td>
  <td>ESPN</td>
  <td>YES</td>
</tr>
<tr>
  <td>86</td>
  <td>云闪付</td>
  <td>NO</td>
  <td>IRS2Go</td>
  <td>NO</td>
</tr>
<tr>
  <td>87</td>
  <td>QQ</td>
  <td>NO</td>
  <td>Ring</td>
  <td>YES</td>
</tr>
<tr>
  <td>88</td>
  <td>交管 12123</td>
  <td>NO</td>
  <td>Wish</td>
  <td>YES</td>
</tr>
<tr>
  <td>89</td>
  <td>拼多多</td>
  <td>YES</td>
  <td>ESPN</td>
  <td>YES</td>
</tr>
<tr>
  <td>90</td>
  <td>京东</td>
  <td>YES</td>
  <td>Gmail</td>
  <td>YES</td>
</tr>
<tr>
  <td>91</td>
  <td>好看视频</td>
  <td>YES</td>
  <td>Amazon</td>
  <td>NO</td>
</tr>
<tr>
  <td>92</td>
  <td>铁路 12306</td>
  <td>NO</td>
  <td>Robinhood</td>
  <td>YES</td>
</tr>
<tr>
  <td>93</td>
  <td>大麦</td>
  <td>YES</td>
  <td>YouTube</td>
  <td>NO</td>
</tr>
<tr>
  <td>94</td>
  <td>大众点评</td>
  <td>NO</td>
  <td>Reddit</td>
  <td>YES</td>
</tr>
<tr>
  <td>95</td>
  <td>酷狗铃声</td>
  <td>NO</td>
  <td>OfferUp</td>
  <td>YES</td>
</tr>
<tr>
  <td>96</td>
  <td>抖音</td>
  <td>NO</td>
  <td>Musi</td>
  <td>NO</td>
</tr>
<tr>
  <td>97</td>
  <td>剪映</td>
  <td>YES</td>
  <td>Widgetsmith</td>
  <td>YES</td>
</tr>
<tr>
  <td>98</td>
  <td>货拉拉</td>
  <td>NO</td>
  <td>eBay</td>
  <td>YES</td>
</tr>
<tr>
  <td>99</td>
  <td>腾讯会议</td>
  <td>NO</td>
  <td>Chick-fil-A</td>
  <td>YES</td>
</tr>
<tr>
  <td>100</td>
  <td>腾讯视频</td>
  <td>NO</td>
  <td>Cash App</td>
  <td>YES</td>
</tr>
</tbody>
</table></div><div class="blockquote"><blockquote><p>在 GitHub 上，其实还有一份统计数据 <a href="https://github.com/flexih/SnakeList">Snake List</a>，是 <a href="https://github.com/flexih">Flexih</a> 统计的，除了 Swift 技术外，还统计了 Weex，React Native，Flutter 等技术的情况，大家可以作为参考。</p>
</blockquote></div>
<h3>2021 年</h3>
<p>在国外 Top 100 的免费应用中，Swift 混编占比 91%。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863910_456104.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1116/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863910_456104.png" alt="image.png"loading="lazy" decoding="async" width="1116" height="736" /></picture></figure></div><p>在国内 Top 100 的免费应用中，Swift 混编占比 59%。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863903_541881.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1116/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863903_541881.png" alt="image.png"loading="lazy" decoding="async" width="1116" height="736" /></picture></figure></div><p>在之前的文章中，<a href="https://mp.weixin.qq.com/s/Ib48PWpKJNALlNoL4lm4-g">《一次关于 Swift 在 iOS 生态圈里的现状调研》</a>，我们也整理过一些数据。</p>
<p>在 2019 年，国内的 Swift 混编应用占比为 22%，国外的 Swift 混编应用占比 78%，
在 2020 年，国内的 Swift 混编应用占比为 30.4%，国外的 Swift 混编应用占比 82.3%。</p>
<p>如果将近几年的数据连着看，Swift 在国内外的变化趋势如下图所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304863894_148689.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1120/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304863894_148689.png" alt="image.png"loading="lazy" decoding="async" width="1120" height="752" /></picture></figure></div><h3>数据解读</h3>
<p>纯看数据的话：</p>
<ul>
<li>在国内，Swift 混编占比达 59%，较去年的 30%，又增长了 29%，整体占比也已经过半了！</li>
<li>在国外，Swift 混编占比 91%，较去年的82.3%，又增长 9%，纯 Objective-C 的应用也只有个位数占比了。</li>
</ul>
<p>那么我们再分析一些数据背后的内容：</p>
<ul>
<li>从表面看，除了去年提到的 BAT 之外，今日头条，快手，滴滴，支付宝，京东，拼多多等一众应用也都在今年完成了 Swift 的初体验，比较有意思的是美团系的应用（美团，大众点评，美团外卖）目前似乎还没有任何动静。</li>
<li>虽然国内的 Swift 混编占比变高，但我个人认为，这并代表国内大部分公司要开始转型 Swift 技术了，这样的变化，主要是因为去年 Apple 新增的 Widget 技术导致的，因为想开发 Widget 必须使用 Swift 相关的技术，而上面提到的各个应用，大多都提供了相应的小组件。</li>
<li>如果历史有可以借鉴的地方，那么 2021 年的国内 Swift 占比（59%）与 2019 年的国外占比（78%）还算比较接近，至少不像去年（30% 和 82.3%）的差距那么大，那么按照这个趋势发展的话，我们是否可以预言在未来的 3-5 年内，国内的 Swift 混编应用占比也将达到 90% 左右？</li>
</ul>
<div class="blockquote"><blockquote><p>PS：在写这篇文档的时候，发现微博也支持小组件了，所以估计上面的这个数据又得增加 1% 了。</p>
</blockquote></div>
<h2>总结与展望</h2>
<p>在做完了今年的调研后，我们能得出什么结论呢？</p>
<ul>
<li>虽然看起来现阶段的 Swift 还是在一个积累的过程，但随着 WidgetKit 这个标志性的 SDK 诞生，我相信这个发展阶段会从积累阶段慢慢转向发展期，毕竟现在 ABI 稳定了，Module 稳定了，对二进制组件的支持也有了，还有 Swift 语言本身的版本变化也逐渐稳定了，这些都给与了 Swift 很好的支持。</li>
<li>Swift 的发展方向绝不只是为了 Apple 生态体系内的那点事儿，这个从社区的规划也好，从 Timac 的那几篇分析文章也好，我们都可以看出它在多元发展上的决心，Swift 真的很想破圈。</li>
<li>国内的 Swift 发展被去年的 iOS 14 新特性给盘活了，WidgetKit 功不可没，虽然还不能给出大部分公司都将转型的结论，但至少绝大部分互联网的头部公司已经兼容了 Swift 的开发，这是一个好的开始，相信在可见的未来，Apple 的转型决心必然会让国内的公司会更加重视这方面工作的重要性。</li>
</ul>
<p>在最后，我来说说这一年的一些其他见闻：</p>
<p>虽然不久前 Google 归档了 <a href="https://github.com/tensorflow/swift">Swift for TensorFlow</a> 项目，让很多人看衰 Swift 在机器学习或者人工智能方面的发展，但其实我觉得是有点没必要，Swift 在这方面的发展其实并不依赖 Google，Apple 自己在这方面就很有建树，如果感兴趣应该看看 <a href="https://machinelearning.apple.com/">Machine Learning Research at Apple</a> 这个网站，这才代表 Apple 和 Swift 在机器学习方面的真实水平。</p>
<p>另外，虽然还是能在某些技术群里看到 “Swift 无用”，”Swift 火不了”，”我们不需要用 Swift 开发” 的字眼，但这样的数量相比于前几年而言，真的越来越少了。</p>
<p>另外据我所知，字节跳动和快手团队正在大力发展 Swift 方面的建设，虽然这只是国内诸多公司的个例，但我相信随着这些头部大厂的加入，Swift 成为原生开发的主流趋势会在国内越来越明显，当然不得不承认，跨端技术在国内也有着极大的市场份额，所以估计未来作为 iOS 端上的程序员，可能要具备 Objective-C，Swift，JavaScript/TypeScript 和 Flutter 的语言技术栈。</p>
<p>好了，今年的调研报告就到此结束了，我们明年见！</p>
<h2>参考文档</h2>
<p><a href="https://mp.weixin.qq.com/s/Ib48PWpKJNALlNoL4lm4-g">SketchK - 一次关于 Swift 在 iOS 生态圈里的现状调研</a>
<a href="https://www.hackingwithswift.com/articles/218/whats-new-in-swift-5-3">Paul Hudson - Hacking with Swift - What’s new in Swift 5.3?</a>
<a href="https://www.hackingwithswift.com/articles/228/whats-new-in-swift-5-4">Paul Hudson - Hacking with Swift - What’s new in Swift 5.4?</a>
<a href="https://forums.swift.org/t/on-the-road-to-swift-6/32862">Swift.org forum - On the road to Swift 6</a>
<a href="https://www.tiobe.com/tiobe-index/">TIOBE</a>
<a href="https://pypl.github.io/PYPL.html">PYPL</a>
<a href="https://madnight.github.io/githut/#/pull_requests/2021/1">GitHut 2.0</a>
<a href="https://developer.apple.com/documentation/technologies">Apple - Apple Developer Documentation</a>
<a href="https://blog.timac.org/2020/1122-comparing-iphone-os-with-ios-14-using-tree-maps/">Timac - Comparing iPhone OS 1.0 with iOS 14 using tree maps</a>
<a href="https://blog.timac.org/2020/1019-evolution-of-the-programming-languages-from-iphone-os-to-ios-14/">Timac - Evolution of the programming languages from iPhone OS 1.0 to iOS 14</a>
<a href="https://blog.timac.org/2020/0927-state-of-swift-ios14/">Timac - Apple’s use of Swift and SwiftUI in iOS 14</a>
<a href="https://mp.weixin.qq.com/s/vF_oOWFLimlyRi4mZpgpeQ">Timac - 如何检测 iOS 应用程序是否使用 Swift？</a>
<a href="https://github.com/ZRTransmitter/SwiftAppAnalyzer">OneeMe - Swift App Analyzer</a>
<a href="https://github.com/flexih/SnakeList">Flexih - Snake List</a>
<a href="https://machinelearning.apple.com/">Apple - Machine Learning Research at Apple</a></p>
]]></content:encoded></item><item><title><![CDATA[使用 Swift 编写 CLI 工具的入门教程]]></title><guid>https://swiftsiqi.com/posts/using-swift-to-write-a-cli-tool</guid><link>https://swiftsiqi.com/posts/using-swift-to-write-a-cli-tool</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 26 Dec 2020 11:03:21 +0000</pubDate><content:encoded><![CDATA[<p>为何要使用 Swift 编写脚本工具？
如何从头搭建一个 Swift CLI 项目？
如果你想知道答案，就来读读这篇文章吧！</p>
<h2>概述</h2>
<p>最近在工作中过程中基于 Swift 开发了两款命令行工具：</p>
<ul>
<li><a href="https://www.sketchk.xyz/2020/12/26/Write-a-Swift-CLI/">Nezuko</a> ，一款面向美团壳工程的，类 <code>create-react-app</code> 的脚手架工具。</li>
<li><a href="https://github.com/SketchK/import-sanitizer">ImportSanitizer</a>，一款能够修复不规范头文件引用方式的自动化工具。</li>
</ul>
<p>在整个开发过程中，我们体会到了 Swift 带来的一些变化，当然这里既有好的，也有坏的，不过总的来说，使用 Swift 进行脚本开发还是一件让人愉悦的事情，所以我们迫不及待的邀请你，也就是这篇文章的读者，和我们一起加入 Swift 开发的大军中！</p>
<p>这篇文章通过 Step-By-Step 的方式，指导你完成一个基于 Swift 的命令行工具，不过在开始之前，我们先聊聊为什么要写脚本，以及为什么选用 Swift。</p>
<h3>Why Scripting</h3>
<p>对于软件工程师来说，我们经常会遇到这样的工作，例如重命名设计师提供的图片素材，在海量的数据中提取一些特定信息。这种工作有一些共性，就是它的逻辑很简单，就像代码里的 <code>if-else</code> 一样，只要你够严谨，就一定能得到想要的答案，</p>
<p>但这种机械性的工作很容易因为人为的因素导致错误，例如手抖，眼瞎，以及间歇性失忆，哈哈，而这时候，机器会显得比人靠谱多了！</p>
<p>同时，重复同样的工作是一件低效的事情，我们完全可以通过编写相应的代码将任务自动化，这将极大的提升我们的工作效率。</p>
<p>当然可能有人会说，我还是自己弄吧，但你不觉得这种重复的，机械性的工作很无聊么，我们可是要改变世界的工程师啊！</p>
<h3>Why Swift</h3>
<p>至于为什么使用 Swift 写脚本，我想有人可能会给出这样的答案:</p>
<div class="blockquote"><blockquote><p>swift 真的太棒了，我喜欢 Swift，它是最好的语言！我要用它写后端，要用它写前端，用它写 iOS，写 Android，用它写 CLI，总之，我要用它解决一切的编程问题！</p>
</blockquote></div>
<p>但说实话，这是一个很主观的判断，并不应该成为我们使用 Swift 写 CLI 的客观因素，在实际使用了一段时间后，我认为下面几个因素才是我们使用 Swift 编写脚本的主要原因：</p>
<ul>
<li>降低了 App 开发者编写脚本的门槛，减少了上下文切换的负担</li>
<li>Swift 提供了一些真的很不错的内置库，例如 combine，core graphics，urlsession 等</li>
<li>可以将 App 里的代码引入脚本，避免重复工作</li>
</ul>
<p>关于前两点，我想大家应该会比较好理解，毕竟每次在 Swift 代码和 Bash，Ruby，Python，JavaScript 中切换，会让我产生一种深深的抗拒感，另外在 iOS 上已经得到证明的 core graphics 有理由让我们相信它的品质，同时天然内置的 Combine 让我们在异步编程上有了很好的体验，这让我想起被 JavaScript 里 yield 支配的恐惧，问我为什么要用 yield，不妨来试试 React + Redux + Redux-Saga 的大礼包呀!</p>
<p>关于最后一点，我想展开说说，一方面是因为它基于我真实的开发体验，另一方面是它确实让我真正意识到 Swift 写脚本的优势所在。</p>
<p>在年初的时候，我曾经一度痴迷 SpriteKit，并尝试开发一款属于自己的横版过关游戏，这其中涉及到大量的图片处理，例如使用 Animation 的方式将多张静态图片整合成动态效果，大体的效果就像下面的 gif 一样</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878761_404781.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_580/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304878761_404781.png" alt="image.png"loading="lazy" decoding="async" width="580" height="430" /></picture></figure></div><p>这背后的代码大概如下所示，通过读取相应顺序和数量的图片构建 gif 动画</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">Animation</span><span class="p">(</span>
</div><div class="line">    <span class="n">name</span><span class="p">:</span> <span class="s">&quot;Units/swordsman/male/attack/right&quot;</span><span class="p">,</span>
</div><div class="line">    <span class="n">frameCount</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</div><div class="line">    <span class="n">duration</span><span class="p">:</span> <span class="mf">1.12</span><span class="p">,</span>
</div><div class="line">    <span class="n">tintColor</span><span class="p">:</span> <span class="p">.</span><span class="n">blue</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>虽然看起来代码不多，手写一下就 ok 了，但你得知道，就这样一个杂兵角色，就会有攻击，跳跃，行走，跑动等等等动作，再加上各个方向，以及特殊效果，如果再考虑到，我们的游戏里面大概有 20 多个兵种和 4 个英雄，我想你大概已经体会到这个工作的痛苦了！</p>
<p>在一开始，我用的是 Ruby 来解决重复代码的生成工作，但这里为了减少上下文切换，我将采用 Swift 类型的伪代码来做展示，方便大家快速理解</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">unitKinds</span> <span class="p">=</span> <span class="p">[</span><span class="s">&quot;swordsman&quot;</span><span class="p">,</span> <span class="s">&quot;archer&quot;</span><span class="p">,</span> <span class="s">&quot;knight&quot;</span><span class="p">,</span> <span class="s">&quot;catapult&quot;</span><span class="p">]</span>
</div><div class="line">
</div><div class="line"><span class="k">for</span> <span class="n">kind</span> <span class="k">in</span> <span class="n">unitKinds</span> <span class="p">{</span>
</div><div class="line">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">config</span> <span class="p">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">File</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="s">&quot;</span><span class="si">\(</span><span class="n">kind</span><span class="p">.</span><span class="n">identifer</span><span class="si">)</span><span class="s">/Config&quot;</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">        <span class="k">continue</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="k">try</span> <span class="n">codeGenerator</span><span class="p">.</span><span class="n">generateCode</span><span class="p">(</span><span class="n">from</span><span class="p">:</span> <span class="n">config</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>虽然这么写已经帮我节省了不少时间，但我还是得每次手动维护 unitKinds 数组，并保持它与游戏中的模型数据同步，这其实也挺烦人的，不是么？</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">class</span> <span class="nc">Unit</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">enum</span> <span class="nc">Kind</span><span class="p">:</span> <span class="nb">Int</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="n">swordsman</span>
</div><div class="line">        <span class="k">case</span> <span class="n">archer</span>
</div><div class="line">        <span class="k">case</span> <span class="n">knight</span>
</div><div class="line">        <span class="k">case</span> <span class="n">catapult</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>某日，看着上面的游戏模型数据，我突然顿悟，如果我用 Swift 编写脚本的话，我完全可以复用游戏里的数据模型啊！于是乎，便有了下面的代码：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">GameModels</span>
</div><div class="line">
</div><div class="line"><span class="k">for</span> <span class="n">kind</span> <span class="k">in</span> <span class="n">EnumSequence</span><span class="p">&lt;</span><span class="n">Unit</span><span class="p">.</span><span class="n">Kind</span><span class="p">&gt;()</span> <span class="p">{</span>
</div><div class="line">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">config</span> <span class="p">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">File</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="s">&quot;</span><span class="si">\(</span><span class="n">kind</span><span class="p">.</span><span class="n">identifer</span><span class="si">)</span><span class="s">/Config&quot;</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">        <span class="k">continue</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="k">try</span> <span class="n">codeGenerator</span><span class="p">.</span><span class="n">generateCode</span><span class="p">(</span><span class="n">from</span><span class="p">:</span> <span class="n">config</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>至此，我终于做到了不用再手动维护脚本里的任何代码，就可以直接与 App 里的源码保持一致，这样是不是很 cool！同样的道理，我们还可以应用到很多方面，例如直接利用网络层的 modle 文件生成 mock 数据，而不用在 JSON 编辑器上小心翼翼的粘贴复制了！</p>
<p>所以还在等什么呢，让我们开始写一个 Swift CLI 吧！</p>
<h2>使用 SPM 搭建开发框架</h2>
<p>为了开发 command line tool（CLI），我们需要创建一个新的文件夹，并使用 swift package manager（SPM）来初始化项目</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ mkdir CommandLineTool
</div><div class="line">$ <span class="nb">cd</span> CommandLineTool
</div><div class="line">$ swift package init --type executable
</div></code></pre></div>
</div>
<p>最后一行的 <code>type executable</code> 参数将告诉 SPM，我们想创建一个 CLI，而不是一个 Framework。</p>
<h3>项目里的文件</h3>
<p>在 SPM 初始化项目后，我们会得到如下的一个文件夹结构</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">.
</div><div class="line">├── Package.swift
</div><div class="line">├── README.md
</div><div class="line">├── Sources
</div><div class="line">│   └── CommandLineTool
</div><div class="line">│       └── main.swift
</div><div class="line">└── Tests
</div><div class="line">    ├── CommandLineToolTests
</div><div class="line">    │   ├── CommandLineToolTests.swift
</div><div class="line">    │   └── XCTestManifests.swift
</div><div class="line">    └── LinuxMain.swift
</div></code></pre></div>
</div>
<p>其中有几个需要关心的文件</p>
<ul>
<li><code>Package.swift</code> 文件： 用于描述当前 Package 的信息及其依赖，需要记住的是，在 SPM 的世界里，不再有 Pod 的概念，与之对应概念是 Package，而 CLI 本身也是一个 Package</li>
<li><code>main.swift</code> 文件：这个文件在 Sources 目录下，它代表整个命令行工具的入口，另外记住不要更换这个文件的名字！</li>
<li><code>Tests</code> 文件夹：这个文件夹是用于放置测试代码的。</li>
<li><code>.gitignore</code> 文件：通过这个文件，git 会自动忽略 SPM 生成的 build 文件夹（<code>.build</code> 目录）以及 Xcode Project</li>
</ul>
<h3>将代码划分为 framework 和 executable</h3>
<p>我的一个个人建议是，在一开始就最好将源代码分成两个模块，一个是 framework 模块，一个是 executable 模块。</p>
<p>这样做的原因有 2 点：</p>
<ul>
<li>会让测试变得更加容易</li>
<li>让你的命令行工具也可以作为其他工具依赖的 Package</li>
</ul>
<p>具体怎么做呢？</p>
<p>首先，我们要保证 Sources 目录下有两个文件夹，一个用于存放 executable 相关的逻辑，一个用于存放 framework 相关的逻辑，就像下面一样：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ <span class="nb">cd</span> Sources
</div><div class="line">$ mkdir CommandLineToolCore
</div></code></pre></div>
</div>
<p>SPM 的一个非常好的方面是，它使用文件系统作为它的处理依据，也就是说，只要采用上述操作提供的文件结构，就等于定义了两个模块。</p>
<p>紧接着，我们在 <code>Package.swift</code> 里定义了两个target， 一个是 <code>CommandLineTool</code> 模块，一个是 <code>CommandLineToolCore</code></p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">PackageDescription</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">package</span> <span class="p">=</span> <span class="n">Package</span><span class="p">(</span>
</div><div class="line">    <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineTool&quot;</span><span class="p">,</span>
</div><div class="line">    <span class="n">targets</span><span class="p">:</span> <span class="p">[</span>
</div><div class="line">        <span class="p">.</span><span class="n">target</span><span class="p">(</span>
</div><div class="line">            <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineTool&quot;</span><span class="p">,</span>
</div><div class="line">            <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&quot;CommandLineToolCore&quot;</span><span class="p">]</span>
</div><div class="line">        <span class="p">),</span>
</div><div class="line">        <span class="p">.</span><span class="n">target</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineToolCore&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="p">]</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>通过上面这种方式，我们让 executable 模块依赖了 framework 模块。</p>
<h3>构建 Xcode 项目</h3>
<p>为了能方便的运行和调试代码，我们还需要使用配套的开发工具！</p>
<p>好消息是 SPM 可以根据文件信息自动创建 Xcode 工程，这意味着我们可以使用 Xcode 来开发 CLI 了。</p>
<p>而且在 <code>.gitignore</code> 中会自动忽略这个工程项目，这同时意味着，我们不需要更新 Xcode Project 文件，也不需要担心这类文件的冲突问题，只需要通过下面的命令即可完成工程文件的生成。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift package generate-xcodeproj
</div></code></pre></div>
</div>
<p>记得需要在根目录下执行上面的命令，另外在执行过程中会得到一个 warning，让我们暂且忽略它，在后面我们会将其修复！</p>
<h2>开始动手</h2>
<h3>定义程序入口</h3>
<p>为了能够在命令行和测试用例中方便的运行我们的代码，我们最好不要在 <code>main.swift</code> 中添加过多的逻辑，而是通过程序调用的方式唤起 framework 中的主逻辑。</p>
<p>为了实现这样的目的，我们需要创建一个名为 <code>CommandLineTool.swift</code> 的文件，将其放在 framework 模块中（<code>Sources/CommandLineToolCore</code>），它里面的内容如下</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">Foundation</span>
</div><div class="line">
</div><div class="line"><span class="kd">public</span> <span class="kr">final</span> <span class="kd">class</span> <span class="nc">CommandLineTool</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">arguments</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span>
</div><div class="line">
</div><div class="line">    <span class="kd">public</span> <span class="kd">init</span><span class="p">(</span><span class="n">arguments</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span> <span class="p">=</span> <span class="n">CommandLine</span><span class="p">.</span><span class="n">arguments</span><span class="p">)</span> <span class="p">{</span> 
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">arguments</span> <span class="p">=</span> <span class="n">arguments</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">run</span><span class="p">()</span> <span class="kr">throws</span> <span class="p">{</span>
</div><div class="line">        <span class="bp">print</span><span class="p">(</span><span class="s">&quot;Hello world&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>同时在 <code>main.swift</code> 中添加 <code>run()</code> 方法</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">CommandLineToolCore</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">tool</span> <span class="p">=</span> <span class="n">CommandLineTool</span><span class="p">()</span>
</div><div class="line">
</div><div class="line"><span class="k">do</span> <span class="p">{</span>
</div><div class="line">    <span class="k">try</span> <span class="n">tool</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</div><div class="line"><span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="s">&quot;Whoops! An error occurred: </span><span class="si">\(</span><span class="n">error</span><span class="si">)</span><span class="s">&quot;</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h3>Hello，World</h3>
<p>让我们用命令行看看执行效果吧！不在在真正的运行前，我们还需要完成编译工作，让我们在根目录下执行 <code>swift build</code> 吧，然后再执行 <code>swift run</code> 命令。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift build
</div><div class="line">$ swift run
</div><div class="line">&gt; Hello world
</div></code></pre></div>
</div>
<div class="blockquote"><blockquote><p>我们其实可以通过直接调用 <code>swift run</code> 命令来达到运行程序的目的，因为如果需要的话，它会自动编译我们的项目，但学习一下底层命令的工作原理总是有益的。</p>
</blockquote></div>
<h3>增加依赖</h3>
<p>除非你正在构建一些十分“特殊”的东西，否则你会发现自己需要为你的命令行工具添加一些依赖关系，毕竟有好用的轮子为啥不用呢？</p>
<p>任何 Swift Package 都可以被添加为依赖项，只需在 <code>Package.swift</code> 中指定它。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">PackageDescription</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">package</span> <span class="p">=</span> <span class="n">Package</span><span class="p">(</span>
</div><div class="line">    <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineTool&quot;</span><span class="p">,</span>
</div><div class="line">    <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span>
</div><div class="line">        <span class="p">.</span><span class="n">package</span><span class="p">(</span>
</div><div class="line">            <span class="n">name</span><span class="p">:</span> <span class="s">&quot;Files&quot;</span><span class="p">,</span>
</div><div class="line">            <span class="n">url</span><span class="p">:</span> <span class="s">&quot;https://github.com/johnsundell/files.git&quot;</span><span class="p">,</span>
</div><div class="line">            <span class="n">from</span><span class="p">:</span> <span class="s">&quot;4.2.0&quot;</span>
</div><div class="line">        <span class="p">)</span>
</div><div class="line">    <span class="p">],</span>
</div><div class="line">    <span class="n">targets</span><span class="p">:</span> <span class="p">[</span>
</div><div class="line">        <span class="p">.</span><span class="n">target</span><span class="p">(</span>
</div><div class="line">            <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineTool&quot;</span><span class="p">,</span>
</div><div class="line">            <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&quot;CommandLineToolCore&quot;</span><span class="p">]</span>
</div><div class="line">        <span class="p">),</span>
</div><div class="line">        <span class="p">.</span><span class="n">target</span><span class="p">(</span>
</div><div class="line">            <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineToolCore&quot;</span><span class="p">,</span>
</div><div class="line">            <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&quot;Files&quot;</span><span class="p">])</span>
</div><div class="line">    <span class="p">]</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>上面我添加了对 <code>Files</code> 组件的依赖，它可以让我们在 Swift 中轻松处理文件和文件夹的相关操作。在后面的教程中，我们将使用它在当前文件夹中创建一个文件。</p>
<h3>安装/更新依赖</h3>
<p>一旦我们声明了新的依赖关系，只需要求 SPM 解析新的依赖关系并安装它们，然后重新生成 Xcode 项目即可。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift package update
</div><div class="line">$ swift package generate-xcodeproj
</div></code></pre></div>
</div>
<h3>参数解析</h3>
<p>让我们修改一下 <code>CommandLineTool.swift</code> 里的内容。</p>
<p>将其从打印 <code>Hello, World</code> 的逻辑变为根据命令行参数创建文件的逻辑</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">Foundation</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">Files</span>
</div><div class="line">
</div><div class="line"><span class="kd">public</span> <span class="kr">final</span> <span class="kd">class</span> <span class="nc">CommandLineTool</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">arguments</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span>
</div><div class="line">
</div><div class="line">    <span class="kd">public</span> <span class="kd">init</span><span class="p">(</span><span class="n">arguments</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span> <span class="p">=</span> <span class="n">CommandLine</span><span class="p">.</span><span class="n">arguments</span><span class="p">)</span> <span class="p">{</span> 
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">arguments</span> <span class="p">=</span> <span class="n">arguments</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">run</span><span class="p">()</span> <span class="kr">throws</span> <span class="p">{</span>
</div><div class="line">        <span class="k">guard</span> <span class="n">arguments</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">            <span class="k">throw</span> <span class="n">Error</span><span class="p">.</span><span class="n">missingFileName</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        
</div><div class="line">        <span class="c1">// The first argument is the execution path</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">fileName</span> <span class="p">=</span> <span class="n">arguments</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</div><div class="line">
</div><div class="line">        <span class="k">do</span> <span class="p">{</span>
</div><div class="line">            <span class="k">try</span> <span class="n">Folder</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">createFile</span><span class="p">(</span><span class="n">at</span><span class="p">:</span> <span class="n">fileName</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">            <span class="k">throw</span> <span class="n">Error</span><span class="p">.</span><span class="n">failedToCreateFile</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">public</span> <span class="kd">extension</span> <span class="nc">CommandLineTool</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">enum</span> <span class="nc">Error</span><span class="p">:</span> <span class="n">Swift</span><span class="p">.</span><span class="n">Error</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="n">missingFileName</span>
</div><div class="line">        <span class="k">case</span> <span class="n">failedToCreateFile</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>如上所述，我们把对 <code>Folder.current.createFile()</code> 的调用包装在自己的 <code>do、try、catch</code> 中，以便为用户提供统一的，自定义的错误 API。</p>
<h3>Argument Parser</h3>
<p>除了刚才提到的参数解析方式，Apple 官方还提过了一个更优化的解决方案 - <a href="https://github.com/apple/swift-argument-parser">Swift Argument Parser</a></p>
<p>这里我们做一下简单的介绍，以官方代码为参考示例：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">ArgumentParser</span>
</div><div class="line">
</div><div class="line"><span class="kd">struct</span> <span class="nc">Repeat</span><span class="p">:</span> <span class="n">ParsableCommand</span> <span class="p">{</span>
</div><div class="line">    <span class="p">@</span><span class="n">Flag</span><span class="p">(</span><span class="n">help</span><span class="p">:</span> <span class="s">&quot;Include a counter with each repetition.&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">includeCounter</span> <span class="p">=</span> <span class="kc">false</span>
</div><div class="line">
</div><div class="line">    <span class="p">@</span><span class="n">Option</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="p">.</span><span class="n">shortAndLong</span><span class="p">,</span> <span class="n">help</span><span class="p">:</span> <span class="s">&quot;The number of times to repeat &#39;phrase&#39;.&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">count</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span>
</div><div class="line">
</div><div class="line">    <span class="p">@</span><span class="n">Argument</span><span class="p">(</span><span class="n">help</span><span class="p">:</span> <span class="s">&quot;The phrase to repeat.&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">phrase</span><span class="p">:</span> <span class="nb">String</span>
</div><div class="line">
</div><div class="line">    <span class="kr">mutating</span> <span class="kd">func</span> <span class="nf">run</span><span class="p">()</span> <span class="kr">throws</span> <span class="p">{</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">repeatCount</span> <span class="p">=</span> <span class="bp">count</span> <span class="p">??</span> <span class="p">.</span><span class="bp">max</span>
</div><div class="line">
</div><div class="line">        <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">1.</span><span class="p">..</span><span class="n">repeatCount</span> <span class="p">{</span>
</div><div class="line">            <span class="k">if</span> <span class="n">includeCounter</span> <span class="p">{</span>
</div><div class="line">                <span class="bp">print</span><span class="p">(</span><span class="s">&quot;</span><span class="si">\(</span><span class="n">i</span><span class="si">)</span><span class="s">: </span><span class="si">\(</span><span class="n">phrase</span><span class="si">)</span><span class="s">&quot;</span><span class="p">)</span>
</div><div class="line">            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">                <span class="bp">print</span><span class="p">(</span><span class="n">phrase</span><span class="p">)</span>
</div><div class="line">            <span class="p">}</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="nb">Repeat</span><span class="p">.</span><span class="n">main</span><span class="p">()</span>
</div></code></pre></div>
</div>
<p>我们可以看到，它的使用方式并不麻烦。</p>
<ul>
<li>首先遵守 ParsableCommand 协议</li>
<li>其次声明一个参数类型（Flag，Option，Argument），定义你需要从命令行中收集的信息，并用 ArgumentParser 的属性包装器来装饰每个存储属性</li>
<li>最后在 <code>run()</code> 方法中实现核心逻辑。</li>
</ul>
<p>在实际的运行过程中，ArgumentParser 会解析命令行参数，实例化你的命令类型。同时 ArgumentParser 会使用属性名，类型信息，以及你在属性包装器里提供的细节，来提供有用的错误信息和帮助信息，具体效果如下所示。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ repeat hello --count <span class="m">3</span>
</div><div class="line">hello
</div><div class="line">hello
</div><div class="line">hello
</div><div class="line">$ repeat --count <span class="m">3</span>
</div><div class="line">Error: Missing expected argument <span class="s1">&#39;phrase&#39;</span>.
</div><div class="line">Usage: repeat <span class="o">[</span>--count &lt;count&gt;<span class="o">]</span> <span class="o">[</span>--include-counter<span class="o">]</span> &lt;phrase&gt;
</div><div class="line">  See <span class="s1">&#39;repeat --help&#39;</span> <span class="k">for</span> more information.
</div><div class="line">$ repeat --help
</div><div class="line">USAGE: repeat <span class="o">[</span>--count &lt;count&gt;<span class="o">]</span> <span class="o">[</span>--include-counter<span class="o">]</span> &lt;phrase&gt;
</div><div class="line">
</div><div class="line">ARGUMENTS:
</div><div class="line">  &lt;phrase&gt;                The phrase to repeat.
</div><div class="line">
</div><div class="line">OPTIONS:
</div><div class="line">  --include-counter       Include a counter with each repetition.
</div><div class="line">  -c, --count &lt;count&gt;     The number of <span class="nb">times</span> to repeat <span class="s1">&#39;phrase&#39;</span>.
</div><div class="line">  -h, --help              Show <span class="nb">help</span> <span class="k">for</span> this command.
</div></code></pre></div>
</div>
<p>由于本文的例子较为简单，我们这里就不增加 ArgumentParser 的依赖来增加项目的复杂度了。</p>
<p>即使如此，我相信通过上面的介绍，你也大致了解到了 ArgumentParser 的使用方式了，记得将其用在你自己的项目中吧！</p>
<h2>编写单测</h2>
<p>我们几乎已经准备好发布这个命令行工具了，但在这样做之前，我们还是需要通过编写一些测试来确保它真正的按照预期工作。</p>
<p>由于我们之前将整个项目划分成了 framework 和 executable 的结果，所以测试将变得十分容易。我们所要做的就是以程序调用的方式运行它，并断言它创建了一个具有指定名称的文件。</p>
<p>首先在 <code>Package.swift</code> 文件中添加一个测试模块，在你的 <code>target</code> 数组中添加以下内容。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">.</span><span class="n">testTarget</span><span class="p">(</span>
</div><div class="line">    <span class="n">name</span><span class="p">:</span> <span class="s">&quot;CommandLineToolTests&quot;</span><span class="p">,</span>
</div><div class="line">    <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&quot;CommandLineToolCore&quot;</span><span class="p">,</span> <span class="s">&quot;Files&quot;</span><span class="p">]</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>最后，重新生成 Xcode 项目。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift package generate-xcodeproj
</div></code></pre></div>
</div>
<p>再次打开 Xcode 项目，跳到 <code>CommandLineToolTests.swift</code> 中，添加以下内容。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">Foundation</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">XCTest</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">Files</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">CommandLineToolCore</span>
</div><div class="line">
</div><div class="line"><span class="kd">class</span> <span class="nc">CommandLineToolTests</span><span class="p">:</span> <span class="n">XCTestCase</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">testCreatingFile</span><span class="p">()</span> <span class="kr">throws</span> <span class="p">{</span>
</div><div class="line">        <span class="c1">// Setup a temp test folder that can be used as a sandbox</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">tempFolder</span> <span class="p">=</span> <span class="n">Folder</span><span class="p">.</span><span class="n">temporary</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">testFolder</span> <span class="p">=</span> <span class="k">try</span> <span class="n">tempFolder</span><span class="p">.</span><span class="n">createSubfolderIfNeeded</span><span class="p">(</span>
</div><div class="line">            <span class="n">withName</span><span class="p">:</span> <span class="s">&quot;CommandLineToolTests&quot;</span>
</div><div class="line">        <span class="p">)</span>
</div><div class="line">
</div><div class="line">        <span class="c1">// Empty the test folder to ensure a clean state</span>
</div><div class="line">        <span class="k">try</span> <span class="n">testFolder</span><span class="p">.</span><span class="n">empty</span><span class="p">()</span>
</div><div class="line">
</div><div class="line">        <span class="c1">// Make the temp folder the current working folder</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">fileManager</span> <span class="p">=</span> <span class="n">FileManager</span><span class="p">.</span><span class="k">default</span>
</div><div class="line">        <span class="n">fileManager</span><span class="p">.</span><span class="n">changeCurrentDirectoryPath</span><span class="p">(</span><span class="n">testFolder</span><span class="p">.</span><span class="n">path</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">        <span class="c1">// Create an instance of the command line tool</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">arguments</span> <span class="p">=</span> <span class="p">[</span><span class="n">testFolder</span><span class="p">.</span><span class="n">path</span><span class="p">,</span> <span class="s">&quot;Hello.swift&quot;</span><span class="p">]</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">tool</span> <span class="p">=</span> <span class="n">CommandLineTool</span><span class="p">(</span><span class="n">arguments</span><span class="p">:</span> <span class="n">arguments</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">        <span class="c1">// Run the tool and assert that the file was created</span>
</div><div class="line">        <span class="k">try</span> <span class="n">tool</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</div><div class="line">        <span class="n">XCTAssertNotNil</span><span class="p">(</span><span class="k">try</span><span class="p">?</span> <span class="n">testFolder</span><span class="p">.</span><span class="n">file</span><span class="p">(</span><span class="n">named</span><span class="p">:</span> <span class="s">&quot;Hello.swift&quot;</span><span class="p">))</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>此外，还可以添加另一个测试，以验证在没有给定文件名或文件创建失败时是否抛出了正确的错误。</p>
<p>要运行测试，只需在命令行上运行 <code>swift test</code> 即可。</p>
<h2>安装工具</h2>
<p>现在我们已经构建并测试了我们的命令行工具！下面开始，我们会尝试安装它，并使它能够在任何地方运行。</p>
<p>要做到这一点，需要在 <code>swift build</code> 后面增加 release 的配置，也就是 <code>-c relase</code> 参数，然后将编译后的二进制文件移到 <code>/usr/local/bin</code>。</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift build -c release
</div><div class="line">$ <span class="nb">cd</span> .build/release
</div><div class="line">$ cp -f CommandLineTool /usr/local/bin/commandlinetool
</div></code></pre></div>
</div>
<h2>调试技巧</h2>
<p>命令行大多是需要输入参数的，所以在实际的开发过程中，我们如何在 Xcode 里添加入参呢？</p>
<p>首先，在 Xcode 的 Toolbar 中，我们点击 choose scheme 面板中的 <code>Edit Scheme...</code> 按钮</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878650_009483.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878650_009483.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878650_009483.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878650_009483.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878650_009483.png" alt="image.png"loading="lazy" decoding="async" width="2330" height="346" /></picture></figure></div><p>在弹出的界面中点击左侧 Run 面板，并继续点击右侧的 Argument 的 Tab 按钮，我们会看到如下的界面，此时我们可以在 Arguments Passed On Launch 中添加命令行所需的参数，例如这里我们添加了一个 <code>Hello.swift</code> 的参数。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878644_335617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878644_335617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878644_335617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878644_335617.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878644_335617.png" alt="image.png"loading="lazy" decoding="async" width="1884" height="1060" /></picture></figure></div><p>此时，我们再次通过 <code>CMD+R</code> 的方式运行程序，就会在构建产物的目录中，看到生成的 <code>Hello.swift</code> 文件</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878637_925255.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_796/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304878637_925255.png" alt="image.png"loading="lazy" decoding="async" width="796" height="330" /></picture></figure></div><h2>让开源社区加速你的开发</h2>
<p>除了 Foundation 自带的 Combine，CoreGraphes，URLSession 等，在官方的技术社区还有很多不错的组件能够加速你的开发，例如</p>
<ul>
<li><a href="https://github.com/apple/swift-tools-support-core">Swift Tool Support Core</a>：SPM 和 llbuild 里通用的基础架构代码，可以将其看做是 Foundation 在 CLI 方向的加强库。</li>
<li><a href="https://github.com/apple/swift-nio">Swift NIO</a>：如果 URLSession 不能满足你的需求或者你需要进行跨平台开发，SwiftNIO 这个网络库应该是能满足你的诉求，它是一个跨平台异步事件驱动的网络应用框架，用于快速开发可维护的高性能协议服务器和客户端。</li>
<li><a href="https://github.com/apple/swift-log">Swift Log</a>：一个用于做日志记录工作的组件。</li>
<li><a href="https://github.com/apple/swift-metrics">Swift Metrics</a>：当我们需要为某个系统，某个服务做监控，做统计的时候，Swift Metrics 就是你的不二之选！</li>
<li><a href="https://github.com/apple/swift-crypto">Swift Crypto</a>：一个跨平台的加密库，基于 Apple 自身的 CryptoKit 改造而来。</li>
<li><a href="https://github.com/apple/swift-numerics">Swift numerics</a>：这个库为 Swift 提供了许多与数值计算相关的功能模块。</li>
<li><a href="https://github.com/apple/swift-protobuf">Swift Protobuf</a>：如果你在网络通信的过程中传输的是 Protobuf 类型的文件，可以通过这个库进行解析</li>
<li><a href="https://github.com/apple/swift-atomics">Swift Atomics</a>：为各种 Swift 类型提供原子操作，包括整数和指针值。</li>
<li><a href="https://github.com/swift-server/swift-backtrace">Swift Backtrace</a>：这个 Package 为项目提供了自动打印程序崩溃信息的能力。</li>
</ul>
<p>总之，这个社区在不断的发展中，很多新的官方库也在如火如荼的建设中，如果你发现这里的内容还不够用，可以关注一下 <a href="https://github.com/vapor">Vapor</a>，<a href="https://github.com/PerfectlySoft">PerfectlySoft Inc</a>， <a href="https://github.com/swiftwasm">SwiftWasm</a> 和 <a href="https://github.com/crossroadlabs">Crossroad Labs</a> 的 Group，也能找到很多不错的 package 资源。</p>
<p>当然，也有很多个人开发者提供了不错的 Package 资源，例如：</p>
<ul>
<li><a href="https://github.com/artsabintsev/guitar">Guitar</a>：这绝对会是你在开发中需要到的东西，一个正则匹配加强库！</li>
<li><a href="https://github.com/onevcat/Rainbow">Rainbow</a>：可以对命令行里的输出内容增加文本颜色，背景样式等。</li>
<li><a href="https://github.com/kareman/SwiftShell">SwiftShell</a>：可以在 Swift 里调用 Shell 命令的 Package</li>
<li><a href="https://github.com/mxcl/swift-sh">Swift-SH</a>：同样是一个 Swift 里调用 Shell 的 Package，介绍它的原因是因为它的作者是 Homebrew 的开发者 mxcl</li>
<li><a href="https://github.com/JohnSundell/Files">Files</a>：与文件操作相关的 Package，在教程里已经提及过。</li>
<li><a href="https://github.com/mxcl/Path.swift">Path</a>：Files 在处理一些路径上还是有短板，mxcl 开发的这个组件很好的补充了 Files 的功能。</li>
<li><a href="https://github.com/johnsundell/releases">Release</a>：可以通过 Swift 脚本或命令行工具轻松解析 Git 仓库中的发布版本，支持远程仓库和本地仓库。</li>
<li><a href="https://github.com/johnsundell/xgen">XGen</a>：通过 Swift 脚本或命令行工具中轻松地生成 Xcode Project 和 Playground。</li>
</ul>
<h2>总结</h2>
<p>通过这篇文章，你已经掌握了如何从零编写 Swift CLI 项目的所有基础知识，也了解了社区里的一些优秀资源，十分期待你开始使用 Swift 编写属于自己的命令行工具！</p>
]]></content:encoded></item><item><title><![CDATA[WWDC20 10654 - Create Swift Playground content for iPad and Mac]]></title><guid>https://swiftsiqi.com/posts/WWDC20-10654</guid><link>https://swiftsiqi.com/posts/WWDC20-10654</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 12 Aug 2020 10:58:33 +0000</pubDate><content:encoded><![CDATA[<p>这是一个麻雀虽小，但五脏俱全的 Session，在短短的 8 分钟里，不仅包含了功能介绍，也有相应的代码演示。</p>
<p>不过整个 session 都是围绕在如何利用 Swift Playground 为 iPad 和 Mac 创建优质内容而展开的，所以都是很细节和琐碎的点。</p>
<h2>引子</h2>
<p>这是一个麻雀虽小，但五脏俱全的 Session，在短短的 8 分钟里，不仅包含了功能介绍，也有相应的代码演示。</p>
<p>不过整个 session 都是围绕在如何利用 Swift Playground 为 iPad 和 Mac 创建优质内容而展开的，所以都是很细节和琐碎的点。</p>
<p>下面的脑图展示了这个 session 的所有知识点，方便你复习和加深理解：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879065_517167.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879065_517167.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879065_517167.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879065_517167.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879065_517167.png" alt="image.png"loading="lazy" decoding="async" width="1836" height="960" /></picture></figure></div><h2>Swift Playground 在界面上的改变</h2>
<p>如果使用过 Swift Playground ，那么你应该能回忆起，在 iPad 上是可以看到代码补全功能提供的 token。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879057_978104.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879057_978104.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879057_978104.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879057_978104.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879057_978104.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><p>为了适配 Mac 平台，Apple 团队为 Swift Playground 的代码补全功能做了进一步的优化。</p>
<p>现在，在 Mac 上不仅可以看到这些 token，还可以看到相应的帮助文档</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879051_009076.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879051_009076.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879051_009076.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879051_009076.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879051_009076.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><p>除此之外，帮助文档没有对语言类型做限制，我们完全可以将文档的语言类型变为使用者的本地语言，通过这样的方式来降低他们的理解成本</p>
<p>如果想创建 API 的帮助文档，可以使用 3 个 <code>/</code> 来声明，除了描述方法外，也可以为参数添加说明，下图就是一个例子</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879043_762839.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879043_762839.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879043_762839.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879043_762839.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879043_762839.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><p>在 iPad 上的话，可以通过 Quick Help 的弹窗或者代码补全的提示条来展示相应的代码说明，它的效果如下所示</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879035_831274.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879035_831274.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879035_831274.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879035_831274.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879035_831274.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><h2>针对不同平台展示定制化内容的能力</h2>
<p>在 Playground book 格式的文件中，提供了两个新的 API 用来区分不同平台的定制化能力，它们分别是 <code>supportedDevices</code> 和 <code>requiredCapbilities</code></p>
<div class="blockquote"><blockquote><p>这里要记住一点，Playground 和 Playground Book 是两个不一样的东西，在 Xcode 或者 Swift Playground 里是无法直接触发 Playground Book 的 target 或者 Project 的，需要到官方下载 [Swift Playgrounds Author Template](<a href="https://developer.apple.com/download/more/?=Swift">https://developer.apple.com/download/more/?=Swift</a> Playgrounds Author Template), 如果你想更进一步了解这两种格式的区别，建议阅读 <a href="https://developer.apple.com/documentation/swift_playgrounds/creating_and_running_a_playground_book">官方的说明文档</a> 和 <a href="https://medium.com/@barbulescualex/using-swift-playgrounds-playground-books-87c2707be2b5">Using Swift Playgrounds &amp; Playground Books</a></p>
</blockquote></div>
<p><code>supportedDevices</code> 这个 key 是用来区分不同平台的，例如是 iPad 还是 Mac，而 <code>requiredCapbilities</code> 则是用来明确平台能力的，例如需要 AR 能力（Mac 拥有，而 iPad 没有），需要 WIFI 能力（两个平台都具备）。</p>
<p>它们的设定是在 manifest 文件和 feed json 文件中进行的，需要设定相应的 key 值，具体的示例可以参考下面的两张图</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879020_75363.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879020_75363.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879020_75363.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879020_75363.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879020_75363.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879013_8321495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879013_8321495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879013_8321495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879013_8321495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879013_8321495.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><p>关于 <code>requiredCapbilities</code> 支持的键值，可以查看 UIRequiredDeviceCapabilities 的 API 说明，这里放一个<a href="https://developer.apple.com/documentation/bundleresources/information_property_list/uirequireddevicecapabilities">传送门</a></p>
<p>已经知道了这些能力，那么在代码里如何使用呢，下面就是一段示例代码，我们可以根据不同的平台来编译不同的代码</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879006_963786.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879006_963786.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879006_963786.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879006_963786.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879006_963786.png" alt="image.png"loading="lazy" decoding="async" width="1926" height="1080" /></picture></figure></div><p>为了让 Playground Book 的体验更好，我们肯定希望开发者去适配更多的能力，例如 AR，重力感应，GPS等，但如果用户一开始是在 Mac 上使用的话，可能会无法看到这些功能，例如你的逻辑是只有检测到具备这个功能才显示某个按钮，如果不具备则隐藏。</p>
<p>那么我们要怎么做才能暗示用户呢，Apple 的建议是在语言的描述上做好引导，例如在不同的平台上，对操作的描述是这样的</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879000_217977.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879000_217977.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879000_217977.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879000_217977.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879000_217977.png" alt="image.png"loading="lazy" decoding="async" width="1926" height="1080" /></picture></figure></div><p>如果是一个通用的功能，说辞可以选择 tap 或者 select</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878993_597609.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878993_597609.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878993_597609.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878993_597609.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878993_597609.png" alt="image.png"loading="lazy" decoding="async" width="1926" height="1080" /></picture></figure></div><h2>适配系统的设置规则</h2>
<p>在开发实际内容的过程中，我们要考虑到系统的设置，例如主色调，副色调和暗色模式等！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878893_326654.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878893_326654.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878893_326654.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878893_326654.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878893_326654.jpg" alt="11 (1).jpg"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><div class="blockquote"><blockquote><p>这张图单纯就是觉得主讲的妹子蛮可爱的，想骗你们看 session…..</p>
</blockquote></div>
<p>Apple 提供的原生组件会自动响应这些变化，而开发者自定义的组件需要适配才能实现同样的功能，这一点需要开发者做好，涉及的资源建议放到 Asset catalog 中</p>
<p>除了上面的问题外，在开发跨平台的内容时，还需要关注 live view 的 Safe Area。</p>
<p>例如下面的视图中，左侧是在 ipad 上运行的 Live View, 右侧则是在 Mac 上运行的效果，仔细观察的话，会发现右侧的 Live view 在右上角多出有一些按钮，这就会导致两个 Live View 的 Safe Area 的区域不一致</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878977_117603.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878977_117603.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878977_117603.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878977_117603.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878977_117603.png" alt="image.png"loading="lazy" decoding="async" width="1926" height="1080" /></picture></figure></div><p>之前开发者会使用 <code>liveViewSafeAreaGuide</code> 这个特殊的 API 获取相关值，现在终于可以直接使用 <code>safeAreaLayoutGuide</code> 这个更通用，更好理解的 API 了</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878970_29495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878970_29495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878970_29495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878970_29495.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878970_29495.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1608" /></picture></figure></div><h2>总结</h2>
<p>这篇 Session 的内容就到此结束了，我们复习一下整个文章的内容，大体有 3 部分</p>
<ol>
<li>Swift Playground 针对不同平台做了 UI 上的调整，使其更符合每个平台的使用习惯</li>
<li>提供了更加强大的定制化能力，不仅包括了平台定制，还包括了能力定制</li>
<li>开发者需要做好适配系统设置的工作，Apple 也为开发者提供了一些便捷能力</li>
</ol>
<p>另外，如果各位有时间的话，强烈推荐观看一下 Explore Swan’s Quest 里的四个 Session，加起来的全部观影时间也就 35 分钟，但看完以后，你会发现原来 Swift Playground 竟然还有这么多有意思的用法，感觉像打开了一个新的世界一样。</p>
<p>相信我，这绝对不是骗你。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879080_931852.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1400/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879080_931852.png" alt="image.png"loading="lazy" decoding="async" width="1400" height="1104" /></picture></figure></div>]]></content:encoded></item><item><title><![CDATA[WWDC20 10680 - Refine Objective-C frameworks for Swift]]></title><guid>https://swiftsiqi.com/posts/WWDC20-10680</guid><link>https://swiftsiqi.com/posts/WWDC20-10680</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 02 Jul 2020 11:10:09 +0000</pubDate><content:encoded><![CDATA[<p>每一年的 WWDC 里都会有一些类型 Apple 工程师教你如何写代码的 Session，这些 Seesion 的内容都偏向最佳实践，告诉你如何写出 Apple 风格的代码，解答你对代码里的各种疑惑，甚至给出你如何继续深入研究的方向，这对开发者来说，是一个非常好的学习机会。</p>
<p>在这个 Session 中，Apple 的工程师将告诉我们如何改造现有的 Objective-C 框架，使其能够更符合 Swift 的使用体验，所以你不仅能学习很多实际的技巧，也会进一步了解他们背后的思考。</p>
<p>话不多说，来看正文吧！</p>
<h2>引子</h2>
<p>每一年的 WWDC 里都会有一些类型 Apple 工程师教你如何写代码的 Session，这些 Seesion 的内容都偏向最佳实践，告诉你如何写出 Apple 风格的代码，解答你对代码里的各种疑惑，甚至给出你如何继续深入研究的方向，这对开发者来说，是一个非常好的学习机会。</p>
<p>在这个 Session 中，Apple 的工程师将告诉我们如何改造现有的 Objective-C 框架，使其能够更符合 Swift 的使用体验，所以你不仅能学习很多实际的技巧，也会进一步了解他们背后的思考。</p>
<p>话不多说，来看正文吧！</p>
<h3>背景介绍</h3>
<p>相比于六年前推出的 Swift 语言，Objective-C 在 Apple 生态圈的历史更为悠久，导致了历史包袱比较重的或者现有的工程中还会持续存在许多 Objective-C 的框架，这些框架不是孤立的，会与 Swift 的框架产生依赖关系和调用关系。而这种微妙的关系产生了许多棘手的问题。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878444_327549.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878444_327549.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878444_327549.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878444_327549.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878444_327549.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1184" /></picture></figure></div><p>不光社区里的开发者会遇到这样的问题，Apple 公司的工程师也无法例外，在 Session 里，Brent 用这样一段话来形容这个问题，我感觉很贴切:</p>
<div class="blockquote"><blockquote><p>We understand that, because Apple is in the same boat. We probably have more Objective-C frameworks than anyone in the world.</p>
</blockquote></div>
<p>所以如何让 Objective-C 框架更好的为 Swift 服务也是他们要解决的问题之一，虽然 Swift 编译器在转换 Objective-C 接口时做了很多不错的优化工作，但很难满足所有开发者的期望，不过这不代表你没有办法去优化它，因为今天的 Session 就是做这个事儿的。</p>
<p>从改变途径来看的话，主要是通过以下几种方式：</p>
<ul>
<li>遵循编译器的某些规则</li>
<li>在头文件里进行特殊标注</li>
<li>用 Swift 做中间层，重新封装原有代码</li>
<li>根据自己的喜好进一步优化</li>
</ul>
<h3>知识目录</h3>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878437_034934.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878437_034934.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878437_034934.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878437_034934.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878437_034934.png" alt="image.png"loading="lazy" decoding="async" width="2794" height="1912" /></picture></figure></div><h2>Demo 工程</h2>
<p>这个 Session 是围绕一个用于描述 NASA 载⼈航天计划的 SDK 展开的，这个 SDK 的名字叫做 SpaceKit，它是 Objective-C 编写的。</p>
<p>现在这个 SDK 会被一些 Swift 代码调用，所以我们要通过一些改造，使其更符合 Swift 的使用习惯。</p>
<h3>如何查看编译器生成的 Swift 接口</h3>
<p>考察一个 Objective-C SDK 是否符合 Swift 的使用习惯，最重要的一点就是看它生成的 Swift API 质量。那么，我们如何查看 Swift Compiler 自动生成的接口呢？</p>
<p>在 Objective-C 的头文件里，点击左上角的 Related Items 按钮，选择 Generated Interface 后，就会出现满足不同 Swift 版本的接口文件。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878428_226155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878428_226155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878428_226155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878428_226155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878428_226155.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1184" /></picture></figure></div><p>点开后，它的样子大体如下</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878421_322869.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878421_322869.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878421_322869.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878421_322869.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878421_322869.png" alt="image.png"loading="lazy" decoding="async" width="2112" height="1184" /></picture></figure></div><p>这个功能对于我们理解如何生成更符合 Swift 使用习惯的 API 来说是非常重要的，所以希望你能掌握这个技巧！</p>
<h2>自动生成的接口利与弊</h2>
<p>下面是根据 Objective-C 源码自动生成的 API 接口，我们可以看到 Swift 编译器已经做了不少的优化，例如：</p>
<ul>
<li>将 NSString，NSDate 类型转换成了 String，Date；</li>
<li>将 Objective-C 里的初始化方法转换成了 Swift 里的构造器方法；</li>
<li>将原有的 <code>- (NSSet *)previousMissionsFlownByAstronaut:(SKAStronaut *)astronaut</code> 的方法名优化成了 <code>previousMissionFlown(by astronaut:)</code></li>
<li>将原有的 <code>-(BOOL)saveToURL:(NSURL *)url error:(NSError **)error</code> 的错误处理 API 改成了 Swift 风格的 API</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878406_1694.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1056/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304878406_1694.png" alt="image.png"loading="lazy" decoding="async" width="1056" height="1517" /></picture></figure></div><p>但这样的 API 接口还存在多问题，让我们列举一下：</p>
<ul>
<li>SKMission 的问题：<ul>
<li>过多的隐式解析可选类型</li>
<li><code>crew</code> 属性里的 Any 定义过于模糊</li>
<li><code>save(to url:)</code> 可能会在不该抛异常的时机点抛异常</li>
<li><code>previousMissionFlown(by astronaut:)</code> 的方法名还不够优雅</li>
</ul>
</li>
<li>SKAstronaut 的问题：<ul>
<li>构造器之间关系不够清晰</li>
</ul>
</li>
<li>SKErrorDomain 和 SKErrorCode 的问题：<ul>
<li>NSError 风格的 API 在 Swift 里的使用体验非常不好，尤其在 try catch 中</li>
</ul>
</li>
<li>SKCapsule 和 SKRocket 类型的常量<ul>
<li>用于枚举的字符串常量在 Swift 里更适合使用 enum 类型来描述</li>
<li><code>SKCapsuleApolloCSM</code> 的 API 消失了</li>
<li><code>SKRocketStageCount(_ rocket: String!) -&gt; Unit</code> 的 API 还有不少潜在的风险</li>
</ul>
</li>
</ul>
<p>如果你还看不出上面存在的所有问题，也无法提供所有问题的解决方案，那么这篇文章将十分适合你阅读。</p>
<p>所以让我一起来看看 Apple 工程师给出的解决方案吧！</p>
<h2>改进的方法</h2>
<p>如果想解决上面提到的各种问题，可以从下面四个方向入手：</p>
<ul>
<li>提供更丰富的类型信息</li>
<li>遵守 Objective-C 的约定</li>
<li>解决缺少 API 的问题</li>
<li>改善框架在 Swift 里的使用体验</li>
</ul>
<h3>提供更丰富的类型信息</h3>
<h4>增加 nullability 的描述信息</h4>
<p>Objective-C 指针既可以是一个有效值，也可以是空值，例如 null 或者 nil，这与 Swift 里的可选值行为十分相似。</p>
<p>如果我们再仔细想一下，就会发现在 Objective-C 里面，每个指针类型实际上都是可选类型，每个非指针类型都是非可选类型。可是大部分时间，一个属性或者方法不会处理输入值是 nil 的情况，或者永远不会返回 nil。</p>
<p>所以，默认情况下 Swift 会把 Objective-C 里的指针当做隐式解析可选类型，因为它认为这个值大部分情况下不会是 nil，但它也不完全确定。</p>
<p>虽说这种转换规则没什么毛病，但大量的隐式解析可选类型让代码变得意图模糊，好在我们有两个关键字注解可以去描述这个意图，他们分别是 nonnull 和 nullable</p>
<p>这两个注解在 Objective-C 里面只是用于记录开发者的意图，不是强制的。但 Swift 会用到这些信息来决定是否转换为可选类型。</p>
<p>另外需要注意的是，在标注完 nullability 后，原有的 Objective-C 代码可能会出现一些新的警告，这里请认真检查并按照提示进行修改，这会让你的代码更健壮。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864710_222711.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864710_222711.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864710_222711.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864710_222711.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864710_222711.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>除了 nonnull 和 nullable 意外，还有一对配合使用的宏 <code>NS_ASSUME_NONNULL_BEGIN</code> 和 <code>NS_ASSUME_NONNULL_END</code> 可以让我们的代码更清爽。</p>
<p>在这两个宏包裹的代码片段中，属性，⽅法参数和返回值的默认注解都是 nonnull 类型的，这样一来，我们就可以删掉许多冗余的代码。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864696_98868.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864696_98868.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864696_98868.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864696_98868.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864696_98868.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>但是这些方法并不适用所有的场合，例如你将 nonnull 直接放在常量前会触发编译器错误。还好这种错误是有解决办法的！</p>
<p>nonull 和 nullable 只能在方法和属性上使用，如果想拓展其使用场景，就需要直接调用这两关键字底层的内容，也就是 <code>_Nonnull</code> 和 <code>_Nullable</code>。</p>
<p>这两种注解除了可以用在全局常量，全局函数的场景外，也适用于任何 Objective-C 任何地方的指针类型，甚至那种指向指针类型的指针。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864675_501906.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864675_501906.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864675_501906.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864675_501906.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864675_501906.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>现在我们看到 SKRocketSaturnV 终于如期所愿的摆脱了隐式解析可选类型！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864662_181948.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864662_181948.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864662_181948.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864662_181948.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864662_181948.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>然后我们看看下面的 API 可能存在的问题，在这里我们从 API 层面假设 capsule 是一个非空值，但可能这是不合理的，并不是每次的飞行计划都需要载人，不是么？</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878338_959399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878338_959399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878338_959399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878338_959399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878338_959399.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>那么，如果 Objective-C 返回了⼀个 nil 值，⽽在 API 层⾯，Swift 认为这是⼀个⾮空值，又会发⽣什么呢？</p>
<p>如果是 NSString 或者 NSArray 的话，Swift 会得到⼀个空的字符串或者数组，这可能会引起一些问题，但对于其他类型，可能会拿到⼀个⽆效的对象，总之，可能与你的预期不⼀样。</p>
<p>如果是 Objective-C 对象，你可能很难注意到这⼀点，因为 Objective-C 会忽略 nil，但在某些 case 下，你可以会因为 null 指针崩溃或者触发异常⾏为。</p>
<p>编译器不会对这种⾏为作出任何的承诺，所以改变 release mode 或者 xcode 版本可能有不同的表现！</p>
<p>不论怎样，需要记住的是，当你头⽂件⾥某个东西不会是 nil 的时候，Swift 不会对其强制解包，所以你不会在返回 nil 的地⽅看到崩溃。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878332_103607.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878332_103607.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878332_103607.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878332_103607.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878332_103607.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>那么对于这种 case，就没有解决办法了么？</p>
<p>好消息是 Objective-C 编译器和 Clang 的静态检查能够很好的解决这个问题，所以在写好 nullability 的注解后，最好关注⼀下编译器警告和静态分析结果！</p>
<p>就如下图所示一样</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878324_523155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878324_523155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878324_523155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878324_523155.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878324_523155.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>当然我们知道，开发者可能还会遇到一些特殊的 case，在这些 case 里，他们没法确定代码到底是有值，还是没有值，所以 Apple 还提过了 <code>_Null_unspecified</code> 的注解词。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878318_559854.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878318_559854.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878318_559854.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878318_559854.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878318_559854.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>被 <code>_Null_unspecified</code> 标注的内容会被转换成隐式解析可选类型，这种类型在 Swift 里的使用场景大体如下，例如某个属性在其⽣命周期早期为 nil，之后再不会是 nil 的情况。</p>
<p>当然，在你⽆法确定的 case 里也可以这样使用，因为</p>
<ul>
<li>如果一切按照预期，你可以⽆需解包，继续使⽤</li>
<li>如果返回的是 nil，你会稳定的复现这个 bug，⽽不是⼀些奇怪的⾏为</li>
</ul>
<h4>利用泛型约束接口</h4>
<p>原有的接口中，没有对 crew 这个数组里的元素进行约束，这会使得其转换到 Swift 的 API 时，将其中的元素描述为 Any。</p>
<p>虽然也不是什么大的毛病，但用起来确实会显得有点别扭！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878311_201641.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878311_201641.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878311_201641.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878311_201641.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878311_201641.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>我们都知道 Objective-C 也提供了一些泛型的能力，所以我们完全可以将其优化到一个更好的层次上，通过在 Objective-C 里添加相应的语法内容，就可以将其在 Swift 的使用体验改善不少。</p>
<p>当然除了 NSArray，NSDictionary 等基本类型也适用这个技巧！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878304_901455.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878304_901455.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878304_901455.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878304_901455.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878304_901455.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><h4>对于数字处理统一使用 Int</h4>
<p>我们先看看这样一段代码，下⾯的函数返回⼀个计数值，很显然，你在喊倒计时的时候，数值不会为负，所以在 Objective-C ⾥⾯以 NSUInteger 的形式返回。</p>
<p>这样的声明，意味着在 Swift 里会返回⼀个 UInt，而这意味打破了 Swift 的使用习惯。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878298_055234.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878298_055234.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878298_055234.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878298_055234.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878298_055234.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>当我们想对比特位进行运算的时候，我们通常会使用 unsigned 类型的数据，因为 signed 的数据在处理起来会有些麻烦，而且在这种场景下，我们还十分关注数据的位数，但是由于 NSUInteger 的大小会因架构不同而产生一些变化，导致使用它的人并不多。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878284_387259.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878284_387259.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878284_387259.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878284_387259.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878284_387259.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>与此初衷不同的是，大多数人使用 NSUInteger 是为了表明这个数值是⾮负的，虽然这种用法是可行的，但它还是会存在一些严重的安全漏洞，所以这种设计思路并没有被 Swift 采用。</p>
<p>Swift 采取的策略是在进⾏有符号运算时，要求开发者必须将⽆符号类型转换为有符号类型，如果 Swift 在处理⽆符号运算时，产⽣了负值，就会直接停⽌运算。</p>
<p>也正是这样的策略，会让 Swift 中的 Int 和 UInt 在混合起来使用的时候变得很麻烦，当然，这在 Objective-C ⾥⾯的也是一个棘手的问题。</p>
<p>所以混合使用 Int 和 UInt 并不是 Swift 里的最佳实践，在 Swift 里面，我们建议将所有进行数值计算的类型声明为 Int，即使它永远不可能为负数。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878277_596907.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878277_596907.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878277_596907.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878277_596907.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878277_596907.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>对于 Apple 自己的框架，他们设置了一个白名单用于将 NSUInteger 转换为 Int。</p>
<p>对于开发者而言，决定权在我们自己手里，我们可以⾃⾏选择是否使⽤ NSInteger，但 Apple 的工程师强烈推荐你这么做。</p>
<p>或许在 Objective-C ⾥⾯差距不是很⼤，但在 Swift ⾥⾯很重要！</p>
<h4>将字符串类型的常量变得更有条理</h4>
<p>下面我们来看看这样一段代码，从某种角度上来说，<code>SKRocketStageCount</code> 这个 API 很容易被滥用，因为只要传一个字符串就行了，但其实我们希望传入的是以 <code>SKRocket</code> 为前缀的常量字符串。</p>
<p>可惜 Swift 无法感知这一切，它能看到的只是函数需要的是字符串⽽已，如果传了其他值的字符串，就会出现不符合预期的情况。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878269_92675.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878269_92675.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878269_92675.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878269_92675.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878269_92675.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>在 Swift ⾥通常会把这些常量变成⼀个具有字符串原始值的枚举或者结构体，然后改变函数的入参类型，使其接受相应的枚举或者结构体类型。</p>
<p>那么我们怎么去改造这个接口呢？我们先说个最简单的方法：</p>
<p>使⽤ typedef 将常量分组，并将涉及此常量的地⽅改为新的类型。而在 Swift 中，typedef 会被转换成 typealias</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878263_844221.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878263_844221.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878263_844221.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878263_844221.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878263_844221.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>这已经使得代码发生了一些变化，不过这还不是最终效果！</p>
<p>此时，你只需要在 typedef 后⾯加上 <code>NS_STRING_ENUM</code> 即可, 此时，原有的字符串常量将以结构体的⽅式导⼊到 Swift 中, 而且，你注意到没有，<code>SKRocketStageCount</code> 的⼊参类型彻底的变了！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878257_178089.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878257_178089.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878257_178089.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878257_178089.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878257_178089.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>怎么样，一共就 2 步，就能得到原汁原味的 Swift API，是不是还不错！</p>
<p>这样的使用方式，可以在 Apple 的框架里看到不少实实在在的例子，例如 NSAttributedStringKey，NSCalendarIdentifier，NSNotificationName，NSNotificationUserInfoKey 等。</p>
<p>所以放心使用它吧！</p>
<h3>遵守 Objective-C 的约定</h3>
<h4>关于构造器的相关约定</h4>
<p>接下来，我们来看看构造器方面的问题。</p>
<p>下面的代码中，SKAstronaut 有两个初始化构造器，⼀个入参类型为 PersonNameComponents, ⼀个入参类型为字符串，</p>
<p>这就意味着，如果要声明⼀个 SKAstronaut 的⼦类，就需要重写两个⽅法，但 NSPersonNameComponents 本质也代表一个字符串，所以这样的工作显得有点多于。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878247_521201.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878247_521201.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878247_521201.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878247_521201.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878247_521201.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>同时，你还会在使用的时候发现，莫名其妙的多出来一个构造器，它没有任何的入参，我们在源⽂件⾥找不到任何与此相关的定义，但其实它来⾃ superclass。 因为 SKAstronaut 继承⾃ NSObject。</p>
<p>虽然有这么一个方法，你也能调⽤，但很可能它⽆法正常⼯作！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878241_105429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878241_105429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878241_105429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878241_105429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878241_105429.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>如果你深入分析上面的两个问题会发现，它们的内核是一样的。</p>
<p>在 Objective-C 中，有⼀个关于初始化器的约定，它确保开发者知道如何写⼀个总是能被正确初始化的⼦类。</p>
<p>这个约定的大体内容是这样的，将初始化器分为两类，designated 和 convenience。 你需要覆盖所有 designated 初始化器，以便安全地继承 convenience 的初始化器。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878234_1192.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878234_1192.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878234_1192.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878234_1192.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878234_1192.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>这个约定和 Swift 里面的构造器约定十分相似，但它们有个本质的区别！</p>
<p>Objective-C 的这种构造器约定不是语⾔级别的强制规则，更多的是⼀个开发者之间的约定，例如 convenience 必须选择⼀个 designated 的接口，但实际上很多 Objective-C 的类并没这么做，这也意味着如果有⼦类的话，如何正确构造它会成为⼀个头⼤的问题！</p>
<p>这是⼀个⾮常⾮常不好的事情，尤其对框架使⽤者⽽⾔，如果想写出⾼质量的代码，就必须阅读源码，或者逆向来观察它的行为，甚至通过猜测的方式， ⽽这都会导致⼦类出现异常的概率变⼤。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878225_719182.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878225_719182.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878225_719182.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878225_719182.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878225_719182.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>如果你忘了重写⼀些必要的构造器，作为框架的维护者是不会收到警告的，⽽使⽤者恰巧使⽤了这个 API，那就意味着这个类的初始化可能出现了问题，使⽤者会感觉很痛苦，为什么写个构造器这么难？</p>
<p>所以作为框架的维护者，我们需要去直面这个问题！</p>
<p>通常 designated 构造器会调⽤ [super init] 这个方法，而 convenience 构造器会调⽤⾃⾝的某个 designated 构造器</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878215_7602215.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878215_7602215.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878215_7602215.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878215_7602215.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878215_7602215.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>所以，我们需要在 designated 构造器后面添加 <code>NS_DESIGNATED_INITIALIZER</code>, 对于 convenience 类型的构造器，你不需要做任何事情</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878208_4317465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878208_4317465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878208_4317465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878208_4317465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878208_4317465.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>在添加完 <code>NS_DESIGNATED_INITIALIZER</code> 以后，可能会遇到一些错误提示，它会要求你重写⽗类的 designated 构造器，因为这是一个潜在的 bug，如果有⼈使⽤了⽗类的 designated 构造器，而你没有对此进行处理，对象的构造就可能会出现问题。</p>
<p>所以</p>
<p>如果你想支持这些父类构造器，就去实现它！</p>
<p>如果你不想支持这些父类构造器，就需要完成如下的工作</p>
<ul>
<li>在 <code>.m</code> 文件里重写父类构造器，并调用 <code>doesNotRecognizeSelector</code> 方法</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878199_953621.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878199_953621.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878199_953621.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878199_953621.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878199_953621.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><ul>
<li>在 <code>.h</code> 文件里用 <code>NS_UNAVAILABLE</code> 声明对应的 API</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878193_215926.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878193_215926.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878193_215926.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878193_215926.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878193_215926.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>除此之外，还需要提醒的有两点</p>
<ul>
<li>除了关注父类的 designated 构造器，开发者也需要关注 convenience 类型的构造器。</li>
<li><code>NS_UNAVAILABLE</code> 的 API 不会被继承</li>
</ul>
<p>通过这些改造，你的 Swift 接口将会变得清晰明了！</p>
<h4>关于错误处理的相关约定</h4>
<p>让我们在看看下面的代码</p>
<p>在整个文章的开始部分，我们提到这段代码可能会在不该抛出异常的时候抛出异常，至于原因，其实在这个 API 的注释里就已经说明了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878180_962551.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878180_962551.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878180_962551.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878180_962551.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878180_962551.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>可能有人会问，看起来没什么问题啊？</p>
<p>这其实就是问题本身，可能我们会认为如果⼀个⽅法要发出失败的信号，就意味着必须要返回 false 且为 error 设置⼀个 non-nil 值；如果只返回⼀个 false 并不是真的失败。</p>
<p>但事实上，通常的约定是：返回 false 就是失败！即使 error 是 nil。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878173_493706.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878173_493706.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878173_493706.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878173_493706.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878173_493706.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>Apple 的工程师并不建议把 error 设置为 nil，因为这样的话，使用者就无法感知发⽣了什么，但如果你坚持这样做，也就是返回 false 的同时，返回了一个 error 为 nil 的值，从惯用的约定来看，它仍然代表失败！</p>
<p>所以在 swift ⾥调⽤这种 Objective-C 的⽅法时，它会⾃动导⼊ throws，而且 swift 会认为你遵循刚才提到的约定，所以只要⽅法返回 false 它就会抛出异常！</p>
<p>但是 Swift 又不允许你抛出异常的时候，提供的信息是 nil。如果没有 error，swift 会抛出⼀个基础库里⾮公开的 error 类型，由于这是⼀个⾮公开的类型，你是⽆法 catch 这些信息的，但是你可以在 logs ，debugger 或者 error message ⾥看到相关的信息。</p>
<p>这意味着⼀些 Obective-C 代码即使没有失败也返回了 false，或者失败了但没有告诉你原因。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878164_875148.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878164_875148.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878164_875148.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878164_875148.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878164_875148.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>所以回到一开始的那段代码上，⽂档注释⾥说明了在某些情况下，例如有东西需要保存的时候，会直接返回 false，虽然其实这不能算是失败，但是 Swift 会因为 false 抛出异常。由于⽅法没有设置正确的 Error 信息，就会提到我们前⾯说的情况，抛出⼀个基础库里⾮公开的 error 信息。</p>
<p>所以如何解决它呢？</p>
<p>最简单的方法就是去掉特殊情况，让 false 总是意味着失败，⽽且让该⽅法遵守前⾯提到的约定。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878158_465889.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878158_465889.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878158_465889.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878158_465889.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878158_465889.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>不过这也引入了新的问题，如果你的用户需要判断那种特殊情况的时候，这种⽅法就⾏不通了！</p>
<p>另外一种方式是，在 API 后面标注 <code>NS_SWIFT_NOTHROW</code> 来告诉 Swift 你不想遵守那个约定，这样的话，Swift 会让你⼿动处理错误相关的代码</p>
<p>虽然这解决了问题，但并不是最好的方式，建议将这个 API 废弃，此时你只需要在后面添加 <code>DEPRECATED_ATTRIBUTE</code> 即可</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878150_535402.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878150_535402.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878150_535402.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878150_535402.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878150_535402.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>在 Objective-C 里最好的解决方案是重新写一个 API 并返回额外的信息来表示刚才提到的特殊场景。</p>
<p>例如可以添加⼀个布尔输出参数来说明⽂件是否真的被保存了，然后返回值就可以遵守之前的约定了。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878142_877012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878142_877012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878142_877012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878142_877012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878142_877012.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>目前来看，这是在 Objective-C 里的最佳解决方案了！</p>
<p>如果你还想进一步优化，我们还是有办法的，只是我们需要用 Swift 将其包裹一下并对外保留 Swift 接口，而非原先的 Objective-C 接口。</p>
<p>在 Swift 里，我们可以提供这样的一段代码来优化使用体验</p>
<p>在 Objective-C 里面，我们要小心返回值，因为他的惯用约定会认为返回 false 即是失败，而在 Swift 里，我们则可以忽略这一点，返回值与抛出异常或者失败是完全没有关系的！</p>
<p>所以代码会变成下面的样子！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304878131_873683.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304878131_873683.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304878131_873683.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304878131_873683.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304878131_873683.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><div class="blockquote"><blockquote><p>为了让这个文章的内容更连贯，我将原 Session 里这部分的内容做了精简，此处还提到了以下几个知识点:</p>
<ul>
<li><p>在框架的头文件里，也就是 umbrella header 里，不要导入 generated header，也就是系统自动生成的 <code>&lt;framework&gt;-Swift.h</code>，这个头文件会声明 swift ⽂件⾥所有被 @objc 标记的内容</p>
<ul>
<li>不这么做的原因是会产生循环依赖，因为不导⼊ umbrella header ⾥的内容，Swift ⽆法⽣成 <code>&lt;framework&gt;-Swift.h</code>，但如果 umbrella header 导⼊了 <code>&lt;framework&gt;-Swift.h</code>，那么 swift 就会试图读取这个还没⽣成的⽂件，而这就会造成问题！</li>
</ul>
</li>
<li><p>在某些平台上，Swift 的 bool 和 Objective-C 的 bool 略有不同，主要是在内存表示方面的问题。</p>
<ul>
<li>通常，swift 会做⼀层转换，但是现在的代码⾥你操作的是指针，也就是将 Swift 的 bool 指针直接扔给 Objective-C 使⽤，而对于这种 case 来说，Swift 还没覆盖，所以我们需要做⼿动转换，声明⼀个 ObjcBool 类型即可</li>
</ul>
</li>
</ul>
</blockquote></div>
<p>现在我们的 Swift 使用者终于可以使用到非常舒服的 API 了，不过还有一个问题就是，他在这种场景下，依然能接触到 Objective—C 里面提供的新 API，<code>-(BOOL)saveToURL:(nonnull NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error</code>。</p>
<p>他一定会面临这样的处境，我该用哪个呢？</p>
<p>所以作为框架的维护者，你现在的情况是 ：</p>
<ul>
<li>希望 Objective—C 的使⽤者用到原先的 API，</li>
<li>希望 Swift 的使用者用到 Swift 里提供的 API</li>
</ul>
<p>针对这种情况，你也有相应的解决办法，此时你需要</p>
<ul>
<li>在原有的头⽂件⾥对相应的 Objective—C 的⽅法标记 <code>NS_REFINED_FOR_SWIFT</code></li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864573_302073.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864573_302073.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864573_302073.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864573_302073.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864573_302073.jpg" alt="37 (1).jpg"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><ul>
<li>修改 Swift 里的代码实现</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864560_857318.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864560_857318.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864560_857318.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864560_857318.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864560_857318.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>可能有人会好奇，<code>NS_REFINED_FOR_SWIFT</code> 到底干了什么？</p>
<p>其实这个标记做的事情很简单，它把对应的 Swift 版本 API 进行了改造，改造的内容就是在其开头增加了两个下划线在开头，</p>
<p>当 Xcode 看到这样的 api 时，会让编辑器将其隐藏起来，例如代码补全的时候，但它不代表你不可以调用，所以在刚才的 Swift 文件里，我们看到了 <code>self.__save(to:url, wasDirty: &amp;wasDirty)</code> 的代码。</p>
<p>此时，我们仍然使⽤了 Objective—C 的实现，⽽且使⽤者也⽆法直接调取那些我们不想让他获取的 API 了！真是一个让人开心的结果！</p>
<h3>解决缺少 API 的问题</h3>
<p>通常 Swift 编译器会导⼊ Objective—C 头⽂件⾥的⼀切，但如果它⽆法识别如何导⼊的时候，就会忽略这些内容，进而导致某些 API 没有在 Swift 里展示。</p>
<p>这些场景会是如下的这些情况：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864548_94202.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864548_94202.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864548_94202.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864548_94202.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864548_94202.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>回到代码上，我们可以看到 SKCapsuleApolloCSM 这个 API 消失了！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864541_215332.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864541_215332.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864541_215332.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864541_215332.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864541_215332.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>在这里，我们用宏定义了一些字符串，在这里宏本质上只是⼀个⽂本⽚段，你可以在 Objective—C 源码的任何地⽅使⽤它。</p>
<p>同⼀个宏在不同的地⽅可能有不同含义，而 Swift 本⾝是没法搞清楚这些的，所以 Swift 只能识别符合某些特定模式的宏，这种模式主要是⽤来声明常量的。</p>
<p>它允许你为另外⼀个宏命名，或者给宏设置某个值，但是两者同时使用就会出现识别问题，</p>
<p>所以来看第四个宏，它替换了另外⼀个宏，并在原本的内容上增加了 “.csm”，这对 Swift 来说有点超出其能力范围了！</p>
<p>有许多⽅法解决这个问题，最简单的就是⽤完整的字符串来表达这个宏，而不是用相对复杂的宏拼接。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864531_228998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864531_228998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864531_228998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864531_228998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864531_228998.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>但如果你是⽤这些字符串做枚举，建议你把它转成真正的字符串常量，这样就可以像前面的 SKRocket 常量⼀样进⾏字符 串枚举了！那才是好的最佳实践！</p>
<h3>改善框架在 Swift 里的使用体验</h3>
<h4>如何改善 Swift 的 API 名称</h4>
<p>Swift 和 Objective—C 的命名风格是有所不同，例如 Swift 的 API 是由基名（previousMissionsFlown）和参数标签（by）组成的，⽽ Objective—C 基本上只有参数标签(previousMissionsFlownByAstronaut)，没有单独的基名，所以基名的信息会包含在第⼀个参数标签⾥，这也导致了 Objective—C 的方法名会显得略长一些。</p>
<p>为了解决 API 风格上的问题，Swift 会根据一些规则重命名，通常这个结果还不错，但这毕竟是计算机的审美结果，很难满足开发者的诉求。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864521_538248.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864521_538248.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864521_538248.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864521_538248.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864521_538248.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>例如某些开发者会认为 flown 应该是参数标签⾥的⼀部分，⽽不是 base name，因为这个⽅ 法获取的是以前的任务列表，它们是某个宇航员所执⾏的任务！</p>
<div class="blockquote"><blockquote><p>当然这不是绝对的，这⾥只是个假设。</p>
</blockquote></div>
<p>所以为了解决这个问题，我们使⽤ <code>NS_SWIFT_NAME</code> 重新命名这个⽅法</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864514_178884.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864514_178884.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864514_178884.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864514_178884.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864514_178884.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>好了！这个 API 终于满足你的诉求了！</p>
<p>或许你会说我知道 <code>NS_SWIFT_NAME</code> 能重命名方法名，但 <code>NS_SWIFT_NAME</code> 的能力还有很多施展空间！</p>
<p>例如下面的枚举！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864506_4614315.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864506_4614315.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864506_4614315.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864506_4614315.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864506_4614315.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>或许，乍一看，这个枚举其实已经写得挺好的了，但其实也有不少改进的空间。</p>
<p>可能你会想到⽤ <code>NS_SWIFT_NAME</code> 删除其前缀 SK，因为在 Swift 里面没有这种做法，但不推荐这么做， ⼤部分 Objective—C 的类都会将框架前缀与⼀个像 query 或者 record 的词组合起来，例如 SKFuleKind ⾥的 SK 和 FuleKind。</p>
<p>所以我们需要用别的方法来优化使用体验，针对目前框架，我们有一个 SKFule 类，此时我们可以让这个枚举和 SKFule 联合使⽤，所以我们将其改为 SKFuel.Kind</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864496_963131.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864496_963131.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864496_963131.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864496_963131.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864496_963131.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>除了重命名枚举外，<code>NS_SWIFT_NAME</code> 的另外一个常见使⽤场景是处理那些与 Swift 风格差异过大的 API，这在许多 C 语言的库里十分常见，例如全局函数，全局变量等。</p>
<p>像上面的例子中，SKFuelKindToString 就是一个全局函数，我们不仅可以⽤ <code>NS_SWIFT_NAME</code> 对其重命名，还可以去掉额外的信息，添加⼀个参数标签。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864487_499954.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864487_499954.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864487_499954.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864487_499954.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864487_499954.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>不过刚才处理全局函数的例子只是展示了 <code>NS_SWIFT_NAME</code> 能力的冰山一角！下面我们会再展开几个例子：</p>
<p>⾸先你可以将 global function 转换成 static method，做法是在 <code>NS_SWIFT_NAME</code> 里指明 Objective—C 的类型并在 类型后面使用点语法声明⽅法名</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864479_337012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864479_337012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864479_337012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864479_337012.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864479_337012.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>然后，你还可以将其变为实例⽅法！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864448_132257.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864448_132257.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864448_132257.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864448_132257.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864448_132257.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>最后，你也可以将某个⽅法变为⼀个属性，只需要在前⾯增加⼀个 getter，同理 setter</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864436_890138.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864436_890138.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864436_890138.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864436_890138.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864436_890138.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>将这些技术应⽤在充满 C 函数的框架⾥，可以很好的重塑 API，如果你用过 Core Graphics 的话，你会深有体会！</p>
<p>下⾯要说说 <code>NS_SWIFT_NAME</code> 的能⼒边界，例如刚才的那个 getter 例子，即使你将⽅法名改为 description，你也⽆法让这个类型遵守 string convertible protocol。</p>
<p>但是我们可以通过在 Swift 文件里添加扩展来使其满足 protocol conformance！</p>
<p>如下图所示，给 SKFuel.Kind 写了⼀个扩展，并使其符合⾃定义字符串转换协议，由于 Objective-C 头文件已经⽤ <code>NS_SWIFT_NAME</code> 提供了相应的属性，所以写成这样，我们的 SKFuel.Kind 已经遵守了相应的协议并满足其使用要求。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864426_251328.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864426_251328.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864426_251328.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864426_251328.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864426_251328.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><div class="blockquote"><blockquote><p>为了让这个文章的内容更连贯，我将原 Session 里这部分的内容做了精简，此处还提到了以下几个知识点:</p>
<ul>
<li>我们应当注意到刚才的 Swift 文件，在那里我们可以写出任何你想提供的 Swift 版本专⽤的 API。例如整合了 UIView 的 SwiftUI 组件，或者将原有 API 的 completion handler 换成 Combine 的 API.</li>
<li>如果对 SwiftUI 和 Combine 感兴趣，可以查看去年的两个相关 Session，<a href="https://developer.apple.com/videos/play/wwdc2019/231/">Integrating SwiftUI</a> 和 <a href="https://developer.apple.com/videos/play/wwdc2019/721/">Combine in Practice</a></li>
</ul>
</blockquote></div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864405_104469.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864405_104469.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864405_104469.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864405_104469.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864405_104469.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><h4>如何提升 Error Code 在 Swift 里的使用体验</h4>
<p>error code 枚举在许多框架里都能见到，它通常会和 NSError ⼀起使⽤。</p>
<p>通常这类代码会分为两个部分</p>
<ul>
<li>声明⼀个带有特定错误代码的 <code>NS_ENUM</code></li>
<li>为了防⽌错误码与其他框架的错误码发⽣冲突，还会声明了⼀个用于表明作用域的字符串常量。</li>
</ul>
<p>下⾯的代码在接口层⾯是没有问题的，但在使⽤时，就会出现⽐较明显的问题！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864386_919002.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864386_919002.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864386_919002.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864386_919002.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864386_919002.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>假设我们有这样⼀个场景：我们需要考虑在执⾏某次飞⾏任务的过程中，如果发射终⽌了，我们要确保救援队能去营救宇航员！</p>
<p>所以代码会是如下的样⼦：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864379_497917.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864379_497917.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864379_497917.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864379_497917.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864379_497917.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>先调⽤发射任务的代码，如果是发射终⽌错误，我们需要对齐进行 catch，然后就是如何 catch 到特定的 case 了, 这里需要 error 中的作用域（domain）和错误码（code）信息</p>
<ul>
<li>⾸先我们要把它作为⼀个 NSError 来捕获。</li>
<li>接下来，我们需要确保错误是 SKError 的作用域，⽽不是其他可能使⽤相同错误代码的作用域。</li>
<li>然后我们需要将错误代码从⼀个 Int 转换成⼀个 SKErrorCode</li>
<li>最后我们才可以检查它是否是我们想要的情况</li>
</ul>
<p>看看上面这一坨代码，这只是为了匹配一个错误而已，真的有点复杂了！</p>
<p>毕竟这在 Swift 里可以通过一行模式匹配就搞定！所以我们有办法解决这个问题么？</p>
<p>答案很简单，将 <code>NS_ENUM</code> 替换成 <code>NS_ERROR_ENUM</code>, ⽤错误的作用域替（SKErrorDomain）换原始类型（NSInteger）</p>
<p>此时，我们再看 Swift 接口文件的话，我们会发现它已经不再是静态常量，⽽是结构体了！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864371_727629.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864371_727629.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864371_727629.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864371_727629.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864371_727629.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>它里面的具体内容会如下所示，是不是变得很 Swift 了？</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864364_954128.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864364_954128.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864364_954128.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864364_954128.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864364_954128.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>除了上⾯的优化，Swift 编译器还做了如下的调整</p>
<ul>
<li>SKError 自动遵循了 Error 协议，使得其使用习惯更贴近 Swift 的⽤法。</li>
<li>提供一个 tilde equal 操作符，这就是 case 和 catch 语句匹配时使⽤的匹配操作符，</li>
</ul>
<p>SKError 的这部分内容在⽣成界⾯是不可见的，但编译器真的会合成他们，并供你使⽤！⽽你要做的就是改⼀⾏代码！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864356_738987.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864356_738987.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864356_738987.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864356_738987.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864356_738987.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>至此，我们终于打造出来一个比较不错的 API 接口了！</p>
<h2>总结</h2>
<p>现在回到文章最开始的地方，思考⼀个问题，在⽣成的 Swift 接口中，并不是所有的 API 都有明显的缺陷，例如最后一个 Error Code 的案例，可能只有当我们看到 SKErrorCode 被使⽤的时候，才意识到这里有改进的空间。</p>
<p>虽然查看编译器生成的 Swift 头文件是一个好的方法。但⽣ 成的接口并不是全部，真正重要的是使用者在实际使⽤过程中写出的调⽤代码。</p>
<p>所以当我们在思考如何打造一个更适合 Swift 使用的接口是，不光要看看⽣成的接口。也应该考虑实际的使用场景。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304864346_091941.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304864346_091941.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304864346_091941.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304864346_091941.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304864346_091941.png" alt="image.png"loading="lazy" decoding="async" width="2844" height="1606" /></picture></figure></div><p>总之，在 Session 中，Apple 的工程师提供给了我们很多在 Swift 里改善 Objective-C 框架体验的方法，也着重提醒了我们，相比于关注头文件，我们要更关注实际的使用体验和使用场景。</p>
<p>相信大家一定都有不少的收货，可能有人会问，这就是所有的手段了么？当然，Session 里面并没有展示全部的细节，如果你想进一步了解相关的内容，可以阅读以下内容：</p>
<ul>
<li>Swift Document 里的 “Language Interoperability” 章节，<a href="https://developer.apple.com/documentation/swift#2984901">传送门在此</a></li>
</ul>
<p>如果你对 Swift API 本身的风格还不够了解，建议先阅读下面的文档：</p>
<ul>
<li>Swift API Design Guidelines: <a href="https://github.com/SketchK/the-swift-api-design-guidelines-in-chinese">中文传送门</a>，<a href="https://swift.org/documentation/api-design-guidelines/">英文传送门</a></li>
</ul>
<p>当然如果你想了解这种混编过程的细节，建议您观看 <a href="https://developer.apple.com/videos/wwdc2018">WWDC 18 Behind the Scenes of Xcode Build Process</a></p>
]]></content:encoded></item><item><title><![CDATA[编译系统（Compilation System）和编译流程（Compilation pipeline）到底是什么]]></title><guid>https://swiftsiqi.com/posts/what-is-compilation-system-and-compilation-pipeline</guid><link>https://swiftsiqi.com/posts/what-is-compilation-system-and-compilation-pipeline</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 07 Jun 2020 10:50:23 +0000</pubDate><content:encoded><![CDATA[<p>在<a href="https://sketchk.xyz/2020/05/24/What-is-Compiler/">编译到底是什么</a>一文中，我们了解了编译在计算机编程中扮演的角色和作用，这次我们将探讨编译系统是由哪些角色组成的！</p>
<h2>回顾</h2>
<p>从某种程度上来说：</p>
<p>编译，其实就是把源代码变成目标代码的过程。如果源代码编译后要在操作系统上运行，那目标代码就是汇编代码，我们再通过汇编和链接的过程形成可执行文件，然后通过加载器加载到操作系统里执行。如果编译后是在解释器里执行，那目标代码就可以不是汇编代码，而是一种解释器可以理解的中间形式的代码即可。</p>
<h2>组成编译系统的基本元素</h2>
<p>通常来说，编译系统是由 4 个部分组成</p>
<ul>
<li>预处理(preprocessor)：负责引用 header file，libraries 的文件并组成完整的代码，以 C 语言为例，就是根据 <code>#</code> 开头的指令，例如 <code>#include &lt;stdio.h&gt;</code> 插入 <code>stdio.h</code> 的内容</li>
<li>编译器(compiler)：把重新组合好的代码再转交给 compiler，并转化成汇编语言（assembly language），使用汇编语言最重要的原因是不同的高级语言都可以变成汇编语言，汇编码也比机器码好 debug，PS：汇编语言会因硬件的架构而有所不同。</li>
<li>汇编器(assembler)：将汇编语言转换为机器码（machine code），并打包成重新定位的目标文件（object file）</li>
<li>链接器(linker)：负责合并所有的 object file 并产生可执行的文件，它可以被加载到内存中执行。</li>
</ul>
<p>我们再把流程绘制成如下的图示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879342_885796.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_292/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879342_885796.png" alt="image.png"loading="lazy" decoding="async" width="292" height="702" /></picture></figure></div><h2>编译器的组成</h2>
<p>知道了编译系统后，我们再探究下编译系统里面编译器（compiler）环节。</p>
<p>按照<a href="https://book.douban.com/subject/1134994//">龙书</a>里的说法，我们可以将编译器里做的事情分为两个阶段：</p>
<ol>
<li>分析（Analysis）: 又称为 Compiler 的前端处理（front-end）,分析与解构原始代码，并将其整理成中间代码（intermediate representation）与符号表（symbol table）并传给下一个阶段，当中如果发现任何问题就会提示报错</li>
<li>生成（Synthesis）：又称为 compiler 的后端处理（back-end）,根据符号表与中间代码产出目标代码</li>
</ol>
<p>为了理解编译器的作用，我举一个很简单的例子。这里有一段 C 语言的程序，我们一起来看看它的编译过程。</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="kt">int</span><span class="w"> </span><span class="nf">foo</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">a</span><span class="p">){</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="kt">int</span><span class="w"> </span><span class="n">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">b</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>这段源代码，如果把它编译成汇编代码，大致是下面这个样子：</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="p">.</span><span class="n">section</span><span class="w">    </span><span class="n">__TEXT</span><span class="p">,</span><span class="n">__text</span><span class="p">,</span><span class="n">regular</span><span class="p">,</span><span class="n">pure_instructions</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">.</span><span class="n">globl</span><span class="w">  </span><span class="n">_foo</span><span class="w">                    </span><span class="err">##</span><span class="w"> </span><span class="o">--</span><span class="w"> </span><span class="n">Begin</span><span class="w"> </span><span class="n">function</span><span class="w"> </span><span class="n">foo</span><span class="w"></span>
</div><div class="line"><span class="nl">_foo</span><span class="p">:</span><span class="w">                                   </span><span class="err">##</span><span class="w"> </span><span class="err">@</span><span class="n">foo</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">pushq</span><span class="w">   </span><span class="o">%</span><span class="n">rbp</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">movq</span><span class="w">    </span><span class="o">%</span><span class="n">rsp</span><span class="p">,</span><span class="w"> </span><span class="o">%</span><span class="n">rbp</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">movl</span><span class="w">    </span><span class="o">%</span><span class="n">edi</span><span class="p">,</span><span class="w"> </span><span class="mi">-4</span><span class="p">(</span><span class="o">%</span><span class="n">rbp</span><span class="p">)</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">movl</span><span class="w">    </span><span class="mi">-4</span><span class="p">(</span><span class="o">%</span><span class="n">rbp</span><span class="p">),</span><span class="w"> </span><span class="o">%</span><span class="n">eax</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">addl</span><span class="w">    </span><span class="n">$3</span><span class="p">,</span><span class="w"> </span><span class="o">%</span><span class="n">eax</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">movl</span><span class="w">    </span><span class="o">%</span><span class="n">eax</span><span class="p">,</span><span class="w"> </span><span class="mi">-8</span><span class="p">(</span><span class="o">%</span><span class="n">rbp</span><span class="p">)</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">movl</span><span class="w">    </span><span class="mi">-8</span><span class="p">(</span><span class="o">%</span><span class="n">rbp</span><span class="p">),</span><span class="w"> </span><span class="o">%</span><span class="n">eax</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">popq</span><span class="w">    </span><span class="o">%</span><span class="n">rbp</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="n">retq</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>你可以看出，源代码和目标代码之间的差异还是很大的。那么，我们怎么实现这个翻译呢？</p>
<p>其实，编译和把英语翻译成汉语的大逻辑是一样的。前提是你要懂这两门语言，这样你看到一篇英语文章，在脑子里理解以后，就可以把它翻译成汉语。编译器也是一样，你首先需要让编译器理解源代码的意思，然后再把它翻译成另一种语言。</p>
<p>表面上看，好像从英语到汉语，一下子就能翻译过去。但实际上，大脑一瞬间做了很多个步骤的处理，包括识别一个个单词，理解语法结构，然后弄明白它的意思。同样，编译器翻译源代码，也需要经过多个处理步骤，</p>
<p>所以，我们将编译器的两个工作环节进行更细致的划分</p>
<ul>
<li>分析 (analysis):<ul>
<li>词法分析器 (lexical analyzer)，也称 scanner，建立 symbol table</li>
<li>语法分析器 (syntax analyzer)，也称 parser</li>
<li>语义分析器 (semantic analyzer)</li>
<li>生成中间码 (intermediate code generator)</li>
<li>中间码最佳化 (code optimizer) (optional &amp; machine-independent)</li>
</ul>
</li>
<li>生成 (synthesis):<ul>
<li>目标代码生成器 (code generator)</li>
<li>目标代码最佳化 (machine-independent code optimizer) (optional &amp; machine-independent)</li>
</ul>
</li>
</ul>
<p>流程图如下：</p>
<div class="blockquote"><blockquote><p>因为 Symbol Table 会在各个步骤都会使用到，因此将其独立画出来</p>
</blockquote></div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879327_494472.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_437/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879327_494472.png" alt="image.png"loading="lazy" decoding="async" width="437" height="956" /></picture></figure></div><p>当然国外也有人将其这样划分</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879319_939773.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879319_939773.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879319_939773.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879319_939773.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879319_939773.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="1000" /></picture></figure></div><div class="blockquote"><blockquote><p>现代语言，在语法解析过程，是可以不依赖符号表的，所以符号表一般都推迟到语义分析阶段才去建立，例如 Java 语言。
当然也有个别语言，在语法解析阶段需要查一下符号表（也就是获取上下文信息），才能知道某个标识符应该位于哪个语法中。所以，这个时候符号表会提前建立，在词法分析的时候就开始建立。</p>
</blockquote></div>
<h2>词法分析</h2>
<p>首先，编译器要读入源代码。</p>
<p>在编译之前，源代码只是一长串字符而已，这显然不利于编译器理解程序的含义。所以，编译的第一步，就是要像读文章一样，先把里面的单词和标点符号识别出来。程序里面的单词叫做 Token，它可以分成关键字、标识符、字面量、操作符号等多个种类。把字符串转换为 Token 的这个过程，就叫做词法分析。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879312_3588295.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879312_3588295.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879312_3588295.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879312_3588295.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879312_3588295.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="880" /></picture></figure></div><div class="blockquote"><blockquote><p>把字符串转换为 Token（注意：其中的空白字符，代表空格、tab、回车和换行符，EOF 是文件结束符）</p>
</blockquote></div>
<h2>语法分析</h2>
<p>识别出 Token 以后，离编译器明白源代码的含义仍然有很长一段距离。下一步，我们需要让编译器像理解自然语言一样，理解它的语法结构。这就是第二步，语法分析。</p>
<p>上语文课的时候，老师都会让你给一个句子划分语法结构。比如说：“我喜欢又聪明又勇敢的你”，它的语法结构可以表示成下面这样的树状结构。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879305_831587.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879305_831587.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879305_831587.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879305_831587.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879305_831587.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="1580" /></picture></figure></div><p>那么在编译器里，语法分析阶段也会把 Token 串，转换成一个体现语法规则的、树状的数据结构，这个数据结构叫做抽象语法树（AST，Abstract Syntax Tree）。我们前面的示例程序转换为 AST 以后，大概是下面这个样子：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879298_775499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879298_775499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879298_775499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879298_775499.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879298_775499.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="1480" /></picture></figure></div><p>这样的一棵 AST 反映了示例程序的语法结构。比如说，我们知道一个函数的定义包括了返回值类型、函数名称、0 到多个参数和函数体等。这棵抽象语法树的顶部就是一个函数节点，它包含了四个子节点，刚好反映了函数的语法。</p>
<p>再进一步，函数体里面还可以包含多个语句，如变量声明语句、返回语句，它们构成了函数体的子节点。然后，每个语句又可以进一步分解，直到叶子节点，就不可再分解了。而叶子节点，就是词法分析阶段生成的 Token（图中带边框的节点）。对这棵 AST 做深度优先的遍历，你就能依次得到原来的 Token。</p>
<h2>语义分析</h2>
<p>生成 AST 以后，程序的语法结构就很清晰了，编译工作往前迈进了一大步。但这棵树到底代表了什么意思，我们目前仍然不能完全确定。</p>
<p>比如说，表达式“a+3”在计算机程序里的完整含义是：“获取变量 a 的值，把它跟字面量 3 的值相加，得到最终结果。”但我们目前只得到了这么一棵树，完全没有上面这么丰富的含义。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879291_328152.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879291_328152.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879291_328152.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879291_328152.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879291_328152.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="400" /></picture></figure></div><p>这就好比西方的儿童，很小的时候就能够给大人读报纸。因为他们懂得发音规则，能念出单词来（词法分析），也基本理解语法结构（他们不见得懂主谓宾这样的术语，但是凭经验已经知道句子有不同的组成部分），可以读得抑扬顿挫（语法分析），但是他们不懂报纸里说的是什么，也就是不懂语义。这就是编译器解读源代码的下一步工作，语义分析。</p>
<h3>那么，怎样理解源代码的语义呢</h3>
<p>实际上，语言的设计者在定义类似“a+3”中加号这个操作符的时候，是给它规定了一些语义的，就是要把加号两边的数字相加。你在阅读某门语言的标准时，也会看到其中有很多篇幅是在做语义规定。在 ECMAScript（也就是 JavaScript）标准 2020 版中，Semantic 这个词出现了 657 次。下图是其中加法操作的语义规则，它对于如何计算左节点、右节点的值，如何进行类型转换等，都有规定。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879282_683955.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1522/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879282_683955.png" alt="image.png"loading="lazy" decoding="async" width="1522" height="1224" /></picture></figure></div><div class="blockquote"><blockquote><p>ECMAScript 标准中加法操作的语义规则</p>
</blockquote></div>
<p>所以，我们可以在每个 AST 节点上附加一些语义规则，让它能反映语言设计者的本意。</p>
<ul>
<li>add 节点：把两个子节点的值相加，作为自己的值；</li>
<li>变量节点（在等号右边的话）：取出变量的值；</li>
<li>数字字面量节点：返回这个字面量代表的值。</li>
</ul>
<p>这样的话，如果你深度遍历 AST，并执行每个节点附带的语义规则，就可以得到 a+3 的值。这意味着，我们正确地理解了这个表达式的含义。运用相同的方法，我们也就能够理解一个句子的含义、一个函数的含义，乃至整段源代码的含义。</p>
<p>这也就是说，AST 加上这些语义规则，就能完整地反映源代码的含义。这个时候，你就可以做很多事情了。比如，你可以深度优先地遍历 AST，并且一边遍历，一边执行语法规则。那么这个遍历过程，就是解释执行代码的过程。你相当于写了一个基于 AST 的解释器。</p>
<p>不过在此之前，编译器还要做点语义分析工作。那么这里的语义分析是要解决什么问题呢？</p>
<p>给你举个例子，如果我把示例程序稍微变换一下，加一个全局变量的声明，这个全局变量也叫 a。那你觉得“a+3”中的变量 a 指的是哪个变量？</p>
<div class="block-code" data-language="c"><div class="highlight"><pre><span></span><code><div class="line"><span class="kt">int</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">       </span><span class="c1">//全局变量</span>
</div><div class="line"><span class="kt">int</span><span class="w"> </span><span class="nf">foo</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">a</span><span class="p">){</span><span class="w">   </span><span class="c1">//参数里有另一个变量a</span>
</div><div class="line"><span class="w">  </span><span class="kt">int</span><span class="w"> </span><span class="n">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span><span class="w">  </span><span class="c1">//这里的a指的是哪一个？</span>
</div><div class="line"><span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="n">b</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>我们知道，编译程序要根据 C 语言在作用域方面的语义规则，识别出“a+3”中的 a，所以这里指的其实是函数参数中的 a，而不是全局变量的 a。这样的话，我们在计算“a+3”的时候才能取到正确的值。</p>
<p>而把“a+3”中的 a，跟正确的变量定义关联的过程，就叫做引用消解（Resolve）。这个时候，变量 a 的语义才算是清晰了。</p>
<p>变量有点像自然语言里的代词，比如说，“我喜欢又聪明又勇敢的你”中的“我”和“你”，指的是谁呢？如果这句话前面有两句话，“我是春娇，你是志明”，那这句话的意思就比较清楚了，是“春娇喜欢又聪明又勇敢的志明”。</p>
<p>引用消解需要在上下文中查找某个标识符的定义与引用的关系，所以我们现在可以回答前面的问题了，语义分析的重要特点，就是做上下文相关的分析。</p>
<p>在语义分析阶段，编译器还会识别出数据的类型。比如，在计算“a+3”的时候，我们必须知道 a 和 3 的类型是什么。因为即使同样是加法运算，对于整型和浮点型数据，其计算方法也是不一样的。</p>
<p>语义分析获得的一些信息（引用消解信息、类型信息等），会附加到 AST 上。这样的 AST 叫做带有标注信息的 AST（Annotated AST/Decorated AST），用于更全面地反映源代码的含义。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879270_272385.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879270_272385.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879270_272385.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879270_272385.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879270_272385.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="1600" /></picture></figure></div><p>好了，前面我所说的，都是如何让编译器更好地理解程序的语义。不过在语义分析阶段，编译器还要做很多语义方面的检查工作。</p>
<p>在自然语言里，我们可以很容易写出一个句子，它在语法上是正确的，但语义上是错误的。比如，“小猫喝水”这句话，它在语法和语义上都是对的；而“水喝小猫”这句话，语法是对的，语义上则是不对的。</p>
<p>计算机程序也会存在很多类似的语义错误的情况。比如说，对于“int b = a+3”的这个语句，语义规则要求，等号右边的表达式必须返回一个整型的数据（或者能够自动转换成整型的数据），否则就跟变量 b 的类型不兼容。如果右边的表达式“a+3”的计算结果是浮点型的，就违背了语义规则，就要报错。</p>
<p>总结起来，在语义分析阶段，编译器会做语义理解和语义检查这两方面的工作。词法分析、语法分析和语义分析，统称编译器的前端，它完成的是对源代码的理解工作。</p>
<h3>做完语义分析以后，接下来编译器要做什么呢</h3>
<p>本质上，编译器这时可以直接生成目标代码，因为编译器已经完全理解了程序的含义，并把它表示成了带有语义信息的 AST、符号表等数据结构。</p>
<p>生成目标代码的工作，叫做后端工作。做这项工作有一个前提，就是编译器需要懂得目标语言，也就是懂得目标语言的词法、语法和语义，这样才能保证翻译的准确性。这是显而易见的，只懂英语，不懂汉语，是不可能做英译汉的。通常来说，目标代码指的是汇编代码，它是汇编器（Assembler）所能理解的语言，跟机器码有直接的对应关系。汇编器能够将汇编代码转换成机器码。</p>
<p>熟练掌握汇编代码对于初学者来说会有一定的难度。但更麻烦的是，对于不同架构的 CPU，还需要生成不同的汇编代码，这使得我们的工作量更大。所以，我们通常要在这个时候增加一个环节：先翻译成中间代码（Intermediate Representation，IR）。</p>
<h2>中间代码</h2>
<p>中间代码（IR），是处于源代码和目标代码之间的一种表示形式。</p>
<p>我们倾向于使用 IR 有两个原因。</p>
<p>第一个原因，是很多解释型的语言，可以直接执行 IR，比如 Python 和 Java。这样的话，编译器生成 IR 以后就完成任务了，没有必要生成最终的汇编代码。</p>
<p>第二个原因更加重要。我们生成代码的时候，需要做大量的优化工作。而很多优化工作没有必要基于汇编代码来做，而是可以基于 IR，用统一的算法来完成。</p>
<h2>优化</h2>
<p>那为什么需要做优化工作呢？这里又有两大类的原因。</p>
<p>第一个原因，是源语言和目标语言有差异。源语言的设计目的是方便人类表达和理解，而目标语言是为了让机器理解。在源语言里很复杂的一件事情，到了目标语言里，有可能很简单地就表达出来了。</p>
<p>比如“I want to hold your hand and with you I will grow old.” 这句话挺长的吧？用了 13 个单词，但它实际上是诗经里的“执子之手，与子偕老”对应的英文。这样看来，还是中国文言文承载信息的效率更高。</p>
<p>同样的情况在编程语言里也有。以 Java 为例，我们经常为某个类定义属性，然后再定义获取或修改这些属性的方法：</p>
<div class="block-code" data-language="java"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">Class</span><span class="w"> </span><span class="n">Person</span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="nf">getName</span><span class="p">(){</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">setName</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">newName</span><span class="p">){</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="na">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">newName</span><span class="w"></span>
</div><div class="line"><span class="w">  </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>如果你在程序里用“person.getName()”来获取 Person 的 name 字段，会是一个开销很大的操作，因为它涉及函数调用。在汇编代码里，实现一次函数调用会做下面这一大堆事情：</p>
<div class="block-code"><pre><code>#调用者的代码
保存寄存器1   #保存现有寄存器的值到内存
保存寄存器2
...
保存寄存器n

把返回地址入栈
把person对象的地址写入寄存器，作为参数
跳转到getName函数的入口

#_getName 程序
在person对象的地址基础上，添加一个偏移量，得到name字段的地址
从该地址获取值，放到一个用于保存返回值的寄存器
跳转到返回地</code></pre></div>
<p>你看了这段伪代码，就会发现，简单的一个 getName() 方法，开销真的很大。保存和恢复寄存器的值、保存和读取返回地址，等等，这些操作会涉及好几次读写内存的操作，要花费大量的时钟周期。但这个逻辑其实是可以简化的。</p>
<p>怎样简化呢？就是跳过方法的调用。我们直接根据对象的地址计算出 name 属性的地址，然后直接从内存取值就行。这样优化之后，性能会提高好多倍。</p>
<p>这种优化方法就叫做内联（inlining），也就是把原来程序中的函数调用去掉，把函数内的逻辑直接嵌入函数调用者的代码中。在 Java 语言里，这种属性读写的代码非常多。所以，Java 的 JIT 编译器（把字节码编译成本地代码）很重要的工作就是实现内联优化，这会让整体系统的性能提高很大的一个百分比！</p>
<p>总结起来，我们在把源代码翻译成目标代码的过程中，没有必要“直译”，而是可以“意译”。这样我们完成相同的工作，对资源的消耗会更少。</p>
<p>第二个需要优化工作的原因，是程序员写的代码不是最优的，而编译器会帮你做纠正。比如下面这段代码中的 bar() 函数，里面就有多个地方可以优化。甚至，整个对 bar() 函数的调用，也可以省略，因为 bar() 的值一定是 101。这些优化工作都可以在编译期间完成。</p>
<div class="block-code" data-language="java"><div class="highlight"><pre><span></span><code><div class="line"><span class="kt">int</span><span class="w"> </span><span class="nf">bar</span><span class="p">(){</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="o">*</span><span class="mi">10</span><span class="p">;</span><span class="w">  </span><span class="c1">//这里在编译时可以直接计算出100这个值，这叫做“常数折叠”</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span><span class="w">     </span><span class="c1">//这个变量没有用到，可以在代码中删除，这叫做“死代码删除”</span><span class="w"></span>
</div><div class="line">
</div><div class="line">
</div><div class="line"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">a</span><span class="o">&gt;</span><span class="mi">0</span><span class="p">){</span><span class="w">       </span><span class="c1">//因为a一定大于0，所以判断条件和else语句都可以去掉</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">a</span><span class="o">+</span><span class="mi">1</span><span class="p">;</span><span class="w"> </span><span class="c1">//这里可以在编译器就计算出是101</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="k">else</span><span class="p">{</span><span class="w"></span>
</div><div class="line"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">a</span><span class="o">-</span><span class="mi">1</span><span class="p">;</span><span class="w"></span>
</div><div class="line"><span class="w">    </span><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="p">}</span><span class="w"></span>
</div><div class="line"><span class="kt">int</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bar</span><span class="p">();</span><span class="w">      </span><span class="c1">//这里可以直接换成 a=101</span><span class="w"></span>
</div></code></pre></div>
</div>
<p>综上所述，在生成目标代码之前，需要做的优化工作可以有很多，这通常也是编译器在运行时，花费时间最长的一个部分。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879249_177547.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879249_177547.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879249_177547.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879249_177547.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879249_177547.png" alt="image.png"loading="lazy" decoding="async" width="2284" height="1280" /></picture></figure></div><p>而采用中间代码来编写优化算法的好处，是可以把大部分的优化算法，写成与具体 CPU 架构无关的形式，从而大大降低编译器适配不同 CPU 的工作量。并且，如果采用像 LLVM 这样的工具，我们还可以让多种语言的前端生成相同的中间代码，这样就可以复用中端和后端的程序了。</p>
<h2>生成目标代码</h2>
<p>编译器最后一个阶段的工作，是生成高效率的目标代码，也就是汇编代码。这个阶段，编译器也有几个重要的工作。</p>
<p>第一，是要选择合适的指令，生成性能最高的代码。</p>
<p>第二，是要优化寄存器的分配，让频繁访问的变量（比如循环变量）放到寄存器里，因为访问寄存器要比访问内存快 100 倍左右。</p>
<p>第三，是在不改变运行结果的情况下，对指令做重新排序，从而充分运用 CPU 内部的多个功能部件的并行计算能力。</p>
<p>目标代码生成以后，整个编译过程就完成了。</p>
<h2>编译器流程总结</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879239_361656.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879239_361656.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879239_361656.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879239_361656.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879239_361656.png" alt="image.png"loading="lazy" decoding="async" width="2408" height="2698" /></picture></figure></div><h2>Swift 编译器的不同之处</h2>
<p>我们可以在 Swift 官网看到一篇名为 <a href="https://swift.org/swift-compiler/#compiler-architecture">Swift Compiler</a> 的文章，它从比较高的层面讲述了 Swift 编译器的主要流程，这里我们直接通过翻译的方式来介绍这些步骤：</p>
<ul>
<li>解析（Parsing）：解析器是一个简易的递归下降解析器（在 lib/Parse 中实现），并带有完整手动编码的词法分析器。这个分析器会生成 AST，但不包含任何语义信息或者类型信息，并且会忽略源码上的语法错误。</li>
<li>语义分析（Semantic Analysis）：语义分析阶段（在 lib/Sema 中实现）负责获取已解析的 AST（抽象语法树）并将其转换为格式正确且类型检查完备的 AST，以及在源代码中提示出现语义问题的警告或错误。语义分析包含类型推断，如果可以成功推导出类型，则表明此时从已经经过类型检查的最终 AST 生成代码是安全的。</li>
<li>Clang 导入器（Clang Importer）：Clang 导入器（在 lib/ClangImporter 中实现）负责导入 Clang 模块，并将导出的 C 或 Objective-C API 映射到相应的 Swift API 中。最终导入的 AST 可以被语义分析引用。</li>
<li>SIL 生成（SIL Generation）：Swift 中间语言（Swift Intermediate Language，SIL）是一门高级且专用于 Swift 的中间语言，适用于对 Swift 代码的进一步分析和优化。SIL 生成阶段（在 lib/SILGen 中实现）将经过类型检查的 AST 弱化为所谓的「原始」SIL。SIL 的设计在 docs/SIL.rst 有所描述。</li>
<li>SIL 保证转换（SIL Guaranteed Transformations）：SIL 保证转换阶段（在 lib/SILOptimizer/Mandatory 中实现）负责执行额外且影响程序正确性的数据流诊断（比如使用未初始化的变量）。这些转换的最终结果是「规范」SIL。</li>
<li>SIL 优化（SIL Optimizations）：SIL 优化阶段（在 lib/Analysis、lib/ARC、lib/LoopTransforms 以及 lib/Transforms 中实现）负责对程序执行额外的高级且专用于 Swift 的优化，包括（例如）自动引用计数优化、去虚拟化、以及通用的专业化。</li>
<li>LLVM IR 生成（LLVM IR Generation）：IR 生成阶段（在 lib/IRGen 中实现）将 SIL 弱化为 LLVM LR，此时 LLVM 可以继续优化并生成机器码。</li>
</ul>
<p>从原文的内容里，我们可以看到 Swift 编译器主要是在前端部分增加了一些环节，主要是在语义分析和中间代码生成的过程中增加了几个步骤：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879229_133989.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304879229_133989.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304879229_133989.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304879229_133989.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304879229_133989.png" alt="image.png"loading="lazy" decoding="async" width="1728" height="972" /></picture></figure></div><div class="blockquote"><blockquote><p>这一部分后续会继续完善，目前的水平和实践还只能到解释这么多</p>
</blockquote></div>
<p>今天的内容就先到这了，希望通过这篇文章你能对编译系统，编译流程和编译环节做的事情有了一个初步的概念！</p>
<h2>参考文献</h2>
<ul>
<li><a href="https://jkrvivian.com/blog/compiler/introduction-to-compiler-i/">噢！我的編譯器 – Introduction I</a></li>
<li><a href="https://jkrvivian.com/blog/compiler/introduction-to-compiler-ii/">噢！我的編譯器 – Introduction II</a></li>
<li><a href="https://time.geekbang.org/column/article/242479">编译原理实战课：01 | 编译的全过程都悄悄做了哪些事情？</a></li>
<li><a href="https://swift.org/swift-compiler/">Swift Compiler</a></li>
<li><a href="https://www.vadimbulavin.com/xcode-build-system/">Understanding Xcode Build System</a></li>
<li><a href="https://www.polidea.com/blog/how-to-build-swift-compiler-based-tool-the-step-by-step-guide/">How to Build Swift Compiler-Based Tool? The Step-by-Step Guide</a></li>
<li><a href="https://dmtopolog.com/code-optimization-for-swift-and-objective-c/">Compiler code optimization for Swift and Objective-C</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[编译到底是什么？]]></title><guid>https://swiftsiqi.com/posts/compiler-101</guid><link>https://swiftsiqi.com/posts/compiler-101</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 24 May 2020 10:42:09 +0000</pubDate><content:encoded><![CDATA[<p>因为工作原因，最近要做包管理工具方面的开发，需要对 Compiler 有一些最基本的理解，写这篇文章的目的有两个:</p>
<ul>
<li>为了记录和整理自己的近期的学习内容，方便日后查阅</li>
<li>抛开大段代码和抽象概念，通过通俗易懂的写作方式来加深自己对这些概念的理解</li>
</ul>
<p>废话不多说，我们一起看看内容吧！</p>
<h2>需要了解的概念</h2>
<p>在看了不少关于编译相关的文章之后，我发现下面的词汇是大量出现的。</p>
<p>知道这些词汇代表的意思，以及对应的层次，能够更好地看懂别人所要表达的意思。</p>
<h3>高级语言代码 High-Level Code</h3>
<p>高级语言代码，自然是指由高级编程语言编写代码，对计算机的细节有更高层次的抽象。</p>
<p>相对于低级编程语言（low-level programming language）更接近自然语言（人类的语言），集成一系列的自动工具（垃圾回收，内存管理等），会让程序员更快乐的编写出更简洁，更易读的程序代码。</p>
<h3>低级语言代码 Low-Level Code</h3>
<p>低级语言代码，指由低级编程语言编写的代码，相对高级语言，少了更多的抽象概念，更加接近于汇编或者机器指令。但是这也意味着代码的可移植性很差。</p>
<p>在我看来，高与低，只是一组相对词而已。越高级的语言，性能、自由度越不及低级语言。但是在抽象、可读可写性、可移植性越比低级语言优秀。
在以前的年代，C/C++语言相对汇编语言，机器指令来说，肯定是高级语言。</p>
<p>而到了今天，我们更多人对C语言偏向认知为「低级语言」。
或许未来世界的开发者，看我们现在所熟悉的Java、PHP、Python、ECMAScript等等，都是「low」到爆的语言。</p>
<h3>汇编语言 Assembly Language</h3>
<p>汇编语言作为一门低级语言，对应于计算机或者其他可编程的硬件。它和计算机的体系结构以及机器指令是强关联的。换句话说，就是不同的汇编语言代码对应特定的硬件，所以不用谈可移植性了。</p>
<p>相对于需要编译和解释的高级语言代码来说，汇编代码只需要翻译成机器码就可以执行了。所以汇编语言也往往被称作象征性机器码(symbolic machine code)</p>
<h3>字节码 Byte Code</h3>
<p>字节码严格来说不算是编程语言，而是高级编程语言为了种种需求（可移植性、可传输性、预编译等）而产生的中间码（Intermediate Code）。它是由一堆指令集组成的代码，例如在 javac 编译过后的 java 源码产生的就是字节码。</p>
<p>源码在编译的过程中，是需要进行「词法分析 → 语法分析 → 生成目标代码」等过程的，在预编译的过程中，就完成这部分工作，生成字节码。
然后在后面交由解释器（这里通常指编程语言的虚拟机）解释执行，省去前面预编译的开销。</p>
<h3>机器码 Machine Code</h3>
<p>机器码是一组可以直接被 CPU 执行的指令集，每一条指令都代表一个特定的任务，或者是加载，或者是跳转，亦或是计算操作等等。所有可以直接被 CPU 执行的程序，都是由这么一系列的指令组成的。</p>
<p>机器码可是看作是编译过程中，最低级的代码，因外再往下就是交由硬件来执行了。
当然机器码也是可以被编辑的，但是以人类难以看懂的姿势存在，可读性非常差。</p>
<h2>建立模糊的印象</h2>
<p>如果要用一种现实生活中的职业来形容编译器的作用，我想<strong>翻译官</strong>是一个不错的选择。不论是同声传译，还是各个节目或者动漫的专业字幕组，反正只要能够把 A 语言流畅的翻译成 B 语言的都算。</p>
<p>但翻译的工作并不是那么简单，需要理解某种语言的文字，语法才能进行，当然更专业的人还能使用精简的句子传达意境。总之，这里的 ”翻译“ 其实不仅仅是<strong>翻译</strong>，还要再经过<strong>编辑</strong>，这也就就是 “compile“ ，<strong>编译</strong>的意思。</p>
<h2>编译器 Compiler</h2>
<p>在有了一个模糊的印象后，我们在聚焦到 compiler 上，compiler 就是计算机编程语言里的翻译官，不同的 compiler 会编译成不同的语言，有可能是转换成机器语言（machine code）, byte code, 甚至是另外一种语言，如图：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879964_948173.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_403/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879964_948173.png" alt="image.png"loading="lazy" decoding="async" width="403" height="79" /></picture></figure></div><p>最终产出的 target program 是能够被直接执行的，所以程序的编译到执行应该是这样的：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879958_578218.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_403/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879958_578218.png" alt="image.png"loading="lazy" decoding="async" width="403" height="315" /></picture></figure></div><div class="blockquote"><blockquote><p>这种方式也叫做提前编译，Ahead-Of-Time Compilation（AOT），wiki 传送门：<a href="https://www.wikiwand.com/en/Compiler">点我</a></p>
</blockquote></div>
<h2>直译器 Interpreter</h2>
<p>还有另外一种语言处理的工具：直译器（Interpreter），相较于上图，compiler 是编译 source code 后产出可执行的代码，由使用者输入 input 后，再得到 output。而直译器是 source code 与 input 一起给出，直接编译并执行，产出 output，而使用直译器的语言有耳熟能详的 Python，它的架构如下：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879950_90275.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_222/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879950_90275.png" alt="image.png"loading="lazy" decoding="async" width="222" height="293" /></picture></figure></div><p>另外 compiler 与 interpreter 在速度上也有一定的差异，compiler 产生的 target program 执行的比 interpreter 快。但 interpreter 的纠错能力又比较好，因为它是一行行的检查与执行程序中的代码。</p>
<div class="blockquote"><blockquote><p>关于 Interpreter，有的翻译叫做直译器，有的叫做解释器，wiki 传送门：<a href="https://www.wikiwand.com/en/Interpreter_(computing">点我</a>)</p>
</blockquote></div>
<h2>编译器与直译器的异同</h2>
<h3>表现 Behavior</h3>
<ul>
<li>编译器把源代码转换成其他的更低级的代码(例如二进制码、机器码)，但是不会执行它。</li>
<li>直译器会读取源代码，并且直接生成指令让计算机硬件执行，不会输出另外一种代码。</li>
</ul>
<h3>性能 Performance</h3>
<ul>
<li>编译器会事先用比较多的时间把整个程序的源代码编译成另外一种代码，后者往往较前者更加接近机器码，所以执行的效率会更加高。时间是消耗在预编译的过程中。</li>
<li>直译器会一行一行的读取源代码，解释，然后立即执行。这中间往往使用相对简单的词法分析、语法分析，压缩解释的时间，最后生成机器码，交由硬件执行。直译器适合比较低级的语言。但是相对于预编译好的代码，效率往往会更低。如何减少解释的次数和复杂性，是提高直译器效率的难题。</li>
</ul>
<h2>Compilation + Interpretation</h2>
<p>再来，就势必要提及赫赫有名的 Java，为什么呢？Java 是一个结合 Compilation 和 Interpretation 的程序语言，这是什么意思呢？</p>
<p>就是 Java 会先编译成 byte code，接着再直译成机器码，这样的好吃是，Java 经历过一次编译，就可以通过虚拟机（Virtual machine）在不同的机器上直接执行。</p>
<p>沿用文章开始的翻译人员例子，byte code 就像是目前的通用国际语言 - 英语。只要将 A 语言翻译成英文，且 B 国人人能直接把英语翻译成自己的语言（当然前提是大家都会英文），此时，大家的交流就没有任何障碍了，整体的架构如下：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879940_947342.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_260/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879940_947342.png" alt="image.png"loading="lazy" decoding="async" width="260" height="507" /></picture></figure></div><div class="blockquote"><blockquote><p>这种方式也叫做即时编译，Just-In-Time Compilation（JIT），wiki 传送门：<a href="https://www.wikiwand.com/en/Just-in-time_compilation">点我</a></p>
</blockquote></div>
<h2>结合到实际</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304879934_135354.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_423/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304879934_135354.png" alt="image.png"loading="lazy" decoding="async" width="423" height="475" /></picture></figure></div><p>从左往右看，</p>
<ul>
<li>以 Java 为例，我们在文本编译器写好了 Java 代码，交由编译器编译成 Java Bytecode。然后 Bytecode 交由 JVM 来执行，这时候 JVM 充当了直译器的角色，在解释 Bytecode 成 Machine Code 的同时执行它，返回结果。</li>
<li>以 BASIC 语言（早期的可以由计算机直译的语言） 为例，通过文本编译器编写好，不用经历编译的过程，就可以直接交由操作系统内部来进行解释然后执行。</li>
<li>以 C 语言为例，我们在文本编译器编写好源代码，然后运行 <code>gcc hello.c</code> 编译出 <code>hello.out</code> 文件，该文件由一系列的机器指令组成的机器码，可以直接交由硬件来执行。</li>
</ul>
<h2>从抽象里看本质</h2>
<p>无论是编译 (Compiler)，还是直译 (Interpreter)，甚至是即时编译。
本质还是人与计算机的交流形式，人的语言最终转换成机器语言。</p>
<p>一句 Hello World，经过一些列的编译和直译，最终转换成一系列包含机器指令的那些 0 和 1，机器傻傻执行完之后，告诉你结果。</p>
<p>就这么一个过程，我们就需要很多的翻译官。
有些翻译官可以做到同声传译（直译），有些翻译官却只能把我们的意图记下来再全部翻译（编译）给计算机。</p>
<p>而往往一个翻译官能力有限，也只能把你的语言，翻译成另外一种低级点的语言，再由另外懂这个语言的翻译官来翻译更接近计算机能读得懂的语言。</p>
<h2>总结</h2>
<p>这篇文章从一些与编译相关的常见概念说起，通俗的描述了编译原理范畴内的编译器与直译器：</p>
<ul>
<li>编译 Compile：把整个程序源代码翻译成另外一种代码，然后等待被执行，发生在运行之前，产物是另一份代码。</li>
<li>直译 Interpret：把程序源代码一行一行的读懂然后执行，发生在运行时，产物是运行结果。</li>
</ul>
<p>同时我们还用一些常见的计算机编程语言作为例子，浅显的解释了它们的编译过程。</p>
<p>希望通过这篇文章，你能对编译在计算机领域里扮演的角色和功能形成一个清晰的认知。</p>
]]></content:encoded></item><item><title><![CDATA[Xcode Concept 学习笔记]]></title><guid>https://swiftsiqi.com/posts/xcode-concepts-summary</guid><link>https://swiftsiqi.com/posts/xcode-concepts-summary</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 14 May 2020 10:38:05 +0000</pubDate><content:encoded><![CDATA[<p>只怪自己当年学东西不够扎实，这次让我好好理解一下 Xcode 里的相关基础概念吧！</p>
<p>如果使用 Xcode 进行开发，我们常常会与这么几个概念打交道：Workspace，Project，Target，Scheme 和 Build Setting。</p>
<p>官方对这些概念的解释可以参考这篇文档 - <a href="https://developer.apple.com/library/archive/featuredarticles/XcodeConcepts/Concept-Projects.html">Xcode Concepts</a>。</p>
<p>虽然文档本身已经被列为 Archived 的状态，但大毛病没有，还是可以拿来再学习一遍，所以后面的内容将围绕它展开。</p>
<p>另外在学习和调研的过程中，我发现了一个蛮有价值的博客，地址是 <a href="https://pewpewthespells.com/">pewpewthespells.com</a>，里面有不少关于 Xcode 的文章，估计是个做 CI/CD 工作的妹子，下面的几篇文章可以当做索引手册收藏起来：</p>
<ul>
<li><a href="https://pewpewthespells.com/blog/xcconfig_guide.html">The Unofficial Guide to xcconfig files</a></li>
<li><a href="https://pewpewthespells.com/blog/managing_xcode.html">Managing Xcode</a></li>
<li><a href="https://pewpewthespells.com/blog/buildsettings.html">Xcode Build Settings Reference</a></li>
</ul>
<h2>概述</h2>
<ul>
<li><strong>Target</strong> -一个 target 代表一个产品</li>
<li><strong>Project</strong> - 对应 <code>.xcodeproj</code> 类型的文件，project 包含了构建产品所需的源文件，一个 project 可以有多个 target</li>
<li><strong>Workspace</strong> - 对应 <code>.xcworkspace</code> 文件，用来组织管理 project 和其他文档的，workspace 可以包含多个 project，project 可以属于多个 workspace</li>
<li><strong>Scheme</strong> - scheme 决定了哪个 target 去运行，它可以针对编译，运行，测试，打包等进行配置</li>
<li><strong>Build Setting</strong> - build settings 就是构建产品时的一些设置，target 可以覆盖 project 一些相同的设置</li>
</ul>
<h2>Xcode Target</h2>
<p>一个 Target 确定一个产物（product）的构建，包括一些指令（instructions），例如怎么从一个 Project 或者 Workspace 的一堆文件导出一个产物。简单来说，一个 Target 就定义了一个产物，一个 Target 对应一个 Product，它管理着一个产物的 Build System 的“输入”（一堆源文件和一些处理这些源文件的 Instruction）。 Projects 可以包含一个或者多个 Target，它们代表不同的产物，例如：如果你的产物需要做 Lite 和 Pro 版本，那么你可以考虑采取两个 Target 来处理。</p>
<p>构建一个产物的 Instructions（指令）的表现形式是构建设置(Build Settings)，构建规则(Buidling Rules)和构建参数(Build Phases)，这些都可以在 Xcode 的 Project Editor 中调整。一个 Target 的 Build Settings 继承 Project 的 Build Settings，但是可以重写覆盖 Project Settings。同时间内只能有一个 Active Target，Xcode Scheme 能够指定 Active Target。</p>
<p>一个 Target 可以跟其他 Target 相关联。如果一个 Target 在构建的时候需要另外一个 Target 的输出，我们说前者依赖于后者。</p>
<p>如果两个 Target 在相同的 Workspace 里，Xcode 能够发现它们的依赖关系，它能够以需要的顺序构建产品。这样的关系可以被称为隐形从属依赖(Implicit Dependency)。当然你也可以在 Build Setting 里为两个 Targets 指明显示依赖关系(Explicit Dependency)。</p>
<p>例如：在同一个 workspace 中，可以构建一个 library 和一个链接这个 library 的 application。Xcode 可以发现这种依赖关系，并首先自动构建 library。但是，如果想链接某个版本的 library，就需要在 build settings 明确依赖关系，该依赖项会覆盖隐式依赖项。</p>
<h2>Xcode Project</h2>
<p>Xcode Project 是个构建一个或者多个产物所需要的文件，资源，信息等的存储库（repository）。Project 包含用于构建产物的所有元素，并且管理这些元素间的关系。它包含一个或多个 Target，指定怎样去构建产品。Project 在工程里面默认的为所有的 Target 指定 Build Settings，当然每个 Target 可以覆盖 Project 的 Build Settings，去指定自己特有的 Build Settings。</p>
<p>一个 Xcode project 包含下面的信息：</p>
<ul>
<li>源文件的引用：<ul>
<li>源码，包括头文件和实现文件</li>
<li>Libraries and Frameworks</li>
<li>资源文件(plist等)</li>
<li>图片文件</li>
<li>nib 文件(xib, stroyboard等)</li>
</ul>
</li>
<li>用于在结构导航器（ structure navigator）中组织源文件（source files）的组（groups），这里又分物理文件和引用文件</li>
<li>Project 级别的 Build Configurations. 你可以为 Project 指定多个 Build Configuration，例如，Xcode 就默认为我们指定了 Debug 和 Release 的 Build settings，当然你也可以自定义。</li>
<li>Targets，每个 Target 会指定：<ul>
<li>通过 Project 构建的一个产物的引用</li>
<li>构建该产物所需的资源文件的引用</li>
<li>用于构建该产物的构建配置（Build configurations），包括对其他 Targets 和 Settings 的依赖；如果 Targets 的 build configurations 没有配置时，使用 Project 级别的 Build Configurations</li>
</ul>
</li>
<li>用来 Debug 和 Test 程序的可执行环境（Executable Environment），包括：<ul>
<li>从 Xcode run 或 debug 时启动的可执行文件</li>
<li>要传递给可执行文件的命令行参数</li>
<li>程序 run 时要设置的环境变量</li>
</ul>
</li>
</ul>
<p>A project 可以单独存在，也可以被包含在 workspace 里面(cocoapods 就是被包含在 workspace 里面)。</p>
<h2>Xcode Workspace</h2>
<p>一个 Workspace 是一个 Xcode 文档，组合不同的 Project、文档，所以你可以同时管理多个 Project。一个 Workspace 可以包含任意数量的 Xcode projects 和其他文件。除了组织每个 Xcode Projects 中的所有文件外，Workspace 还维护 projects 与他们各自 Targets 之间的隐式/显示关联。</p>
<h3>Workspace 扩展 workflows 的范围</h3>
<p>一个工程文件（Project File）包含指向 project 中所有文件的指针，Build Configurations 和 Project 的其他信息。在 Xcode 3 之前，Projects 之前关联是很复杂的事情，大多数工作流仅限于单个 Project。从 Xcode 4 之后，你可以创建一个 Workspace 去包含多个 Projects 和其他文件。</p>
<p>除了提供被包含在 Xcode Project 中的所有文件的访问外，Workspace 还拓展许多重要的 Xcode Workflows 的范围。例如，由于 indexing（文件索引）遍布整个 Workspace，所以，在 workspace 中， code completion、Jump to Definition 和所有其他的内容感知特性，可以在所有 Projects 中无缝衔接运作。因为 refactoring operations（重构操作）横跨整个 Workspace 的所有内容，所以，你可以在一个 framework project 中重构 API，并且在其他 application projects 中使用这个 framework。构建时，一个 project 可以利用 workspace 中其他 projects 的 products。</p>
<p>workspace 文档包含被囊括的 projects 和其他文件，不再有其他数据。一个 project 可以被多个 workspace 持有。下图展示一个 workspace 包含两个 Xcode projects 以及一个文档 project。</p>
<h3>Workspaces 中的 Projects 共享 Build Directory</h3>
<p>默认情况下，Workspace 下面的 projects 都是在同一个目录下构建的，也就是 Workspace 的编译目录(workspace build directory)。由于是在同一个目录下面，Project 的资源文件都彼此都是可见的，可互相引用的。所以，如果你有多个 Projects 使用相同库的时候，不需要将它分别拷贝到各个 Project 中。</p>
<p>Xcode 会在编译目录下检查文件发现它们的隐形从属依赖。例如，如果 Workspace 中的一个 Project 编译的时候需要链接到相同 Workspace 的其他 Project 某个库，Xcode 会自动帮你先编译那个库，即使构建配置没有显式的指定从属依赖关系。如果需要的话，你可以指定显式从属依赖，但是你必须创建 Project 引用。</p>
<p>Workspace 中的每个 Project 仍然有属于它们自己的独立的标识。你能通过 project 的打开方式控制 project 受不受其他 projects 的影响，例如单独打开 Project 而不是通过 Workspace。因为，一个 Project 可以属于多个 Workspace，你可以任意组合 Projects，而无需重新配置 projects 或者 workspaces。</p>
<p>你可以使用 workspace 默认的 build directory，也可以自己指定一个。注意：如果一个 project 指定一个 build directory，这个 build directory 会覆盖全部所在的 workspace 里的默认 build directory。</p>
<h2>Xcode Scheme</h2>
<p>一个 Xcode Scheme（方案）定义三样东西：一个要生成的目标（targets to build）的集合、building 时使用的配置（configuration）、以及要执行的测试集合。</p>
<p>你可以拥有任意数量的 scheme，但一次只能有一个是活跃状态（active），你可以指定 scheme 是否储存在 project 中（这种方案下，scheme 在每一个包含这个 project 的 workspace 中都可用），或者储存在 workspace 中（仅在当前 workspace 中可用）。选择要激活的 scheme 时，可以选择运行目标（设备）。</p>
<h2>Build Setting</h2>
<p>一个 Build Setting 是一个变量，包含着如何构建产物的信息。例如，可以指定 Xcode 传递给编译器的选项</p>
<p>Build Settings 有 Project 和 Target 两个级别，Project 级别中的 Build Setting 适用项目中所有的 targets，只要该项 setting 没有被 Target 级别的重写覆盖。</p>
<p>每个 Target 管理着创建一个产物的源文件，一个 build configuration 指定一组 build settings，用于以特定的方式构建一个 product。例如，通常有 debug 和 release 俩种分开的 build configurations。</p>
<p>一个 Build Setting 包含两个部分：Setting Title（标题） 和 Definition（定义），类似于 key-value 结构。前者标示该 Build Setting 的名称，后者是一个常量或一个表达式，用于确定 Build Setting 的值。</p>
<p>另外，当你通过 Project 模板新建一个 Project 时，Xcode 会生成一个默认的 Build Settings，你也可以为 Project 或者某个 Target 创建自定义的 Build settings。你还可以设定 Conditional Build Settings，一个 Conditional Build setting 的值取决于是否满足一个或多个先决条件。这个机制也可以被用在指定用于基于目标架构构建产品的SDK。</p>
<h2>参考资料</h2>
<ol>
<li><a href="https://stackoverflow.com/questions/20637435/xcode-what-is-a-target-and-scheme-in-plain-language/20637892#20637892">Xcode: What is a target and scheme in plain language?</a></li>
<li><a href="https://stackoverflow.com/questions/21631313/xcode-project-vs-xcode-workspace-differences">Xcode Project vs. Xcode Workspace - Differences</a></li>
<li><a href="https://joakimliu.github.io/2016/09/24/Xcode-Concepts/">Xcode 相关概念</a></li>
<li><a href="http://www.liugangqiang.com/2018/03/22/iOS%E9%A1%B9%E7%9B%AEProject%E5%92%8CTarget%E9%85%8D%E7%BD%AE%E8%AF%A6%E8%A7%A3/">iOS项目Project和Target配置详解</a></li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Swift 2020 生态调研报告]]></title><guid>https://swiftsiqi.com/posts/Swift-in-China-2020</guid><link>https://swiftsiqi.com/posts/Swift-in-China-2020</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 30 Apr 2020 04:56:03 +0000</pubDate><content:encoded><![CDATA[<p>这是一次调研性质的文章，通过它让我们来看看 Swift 在国内的现状到底是什么样子的？</p>
<h2>Swift 的发展历程</h2>
<h3>概述</h3>
<p>通过官网的 <a href="https://docs.swift.org/swift-book/RevisionHistory/RevisionHistory.html">Document Revision History</a> 和 <a href="https://developer.apple.com/documentation/xcode_release_notes">Xcode Release Notes</a> 梳理了一下 Swift 的发展历程和重大事件。</p>
<div class="block-table"><table><thead>
<tr>
  <th>Swift 版本</th>
  <th>Xcode 版本</th>
  <th>发布时间</th>
  <th>重大事件</th>
</tr>
</thead>
<tbody>
<tr>
  <td>Swift 1.0 ~ 1.2</td>
  <td>6.x</td>
  <td>2014</td>
  <td>语⾔发布</td>
</tr>
<tr>
  <td>Swift 2.0 ~ 2.2.1</td>
  <td>7.x</td>
  <td>2015</td>
  <td>对协议，泛型能力进一步扩展，开始支持 Linux，随后出现了以 Swift 语言为核心的后端框架 Perfect，Vapor，Kitura</td>
</tr>
<tr>
  <td>Swift 3.0 ~ 3.3.1</td>
  <td>8.x</td>
  <td>2016</td>
  <td>发布了 Swift Package Manager，同时以 GCD，Core Graphics 为代表的基础库 API 风格发生了大幅度转变，摆脱了 Objective-C 时代的烙印</td>
</tr>
<tr>
  <td>Swift 4.0 ~ 4.1.3</td>
  <td>9.x</td>
  <td>2017</td>
  <td>在整体的语法，使用和理念上基本定型，提出了 Codable 协议，同时 Xcode 的 Swift Syntax Mirgration 的最低版本固定为 4</td>
</tr>
<tr>
  <td>Swift 4.2 ~ 4.2.4</td>
  <td>10.x</td>
  <td>2018</td>
  <td>Swift社区从邮件列表转向论坛，语言小幅升级，主要是功能完善，性能提升，同年 Swift for TensorFlow 发布并开源</td>
</tr>
<tr>
  <td>Swift 5.0 ~ 5.0.3</td>
  <td>10.2.x</td>
  <td>2019</td>
  <td>ABI 稳定，iOS 12 开始内置 Swift 运行时</td>
</tr>
<tr>
  <td>Swift 5.1 ~ 5.2</td>
  <td>11.x</td>
  <td>2020</td>
  <td>新增 Property Wrapper ，Opaque Type 等新的语法功能，同年 WWDC 上，Apple 发布了 SwiftUI，Combine，Catalyst 等 Swift 语言的专属 SDK</td>
</tr>
<tr>
  <td>Swift 5.3</td>
  <td></td>
  <td></td>
  <td>质量和性能增强，增加对 Windows 和其他 Linux 发行版的支持。</td>
</tr>
</tbody>
</table></div><p>结合着自己的 Swift 学习经历，不难发现：</p>
<p>在 Swift 4 之前，由于语言整体还没定型，确实存在着发一个新版本，学一门新语言的情况，但在 Swift 4 之后，Swift 变化变得收敛了许多，不过也出现了入门容易，精通难的情况，毕竟光 Swift 的语法糖数量就快赶上了 C++ 了。</p>
<h3>语言排行榜</h3>
<p>Swift 语言从诞生之日开始，就一直存在各种各样的争议：一方面的焦点在于 Swift 的应用领域还是集中在 Apple 生态下，让人觉得不够大气，毕竟新时代的语言就是要全能，另一方面的焦点就是 Swift 语言的变化太快，每个版本都是是全新的感觉，这让开发者意识到，东西虽好，但代码还是要一个个的自己改的。</p>
<p>不过随着时间的推移，Swift 在后端，人工智能，物联网上的解决方案和应用场景不断出现，它已经远远不在是一个只能在 Apple 生态下运行的语言，关于这个非常推荐看看 Onevcat 在 GMTC 2019 上的分享：<a href="https://gmtc.infoq.cn/2019/beijing/presentation/1802">在分歧中发展——2019，我们能用 Swift 做什么</a></p>
<p>ABI 在 Swift 5.0 的时候也终于稳定了，虽然 ABI 稳定是使用 binary 发布框架的必要非充分条件，但都 ABI了，module stability 也不会太远了，这些信号都让开发者的信心不断增强。</p>
<p>为了验证这一点，我们也可以从编程语言排行榜 <a href="https://www.tiobe.com/tiobe-index/">TIOBE</a> 和 <a href="http://pypl.github.io/PYPL.html">PYPL</a> 里看出一些端倪。</p>
<p>下图为 TIOBE 2020 年 四月排行榜，Swift 的排名在 11 名，Objective-C 的排名在 17 名，份额差距在 0.6% 左右：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900933_481042.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900933_481042.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900933_481042.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900933_481042.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900933_481042.png" alt="image.png"loading="lazy" decoding="async" width="1910" height="1528" /></picture></figure></div><p>下图为 PYPL 2020 年 四月排行榜，Swift 的排名在 9 名，Objective-C 的排名在 8 名，份额差距在 0.17% 左右：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900925_147135.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1106/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304900925_147135.png" alt="image.png"loading="lazy" decoding="async" width="1106" height="1648" /></picture></figure></div><p>不论哪种排名，我们应该都可以得出这样一个结论：相比于 Objective-C 的下降趋势，Swift 的未来会更让人期待。</p>
<h2>社区活跃度</h2>
<p>观察一个编程语言活跃度的最好地方就是 Github，通过 Pull Requests ，Issues，Pushes，Stars 的情况，我们就可以大致了解到它的情况。</p>
<p>恰好 <a href="https://madnight.github.io/githut/#/stars/2020/1">Githut</a> 提供了这样的能力，通过观察 2020 年第一季度的数据，我们可以清楚的观察到 Swift ，Objective-C 等语言在社区的活跃度。</p>
<h3>整体趋势图</h3>
<p>下面四张图的 Y 轴分别代表了 Pull Requests ，Issues，Pushes，Stars 的数量，蓝色的线代表 Objective-C ，浅橙色的线代表 Swift，深橙色的线代表 Kotlin。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900915_252998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900915_252998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900915_252998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900915_252998.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900915_252998.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="769" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900905_927462.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900905_927462.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900905_927462.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900905_927462.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900905_927462.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="769" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900899_792088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900899_792088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900899_792088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900899_792088.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900899_792088.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="769" /></picture></figure></div><div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900893_371304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900893_371304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900893_371304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900893_371304.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900893_371304.png" alt="image.png"loading="lazy" decoding="async" width="1920" height="769" /></picture></figure></div><p>我们可以发现，从 2016 年开始，Swift 的数据已经超过了 Objective-C，现在有一种取而代之的快速发展趋势，这说明社区中更多的开发者习惯把精力放在 Swift 上，而不是放在 Objective-C 上。</p>
<p>这就给相应的开发者一个提醒，如果你坚持使用 Objective-C，那么你可能会面临一个风险，你所依赖的第三方开发库，他们的原作者很有可能已经不愿意去维护他们了。</p>
<p>同时，我们也可以发现 Swift 和 Kotlin 作为端上的新语言，它们的发展趋势是稳步上升的，这代表它们在社区是受欢迎的，那这也意味着掌握这些新技术会更容易与其他程序员进行交流和沟通，至于未来能不能完全取代现有语言的份额，还需要时间来证明。</p>
<h3>个体情况对比</h3>
<p>如果关注过一些 iOS 相关的 Newsletters，我们应该都可以感受到，相比于 Swift 的第三方开源库，同类型的 Objective-C 开源库明显处于劣势，不论是新增数量，还是活跃度；甚至有些 Objective-C 的库已经被开发者废弃，转而使用 Swift 重构。</p>
<p>下面是两种语言的两个常见方向的第三方开源库的数据对比，我们明显可以看出 Objective-C 项目的活力在下降，甚至出现了 2 年没有更新的状态。</p>
<div class="block-table"><table><thead>
<tr>
  <th>名称</th>
  <th>Massonry</th>
  <th>SnapKit</th>
  <th>Pop</th>
  <th>Hero</th>
</tr>
</thead>
<tbody>
<tr>
  <td>语言</td>
  <td>Objective-C</td>
  <td>Swift</td>
  <td>Objective-C</td>
  <td>Swift</td>
</tr>
<tr>
  <td>Stars</td>
  <td>17.7K</td>
  <td>16.3K</td>
  <td>19.8K</td>
  <td>18.3K</td>
</tr>
<tr>
  <td>Opening issue</td>
  <td>108</td>
  <td>64</td>
  <td>44</td>
  <td>167</td>
</tr>
<tr>
  <td>Opening Pull Request</td>
  <td>19</td>
  <td>16</td>
  <td>14</td>
  <td>14</td>
</tr>
<tr>
  <td>Latest Commit</td>
  <td>2017.09.28</td>
  <td>2019.11.27</td>
  <td>2018.10.12</td>
  <td>2020.04.26</td>
</tr>
<tr>
  <td>Latest release</td>
  <td>2017.09</td>
  <td>2019.08</td>
  <td>2018.10.12</td>
  <td>2019.10.29</td>
</tr>
</tbody>
</table></div><p>对于那些还在持续更新的 Objective-C 库，它的更新频率又是什么样子的呢？我们拿 AFNetworking 和 Alamofire 做一个比对：</p>
<div class="block-table"><table><thead>
<tr>
  <th>AFNetworking 版本</th>
  <th>时间点</th>
  <th>间隔时间</th>
  <th>Alamofire 版本</th>
  <th>时间点</th>
  <th>间隔时间</th>
</tr>
</thead>
<tbody>
<tr>
  <td>4.0.1</td>
  <td>2020.04.21</td>
  <td>1 个月</td>
  <td>5.1.0</td>
  <td>2020.04.05</td>
  <td>0.5 个月</td>
</tr>
<tr>
  <td>4.0.0</td>
  <td>2020.03.31</td>
  <td>12 个月</td>
  <td>5.0.5</td>
  <td>2020.03.24</td>
  <td>0.2 个月</td>
</tr>
<tr>
  <td>2.7.0</td>
  <td>2019.02.13</td>
  <td>7 个月</td>
  <td>5.0.4</td>
  <td>2020.03.16</td>
  <td>0.1 个月</td>
</tr>
<tr>
  <td>3.2.1</td>
  <td>2018.05.05</td>
  <td>5 个月</td>
  <td>5.0.3</td>
  <td>2020.03.15</td>
  <td>0.5 个月</td>
</tr>
<tr>
  <td>3.2.0</td>
  <td>2017.12.16</td>
  <td>25 个月</td>
  <td>5.0.2</td>
  <td>2020.02.24</td>
  <td>0.1 个月</td>
</tr>
<tr>
  <td>2.6.3</td>
  <td>2015.11.11</td>
  <td></td>
  <td>5.0.1</td>
  <td>2020.02.23</td>
  <td></td>
</tr>
</tbody>
</table></div><p>虽然 AFNetworking 还在更新，但其更新的周期实在是让人摸不着头脑，相比于 Alamofire 稳定的更新频率，你到底愿意用哪个呢？</p>
<p>举个实际的例子，比如大家都是做 HTTP 请求的，3.0 协议已经发出，从社区的反馈来看，未来的开发者更愿意把精力放在 Swift 上面，现在 SSL Certificate Verify 的验证，是可以把证书链从上至下全部验证一遍，这在 Alamofire 里已经支持的非常好，而 AFNetworking 在此领域目前是缺失的。</p>
<p>这就有可能在未来的某个时间点，工程所依赖的，第三方的，不可替换的，核心的开发框架有严重的问题或者功能缺失时，项目的进度会受影响，作为开发者会陷入进退两难的状况。</p>
<h2>Apple 的 SDK</h2>
<p>在 Apple 的 <a href="https://developer.apple.com/documentation">Apple Developer Documentation列表</a> 中，我们发现共有 196 个条目，其中 Swift 独占的 SDK 共计 10 个，Objective-C 独占的 SDK 共计 14 个。</p>
<div class="block-table"><table><thead>
<tr>
  <th>维度</th>
  <th>个数</th>
  <th>SDK名称</th>
</tr>
</thead>
<tbody>
<tr>
  <td>Swift 独占</td>
  <td>10</td>
  <td>Swift(Swift Standard Library)，Combine，SwiftUI，RealityKit，CareKit，Create ML(Create ML， Create MLUI)，Playground Support，PlaygroundBluetooth，Apple CryptoKit，Swift Packages(Swift Package Manager)</td>
</tr>
<tr>
  <td>Objective-C 独占</td>
  <td>14</td>
  <td>QTKit (macOS 专属)，Professional Video Applications(FxPlug，macOS 专属)，xcselect （macOS 专属），DarwinNotify，DriverKit（macOS 专属），EndpointSecurity（macOS 专属），HIDDriverKit（macOS 专属），IOUSBHost（macOS 专属），Kernel（macOS 专属），NetworkingDriverKit（硬件驱动相关），PCIDriverKit（硬件驱动相关），SerialDriverKit（硬件驱动相关），USBDriverKit（硬件驱动相关），USBSerialDriverKit（硬件驱动相关）</td>
</tr>
</tbody>
</table></div><p>虽然乍一看 Objective-C 独占的 SDK 较多，但这些 SDK 主要是面向 macOS 和硬件驱动方向的，对 iOS 开发本身没有任何影响。</p>
<p>而 Swift 独占的 SDK 就完全不一样了， Apple 这些年主推的技术方向，例如 AR，AI，Health，SwiftUI，Combine 等一系列 SDK 都只有 Swift 的版本了，这无疑在暗示开发者：来吧，用就用 Swift 吧！</p>
<h2>Swift 在国内外 iOS 客户端的使用现状</h2>
<p>说了这么多，Swift 在 iOS 生态里到底用的怎么样？最直接的办法就是看看它在 Apple Store 里的占有率，这里我们会考察 2019 和 2020 年的情况。</p>
<h3>2019 年</h3>
<p>很庆幸，这部分工作，淘宝团队已经做过了！</p>
<p>相关的内容在 <a href="https://mp.weixin.qq.com/s/oHGkoGzhMs-l8TX6t0831w">从探索到落地，手淘引入 Swift “历险记” 2019</a> 一文中已经披露，这里借用它们原话：</p>
<div class="blockquote"><blockquote><p>我们通过爬虫分析国内外 APP Store 排行榜 Top 1000 的APP，通过文件扫描分析得到结论。</p>
<ul>
<li>国内使用 Swift 的 APP 约占比 22%，美区使用 Swift 的 APP 约占比 78%，其中美区剩余没有使用 Swift 的 APP 大部分来自中国地区本地化的产品，如抖音，快手等，可以得出一个结论，国内还是小众的 Swift，在国外已经是现状。</li>
<li>Github/Stack Over Flow 社区等 Objective-C 开源库和问题提问已经基本停滞，未来我们在落地新技术，Objective-C 可能已经是最坏的打算，加之 WWDC 17年以来，苹果不再提供 Objective-C 的示例，组内同学也多次遇见 Objective-C Bug 去社区提问，毫无热度的情况。</li>
<li>苹果在 WWDC19 年发布了 4 个 Pure Swift 框架，无法简单的被 Objective-C 混编。</li>
</ul>
<p>未来我们极有可能因为苹果的强制推进风格和社区文化的落后产生技术踏空，无法迅速响应业务，甚至无法招聘到会使用 Objective-C 的工程师。</p>
</blockquote></div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900875_159588.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1516/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304900875_159588.png" alt="image.png"loading="lazy" decoding="async" width="1516" height="556" /></picture></figure></div><p>通过了解，淘宝的数据来源是七麦数据提供的，日期为 2019 年 02 月 19 日，<a href="https://www.qimai.cn/rank/index/brand/free/device/iphone/country/cn/genre/5000/date/2019-02-19">国内排行榜传送门2019版</a>和<a href="https://www.qimai.cn/rank/index/brand/free/device/iphone/country/us/genre/5000/date/2019-02-19">国外排行榜传送门2019 版</a></p>
<h3>2020 年</h3>
<p>现在回到 2020 年，Swift 在国内外的应用情况又发生了什么变化呢？</p>
<p>为了得到这个问题的答案，我也打算对国内外免费榜里的 APP 进行扫描，先不说 1000 个 ipa 的下载工作量有多大，256G 的硬盘估计也装不下这么多 APP，索性就弄前 102 名吧！</p>
<div class="blockquote"><blockquote><p>本来是想做前 100 的，只是因为当时下载国内 ipa 的时候多下载了 2 个，索性本着样本越多越接近真实情况的道理就把国外的数据量也放到了 102 个，并没有什么特别原因。</p>
</blockquote></div>
<p>扫描的原理借鉴了 <a href="https://mp.weixin.qq.com/s/vF_oOWFLimlyRi4mZpgpeQ">如何检测 iOS 应用程序是否使用 Swift？</a> 里的思路，相应的工具已经放在 Github 供大家使用，点击<a href="https://github.com/ZRTransmitter/SwiftAppAnalyzer">传送门</a>即可，这里要感谢一下好基友 <a href="https://github.com/ForelaxX">@ForelaxX</a> 制作了这个脚本工具。</p>
<p>App 排行榜的数据来源是七麦数据提供的，日期为 2020 年 4 月 27 日，<a href="https://www.qimai.cn/rank/index/brand/all/device/iphone/country/cn/genre/5000/date/2020-04-27">国内排行榜传送门2020版</a>和<a href="https://www.qimai.cn/rank/index/brand/all/genre/5000/device/iphone/country/us/date/2020-04-27">国外排行榜传送门2020版</a></p>
<p>通过扫描这 102 个 App，最终发现国内的 Swift 占比为 30.4%（31/102），国外的 Swift 占比 82.3%（84/102），相比于 2019 年的数据，国内的 Swift 应用增长了 10% 左右，国外的 Swift 应用增长了 5% 左右。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900825_47601.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1516/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304900825_47601.png" alt="image.png"loading="lazy" decoding="async" width="1516" height="556" /></picture></figure></div><p>下图是结合 2019 年和 2020 年的百分比趋势变化图</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304900814_720191.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304900814_720191.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304900814_720191.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304900814_720191.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304900814_720191.png" alt="image.png"loading="lazy" decoding="async" width="1684" height="1146" /></picture></figure></div><p>下面是扫描的详细结果：</p>
<div class="block-table"><table><thead>
<tr>
  <th>国内 App 版本</th>
  <th>是否使用 Swift</th>
  <th>国外 App 名称</th>
  <th>是否使用 Swift</th>
</tr>
</thead>
<tbody>
<tr>
  <td>拼多多</td>
  <td>NO</td>
  <td>Zoom</td>
  <td>NO</td>
</tr>
<tr>
  <td>腾讯会议</td>
  <td>NO</td>
  <td>TikTok</td>
  <td>NO</td>
</tr>
<tr>
  <td>钉钉</td>
  <td>NO</td>
  <td>Houseparty</td>
  <td>YES</td>
</tr>
<tr>
  <td>个人所得税</td>
  <td>NO</td>
  <td>Youtube</td>
  <td>NO</td>
</tr>
<tr>
  <td>剪映</td>
  <td>YES</td>
  <td>Instagram</td>
  <td>YES</td>
</tr>
<tr>
  <td>交管 12123</td>
  <td>NO</td>
  <td>Facebook</td>
  <td>NO</td>
</tr>
<tr>
  <td>抖音</td>
  <td>NO</td>
  <td>Messenger</td>
  <td>NO</td>
</tr>
<tr>
  <td>微信</td>
  <td>YES</td>
  <td>Amazon</td>
  <td>NO</td>
</tr>
<tr>
  <td>微视</td>
  <td>YES</td>
  <td>Cash App</td>
  <td>YES</td>
</tr>
<tr>
  <td>支付宝</td>
  <td>NO</td>
  <td>DoorDash</td>
  <td>YES</td>
</tr>
<tr>
  <td>QQ</td>
  <td>NO</td>
  <td>American Idol</td>
  <td>YES</td>
</tr>
<tr>
  <td>快手极速版</td>
  <td>NO</td>
  <td>Netflix</td>
  <td>YES</td>
</tr>
<tr>
  <td>手机淘宝</td>
  <td>YES</td>
  <td>Snapchat</td>
  <td>YES</td>
</tr>
<tr>
  <td>淘宝特价版</td>
  <td>NO</td>
  <td>PREQUEL</td>
  <td>YES</td>
</tr>
<tr>
  <td>企业微信</td>
  <td>NO</td>
  <td>Gmail</td>
  <td>YES</td>
</tr>
<tr>
  <td>快手</td>
  <td>NO</td>
  <td>Wish</td>
  <td>YES</td>
</tr>
<tr>
  <td>和平营地</td>
  <td>NO</td>
  <td>Hulu</td>
  <td>YES</td>
</tr>
<tr>
  <td>QQ 音乐</td>
  <td>NO</td>
  <td>Disney+</td>
  <td>YES</td>
</tr>
<tr>
  <td>百度</td>
  <td>YES</td>
  <td>Shop</td>
  <td>YES</td>
</tr>
<tr>
  <td>闲鱼</td>
  <td>NO</td>
  <td>Pinterest</td>
  <td>NO</td>
</tr>
<tr>
  <td>网易云音乐</td>
  <td>NO</td>
  <td>Amazon Prime Video</td>
  <td>YES</td>
</tr>
<tr>
  <td>腾讯视频</td>
  <td>NO</td>
  <td>PayPal</td>
  <td>YES</td>
</tr>
<tr>
  <td>高德地图</td>
  <td>NO</td>
  <td>Spotify</td>
  <td>YES</td>
</tr>
<tr>
  <td>酷狗音乐</td>
  <td>NO</td>
  <td>Google Duo</td>
  <td>YES</td>
</tr>
<tr>
  <td>小红书</td>
  <td>YES</td>
  <td>Discord</td>
  <td>YES</td>
</tr>
<tr>
  <td>美团</td>
  <td>NO</td>
  <td>Venmo</td>
  <td>YES</td>
</tr>
<tr>
  <td>京东</td>
  <td>NO</td>
  <td>Wayfair</td>
  <td>YES</td>
</tr>
<tr>
  <td>夸克</td>
  <td>NO</td>
  <td>Walmart</td>
  <td>YES</td>
</tr>
<tr>
  <td>哔哩哔哩</td>
  <td>NO</td>
  <td>Twitter</td>
  <td>YES</td>
</tr>
<tr>
  <td>瑞幸咖啡</td>
  <td>YES</td>
  <td>WhatsApp</td>
  <td>YES</td>
</tr>
<tr>
  <td>爱奇艺</td>
  <td>NO</td>
  <td>Twitch</td>
  <td>YES</td>
</tr>
<tr>
  <td>BOSS 直聘</td>
  <td>NO</td>
  <td>SHEIN</td>
  <td>YES</td>
</tr>
<tr>
  <td>云闪付</td>
  <td>NO</td>
  <td>Google Chrome</td>
  <td>NO</td>
</tr>
<tr>
  <td>百度网盘</td>
  <td>YES</td>
  <td>Roku</td>
  <td>YES</td>
</tr>
<tr>
  <td>中国建设银行</td>
  <td>NO</td>
  <td>intoLive</td>
  <td>YES</td>
</tr>
<tr>
  <td>阿里巴巴</td>
  <td>NO</td>
  <td>Nike</td>
  <td>YES</td>
</tr>
<tr>
  <td>WPS Office</td>
  <td>NO</td>
  <td>PicsArt</td>
  <td>YES</td>
</tr>
<tr>
  <td>58 同城</td>
  <td>NO</td>
  <td>Google</td>
  <td>YES</td>
</tr>
<tr>
  <td>Keep</td>
  <td>YES</td>
  <td>Calm</td>
  <td>YES</td>
</tr>
<tr>
  <td>微博</td>
  <td>NO</td>
  <td>eBay</td>
  <td>YES</td>
</tr>
<tr>
  <td>闽政通</td>
  <td>YES</td>
  <td>Norton Secure</td>
  <td>YES</td>
</tr>
<tr>
  <td>QQ 浏览器</td>
  <td>NO</td>
  <td>Airtime</td>
  <td>YES</td>
</tr>
<tr>
  <td>中国工商银行</td>
  <td>YES</td>
  <td>YEE</td>
  <td>YES</td>
</tr>
<tr>
  <td>美图秀秀</td>
  <td>YES</td>
  <td>Omegle</td>
  <td>YES</td>
</tr>
<tr>
  <td>安居客</td>
  <td>NO</td>
  <td>Google Driver</td>
  <td>YES</td>
</tr>
<tr>
  <td>七猫小说</td>
  <td>YES</td>
  <td>Tinder</td>
  <td>YES</td>
</tr>
<tr>
  <td>贝壳找房</td>
  <td>NO</td>
  <td>Target</td>
  <td>YES</td>
</tr>
<tr>
  <td>农行掌上银行</td>
  <td>NO</td>
  <td>Google Map</td>
  <td>NO</td>
</tr>
<tr>
  <td>京东金融</td>
  <td>NO</td>
  <td>OfferUp</td>
  <td>YES</td>
</tr>
<tr>
  <td>轻颜相机</td>
  <td>YES</td>
  <td>Grubhub</td>
  <td>YES</td>
</tr>
<tr>
  <td>番茄小说</td>
  <td>NO</td>
  <td>Google Photos</td>
  <td>YES</td>
</tr>
<tr>
  <td>FaceApp</td>
  <td>YES</td>
  <td>Yubo</td>
  <td>YES</td>
</tr>
<tr>
  <td>得物</td>
  <td>YES</td>
  <td>Google Classroom</td>
  <td>YES</td>
</tr>
<tr>
  <td>优酷视频</td>
  <td>NO</td>
  <td>YOLO</td>
  <td>YES</td>
</tr>
<tr>
  <td>秘乐短视频</td>
  <td>YES</td>
  <td>Pandora</td>
  <td>YES</td>
</tr>
<tr>
  <td>网上国网</td>
  <td>NO</td>
  <td>Fonts</td>
  <td>YES</td>
</tr>
<tr>
  <td>哈罗出行</td>
  <td>NO</td>
  <td>Uber Eats</td>
  <td>YES</td>
</tr>
<tr>
  <td>中国银行手机银行</td>
  <td>YES</td>
  <td>Mercari</td>
  <td>YES</td>
</tr>
<tr>
  <td>好省</td>
  <td>YES</td>
  <td>Reddit</td>
  <td>YES</td>
</tr>
<tr>
  <td>喜马拉雅</td>
  <td>NO</td>
  <td>SoundCloud</td>
  <td>YES</td>
</tr>
<tr>
  <td>WIFI 万能钥匙</td>
  <td>NO</td>
  <td>Hangouts Meet</td>
  <td>YES</td>
</tr>
<tr>
  <td>扫描全能王</td>
  <td>NO</td>
  <td>Google Docs</td>
  <td>NO</td>
</tr>
<tr>
  <td>UC 浏览器</td>
  <td>NO</td>
  <td>Instacart</td>
  <td>YES</td>
</tr>
<tr>
  <td>驾考宝典</td>
  <td>NO</td>
  <td>Fitness Coach</td>
  <td>YES</td>
</tr>
<tr>
  <td>天眼查</td>
  <td>NO</td>
  <td>PictureThis</td>
  <td>NO</td>
</tr>
<tr>
  <td>人人视频</td>
  <td>NO</td>
  <td>Xbox</td>
  <td>YES</td>
</tr>
<tr>
  <td>QQ 邮箱</td>
  <td>NO</td>
  <td>Tubi</td>
  <td>YES</td>
</tr>
<tr>
  <td>淘宝直播</td>
  <td>NO</td>
  <td>VideoToLive</td>
  <td>YES</td>
</tr>
<tr>
  <td>西瓜视频</td>
  <td>NO</td>
  <td>Amazon Photos</td>
  <td>YES</td>
</tr>
<tr>
  <td>最珠海</td>
  <td>NO</td>
  <td>Zillow</td>
  <td>YES</td>
</tr>
<tr>
  <td>办事通</td>
  <td>NO</td>
  <td>Robinhood</td>
  <td>YES</td>
</tr>
<tr>
  <td>知乎</td>
  <td>YES</td>
  <td>Hangouts</td>
  <td>YES</td>
</tr>
<tr>
  <td>搜狗输入法</td>
  <td>NO</td>
  <td>News Break</td>
  <td>YES</td>
</tr>
<tr>
  <td>菜鸟裹裹</td>
  <td>NO</td>
  <td>Enlight Video</td>
  <td>NO</td>
</tr>
<tr>
  <td>招商银行</td>
  <td>NO</td>
  <td>Youtube Music</td>
  <td>YES</td>
</tr>
<tr>
  <td>苏宁易购</td>
  <td>NO</td>
  <td>letgo</td>
  <td>YES</td>
</tr>
<tr>
  <td>美团外卖</td>
  <td>NO</td>
  <td>InShot</td>
  <td>NO</td>
</tr>
<tr>
  <td>学习强国</td>
  <td>NO</td>
  <td>Fetch Rewards</td>
  <td>YES</td>
</tr>
<tr>
  <td>饿了么</td>
  <td>NO</td>
  <td>Splice</td>
  <td>YES</td>
</tr>
<tr>
  <td>全民 K 歌</td>
  <td>NO</td>
  <td>Postmates</td>
  <td>YES</td>
</tr>
<tr>
  <td>今日头条</td>
  <td>NO</td>
  <td>ESPN</td>
  <td>YES</td>
</tr>
<tr>
  <td>识货</td>
  <td>YES</td>
  <td>Duolingo</td>
  <td>YES</td>
</tr>
<tr>
  <td>懂车帝</td>
  <td>YES</td>
  <td>Quibi</td>
  <td>YES</td>
</tr>
<tr>
  <td>全球骑士特购</td>
  <td>NO</td>
  <td>Amazon Music</td>
  <td>YES</td>
</tr>
<tr>
  <td>Facetune2</td>
  <td>YES</td>
  <td>Audible</td>
  <td>YES</td>
</tr>
<tr>
  <td>中国联通</td>
  <td>YES</td>
  <td>Microsoft Outlook</td>
  <td>YES</td>
</tr>
<tr>
  <td>平安好车主</td>
  <td>NO</td>
  <td>Esty</td>
  <td>YES</td>
</tr>
<tr>
  <td>腾讯新闻</td>
  <td>NO</td>
  <td>SONIC Drive-in</td>
  <td>YES</td>
</tr>
<tr>
  <td>醒图</td>
  <td>YES</td>
  <td>Funimate Video</td>
  <td>YES</td>
</tr>
<tr>
  <td>酷狗铃声</td>
  <td>NO</td>
  <td>Telegram</td>
  <td>YES</td>
</tr>
<tr>
  <td>百度地图</td>
  <td>NO</td>
  <td>Instacart Shopper</td>
  <td>YES</td>
</tr>
<tr>
  <td>探探</td>
  <td>YES</td>
  <td>AliExpress Shopping</td>
  <td>NO</td>
</tr>
<tr>
  <td>多闪</td>
  <td>NO</td>
  <td>YouTube Studio</td>
  <td>NO</td>
</tr>
<tr>
  <td>中国移动</td>
  <td>NO</td>
  <td>Miscorsoft Teams</td>
  <td>YES</td>
</tr>
<tr>
  <td>作业帮</td>
  <td>NO</td>
  <td>PlayStation</td>
  <td>NO</td>
</tr>
<tr>
  <td>邮政手机银行</td>
  <td>NO</td>
  <td>Bumble</td>
  <td>YES</td>
</tr>
<tr>
  <td>Zoom</td>
  <td>NO</td>
  <td>Messenger Kis</td>
  <td>NO</td>
</tr>
<tr>
  <td>滴滴</td>
  <td>YES</td>
  <td>Facetune2</td>
  <td>YES</td>
</tr>
<tr>
  <td>智联招聘</td>
  <td>YES</td>
  <td>Hoop</td>
  <td>YES</td>
</tr>
<tr>
  <td>虎牙直播</td>
  <td>NO</td>
  <td>TextNow</td>
  <td>YES</td>
</tr>
<tr>
  <td>芒果 TV</td>
  <td>YES</td>
  <td>Vinkle</td>
  <td>YES</td>
</tr>
<tr>
  <td>百度贴吧</td>
  <td>YES</td>
  <td>McDonald’s</td>
  <td>YES</td>
</tr>
</tbody>
</table></div><p>通过这组数据，我们可以分析出很多有意思的东西</p>
<p>首先，BAT 三巨头的门户 APP 都已经具备了 Swift 混编的能力，例如百度系的百度主 App，百度网盘，百度贴吧，阿里系的手淘，芒果视频，腾讯系的微信，微视</p>
<p>其次，国内的<a href="http://www.199it.com/archives/864570.html">独角兽巨头们</a>，似乎也做好了迎接 Swift 的准备，例如字节跳动（剪映，飞书），滴滴出行，自如，网易，小红书，得到，瑞幸咖啡，猿题库，英语流利说等。</p>
<p>最后，一个有意思的现象是，我发现国外 Top 102 的 ipa 总大小为 10G，而国内 Top 102 的 ipa 总大小将近 24G，不知道这是不是用 Swift 编写代码为包大小带来的正向收益，还是我们 ”CMD+C 和 CMD+V“ 的代码复用机制造成的。</p>
<h2>总结与展望</h2>
<p>做完这个调研，能得出什么结论呢？</p>
<ol>
<li>综合 Swift 的发展历史，语言排名，社区活跃度等因素来看，Swift 的发展是处于上升趋势的，可能相比于一些明星语言和明星技术热点，它的表现不是那么突出，但总趋势上升是不可否认的，这同样适用于 Objective-C 的总趋势下降。</li>
<li>Apple 应该会继续增加 Swift 语言在其生态圈的重要性并进行相应的战略部署，即使 Swift 语言还没完全达到 Module Stability，但从推出的 Swift 独占 SDK 已经看出了他们的想法。</li>
<li>国内外各个互联网厂商在转向 Swift 的道路上已经走起来了，即使国内的占比还落后于国外，但总的趋势和涨幅势头已经十分明显了，以 BAT 为首的大部分互联网公司已经完成了 Swift 的混编工作，尤其是它们旗下的明星 App 已经接入了 Swift。</li>
</ol>
<p>拿着这些结论来看看大家平日里对 Swift 的印象，我们又发现了哪些不一样的地方呢？</p>
<p>虽然国内 iOS 圈里常有人说”Swift 无用“，”Swift 火不了“，”我们不需要用 Swift 开发“，但冷静的分析下来，国内的各大厂商真的抛弃 Swift 了么？</p>
<p>通过这份真实的数据，我想答案很明显，它们都没有放弃 Swift，而且都在积极的做准备，这就很像嘴上说着不要不要，但身体却出卖了自己。</p>
<p>国外的 Swift 氛围不用多说，大家都已经可以看出谁会是 iOS 端上未来的主角。这里我们就说说国内的情况：</p>
<p>我们都知道，淘系的主 App —- 手机淘宝在今年完成了 Swift 混编能力的建设，这意味着什么？对于淘系App 来说，这么庞大，复杂的工程都已经具备了混编的能力，那么阿里旗下的其余 App 转型将不再有什么逾越不了的鸿沟。</p>
<p>号称 App 工厂的字节跳动，在很早就尝试了 Swift 的混编开发，早期还限定在一些小型的项目，现在它们已经具备了中大型项目的 Swift 混编能力，这一点可以在它们的海外明星应用 Helo 上的得到验证，目前这款 App 在字节跳动内部的排名已经跻身到前 5 名了，另外从一些渠道得知，它们的明星应用 - 今日头条也将在近期开展 Swfit 混编能力的建设。</p>
<p>而国内那些还在高速发展的公司，例如美团，京东，拼多多，快手等公司，在 Swift 上的探索还显得比较落后，可能还停留在一些小型内部应用或者 B 端的应用上，甚至也有可能完全没有开展过相关的工作。</p>
<p>这么看来，国内厂商的 Swift 格局已经十分清晰，大体会呈现三个梯队：</p>
<p>第一梯队：以 BAT 为代表的顶端团队已经解决了复杂的，巨型的，历史包袱重的工程项目如何使用 Swift 的问题，对他们而言，未来的问题就是怎么将 Swift 用到真正的业务代码上了，玩的好，玩的溜的将是它们要关注的问题了。</p>
<p>第二梯队：以字节跳动，网易为代表的一些公司，已经解决了部分 Swift 混编的问题，对于其内部工程复杂度最高，历史包袱最重的 App 还没有实现混编的能力，例如字节跳动的 Helo 和抖音，网易的网易公开课和网易云音乐，这些公司在近期面临的问题可能会是如何解决混编。</p>
<p>第三梯队：以美团，京东为代表的一些公司，在这方面还没有开展相应的工作或者开展的还比较少，他们目前可能更关注的还是在解决自身业务发展的问题，例如动态化，中台建设，容器化等方面的技术积累与战略部署。</p>
<p>不管怎么看，Swift 在国内的发展既不是完全停滞，也不是无人问津，只是真正玩的人比较“低调”而已，但该来的一定会来。</p>
<p>不知道看完这篇文章，你认为 Swift 在国内的发展会是什么样子呢？</p>
]]></content:encoded></item><item><title><![CDATA[一次让刷新控件好玩起来的尝试]]></title><guid>https://swiftsiqi.com/posts/a-new-trial-of-playable-refresh-demo</guid><link>https://swiftsiqi.com/posts/a-new-trial-of-playable-refresh-demo</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 19 Jan 2020 04:43:25 +0000</pubDate><content:encoded><![CDATA[<h2>写在前面的话</h2>
<p>虽然我所处的团队与业务开发息息相关，但近一年，我个人已经很少写一些业务代码了，做的事情可能更偏向基础技术的建设，技术栈也从 Objective-C 转向了 JavaScript 的世界。</p>
<p>然而，我内心还是想写点 Swift 相关的东西，写点与 iOS 相关的东西，或许可能我内心是真的很热爱它们吧。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901565_431207.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1584/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901565_431207.jpg" alt="1 (1).jpg"loading="lazy" decoding="async" width="1584" height="1186" /></picture></figure></div><p>所以年底的时候，我又重新捡起了 Swift 这门语言 ，并在 <a href="https://github.com/Desgard">瓜瓜</a> 的公众号上持续输出一些与此技术相关的文章，大多是一些使用技巧和语法细节。而我因为工作原因，经常会在 Objective-C，JavaScript，TypeScript，Ruby，Python 之间来回切换，不得不说 Swift 的综合体验还是可以的，除了 Xcode。</p>
<div class="blockquote"><blockquote><p>没错! 它就是 Swift Tips 系列，如果你感兴趣，记得关注我们的公众号 - <a href="https://mp.weixin.qq.com/mp/profile_ext?action=home&amp;__biz=MzA5MTM1NTc2Ng==&amp;scene=124#wechat_redirect">让技术一瓜共食</a></p>
</blockquote></div>
<p>不过，光研究语言本身，是枯燥的，也不立体，我还是想把 Swift 用到一些具体的场景上，毕竟玩起来才有意思！</p>
<p>于是我想起了自己早年间立的一个 flag：<strong>尝试一次游戏开发</strong>。</p>
<p>所以我迅速下单买了 Ray Wenderlich 家的<a href="https://store.raywenderlich.com/products/2d-apple-games-by-tutorials">《2D Apple Games by Tutorials》</a>一书，虽然是 Swift 4 和 iOS 11 的版本，但说实话 Swift 4 到 5 的变化算比较温和，另外教材里的 iOS 系统也只落后现实世界里两个版本，SpriteKit 整体也没有什么翻天覆地的变化，落后的内容补上两个季度的 WWDC 就好了，所以，学就完事了！</p>
<div class="blockquote"><blockquote><p>看完这本书后，我不得不说老外的教材真的很好，只是可惜这书以后不再更新了。</p>
</blockquote></div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901479_481264.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304901479_481264.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304901479_481264.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304901479_481264.jpg?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304901479_481264.jpg" alt="2 (1).jpg"loading="lazy" decoding="async" width="2772" height="2128" /></picture></figure></div><p>大概花了一个月的时间，陆陆续续的把《2D Apple Games by Tutorials》里面的代码和作业也都敲了一遍，自我感觉还不错，问题就来了，怎么把学的东西用起来呢？</p>
<p>一开始我是想做个策略回合制的游戏，与战场女武神，皇家骑士团的游戏类型相似。说实话，在开动之前，我对自己还挺有信心的，因为有做游戏开发的想法，所以很早就有为自己培养一些与游戏相关的技能点，例如原画绘制，音乐制作等，下面是我自己的一些小作品。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901871_472751.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1128/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901871_472751.png" alt="image.png"loading="lazy" decoding="async" width="1128" height="843" /></picture></figure></div><p>但真的进入开发状态后，我发现事情并不像自己想的那么简单……</p>
<p>这里我就不展开说了，只说说核心观点吧，我理解游戏的本质还是在讲故事，至于音乐，原画，动效，编程都是为这个故事而服务的，如果没有一个好的故事，游戏也就失去了灵魂。</p>
<h2>IDEA 的由来</h2>
<p>在意识到自己并没有一个好故事可以说的状态下，我决定把新 get 到的开发技能用到自己的工作场景中，可问题就是怎么把一个轻量级的游戏与日常的 App 结合起来呢？</p>
<p>我脑海里突然想起来了 Chrome 浏览器里的小恐龙!</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901827_359571.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_796/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901827_359571.png" alt="image.png"loading="lazy" decoding="async" width="796" height="500" /></picture></figure></div><p>说实话，我并不清楚做这个 Feature 的初衷是什么，但作为一名用户，在无网的时候玩上一会这样的游戏，既可以转移我的注意力，又可以缓解我无法上网的焦虑感。</p>
<p>所以从用户体验的角度上来说，这是一个很不错的 IDEA！而它很好的启发了我！</p>
<p>带着改善用户体验的想法，我反复观察了自己和周围好朋友使用 App 的习惯。我发现使用户焦虑感增强的几个操作无外乎都是围绕页面刷新的，例如：</p>
<ul>
<li>下拉刷新时转动的指示器</li>
<li>Web 页面的加载条</li>
<li>下载资源时的进度条</li>
<li>刷新失败或者无网状态下的空白页</li>
</ul>
<p>不过从以上操作的频率来看，下拉刷新应该是最频繁的，也是最常见的。</p>
<p>所以决定了，就是你了！让我们做一个更好玩的下拉刷新控件吧！</p>
<ul>
<li>从体验角度的考量</li>
</ul>
<p>在关于下拉操作，国内的应用主要有两种交互形式，一种是以淘宝，京东为代表的“二楼形态”，另一种是以美团，拼多多为代表的“经典形态”，</p>
<p>“经典形态”就不用多说了，“二楼形态”的本质是通过下拉跳转到另一个页面，当然在这个下拉的过程中可以触发刷新，不过我认为这并不是一个必要的过程，在我的理解范围里，微信的下拉操作其实也是一个“二楼形态”，只不过它没有刷新，只是通过下拉跳转到了小程序选择界面。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901817_809447.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304901817_809447.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304901817_809447.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304901817_809447.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304901817_809447.png" alt="image.png"loading="lazy" decoding="async" width="1806" height="787" /></picture></figure></div><p>所以，如果仅仅从这两种传统的思路下手来改进用户体验，提升的空间是十分有限的。</p>
<p>这时候我发现手机版本的 Chrome 在下拉交互上是有点与众不同的！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901396_882323.gif" type="image/webp"><img src="https://i.typlog.com/siqi/8304901396_882323.gif" alt="6 (1).gif"loading="lazy" decoding="async" width="886" height="1071" /></picture></figure></div><p>我发现它的下拉操作是可以与使用者进行更细粒度的交互，这一点很重要！这让我意识到它会赋予下拉刷新很多新的可能性！</p>
<p>试想一下，如果我们在下拉刷新的同时，能玩到小时候 FC 机上的公路战士(Road Fighter)，或者曾经风靡一时的手游 —- 神庙逃亡(Temple Run)会是何种体验！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901767_640734.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304901767_640734.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304901767_640734.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304901767_640734.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304901767_640734.png" alt="image.png"loading="lazy" decoding="async" width="1600" height="1245" /></picture></figure></div><p>当然，在下拉操作里玩塞尔达，无双系列也不是不可能，但体验未必好，因为那种游戏的交互实在太复杂了，不是一个手指左滑右划就能完成的，但像公路战士和神庙逃亡这一类游戏的交互就很简单，逻辑也足够轻量，放在下拉刷新的交互中也显得比较合适。</p>
<p>从体验角度上来说，作为用户，一旦触发下拉刷新就可以玩一把轻松有趣儿的小游戏，让自己从等待网络数据的焦虑中解脱出来，听起来还不错吧？</p>
<ul>
<li>从技术角度的考量</li>
</ul>
<p>作为一名程序员，我们总要需要考虑代码上的改动会带来哪些好的变化，哪些不好的变化。</p>
<p>如果真的在下拉刷新里做个小游戏，从某种程度上来说，它一定会增加工程的复杂性，说不定还会影响到项目的包大小，首页的加载时长，FPS 等性能指标或者业务指标。</p>
<p>那么它能带来哪些好的变化呢？</p>
<p>在回答这个问题之前，我们先看看国内这些年客户端技术的大趋势，在国内，大部分电商都在做动态化相关的事儿，从 RN，Weex，Flutter，再到一些自研的技术，例如天猫的 Tangram，手淘的 Dinamic，美团的 Flexbox 等。</p>
<p>简单来说，它们的出发点可能都是希望自己的 App 做到不依赖客户端发版，就能实现内容上或者逻辑上的更新。但从原理上来说，它们的思路大体都是下发一个布局描述文件，然后绑定实际的业务数据，最终通过渲染引擎变成我们所看到的界面。</p>
<p>不过这也就带来了一些变化，原先的后台只需要下发视图所展示的数据，例如 UILabel 中的内容，而动态化方案不仅要下发 UILabel 展示的内容，还需要下发 UILable 自身的描述信息。这种变化会使整体传输的数据量迅速变大，甚至会使得网络请求的次数变多（一次请求业务数据，一次请求布局文件）。</p>
<p>在性能监控或者业务监控的指标中，页面的加载时长是一种十分常见的指标。但动态化方案因为多了一次转换的过程，所以在某种程度上，它会使这个指标发生恶化的。</p>
<p>虽然我们会做很多事情来优化加载时长，并改善这一指标，但所有的优化都是有尽头的，而且根据二八原则，这样优化在后期的投入产出比是十分有限的，开发者绞尽脑汁的做了一堆优化，但用户可能并不会感知到这 1-2 ms 的优化，反而让代码的维护成本加大了不少。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901744_071018.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_575/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901744_071018.png" alt="image.png"loading="lazy" decoding="async" width="575" height="378" /></picture></figure></div><p>那么我们能不能从另一个角度出发，例如通过一些引导来转移用户的注意力，从而让他们忽略掉这些加载时长？</p>
<p>刚好，前面提到的可交互的游戏式的下拉刷新，它就满足了转移用户注意力的诉求。如果需要一些指标衡量它，我们可以通过下面的方式来观察和推导：</p>
<p>建立两个时间指标，一个是刷新页面所需的时长，从发起网络请求，到页面刷新完毕的总时间 ，T-Net，另一个是用户下拉刷新后的游玩时长，从触发游戏开始，到用户结束游戏的总时间，T-Game。</p>
<p>对于 T-Game - T-Net &gt; 0 的用户, 我们可以假设他们对加载时长不敏感，针对这种情况，我们可以通过在游戏里植入红包或者其他方式增加用户的留存，并改善它们的用户体验。</p>
<p>对于 T-Game - T-Net &lt; 0 的用户，我们可以假设他们对加载时长十分敏感，那么我们就可以对这些用户做有针对性的优化。例如从智能预加载或者推荐策略上入手。</p>
<p>所以从这个角度来看，可交互的游戏式的下拉刷新在技术层面也有了实际的价值和意义！</p>
<ul>
<li>从产品角度的考量</li>
</ul>
<p>在大公司待久了，你总要面对这样一个问题，这个需求到底有什么意义？它能带来哪些实际的利益，能为我们这个部门赚钱么？</p>
<p>这个 feature 同样会面临这样的问题，于是我仔细的思考了自己所处的业务线和业务形态。（PS: 下面的这些思考是我个人的理解，不用过分解读）</p>
<p>我所在的部门负责了国内某电商平台的首页，很像京东，手淘的首页业务，它自身并不包含一个完整的交易链，承担的首要任务是流量分发，为公司内其他业务线带来用户和持续增加的可能性。</p>
<p>说白了，在我的理解范围内，首页就像一个展览厅，为了让展览厅里面的商品卖得更好，我们通常会有两种思路：</p>
<p>第一种：在最合适的位置向顾客展示最合适的商品。
第二种：尽可能的增加展位，展示更多的商品</p>
<p>如果将这个思路放到电商平台的首页模块中，我们也可以看到它们的踪影，例如手淘技术文章里经常提到的千人千面，它本质上就是一种优化推荐策略的方案，它可以看做是第一种方式的变体，而近年来，很多 App 从原先的单卡片流变成双列卡片流，它就是第二种策略的变体，在同样的空间内增加了展示位置，提升了展示密度。</p>
<p>之前我们也说过下拉刷新的两种形态，“二楼形态”就是将下拉刷新作为了一个入口，为 App 内的其他内容进行导流，它本质上是属于第二种策略的。</p>
<p>因为在“经典形态”下，下拉刷新是不能作为一个入口的，而“二楼形态”的出现，使得下拉刷新的操作可以进入到其他页面，这就在有限的空间里增加了更多的展位。</p>
<p>在这个前提下，我们再来看下可交互的游戏式下拉刷新，它与二楼形态很接近，当游戏结束后，它完全能够作为一个入口，例如玩完了某个品牌的红包雨活动后跳转到某个指定频道，另外在游戏过程中，又可以结合一些页面上的元素进行品牌宣传，增加曝光量，例如红包雨里面的背景图，红包的品牌等。</p>
<p>总体来说，相比于传统的“二楼形态”，这种玩法让下拉刷新变得更有“价值”了！</p>
<h2>冷静的分析</h2>
<p>在通过用户角度，技术角度，产品角度三个方面分析后，我认为，这个事儿是有搞头的！但我相信我肯定不是第一个想到这个点子的人，所以我迅速在 Google 上搜索了一下，果然不出所料，Github 上已经有人做过类似的 Demo，这里刚好它们做一些介绍，也说说我的个人观点。</p>
<ul>
<li>BreakOutToRefresh</li>
</ul>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901729_495188.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_470/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901729_495188.png" alt="image.png"loading="lazy" decoding="async" width="470" height="433" /></picture></figure></div><p>这个 Demo 里的游戏实现使用的 SpriteKit 框架完成的，支持 Swift 和 Objective-C。它的 Github 链接是 <a href="https://github.com/dasdom/BreakOutToRefresh%E3%80%82">https://github.com/dasdom/BreakOutToRefresh。</a> 它基本符合了我需要的两个核心功能点，一个是可交互的，一个是游戏化的。</p>
<p>但它实际的游戏体验并没有达到我的预期，主要有以下几个问题：</p>
<ol>
<li>游戏的交互是上下移动的，这和触发刷新的下拉操作有一定相似性，这就导致误操作的几率增大。</li>
<li>游戏中的交互元素体积较小，物体的碰撞感和操作性都比较差，使得游戏的体验并不理想。</li>
<li>即使是这种打方块的游戏，它的逻辑在下拉刷新里也显得略微复杂，尤其在小球弹到边界时，会出现一些奇怪的不符合逻辑的现象，例如小球弹到右边界时会返回而不是游戏结束。</li>
</ol>
<p>当然 BreakOutToRefresh 的作者 dasdom 提到了它的这个 Demo 又是源于 boztalay 的 <a href="https://github.com/boztalay/BOZPongRefreshControl">BOZPongRefreshControl</a>，不过可惜的是 BOZPongRefreshControl 并不是可交互的，我们看到的效果可以简单的理解为是已经预置好的动画，所以这里也就不展开说明了。</p>
<p>至于其他实现，我在市面上还真是再也没找到了，如果你有什么发现，请记得给我留言！</p>
<h2>思考，思考，再思考</h2>
<p>在有了上面的思考后，我开始着手设计自己理解的那种可交互的，游戏化的下拉刷新组件。</p>
<ul>
<li>关于交互方式</li>
</ul>
<p>在 iOS 系统内，常见的手势有以下几种：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901720_455168.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901720_455168.png" alt="image.png"loading="lazy" decoding="async" width="800" height="433" /></picture></figure></div><p>结合到下拉刷新的场景中，我们会发现 Swipe，Pinch，Rotation，Screen Edge Pan 等操作并不合适，相对合适的可能就只剩 Tap，Pan 和 Long Press 了。为了不那么反人类，我们应该避免游戏里的交互操作与刷新的下拉操作发生冲突。</p>
<p>在结合上面几点后，我们发现游戏里操作精灵的方式就只能在左右滑动和点按中选择了。</p>
<p>当然，你可以发明一些新的手势，但这里就不展开讨论了！</p>
<ul>
<li>关于游戏内容</li>
</ul>
<p>在确定了可交互的方式后，我们需要进一步讨论游戏的呈现形式，也就是游戏的内容。在之前的调研中，我们得出了游戏的交互逻辑不能过于复杂，且要避免精灵的体积过小，不知道你脑海里想到了哪些游戏？</p>
<p>在我的脑海里，马上就想起了小时候玩的红白机，例如魂斗罗，超级马里奥，热血物语等，但说实话，这些8 Bit 的游戏还是有太复杂了，放到下拉刷新里显得太重太重了，不光是游戏逻辑，就连交互也有很大的区别，毕竟咱们应该都还都记得 “上上 下下 左左 右右 AB AB” 的秘籍！</p>
<p>所以，我们应该把时间再往前推一推，回到雅达利时代的游戏机上！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901710_326818.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_600/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901710_326818.png" alt="image.png"loading="lazy" decoding="async" width="600" height="648" /></picture></figure></div><p>怎么样，看起来够复古了吧！</p>
<p>雅达利游戏机本身的操作方式十分简单，一个方向键，一个按钮，与我们现在仅有的左右滑动和点按操作相似，另一点是那个时代的游戏逻辑轻量简单，这里我们举几个例子：乒乓，太空侵略者，吃豆人等，所以，我们可以完全借鉴这些当年的雅达利游戏机上的经典作品来实现自身的诉求。</p>
<p>当然，为了让游戏能更接地气，也就是更好的服务到自身的业务中，我这里整合几个实际的游戏场景好了！</p>
<p>例如临近双十一，淘宝要派发红包了！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901698_916632.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_712/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901698_916632.png" alt="image.png"loading="lazy" decoding="async" width="712" height="434" /></picture></figure></div><p>例如正在使用滴滴打车时，可以让等待中的用户模拟一把正在赶路的司机！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901691_398157.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_712/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901691_398157.png" alt="image.png"loading="lazy" decoding="async" width="712" height="434" /></picture></figure></div><p>又例如在下单点外卖的时候，我们可以来一把太空大战！</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901682_725693.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_712/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901682_725693.png" alt="image.png"loading="lazy" decoding="async" width="712" height="406" /></picture></figure></div><h2>Show Me The Code</h2>
<p>在有了上面的设想后，我以抢红包的点子为例开发一个真实的 Demo，这里我并没有像 BreakOutToRefresh 那样把代码抽象成一个 SDK，因为我的初衷是希望大家将这个思路应用到自己的 App 中，而不是那些代码。</p>
<p>这里我也不会对细节做过多的讲解，主要是做一些笼统的介绍和分析，具体的示例代码请移步到我的 Repo 中查看。如果你觉得这个项目很有意思，记得给我一个 Star 哦！</p>
<p>Github 地址：<a href="https://github.com/SketchK/a-playable-refresh-demo">https://github.com/SketchK/a-playable-refresh-demo</a></p>
<ul>
<li>SpriteKit</li>
</ul>
<p>因为之前自学的就是 2D 游戏开发，所欲这里我毫不犹豫的选择了苹果自己的开发框架 SpriteKit，至于语言方面，我选择了 Swift，完全是个人喜好。</p>
<p>示例代码中的核心文件包含以下几个部分：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304901671_392371.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1588/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304901671_392371.png" alt="image.png"loading="lazy" decoding="async" width="1588" height="674" /></picture></figure></div><p>这几个文件承载了游戏的核心逻辑，而其中最最最重要的两个文件就属 GameScene 和 GameView，这里我们先说 GameScene。</p>
<p>这个场景下主要有两个类型的精灵，一种是红包精灵，它在游戏开始后，会不断的从界面上方出现，并逐渐掉落到界面的下方，这里使用了 SKAction 来完成相应的工作，而另一种是英雄精灵，它在游戏里只有一个并在界面初始化的时候被创建，根据手势的左右偏移量进行移动，这里我使用了较为传统的手动计算方式。</p>
<p>这个类型的游戏核心点就是碰撞问题，一方面是英雄与游戏界面的碰撞，这里我们要做到英雄精灵的活动范围不能超出游戏界面，它的逻辑放在了 update 这个生命周期方法中，另一方面就是英雄与红包的碰撞，我们需要通过这个检测它们之间的碰撞来让游戏继续进行和结束，这一部分的逻辑放在了 didEvaluateActions 中。</p>
<ul>
<li>刷新机制</li>
</ul>
<p>至于 GameView ，它承担了两个重要的工作，一个是刷新控件的逻辑，一个是游戏界面的切换逻辑，这两个工作有一定的内在联系，你可以拆分成两个模板，也可以整合到一个模块中，就像代码里的做法，我的方案并不是一个最佳解决方案。</p>
<p>另外刷新机制这块，其实这个 Demo 做的并不完美，如果想深入了解，我建议大家看看社区里一些优秀的下拉刷新组件库，这里你只需要了解游戏界面的切换逻辑是与刷新控件相关联的就好。</p>
<h2>尾声</h2>
<p>看到这里，这篇文章就要结束了，可能干货也不是那么多，主要说了说我在编写这个 Demo 时都想了什么，做了什么，干了什么。</p>
<p>很遗憾的是，我无法将这个想法用在自家的 App 中，但我十分希望它能被用起来，并可以观察到它的实际上线效果。如果有人这么做了，欢迎与我联系讨论！</p>
<p>项目的代码地址为：<a href="https://github.com/SketchK/a-playable-refresh-demo%EF%BC%8C%E5%A6%82%E6%9E%9C%E4%BD%A0%E8%A7%89%E5%BE%97%E5%AE%83%E8%BF%98%E4%B8%8D%E9%94%99%EF%BC%8C%E8%AF%B7%E8%AE%B0%E5%BE%97%E7%BB%99%E6%88%91%E4%B8%80%E4%B8%AA">https://github.com/SketchK/a-playable-refresh-demo，如果你觉得它还不错，请记得给我一个</a> Star 哦！</p>
<p>最后祝大家新年快乐！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 032 - Assigning to self in struct initializers]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-032</guid><link>https://swiftsiqi.com/posts/Swift-Tips-032</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Mon, 30 Dec 2019 04:31:15 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902150_289429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902150_289429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902150_289429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902150_289429.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902150_289429.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="842" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#32-assigning-to-self-in-struct-initializers">Swift Tips 032 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>这段代码为 Bool 类型进行了扩展，并利用扩展为现有的 Bool 类型添加了一个新的构造器：<code>init(input: String)</code>。当输入参数为 <code>y</code>, <code>yes</code>, <code>👍</code> 的时候，构造出来的实例值为 true，其余情况则为 false。</p>
<h3>值类型和类类型的构造过程</h3>
<p>我们都知道，构造器可以通过调用其它构造器来完成实例的部分构造过程，这一过程被称为构造器代理（initializer delegation），这种模式能避免多个构造器间的代码重复。</p>
<p>但是构造器代理的实现规则和组织形式在值类型(value type)和类类型(class type)中有所不同。</p>
<p>值类型是不支持继承的，例如枚举和结构体，所以它们的构造器代理过程相对简单，因为它们只能代理给自己的其它构造器。</p>
<p>类则不同，它可以继承自其它类，这意味着类要确保所有继承下来的存储型属性在构造时能被正确的初始化，这也是为什么类类型在构造过程中有<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization#two-phase-initialization">两段式的构造过程</a>，要遵守<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization#initializer-delegation-for-class-types">构造器代理的三个原则</a>，并进行<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization#an-quan-jian-cha-1">四种构造安全检查</a>！</p>
<p>说了这么多，回到今天的代码上！</p>
<p>通过前面的讲解，我们应该回想起了值类型与类类型在构造过程中的差异，想必这时候的大家，对于代码截图中能直接使用 <code>self</code>，而不需要调用 <code>super.init()</code> 等操作也都能理解了。</p>
<p>如果还是有很多疑惑，不妨重温一下官方手册里关于<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization">构造过程的章节</a>！</p>
<h3>让 Struct 构造器变得更好用一点</h3>
<p>在一些特定的场景下，Swift 能够为 Struct 类型生成一个默认构造器，也可以叫它为逐一成员构造器（memberwise initializer），例如下面的代码，Swift 就为 <code>myStruct</code> 结构体创建了一个构造器：<code>init(myString: String?, myInt: Int?, myDouble: Double?)</code></p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">struct</span> <span class="nc">myStruct</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myString</span><span class="p">:</span> <span class="nb">String</span><span class="p">?</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myInt</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myDouble</span><span class="p">:</span> <span class="nb">Double</span><span class="p">?</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902138_918478.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1184/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304902138_918478.png" alt="image.png"loading="lazy" decoding="async" width="1184" height="106" /></picture></figure></div><p>虽然这个自动生成的默认构造器看起来中规中矩，也帮我们省去了不少打字的功夫，但在每次使用的时候，我们不得不完整的写出三个参数，即使这个参数是没有值的，这确实有点麻烦！</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">aStruct</span> <span class="p">=</span> <span class="n">myStruct</span><span class="p">(</span><span class="n">myString</span><span class="p">:</span> <span class="s">&quot;1&quot;</span><span class="p">,</span> <span class="n">myInt</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span> <span class="n">myDouble</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>
</div></code></pre></div>
</div>
<p>那么有什么好的技巧来解决这个问题呢？</p>
<p>答案就是重新声明整个构造函数并在构造函数的每个参数后面添加默认值为 nil 的逻辑！</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">struct</span> <span class="nc">myStructWithInit</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myString</span><span class="p">:</span> <span class="nb">String</span><span class="p">?</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myInt</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">myDouble</span><span class="p">:</span> <span class="nb">Double</span><span class="p">?</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">myString</span><span class="p">:</span> <span class="nb">String</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">,</span> <span class="c1">//👈</span>
</div><div class="line">         <span class="n">myInt</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">,</span>
</div><div class="line">         <span class="n">myDouble</span><span class="p">:</span> <span class="nb">Double</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">myString</span> <span class="p">=</span> <span class="n">myString</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">myInt</span> <span class="p">=</span> <span class="n">myInt</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">myDouble</span> <span class="p">=</span> <span class="n">myDouble</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>此时，我们再次创建 <code>myStructWithInit</code> 类型的实例，会发现代码提示里的构造函数变成了两个！一个是我们之前见过的样式，另一个是没有任何参数的样式。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902129_94267.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1228/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304902129_94267.png" alt="image.png"loading="lazy" decoding="async" width="1228" height="134" /></picture></figure></div><p>不过，这并不意味着你只有 2 个构造方法可以用哦，现在的你也可以用下面的方式来构造实例了！</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">myStructWithInit</span><span class="p">(</span><span class="n">myString</span><span class="p">:</span> <span class="s">&quot;Something&quot;</span><span class="p">)</span>
</div><div class="line"><span class="c1">//or</span>
</div><div class="line"><span class="n">myStructWithInit</span><span class="p">(</span><span class="n">myString</span><span class="p">:</span> <span class="s">&quot;Something&quot;</span><span class="p">,</span> <span class="n">myInt</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span>
</div><div class="line"><span class="c1">//or</span>
</div><div class="line"><span class="n">myStructWithInit</span><span class="p">(</span><span class="n">myString</span><span class="p">:</span> <span class="s">&quot;Something&quot;</span><span class="p">,</span> <span class="n">myInt</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="n">myDouble</span><span class="p">:</span> <span class="mf">3.0</span><span class="p">)</span>
</div></code></pre></div>
</div>
<p>惊不惊喜，意不意外！现在我们终于可以自由的使用构造函数啦！</p>
<p>之所以能这样做，是因为当我们为函数的某个参数设置了默认值后，再调用该函数时，就可以忽略它。这个知识点可以从官方手册里的<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/06_functions#default-parameter-values">默认函数值</a>一节中找到。</p>
<p>所以，今天你学会如何通过默认参数值这个技巧来创建更灵活，更自由，更方便的结构体构造函数了么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 031 - Recursively calling closures as inline functions]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-031</guid><link>https://swiftsiqi.com/posts/Swift-Tips-031</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 22 Dec 2019 04:29:39 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902249_680399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902249_680399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902249_680399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902249_680399.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902249_680399.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1346" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#31-recursively-calling-closures-as-inline-functions">Swift Tips 031 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>这段代码在 records 函数中内定义了一个名为 iterate 的嵌套函数，当 nextRecord 满足 matches 方法的条件时，它返回 nextRecord 并继续遍历 recordIterator 里的元素，当 nextRecord 不满足 matches 方法的条件时，它通过 iterate 的递归，继续遍历 recordIterator 的元素。</p>
<p>通过这样的方式我们可以记录下 Database 中所有符合条件的 Record 实例。</p>
<h3>嵌套函数</h3>
<p>在 Swift 中，我们也可以把函数定义在某个函数体中，这样的函数被称作嵌套函数。</p>
<p>默认情况下，嵌套函数是对外界不可见的，但是可以被它们的外围函数调用。一个外围函数也可以返回它的某一个嵌套函数，使得这个函数可以在其他域中被使用。</p>
<p>就像示例代码中的 iterate 就是一个嵌套函数，而 records(matching:) 是一个外围函数。</p>
<p>更多关于嵌套函数的话题可以阅读官方手册的<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/06_functions#Nested-Functions">嵌套函数章节</a>。除此之外，我还推荐阅读一下 Matt Neuburg 在 《iOS 10 Programming Fundamentals with Swift》里展示的<a href="http://apeth.com/swiftBook/ch02.html#_function_in_function">内联函数在真实工程里的使用场景</a>，很有启发性！</p>
<p>不过今天的内容远不止这些！</p>
<h3>似懂非懂的 Sequence 和 Iterator</h3>
<p>初看这段代码，不知道你的感受如何？我是觉得好像大体都能看的懂，但很多细节点又不是很明白，例如 <code>AnySequence { AnyIterator(iterate) }</code> 是个什么东西？<code>makeIterator()</code> 返回的对象为什么会有 <code>next()</code> 方法等等。</p>
<p>所以想要完全消化这段代码的全部含义，就得搞清楚 Sequence 和 Iterator 的概念和基本用法。</p>
<h4>Sequence 和 Iterator 是什么</h4>
<p>在 Swift 的世界中，Sequence 代表的是一系列具有相同类型值的集合，并且提供对这些值的迭代能力。</p>
<p>迭代一个 Sequence 最常见的方式就是 for-in 循环，如下：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">for</span> <span class="n">element</span> <span class="k">in</span> <span class="n">someSequence</span> <span class="p">{</span>
</div><div class="line">    <span class="n">doSomething</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">element</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>Sequece 本身并不是什么基类，只是一个协议，这个协议只有一个必须实现的方法 <code>makeIterator()</code>，它需要返回一个 Iterator 且遵守 IteratorProtocol 类型。它的定义如下：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">protocol</span> <span class="nc">Sequence</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Iterator</span><span class="p">:</span> <span class="n">IteratorProtocol</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">makeIterator</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Iterator</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>这也就是说，只要提供一个 Iterator 就能实现一个 Sequence，那么 Iterator 又是什么呢？</p>
<p>Iterator 是一个遵守了 IteratorProtocol 协议的实体，它用来为 Sequence 提供迭代能力。这个协议要求声明了一个 <code>next()</code> 方法，用来返回 Sequence 中的下一个元素，或者当没有下一个元素时返回 nil。associatedtype 声明了元素的类型。 它的定义如下：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">public</span> <span class="kd">protocol</span> <span class="nc">IteratorProtocol</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Element</span>
</div><div class="line">    <span class="kd">public</span> <span class="kr">mutating</span> <span class="kd">func</span> <span class="nf">next</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="kc">Self</span><span class="p">.</span><span class="n">Element</span><span class="p">?</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>对于 Sequence 而言，我们可以用 for-in 来迭代其中的元素，但其实这个功能的背后就是 IteratorProtocol 在起作用。这里我们举一个例子：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">animals</span> <span class="p">=</span> <span class="p">[</span><span class="s">&quot;Antelope&quot;</span><span class="p">,</span> <span class="s">&quot;Butterfly&quot;</span><span class="p">,</span> <span class="s">&quot;Camel&quot;</span><span class="p">,</span> <span class="s">&quot;Dolphin&quot;</span><span class="p">]</span>
</div><div class="line"><span class="k">for</span> <span class="n">animal</span> <span class="k">in</span> <span class="n">animals</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">animal</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="c1">// Antelope  Butterfly  Camel  Dolphin</span>
</div></code></pre></div>
</div>
<p>实际上编译器会把以上代码转换成下面的代码：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">var</span> <span class="nv">animalIterator</span> <span class="p">=</span> <span class="n">animals</span><span class="p">.</span><span class="n">makeIterator</span><span class="p">()</span>
</div><div class="line"><span class="k">while</span> <span class="kd">let</span> <span class="nv">animal</span> <span class="p">=</span> <span class="n">animalIterator</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">animal</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h4>实现一个 Sequence 和 Iterator</h4>
<p>为了加深理解，我们不妨亲自写一个 Sequence，但就像刚才分析的一样，我们需要先实现一个 iterator</p>
<p>假设我们的 Iterator 要实现这样的功能：它接收一个字符串数组，并可以迭代这个数组中所有字符串的首字母。当数组中的最后一个字符串迭代完毕后，退出迭代。</p>
<p>代码如下所示：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">struct</span> <span class="nc">FirstLetterIterator</span><span class="p">:</span> <span class="n">IteratorProtocol</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">strings</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">offset</span><span class="p">:</span> <span class="nb">Int</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">strings</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">])</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">strings</span> <span class="p">=</span> <span class="n">strings</span>
</div><div class="line">        <span class="n">offset</span> <span class="p">=</span> <span class="mi">0</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kr">mutating</span> <span class="kd">func</span> <span class="nf">next</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">String</span><span class="p">?</span> <span class="p">{</span>
</div><div class="line">        <span class="k">guard</span> <span class="n">offset</span> <span class="o">&lt;</span> <span class="n">strings</span><span class="p">.</span><span class="n">endIndex</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">            <span class="k">return</span> <span class="kc">nil</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">string</span> <span class="p">=</span> <span class="n">strings</span><span class="p">[</span><span class="n">offset</span><span class="p">]</span>
</div><div class="line">        <span class="n">offset</span> <span class="o">+=</span> <span class="mi">1</span>
</div><div class="line">        <span class="k">return</span> <span class="nb">String</span><span class="p">(</span><span class="n">string</span><span class="p">.</span><span class="bp">first</span><span class="p">!)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>上面这段代码做了两个事情：</p>
<ol>
<li>这个 Iterator 的需要输入一个字符串数组。</li>
<li>在 <code>next()</code> 中，判断边界，并返回数组中索引为 offset 的字符串的首字母，并把 offset 加 1。</li>
</ol>
<p>这里省去了 Element 类型的声明，编译器可以根据 <code>next()</code> 的返回值类型推断出 Element 的类型。</p>
<p>有了已经实现好的 Iterator，就可以很简单的用它实现 Sequence，在 <code>makeIterator()</code> 中返回这个 Iterator 即可。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">struct</span> <span class="nc">FirstLetterSequence</span><span class="p">:</span> <span class="n">Sequence</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">strings</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">makeIterator</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">FirstLetterIterator</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="n">FirstLetterIterator</span><span class="p">(</span><span class="n">strings</span><span class="p">:</span> <span class="n">strings</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>现在 Sequence 已经实现好了，可以测试一下效果。
我们可以创建一个 FirstLetterSequence，并用 for-in 循环对其迭代：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">for</span> <span class="n">letter</span> <span class="k">in</span> <span class="n">FirstLetterSequence</span><span class="p">(</span><span class="n">strings</span><span class="p">:</span> <span class="p">[</span><span class="s">&quot;apple&quot;</span><span class="p">,</span> <span class="s">&quot;banana&quot;</span><span class="p">,</span> <span class="s">&quot;orange&quot;</span><span class="p">])</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">letter</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="c1">// a b o</span>
</div></code></pre></div>
</div>
<h4>值类型的 Iterator 和引用类型的 Iterator</h4>
<p>一般 Iterator 都是值类型的，值类型的 Iterator 的意思是：当把 Iterator 赋值给一个新变量时，是把原 Iterator 的所有状态拷贝了一份赋值给新的 Iterator，原 Iterator 在继续迭代时不会影响新的 Iterator。</p>
<p>例如用 stride 函数创建一个简单的 Sequence，它从 0 开始，到 9 截止，每次递增 1，即为 <code>[0, 1, 2, …, 8, 9]</code>。然后获取到它的 Iterator，调用 <code>next()</code> 进行迭代。之后我们再做一个赋值操作，创建一个新的 i2，并把 i1 的值赋给 i2，并进行一些操作：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">seq</span> <span class="p">=</span> <span class="bp">stride</span><span class="p">(</span><span class="n">from</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">to</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="n">by</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
</div><div class="line"><span class="kd">var</span> <span class="nv">i1</span> <span class="p">=</span> <span class="n">seq</span><span class="p">.</span><span class="n">makeIterator</span><span class="p">()</span>
</div><div class="line"><span class="n">i1</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(0)</span>
</div><div class="line"><span class="n">i1</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(1)</span>
</div><div class="line">
</div><div class="line"><span class="kd">var</span> <span class="nv">i2</span> <span class="p">=</span> <span class="n">i1</span>
</div><div class="line"><span class="n">i1</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(2)</span>
</div><div class="line"><span class="n">i1</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(3)</span>
</div><div class="line"><span class="n">i2</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(2)</span>
</div><div class="line"><span class="n">i2</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(3)</span>
</div></code></pre></div>
</div>
<p>从打印的结果会发现：i1 和 i2 是两个独立的 Iterator，它们互不影响，赋值时对 i1 做了一份完整的拷贝。所以这里的 Iterator 是一个值类型 Iterator。</p>
<p>当然，我们也可以把任意值类型的 Iterator 变成引用类型的 iterator，而且实施起来也很简单。把任何一个值类型 Iterator 用 AnyIterator 这个包一下就形成了一个引用类型的 Iterator。</p>
<p>结合上面的代码，我们再进行一些操作：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">var</span> <span class="nv">i3</span> <span class="p">=</span> <span class="n">AnyIterator</span><span class="p">(</span><span class="n">i1</span><span class="p">)</span>
</div><div class="line"><span class="kd">var</span> <span class="nv">i4</span> <span class="p">=</span> <span class="n">i3</span>
</div><div class="line"><span class="n">i3</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(4)</span>
</div><div class="line"><span class="n">i4</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(5)</span>
</div><div class="line"><span class="n">i3</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(6)</span>
</div><div class="line"><span class="n">i3</span><span class="p">.</span><span class="n">next</span><span class="p">()</span> <span class="c1">// Optional(7)</span>
</div></code></pre></div>
</div>
<p>引用类型的 Iterator，再赋值给一个新的变量后，新的 Iterator 和原 Iterator 在进行迭代时会互相对对方产生影响。</p>
<h4>基于函数的 Sequence 和 Iterator</h4>
<p>AnyIterator 有一个初始化器，可以传入一个闭包，AnyIterator 会把这个闭包的内容作为调用 <code>next()</code> 时执行的内容。这样创建一个 Iterator 时可以不用创建一个新的 class 或 struct。</p>
<p>例如我们可以这样创建一个斐波那契数列的 Iterator：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">fibsIterator</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">AnyIterator</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">state</span> <span class="p">=</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</div><div class="line">    <span class="k">return</span> <span class="n">AnyIterator</span> <span class="p">{</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">upcomingNumber</span> <span class="p">=</span> <span class="n">state</span><span class="p">.</span><span class="mi">0</span>
</div><div class="line">        <span class="n">state</span> <span class="p">=</span> <span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="mi">1</span><span class="p">,</span> <span class="n">state</span><span class="p">.</span><span class="mi">0</span> <span class="o">+</span> <span class="n">state</span><span class="p">.</span><span class="mi">1</span><span class="p">)</span>
</div><div class="line">        <span class="k">return</span> <span class="n">upcomingNumber</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>然后可以用 AnySequence 来创建 Sequence，AnySequence 也有一个支持传入闭包的初始化器，于是可以把上面的函数名作为参数传入。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">fibsSequence</span> <span class="p">=</span> <span class="n">AnySequence</span><span class="p">(</span><span class="n">fibsIterator</span><span class="p">)</span>
</div><div class="line"><span class="nb">Array</span><span class="p">(</span><span class="n">fibsSequence</span><span class="p">.</span><span class="kr">prefix</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span>
</div><div class="line"><span class="c1">// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]</span>
</div></code></pre></div>
</div>
<p>另外，还有一种更简单的方法来创建 Sequence，用 Swift 标准库中的 sequence 函数。这个函数有两个变体：</p>
<p>第一个是 <code>sequence(first:next:)</code>
第一个参数是 Sequence 中的第一个值，第二个参数传入一个闭包作为 <code>next()</code> 的内容。</p>
<p>例如创建一个从大到小的随机数 Sequence。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">randomNumbers</span> <span class="p">=</span> <span class="n">sequence</span><span class="p">(</span><span class="bp">first</span><span class="p">:</span> <span class="mi">100</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="n">previous</span><span class="p">:</span> <span class="nb">UInt32</span><span class="p">)</span> <span class="k">in</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">newValue</span> <span class="p">=</span> <span class="n">arc4random_uniform</span><span class="p">(</span><span class="n">previous</span><span class="p">)</span>
</div><div class="line">    <span class="k">guard</span> <span class="n">newValue</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="kc">nil</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="k">return</span> <span class="n">newValue</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="nb">Array</span><span class="p">(</span><span class="n">randomNumbers</span><span class="p">)</span>
</div><div class="line"><span class="c1">// [100, 90, 60, 35, 34, 21, 3]</span>
</div></code></pre></div>
</div>
<p>第二个变体是 <code>sequence(state:next:)</code>，这个要更为强大，它可以在迭代过程中修改状态。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">fibsSequence2</span> <span class="p">=</span> <span class="n">sequence</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="p">{</span> <span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="kr">inout</span> <span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">))</span> <span class="p">-&gt;</span> <span class="nb">Int</span><span class="p">?</span> <span class="k">in</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">upcomingNumber</span> <span class="p">=</span> <span class="n">state</span><span class="p">.</span><span class="mi">0</span>
</div><div class="line">    <span class="n">state</span> <span class="p">=</span> <span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="mi">1</span><span class="p">,</span> <span class="n">state</span><span class="p">.</span><span class="mi">0</span> <span class="o">+</span> <span class="n">state</span><span class="p">.</span><span class="mi">1</span><span class="p">)</span>
</div><div class="line">    <span class="k">return</span> <span class="n">upcomingNumber</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="nb">Array</span><span class="p">(</span><span class="n">fibsSequence2</span><span class="p">.</span><span class="kr">prefix</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span>
</div><div class="line"><span class="c1">// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]</span>
</div></code></pre></div>
</div>
<p><code>sequence(frist:next:)</code> 和 <code>sequence(state:next:)</code> 的返回值类型是一个 UnfoldSequence。</p>
<p>可能有人会好奇 unfold 是一个什么概念？其实它出自函数式编程的范畴里，在函数式编程中有 fold 和 unfold 的概念。fold 是把一系列的值变为一个值，例如 <code>reduce</code> 就是一个 fold 操作。unfold 是 fold 的反操作，把一个值展开成一系列的值。</p>
<h3>再回首</h3>
<p>结合着嵌套函数，Sequence 和 Iterator 这些知识点，让我们再重新阅读一下最开始的代码片段，不知道这一次你是否有了什么新的感受？</p>
<p>如何有任何疑问或者建议，欢迎交流！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 030 - Passing self to required Objective-C dependencies]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-030</guid><link>https://swiftsiqi.com/posts/Swift-Tips-030</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 19 Dec 2019 04:28:03 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902329_383814.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902329_383814.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902329_383814.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902329_383814.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902329_383814.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1022" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#30-passing-self-to-required-objective-c-dependencies">Swift Tips 030 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>像 DataLoader 和 Renderer 这样的工具类，八成是要与 Cocoa 框架打交道，例如 URLSession 的 delegate 需要继承 NSObject，CADisplayLink 的 selector 方法需要声明 @objc 关键字，这很不 Swift，为了提升使用体验，SDK 的维护者可以将其隔离在内部，就像今天的示例代码一样。</p>
<h3>一组对比</h3>
<p>为了让大家更好的理解今天的 tips，我们需要将示例中代码完整的呈现出来，不过由于篇幅的原因，我们只针对 DataLoader 进行详细的说明。</p>
<p>假设 SDK 维护者的意图是让使用者在 URL Session 的代理方法<code>urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?)</code>中注入自己的逻辑，</p>
<p>那么这里先用方案 A，也就是 Tips 里的思路来完善代码。</p>
<div class="block-code"><pre><code>// Plan A
// SDK Maintainer's Code
protocol DataLoaderADelegate: class {
    func dataLoader(_ dataLoader: DataLoaderA, session: URLSession, didBecomeInvalidWithError error: Error?)
}

class DataLoaderA: NSObject {
    lazy var urlSession: URLSession = self.makeURLSession()
    weak var urlSessionDelegate: DataLoaderADelegate?;

    private func makeURLSession() -&gt; URLSession {
        return URLSession(configuration: .default, delegate: self, delegateQueue: .main)
    }
}

extension DataLoaderA: URLSessionDelegate {
    internal func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        urlSessionDelegate?.dataLoader(self, session: session, didBecomeInvalidWithError: error)
    }
}

// SDK User's Code
class ControllerA : DataLoaderADelegate {
    lazy var dataLoader: DataLoaderA = {
        return DataLoaderA()
    }()

    func dataLoader(_ dataLoader: DataLoaderA, session: URLSession, didBecomeInvalidWithError error: Error?) {
        // do something
    }
}</code></pre></div>
<p>然后我们用另外一种方式来完成这个需求：</p>
<div class="block-code"><pre><code>// Plan B
// SDK Maintainer's Code
class DataLoaderB {
    var urlSession: URLSession?
}

// SDK User's Code
class ControllerB: NSObject {
    lazy var dataLoader: DataLoaderB = {
        let dataLoader = DataLoaderB()
        dataLoader.urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
        return dataLoader
    }()
}

extension ControllerB: URLSessionDelegate {
    internal func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        // do something
    }
}</code></pre></div>
<h3>不同的视角</h3>
<p>我们可以看到方案 A 和方案 B 在功能上是一致的，但从 SDK 使用者的角度上，体验可是不同的。</p>
<p>我们可以看到方案 B 的使用者，不仅需要让自身继承关系指向 NSObject，而且也需要手动实现 URLSessionDelegate 的代理方法，虽然目前代理方法看起来需要做的不多，但如果有些不痛不痒的 required 的方法（例如需要为 DataLoader 设置一个名称），其实不如将其封装在 SDK 内部。毕竟作为 SDK 的使用者，不需要一遍一遍的去写这些无用的代码。</p>
<p>同样是方案 B，我们换个角度，如果你是 SDK 维护者，虽然代码写的少了，但如果想禁止开发者使用某些URLSession 的代理方法时，或者想添加一些前置逻辑时，你是不是会发现有点力不从心！</p>
<p>接下来，我们看一下方案 A 的使用者，他需要关心的就很少了，只需要遵守对应的代理方法即可实现对应的功能，至于 NSObject 是什么，URLSession 的代理到底有哪些，他统统都不用关心，SDK 支持的能力会在协议里一览无余。代码看起来也少了点 Objective—C 的影子。</p>
<p>同样是方案 A，我们再次将视角换做 SDK 的维护者，虽然代码量相对方案 B 多了许多，但是可控制性变得强了。</p>
<p>综上所述，虽然方案 B 在代码量上有一定优势，但从代码风格和可控性上，还是方案 A 更胜一筹。</p>
<h3>成全别人，恶心自己</h3>
<p>在冯小刚导演的电影《私人订制》里有这么一句经典台词：</p>
<div class="blockquote"><blockquote><p>成全别人，恶心自己</p>
</blockquote></div>
<p>我想这句话，或许是很多基础 SDK 开发者的心声，因为在实现功能的同时，怎么让使用者在调用的时候也感觉到舒适也是需要考虑的。</p>
<p>不管怎样，今天的这个 tips 并不是什么奇淫巧技，更多的是在 SDK 的 API 设计上如何做到更人性化，你 get 到了么？</p>
<p>如果你有什么好的想法，欢迎分享！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 029 - Making weak or lazy properties readonly]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-029</guid><link>https://swiftsiqi.com/posts/Swift-Tips-029</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Mon, 02 Dec 2019 04:26:51 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902397_699142.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902397_699142.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902397_699142.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902397_699142.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902397_699142.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="806" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#29-making-weak-or-lazy-properties-readonly">Swift Tips 029 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>今天的这段代码为 Node 类设置了两个 readonly 的属性：parent 和 children，与其他 read only 属性不太一样的地方是：它们并没有显式的声明 getter 方法，而且看起来也不怎么像一个计算属性（因为只有计算属性才能 read only）。</p>
<div class="blockquote"><blockquote><p>通过为计算属性设置 get 和 set 方法，就可以实现计算属性的只读（只实现 get），可读可写（get 和 set），如果你想了解更多，可以阅读官方手册的<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/10_properties#computed-properties">属性章节</a>。需要注意的是，这里要区分存储属性和计算属性！</p>
</blockquote></div>
<h3>简洁优雅的 <code>private(set)</code></h3>
<p>如果你问一个 Swift 开发者：如果想声明一个公开 getter，隐藏 setter 的属性，最简洁最优雅的方式是什么。</p>
<p>我想九成的开发者会说 —- <code>private(set)</code></p>
<p>但是你有没有好奇过这个“简洁优雅”的由来，如果不用 <code>private(set)</code> 的话怎么做呢？</p>
<h3>实现一个相同的功能吧</h3>
<p>怎么看 <code>parent</code> 和 <code>children</code> 都像个存储属性，但它又做到了 read only，那么它到底是怎么实现只读的呢？</p>
<p>如果你没想到怎么做，来看看下面的代码吧，当然可行的解决方案有很多，下面只是其中一个而已！</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">class</span> <span class="nc">Node</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">_parent</span><span class="p">:</span> <span class="n">Node</span><span class="p">?</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">parent</span><span class="p">:</span> <span class="n">Node</span><span class="p">?</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="n">_parent</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">private</span> <span class="kr">lazy</span> <span class="kd">var</span> <span class="nv">_children</span> <span class="p">=</span> <span class="p">[</span><span class="n">Node</span><span class="p">]()</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">children</span><span class="p">:</span> <span class="p">[</span><span class="n">Node</span><span class="p">]</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="n">_children</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">add</span><span class="p">(</span><span class="n">child</span><span class="p">:</span> <span class="n">Node</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="n">_children</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">child</span><span class="p">)</span>
</div><div class="line">        <span class="n">child</span><span class="p">.</span><span class="n">_parent</span> <span class="p">=</span> <span class="kc">self</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>现在你能理解了么？</p>
<p>我们通过内置一个 private 访问级别的属性(<code>_parent</code>,<code>_children</code>)来持有真正的值，但向外只暴露一个与此关联的计算属性(<code>parent</code>, <code>children</code>)，通过这种方式我们实现了与 <code>private(set)</code> 相同的效果。</p>
<h3>再多说一点</h3>
<p>很显然，<code>private(set)</code>让代码变得更加简洁和优雅了！但是如果我想改变 getter 方法的访问等级，又该怎么办呢？</p>
<div class="blockquote"><blockquote><p>如果你不理解访问等级，建议阅读官方手册里关于<a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/26_access_control">访问控制一节的内容</a></p>
</blockquote></div>
<p>很简单，你只需要在 <code>private(set)</code> 前面加上对应的访问等级修饰符即可改变 getter 方法的访问等级了，例如下面的代码就将 <code>parent</code> 属性的 getter 方法变为了 public 级别。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">public</span> <span class="kd">private</span><span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">parent</span><span class="p">:</span> <span class="n">Node</span><span class="p">?</span>
</div></code></pre></div>
</div>
<p>说了这么多，今天的这个 tip 你喜欢么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 028 - Defining static URLs using string literals]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-028</guid><link>https://swiftsiqi.com/posts/Swift-Tips-028</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 15 Nov 2019 04:25:02 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902511_358225.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902511_358225.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902511_358225.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902511_358225.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902511_358225.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1274" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#28-defining-static-urls-using-string-literals">Swift Tips 028 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>图示里的代码让 URL 遵守了 ExpressibleByStringLiteral 协议，并重写了其对应的构造器，使我们能够通过字符串的字面量直接创建 URL 对象。</p>
<p>如果想在 Swift 3 之前实现同样的功能，我们需要实现 URL 的 <code>init(extendedGraphemeClusterLiteral value: StaticString)</code> 和 <code>init(unicodeScalarLiteral value: StaticString)</code> 方法。</p>
<div class="blockquote"><blockquote><p>示例代码里的 require(hint:) 方法出自 John Sundell 编写的开源库 <a href="https://github.com/johnsundell/require">Require</a>，这个仓库主要的功能是帮助开发者轻松地处理 Optional 值不为 nil 的情况或者由 Optional 造成的崩溃。</p>
</blockquote></div>
<h3>Literal Type 是什么</h3>
<p>在一些国外的 Swift 文章中，经常会提到 Literal Type，这里我们姑且称它为字面量类型。</p>
<p>要说清楚字面量类型，先说明一下什么是 Literal(字面量)，<a href="https://en.wikipedia.org/wiki/Literal_(computer_programming">Wiki 里面的定义</a>) 是这样的。</p>
<div class="blockquote"><blockquote><p>Literal (computer programming) - In computer science, a literal is a notation for representing a fixed value in source code. Almost all programming languages have notations for atomic values such as integers, floating-point numbers, and strings, and usually for booleans and characters; some also have notations for elements of enumerated types and compound values such as arrays, records, and objects. An anonymous function is a literal for the function type.</p>
</blockquote></div>
<p>简单来说，所谓的字面量，就是指一段能够表示特定类型，特定值的源代码表达式，Swift 里面的 10， true， “Hello” 都是字面量。</p>
<p>在得到了字面量的定义后，我们就很容易得出字面类型的定义了！字面量类型就是支持通过字面量进行实例初始化的数据类型，如 Swift 中的 Int，Bool，String 类型。</p>
<p>下面的列表中，展示了在 Swift 语言中，常见的字面量类型和其对应的实例：</p>
<div class="block-table"><table><thead>
<tr>
  <th>字面量类型的名称</th>
  <th>默认的数据类型</th>
  <th>示例</th>
</tr>
</thead>
<tbody>
<tr>
  <td>Integer</td>
  <td>Int</td>
  <td>123, 0b1010, 0o644, 0xFF,</td>
</tr>
<tr>
  <td>Floating-Point</td>
  <td>Double</td>
  <td>3.14, 6.02e23, 0xAp-2</td>
</tr>
<tr>
  <td>String</td>
  <td>String</td>
  <td>“Hello”, “”” . . . “””</td>
</tr>
<tr>
  <td>Extended Grapheme Cluster</td>
  <td>Character</td>
  <td>“A”, “é”, “🇺🇸”</td>
</tr>
<tr>
  <td>Unicode Scalar</td>
  <td>Unicode.Scalar</td>
  <td>“A”, “´”, “\u{1F1FA}”</td>
</tr>
<tr>
  <td>Boolean</td>
  <td>Bool</td>
  <td>true, false</td>
</tr>
<tr>
  <td>Nil</td>
  <td>Optional</td>
  <td>nil</td>
</tr>
<tr>
  <td>Array</td>
  <td>Array</td>
  <td>[1, 2, 3]</td>
</tr>
<tr>
  <td>Dictionary</td>
  <td>Dictionary</td>
  <td>[“a”: 1, “b”: 2]</td>
</tr>
</tbody>
</table></div><div class="blockquote"><blockquote><p>小提示：不能直接使用 nil 的字面量，因为 Swift 的类型推断无法确定 nil 到底是何种类型，这里我们需要这样使用 nil 字面量</p>
<div class="block-code"><pre><code>&gt; nil // ! cannot infer type
&gt; let a = nil as String?// Optional&lt;String&gt;.none
&gt; let b: String? = nil
&gt;</code></pre></div>
</blockquote></div>
<p>除了标准库以为，在 Playground 里面还有几个特定的字面量。</p>
<div class="block-table"><table><thead>
<tr>
  <th>字面量类型的名称</th>
  <th>默认的数据类型</th>
  <th>示例</th>
</tr>
</thead>
<tbody>
<tr>
  <td>Color</td>
  <td>NSColor/UIColor</td>
  <td>#colorLiteral(red: 1, green: 0, blue: 1, alpha: 1)</td>
</tr>
<tr>
  <td>Image</td>
  <td>NSImage / UIImage</td>
  <td>#imageLiteral(resourceName: “icon”)</td>
</tr>
<tr>
  <td>File</td>
  <td>URL</td>
  <td>#fileLiteral(resourceName: “articles.json”)</td>
</tr>
</tbody>
</table></div><h3>利用 Literal Protocol 实现 Literal Type</h3>
<p>既然知道了字面量类型，那么怎么让自己创建的数据类型拥有字面量初始化的能力呢？很简单，我们只需要遵循对应的协议并完成自己的实现即可！所以问题又来了，有哪些协议是开放给开发者使用的呢？</p>
<ul>
<li>ExpressibleByIntegerLiteral</li>
<li>ExpressibleByFloatLiteral</li>
<li>ExpressibleByStringLiteral</li>
<li>ExpressibleByExtendedGraphemeClusterLiteral</li>
<li>ExpressibleByUnicodeScalarLiteral</li>
<li>ExpressibleByBooleanLiteral</li>
<li>ExpressibleByNilLiteral</li>
<li>ExpressibleByArrayLiteral</li>
<li>ExpressibleByDictionaryLiteral</li>
</ul>
<p>其中需要注意的是 <code>ExpressibleByStringLiteral</code> 继承了 <code>ExpressibleByExtendedGraphemeClusterLiteral</code> 和 <code>ExpressibleByUnicodeScalarLiteral</code> 两个协议。</p>
<p>下面的例子展示了如何让一个自定义类型支持数字字面量初始化和字符串字面量初始化。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">struct</span> <span class="nc">Money</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">value</span><span class="p">:</span> <span class="nb">Double</span>
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">Double</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">value</span> <span class="p">=</span> <span class="n">value</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// 实现ExpressibleByIntegerLiteral字面量协议</span>
</div><div class="line"><span class="kd">extension</span> <span class="nc">Money</span><span class="p">:</span> <span class="n">ExpressibleByIntegerLiteral</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">typealias</span> <span class="nb">IntegerLiteralType</span> <span class="p">=</span> <span class="nb">Int</span>
</div><div class="line">    <span class="kd">public</span> <span class="kd">init</span><span class="p">(</span><span class="n">integerLiteral</span> <span class="n">value</span><span class="p">:</span> <span class="nb">IntegerLiteralType</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">Double</span><span class="p">(</span><span class="n">value</span><span class="p">))</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// 实现ExpressibleByStringLiteral字面量协议</span>
</div><div class="line"><span class="kd">extension</span> <span class="nc">Money</span><span class="p">:</span> <span class="n">ExpressibleByStringLiteral</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">public</span> <span class="kd">init</span><span class="p">(</span><span class="n">stringLiteral</span> <span class="n">value</span><span class="p">:</span> <span class="nb">StringLiteralType</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="k">if</span> <span class="kd">let</span> <span class="nv">doubleValue</span> <span class="p">=</span> <span class="nb">Double</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">            <span class="kc">self</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="n">doubleValue</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">            <span class="kc">self</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">intMoney</span><span class="p">:</span> <span class="n">Money</span> <span class="p">=</span> <span class="mi">10</span>     <span class="c1">// 通过整数字面量初始化</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">strMoney</span><span class="p">:</span> <span class="n">Money</span> <span class="p">=</span> <span class="s">&quot;10.2&quot;</span> <span class="c1">// 通过字符串字面量初始化</span>
</div></code></pre></div>
</div>
<h3>Literal 在 Swift 5 里的进化</h3>
<p>在 Swift 5 的发布过程中，核心团队接纳了社区的 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0200-raw-string-escaping.md">SE-0200</a> 提议，这个提议为 Swift 添加了与原始字符串字面量(Raw String Literals)相关的功能，在这项提议中反斜杠(<code>\</code>)和井号(<code>#</code>)在某些场景下是被当作为标点符号而不是转义字符或字符串终止符。这使得许多用法变得更容易，特别是正则表达式。</p>
<p>关于这个知识点可以阅读 Paul Hudson 的文章 - <a href="https://www.hackingwithswift.com/articles/126/whats-new-in-swift-5-0">What’s new in Swift 5.0</a></p>
<p>Swift 5.0 里还有一个与字面量相关的提议 - <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0213-literal-init-via-coercion.md">SE-0213: Literal initialization via coercion</a>，简单来说它就是让字面量的创建更加合理，也更加有效率了。</p>
<h3>Literal Protocol 带来的好处</h3>
<p>通过今天的 Tips，我们应该可以发现字面量协议是一个非常有意思的东西，通过它，我们甚至可以通过一个字符串来初始化一个 UIViewController ！(但我估计不会有人想这么做…..)</p>
<p>虽然，这种自由度确实带来了许多可能性，但我确实没有想到十分贴合的业务场景，不过知道这个小技巧还是没什么不好的吧。</p>
<p>关于 Literal Protocol，你有什么好的使用场景么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 027 - Manipulating points, sizes and frames using math operators]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-027</guid><link>https://swiftsiqi.com/posts/Swift-Tips-027</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 10 Nov 2019 04:23:29 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902606_876523.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902606_876523.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902606_876523.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902606_876523.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902606_876523.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="734" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#27-manipulating-points-sizes-and-frames-using-math-operators">Swift Tips 027 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>这段代码在 CGSize 类型中重载了名为 * 的中缀运算符，新的定义使其能够按照右侧的值等倍扩大 CGSize 中的 width 和 height。</p>
<p>通过这个小 Tips 使代码的可读性变强，也更加简洁！</p>
<h3>运算符也是一等公民</h3>
<p>运算符在 Swift 中是一个函数，而函数在 Swift 中是一等公民，所以正是基于这两点，开发者可以在运算符这个点上做很多有意义的事情。而我最近就发现了一个关于自定义运算符的使用场景，这里与大家探讨一下。</p>
<p>我们都知道 Swift 里的 <code>do, try, catch</code> 在处理异常情况时十分有用，尤其在处理那些可以失败的异步操作时。 <code>do, try, catch</code> 的机制可以让我们在函数出现问题时，轻松的退出当前函数并执行一些相应的操作，例如我们从硬盘里读取笔记的数据模型（如同之前定义的 loadNote 函数一样）</p>
<p>假设我们有如下一段代码：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">extension</span> <span class="nc">NoteManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">enum</span> <span class="nc">LoadingError</span><span class="p">:</span> <span class="n">Error</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="n">invalidFile</span><span class="p">(</span><span class="n">Error</span><span class="p">)</span>
</div><div class="line">        <span class="k">case</span> <span class="n">invalidData</span><span class="p">(</span><span class="n">Error</span><span class="p">)</span>
</div><div class="line">        <span class="k">case</span> <span class="n">decodingFailed</span><span class="p">(</span><span class="n">Error</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">class</span> <span class="nc">NoteManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">loadNote</span><span class="p">(</span><span class="n">fromFileNamed</span> <span class="n">fileName</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="kr">throws</span> <span class="p">-&gt;</span> <span class="n">Note</span> <span class="p">{</span>
</div><div class="line">        <span class="k">do</span> <span class="p">{</span>
</div><div class="line">            <span class="kd">let</span> <span class="nv">file</span> <span class="p">=</span> <span class="k">try</span> <span class="n">fileLoader</span><span class="p">.</span><span class="n">loadFile</span><span class="p">(</span><span class="n">named</span><span class="p">:</span> <span class="n">fileName</span><span class="p">)</span>
</div><div class="line">            <span class="k">do</span> <span class="p">{</span>
</div><div class="line">                <span class="kd">let</span> <span class="nv">data</span> <span class="p">=</span> <span class="k">try</span> <span class="n">file</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
</div><div class="line">                <span class="k">do</span> <span class="p">{</span>
</div><div class="line">                    <span class="k">return</span> <span class="k">try</span> <span class="n">Note</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span>
</div><div class="line">                <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">                    <span class="k">throw</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">decodingFailed</span><span class="p">(</span><span class="n">error</span><span class="p">)</span>
</div><div class="line">                <span class="p">}</span>
</div><div class="line">            <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">                <span class="k">throw</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">invalidData</span><span class="p">(</span><span class="n">error</span><span class="p">)</span>
</div><div class="line">            <span class="p">}</span>
</div><div class="line">        <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">            <span class="k">throw</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">invalidFile</span><span class="p">(</span><span class="n">error</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>上面的代码已经是一种朴素的不能再朴素的写法了，但我相信没人会喜欢读上面的代码，因为它让你很头大……</p>
<p>那么有什么办法能让代码变得更友善一点呢？这里我尝试采用新增自定义运算符的方式来解决这个问题！</p>
<p>下面的代码定义一个新的运算符 <code>~&gt;</code> 并重构了之前的代码。</p>
<div class="blockquote"><blockquote><p>至于为什么选择 <code>~&gt;</code>，是因为它很像 <code>-&gt;</code>（函数返回值）， 但是又不相同，这就意味着可能会返回与正常值不同的东西，例如一个 error ！</p>
</blockquote></div>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kr">infix</span> <span class="kd">operator</span> <span class="o">~&gt;</span>
</div><div class="line">
</div><div class="line"><span class="kd">func</span> <span class="o">~&gt;&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">expression</span><span class="p">:</span> <span class="kr">@autoclosure</span> <span class="p">()</span> <span class="kr">throws</span> <span class="p">-&gt;</span> <span class="n">T</span><span class="p">,</span>
</div><div class="line">           <span class="n">errorTransform</span><span class="p">:</span> <span class="p">(</span><span class="n">Error</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Error</span><span class="p">)</span> <span class="kr">throws</span> <span class="p">-&gt;</span> <span class="n">T</span> <span class="p">{</span>
</div><div class="line">    <span class="k">do</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="k">try</span> <span class="n">expression</span><span class="p">()</span>
</div><div class="line">    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</div><div class="line">        <span class="k">throw</span> <span class="n">errorTransform</span><span class="p">(</span><span class="n">error</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">class</span> <span class="nc">NoteManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">loadNote</span><span class="p">(</span><span class="n">fromFileNamed</span> <span class="n">fileName</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="kr">throws</span> <span class="p">-&gt;</span> <span class="n">Note</span> <span class="p">{</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">file</span> <span class="p">=</span> <span class="k">try</span> <span class="n">fileLoader</span><span class="p">.</span><span class="n">loadFile</span><span class="p">(</span><span class="n">named</span><span class="p">:</span> <span class="n">fileName</span><span class="p">)</span> <span class="o">~&gt;</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">invalidFile</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">data</span> <span class="p">=</span> <span class="k">try</span> <span class="n">file</span><span class="p">.</span><span class="n">read</span><span class="p">()</span> <span class="o">~&gt;</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">invalidData</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">note</span> <span class="p">=</span> <span class="k">try</span> <span class="n">Note</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span> <span class="o">~&gt;</span> <span class="n">LoadingError</span><span class="p">.</span><span class="n">decodingFailed</span>
</div><div class="line">        <span class="k">return</span> <span class="n">note</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>看到了么？这里我们实现了 <code>~&gt;</code> 运算符，它是一个中缀运算符，左边是一个会抛出异常的表达式，右边是一个定义为 <code>(error)-&gt;error</code> 的表达式, 而整个运算符的返回值与左边表达式的返回值一致，不论是正常执行，还是出现异常！</p>
<p>也许你会好奇，为什么在调用 <code>~&gt;</code> 的时候，右边的表达式是 <code>LoadingError.invalidFile</code> 呢，这是因为在 Swift 中，带关联值的枚举本身也是一个函数，如果你有点忘了这一点，不妨看看之前的 <a href="http://sketchk.xyz/2019/09/16/Swift-tips-014/">Swift Tips 014 - Referring to enum cases with associated values as closures</a></p>
<p>代码是不是变得更加整洁了一些呢？当然这种新增的运算符也会增加大家的理解成本，不过为了更优雅，更整洁的代码，这似乎也是可以接受的！</p>
<p>总之，这就是今天我想分享的自定义运算符在 <code>do, try, catch</code> 里的一点实际应用。</p>
<h3>One More Thing</h3>
<p>如果你很喜欢今天的 Tips，不妨关注一下 <a href="https://github.com/JohnSundell/CGOperators">CGOperators</a> 这个仓库，它提供了许多与 Core Graphics 相关的数学操作符，尤其是你经常需要在代码里操作 CGPoint，CGSize 和 CGVector 类型的话，你会发现这是一个非常贴心的小助手！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 026 - Using closure types in generic constraints]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-026</guid><link>https://swiftsiqi.com/posts/Swift-Tips-026</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 09 Nov 2019 04:22:10 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304902681_953376.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304902681_953376.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304902681_953376.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304902681_953376.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304902681_953376.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1058" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#26-using-closure-types-in-generic-constraints">Swift Tips 026 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>这段代码利用扩展的方式为 Sequence 增加了 2 个 API，并通过 <code>where</code> 语句约束了元素的类型为 <code>()-&gt;Void</code> 或者 <code>()-&gt;String</code> 才可以使用其对应的 API。</p>
<p>通过 Swift 泛型的类型约束能力，我们让代码变得又那么优雅了一点！</p>
<h3>关于泛型的理念</h3>
<p>如果我们希望实现一个数组里的元素能够累加的功能，你会想到什么方法么？</p>
<p>当然创建一个类似 <code>sum(_ numbers: [Int])</code> 的 top-level 级别的函数没任何毛病，不过我们今天并不打算采用这种方式。我们要采用另外一种方式：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">extension</span> <span class="nc">Array</span> <span class="k">where</span> <span class="n">Element</span><span class="p">:</span> <span class="n">Numeric</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">sum</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Element</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="bp">reduce</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">+</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>这里我们为 Array 创建了一个扩展，并指明 Element 必须遵循 Numeric 协议。如果你好奇为什么 reduce 也能实现累加的功能，不妨去看看 reduce 函数的定义，这里就不展开说明了。</p>
<p>也许看到这里你会想，这样设计 API 有什么用呢？从功能上，确实它和 <code>sum(_ numbers: [Int])</code> 这样的 top-level 级别的函数没什么区别，但从使用上，我们设计的 API 和其使用类型建立了一种关联，这种关联让我们在使用上更加便利，如果你还是不太明白我在说什么，我们来做个调用上的比对吧。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">totalPrice</span> <span class="p">=</span> <span class="n">sum</span><span class="p">(</span><span class="n">itemPrices</span><span class="p">)</span>    <span class="c1">// itemPrices 和 sum 没有关联，不能通过 itemPrice 寻找到 sum 函数</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">totalPrice</span> <span class="p">=</span> <span class="n">itemPrices</span><span class="p">.</span><span class="n">sum</span><span class="p">()</span>   <span class="c1">// sum 是 itemPrice 类型独有的方法，使用点语法即可搜寻到 sum 函数</span>
</div></code></pre></div>
</div>
<h3>类型约束的变化与增强</h3>
<p>泛型的理念是希望我们可以根据抽象概念的特征来描述类型，而不是直接使用它们的具体类型，这种描述类型的能力是需要编程语言提供相应的约束能力才能实现的。当前版本的 Swift 虽然在泛型编程上有了不少亮点，但它一开始也并不是如此好用。</p>
<div class="blockquote"><blockquote><p>关于泛型的知识点可以在 <a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/22_generics">官方文档</a> 里找到相应的教学内容，本篇 Tips 就不展开介绍了！</p>
</blockquote></div>
<p>在 Swift 3.1 之前，泛型的类型约束只能限制其遵循某个特定的 protocol (例如上面的 Numeric) 或者基于某个特定的 subclasses，虽然也能解决大多数问题，但也有其局限性，随着 Swift 3.1 和 4 之后，Swift 泛型的类型约束能力得到了极大的增强。</p>
<p>举个例子说，如果我们有一个计算包含字符串的集合里的所有单词数的需求。在 Swift 3.1 之后，我们完全可以这样写：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">extension</span> <span class="nc">Collection</span> <span class="k">where</span> <span class="n">Element</span> <span class="p">==</span> <span class="nb">String</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">countWords</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">Int</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="bp">reduce</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="bp">count</span><span class="p">,</span> <span class="n">string</span> <span class="k">in</span>
</div><div class="line">            <span class="kd">let</span> <span class="nv">components</span> <span class="p">=</span> <span class="n">string</span><span class="p">.</span><span class="n">components</span><span class="p">(</span><span class="n">separatedBy</span><span class="p">:</span> <span class="p">.</span><span class="n">whitespacesAndNewlines</span><span class="p">)</span>
</div><div class="line">            <span class="k">return</span> <span class="bp">count</span> <span class="o">+</span> <span class="n">components</span><span class="p">.</span><span class="bp">count</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>在上面的代码中，我们为 Collection 类型增加了扩展，并限定了 Collection 中的 Element 为 String 类型，这样就限制了 API 的使用场景。</p>
<p>另外一个变化就是今天代码截图里的知识点，那就是闭包类型也可以作为类型约束的条件之一了，通过这种能力，我们可以让代码变得更加优雅和简洁。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">extension</span> <span class="nc">Sequence</span> <span class="k">where</span> <span class="n">Element</span> <span class="p">==</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">Void</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">callAll</span><span class="p">()</span> <span class="p">{</span>
</div><div class="line">        <span class="k">for</span> <span class="n">closure</span> <span class="k">in</span> <span class="kc">self</span> <span class="p">{</span>
</div><div class="line">            <span class="n">closure</span><span class="p">()</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="n">observers</span><span class="p">.</span><span class="n">callAll</span><span class="p">()</span>
</div></code></pre></div>
</div>
<h3>协议里的类型约束</h3>
<p>类型约束的另外一个使用场景就是在 protocol 里定义 API。这种在 protocol 里定义 API 的方式算是开发中的一个最佳实践。但如果使用不当，也会带来一些棘手的问题。</p>
<p>这里我们还是举一个实际的例子，我们定义了如下的协议：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">protocol</span> <span class="nc">ModelManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Model</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">models</span><span class="p">(</span><span class="n">matching</span> <span class="n">query</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">Model</span><span class="p">]</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>为了让整个 app 中能够通过一个相同的接口来响应请求并获取数据，我们定义了 ModelManager 并添加了一个对应的 API，</p>
<p>虽然上面的代码乍一看没什么毛病，但细细品来，似乎这个接口设计的还是有点缺陷，毕竟它的输入和输出都绑定了一个具体的类型上，这让它的灵活性受到了极大的限制。</p>
<p>为了解决这个问题，我们可以在 protocol 里使用关联类型，一个叫做 Query 类型，用来抽象查询指令的概念，另一个是 Collection 类型，用来抽象查询后的结果，并且我们在这里设定了 Collection 里的元素与 Model 类型一致。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">protocol</span> <span class="nc">ModelManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Model</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Collection</span><span class="p">:</span> <span class="n">Swift</span><span class="p">.</span><span class="n">Collection</span> <span class="k">where</span> <span class="n">Collection</span><span class="p">.</span><span class="n">Element</span> <span class="p">==</span> <span class="n">Model</span>
</div><div class="line">    <span class="kd">associatedtype</span> <span class="n">Query</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">models</span><span class="p">(</span><span class="n">matching</span> <span class="n">query</span><span class="p">:</span> <span class="n">Query</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Collection</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>通过这种方式，我们可以自由的实现 <code>models(matching query: Query) -&gt; Collection</code> 方法，而且还保证其遵循一定的规律。</p>
<p>例如下面的两个实现，都采用了 Enum 类型来表示 Query 占位类型。在 UserManager 里，我们使用 Array 来表示 Collection 占位类型，而再 MovieManager 里，我们使用了 Dict 来表示 Collection 占位类型。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// case 1</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">UserManager</span><span class="p">:</span> <span class="n">ModelManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">typealias</span> <span class="n">Model</span> <span class="p">=</span> <span class="n">User</span>
</div><div class="line">
</div><div class="line">    <span class="kd">enum</span> <span class="nc">Query</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="n">name</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line">        <span class="k">case</span> <span class="n">ageRange</span><span class="p">(</span><span class="nb">Range</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">models</span><span class="p">(</span><span class="n">matching</span> <span class="n">query</span><span class="p">:</span> <span class="n">Query</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">User</span><span class="p">]</span> <span class="p">{</span>
</div><div class="line">        <span class="p">...</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// case 2</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">MovieManager</span><span class="p">:</span> <span class="n">ModelManager</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">typealias</span> <span class="n">Model</span> <span class="p">=</span> <span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="n">Genre</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="n">Movie</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">    <span class="kd">enum</span> <span class="nc">Query</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="n">name</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line">        <span class="k">case</span> <span class="n">director</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">models</span><span class="p">(</span><span class="n">matching</span> <span class="n">query</span><span class="p">:</span> <span class="n">Query</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">Genre</span> <span class="p">:</span> <span class="n">Movie</span><span class="p">]</span> <span class="p">{</span>
</div><div class="line">       <span class="p">...</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h3>泛型是把双刃剑</h3>
<p>通过泛型，我们可以定义符合自己需求的类型约束，这些约束将提供更为强大的泛型编程能力。像可哈希（hashable） 这种抽象概念，我们完全可以根据它们的概念特征来描述类型，而不是它们的具体类型，就像上面的示例一样。</p>
<p>尤其在 Swift 4 之后，泛型的能力变得十分强大，这让我们可以写出更优秀的代码，但过于抽象的也会增加理解上的负担，所以也不是所有的代码都要进行抽象化和泛型化。</p>
<p>那么你是如何把握好这个度的呢？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 025 - Using associated enum values to avoid state-specific optionals]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-025</guid><link>https://swiftsiqi.com/posts/Swift-Tips-025</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 08 Nov 2019 04:19:48 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://www.sketchk.xyz/2019/11/08/Swift-Tips-025/01.png" type="image/webp"><img src="https://www.sketchk.xyz/2019/11/08/Swift-Tips-025/01.png" alt="01.png"loading="lazy" decoding="async" width="500" height="500" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#25-using-associated-enum-values-to-avoid-state-specific-optionals">Swift Tips 025 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>截图里的上半部分是 Player 类型的定义，在这个定义里面，我们看到它使用 5 个属性来表示游戏里的状态和相关数据。</p>
<p>而截图里的下半部分是对 Player 类型 的定义进行了重构，增加了一个嵌套的枚举类型 State ，它将原先的 5 个属性整合在 State 类型里，并使用了一个名为 state 属性来表明游戏状态。</p>
<h3>为什么能这么做</h3>
<p>首先， Swift 区别于 Objective-C 和其他语言的一个特点就在于它支持嵌套类型。</p>
<p>我们知道枚举常被用于为特定类或结构体实现某些功能。类似地，枚举可以方便的定义工具类或结构体，从而为某个复杂的类型所使用。为了实现这种功能，Swift 支持定义嵌套类型，可以在支持的类型中定义嵌套的枚举、类和结构体。</p>
<p>要在一个类型中嵌套另一个类型，将嵌套类型的定义写在其外部类型的 {} 内，而且可以根据需要定义多级嵌套。</p>
<p>所以我们看到了重构后的 Player 类型里嵌套了一个 State 类型的枚举值。</p>
<p>其次，在 Swift 的枚举值中，我们可以通过关联值和原始值两种方式将一些有用的数据存储在枚举类型中，而且支持的数据类型也十分丰富，不像 Objective-C 一样，只支持整数类型。下面的代码就展示了 Swift 中枚举的关联值和原始值。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// 这是一个使用关联值来描述两种商品条形码的枚举，分别是标有 UPC 格式的一维条形码和标有 QR 码格式的二维码。</span>
</div><div class="line"><span class="kd">enum</span> <span class="nc">Barcode</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">upc</span><span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">)</span>
</div><div class="line">    <span class="k">case</span> <span class="n">qrCode</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// 这是一个使用 ASCII 码作为原始值的枚举。</span>
</div><div class="line"><span class="kd">enum</span> <span class="nc">ASCIIControlCharacter</span><span class="p">:</span> <span class="nb">Character</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">tab</span> <span class="p">=</span> <span class="s">&quot;</span><span class="se">\t</span><span class="s">&quot;</span>
</div><div class="line">    <span class="k">case</span> <span class="n">lineFeed</span> <span class="p">=</span> <span class="s">&quot;</span><span class="se">\n</span><span class="s">&quot;</span>
</div><div class="line">    <span class="k">case</span> <span class="n">carriageReturn</span> <span class="p">=</span> <span class="s">&quot;</span><span class="se">\r</span><span class="s">&quot;</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h3>如何取出枚举里的关联值</h3>
<p>在编辑组内审核这篇 tips 的过程中，某位同事突然提出了一个问题：“如果我想把关联值取出来，应该怎么做呢？”</p>
<p>如果你脑海里马上浮现出来了解决办法，那很棒！可是如果你没什么想法的话，不妨接着往下看。</p>
<p>针对下面的枚举类型，最朴素的方式估计是这样取值的：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">enum</span> <span class="nc">Barcode</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">upc</span><span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">)</span>
</div><div class="line">    <span class="k">case</span> <span class="n">qrCode</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">productBarcode</span> <span class="p">=</span> <span class="n">Barcode</span><span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</div><div class="line">
</div><div class="line"><span class="kd">var</span> <span class="nv">result</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">;</span>
</div><div class="line"><span class="k">switch</span> <span class="n">productBarcode</span> <span class="p">{</span>
</div><div class="line"><span class="k">case</span> <span class="kd">let</span> <span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">):</span>
</div><div class="line">    <span class="n">result</span> <span class="p">=</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span> <span class="k">as</span> <span class="nb">Any</span>
</div><div class="line"><span class="k">case</span> <span class="kd">let</span> <span class="p">.</span><span class="n">qrCode</span><span class="p">(</span><span class="n">string</span><span class="p">):</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">string</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="bp">print</span><span class="p">(</span><span class="n">result</span><span class="p">!)</span>
</div><div class="line"><span class="c1">//&quot;(1, 2, 3, 4)\n&quot;</span>
</div></code></pre></div>
</div>
<p>当然，你可能觉得这样不够优雅，在 Swift 里面，Enum 是不同于 C 和 Objective-C 里的枚举，我们可以在其定义里添加一个方法，就如 Stack Overflow 上的一个<a href="https://stackoverflow.com/questions/24263539/accessing-an-enumeration-association-value-in-swift">高分回答</a>一样：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">enum</span> <span class="nc">Barcode</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">upc</span><span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">)</span>
</div><div class="line">    <span class="k">case</span> <span class="n">qrCode</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">associatedValue</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">Any</span><span class="p">{</span>
</div><div class="line">        <span class="k">switch</span> <span class="kc">self</span> <span class="p">{</span>
</div><div class="line">        <span class="k">case</span> <span class="kd">let</span> <span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">):</span>
</div><div class="line">            <span class="k">return</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
</div><div class="line">        <span class="k">case</span> <span class="kd">let</span> <span class="p">.</span><span class="n">qrCode</span><span class="p">(</span><span class="n">string</span><span class="p">):</span>
</div><div class="line">            <span class="k">return</span> <span class="n">string</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">productBarcode</span> <span class="p">=</span> <span class="n">Barcode</span><span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">anotherResult</span> <span class="p">=</span> <span class="n">productBarcode</span><span class="p">.</span><span class="n">associatedValue</span><span class="p">()</span>
</div><div class="line"><span class="bp">print</span><span class="p">(</span><span class="n">anotherResult</span><span class="p">)</span>
</div><div class="line"><span class="c1">//&quot;(1, 2, 3, 4)\n&quot;</span>
</div></code></pre></div>
</div>
<p>但还有更优雅的方案么？Swift 丰富的语法糖拯救了我们！那就是 <code>if-let-case</code> !</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">enum</span> <span class="nc">Barcode</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">upc</span><span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">)</span>
</div><div class="line">    <span class="k">case</span> <span class="n">qrCode</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">productBarcode</span> <span class="p">=</span> <span class="n">Barcode</span><span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</div><div class="line">
</div><div class="line"><span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="nv">Barcode</span><span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span> <span class="p">=</span> <span class="n">productBarcode</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="c1">//&quot;1 2 3 4\n&quot;</span>
</div></code></pre></div>
</div>
<p>当然你也可以这么取枚举值</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="nv">Barcode</span><span class="p">.</span><span class="n">upc</span><span class="p">(</span><span class="n">a</span><span class="p">)</span> <span class="p">=</span> <span class="n">productBarcode</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="c1">//&quot;(1, 2, 3, 4)\n&quot;</span>
</div></code></pre></div>
</div>
<p>如果你对 <code>if-let-case</code> 感兴趣，不妨看看这个网站 <a href="http://fuckingifcaseletsyntax.com/">How Do I Write If Case Let in Swift?</a></p>
<div class="blockquote"><blockquote><p>附带的推荐一下这个网站：<a href="http://fuckingclosuresyntax.com/">How Do I Declare a Closure in Swift?</a></p>
</blockquote></div>
<h3>这样做的好处</h3>
<p>在原先的 5 个独立属性中，我们很难发现他们之间的关联，但仔细阅读后，我们还是可以发现，这几个属性是互斥的。</p>
<p>就好比 <code>isWaitingForMatchMaking</code> 为 false 的时候，就像喷射战士 2 这种游戏里的对战房间还没创建好，所以像 <code>invitingUser</code> 是不可能有值的，更别说其他的几个属性。</p>
<p>而 <code>playerDefeatedBy</code> 和 <code>roundDefeatedIn</code> 这两个属性其实是存在关联的，我们不可能同时把 <code>playerDefeatedBy</code> 设置为第 3 轮里把我们消灭掉的敌人，而在 <code>roundDefeatedIn</code> 里设置成 1。</p>
<p>所以结合嵌套类型和关联值这两个特性后，我们看到重构后的代码将原有的 5 个属性划分成了游戏的四个状态并相互独立，对于代码逻辑的梳理也变得更加清晰明了。</p>
<p>所以将互斥的业务属性整合到枚举中，这个 tips 你 get 到了么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 024 - Using enums for async result types]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-024</guid><link>https://swiftsiqi.com/posts/Swift-Tips-024</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 01 Nov 2019 04:14:23 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903163_173166.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304903163_173166.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304903163_173166.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304903163_173166.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304903163_173166.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1166" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#24-using-enums-for-async-result-types">Swift Tips 024 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>截图展示了在处理异步回调时，通过使用枚举类型作为回调函数的参数，可以让代码的自解释性和可读性变得更好。</p>
<h3>为什么要使用枚举类型</h3>
<p>假设我们不使用枚举类型作为回调函数的参数，我们的代码大体会如下所示：</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903143_144796.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1598/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304903143_144796.png" alt="image.png"loading="lazy" decoding="async" width="1598" height="734" /></picture></figure></div><p>相比于之前的代码，我们发现 x 的含义变得十分模糊。</p>
<p>作为调用者，我并不能确定 x 等于 true 的时候，代码到底代表着什么含义；如果 API 设计者的本意是将 true 作为 unavailable 的含义，那我上面的这段代码就大错特错了……</p>
<p>所以为了保险起见，我可能十分有必要去查看 API 文档或者看一下 SDK 的内部实现。虽然可以通过改变方法名来改善这一现状，但极有可能会让方法名变得比较啰嗦，这就变得很不 swift 了，不是么？</p>
<p>所以今天的这个小技巧很好的解决了这个问题！</p>
<h3>枚举在 Swift 5 里的一点小变化</h3>
<p>鉴于今天的 tips 比较简短，我们不妨再说说枚举在 Swift 5 里的一点小变化，在 Swift 5 之前，你可以编写一个带有可变参数的枚举值：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">enum</span> <span class="nc">X</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">foo</span><span class="p">(</span><span class="n">bar</span><span class="p">:</span> <span class="nb">Int</span><span class="p">...)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">func</span> <span class="nf">baz</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">X</span> <span class="p">{</span>
</div><div class="line">    <span class="k">return</span> <span class="p">.</span><span class="n">foo</span><span class="p">(</span><span class="n">bar</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> 
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>而现在如果这么做就会出错。现在参数改成了一个数组，并且需要显式传入数组：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">enum</span> <span class="nc">X</span> <span class="p">{</span>
</div><div class="line">    <span class="k">case</span> <span class="n">foo</span><span class="p">(</span><span class="n">bar</span><span class="p">:</span> <span class="p">[</span><span class="nb">Int</span><span class="p">])</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="kd">func</span> <span class="nf">baz</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">X</span> <span class="p">{</span>
</div><div class="line">    <span class="k">return</span> <span class="p">.</span><span class="n">foo</span><span class="p">(</span><span class="n">bar</span><span class="p">:</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>所以要记住，枚举的关联值不能再使用可变参数了哦！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 023 - Working on async code in a playground]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-023</guid><link>https://swiftsiqi.com/posts/Swift-Tips-023</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 30 Oct 2019 04:08:36 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903499_902335.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304903499_902335.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304903499_902335.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304903499_902335.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304903499_902335.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="842" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#23-working-on-async-code-in-a-playground">Swift Tips 023 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>为了使 Playground 具有延时运行的本领，我们需要引入 Playground 的 “扩展包” PlaygroundSupport 框架。现在这个框架中包含了几个与 Playground 的行为交互以及控制 Playground 特性的 API，其中就包括使 Playground 能延时执行的黑魔法，PlaygroundPage 和 needsIndefiniteExecution。</p>
<h3>Playground 里的异步执行</h3>
<p>Playground 中的代码是顶层代码(top-level code)，也就是它是在于全局作用域中的。这些代码将会从上到下执行，并在执行完毕之后立即停止。</p>
<p>我们的异步回调代码一般都无法在程序结束之前获得执行，因此如果我们在 Playground 执行网络，或者其它耗时的异步操作，都无法获得我们想要的结果。</p>
<p>为了让程序在代码执行结束后继续执行，我们可以使用如下代码：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">PlaygroundPage</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">needsIndefiniteExecution</span> <span class="p">=</span> <span class="kc">true</span>
</div></code></pre></div>
</div>
<p>这句代码会让 Playground 永远执行下去 ，当我们获取了需要的结果后，可以使用 <code>PlaygroundPage.current.finishExecution()</code> 停止 Playground 的执行：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">import</span> <span class="nc">PlaygroundSupport</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">Foundation</span>
</div><div class="line"><span class="kd">import</span> <span class="nc">UIKit</span>
</div><div class="line">
</div><div class="line"><span class="n">PlaygroundPage</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">needsIndefiniteExecution</span> <span class="p">=</span> <span class="kc">true</span>
</div><div class="line">
</div><div class="line"><span class="kd">let</span> <span class="nv">url</span> <span class="p">=</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&quot;http://xxx/image/png&quot;</span><span class="p">)</span><span class="o">!</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">session</span> <span class="p">=</span> <span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">task</span> <span class="p">=</span> <span class="n">session</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="kc">_</span><span class="p">,</span> <span class="kc">_</span><span class="p">)</span> <span class="k">in</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">image</span> <span class="p">=</span> <span class="bp">UIImage</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">data</span><span class="p">!)</span>
</div><div class="line">    <span class="n">PlaygroundPage</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">finishExecution</span><span class="p">()</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="n">task</span><span class="p">.</span><span class="n">resume</span><span class="p">()</span>
</div></code></pre></div>
</div>
<h3>关于 Playground 的 Sources 目录</h3>
<p>通常情况下，我们直接在 Playground 上面写代码，然后编译器会实时编译我们代码，并将结果显示出来。这很好，我们可以实时得到代码的反馈。</p>
<p>但是这也会产生一个问题，如果我们写了一个函数，或者自定义了一个 view，这部分代码一般情况下是不会变的，而编译器却会一次又一次地去编译这些代码，最终的结果就是导致效率的低下。</p>
<p>这时，Sources 目录就派上用场了，使用 <code>Cmd</code> + <code>1</code> 打开项目导航栏，可以看到一个 Sources 目录。放到此目录下的源文件会被编译成 module 并自动导入到 Playground 中，并且这个编译只会进行一次(或者我们对该目录下的文件进行修改的时候)，而非每次你敲入一个字母的时候就编译一次。 这将会大大提高代码执行的效率。</p>
<p>但是请注意！</p>
<p>由于此目录下的文件都是被编译成模块导入的，只有被设置成 public 的类型，属性或方法才能在 Playground 中使用。</p>
<h3>重看 Xcode Swift Playground</h3>
<p>Swift 语言是苹果在 WWDC 14 上正式发布的，与之同时发布的 Xcode 6 中也第一次集成了 Playground 功能。两年后的 WWDC 16 上，苹果发布了 iPad 专有的 Swift Playground 软件，帮助大家更好地学习使用 Swift 语言。到今年，Xcode Playground 已经 5 岁了。</p>
<p>这些年 Playground 一直进步，这一点可以在每年的 WWDC 上得到验证，因为你总能发现一些关于它的独立 Session。</p>
<p>今年关于 Playground 的 Session 叫做 <a href="https://developer.apple.com/videos/play/wwdc2019/405/">Swift Playgrounds 3</a>，通过这个 session 我们可以明显感觉到使用 iPad 上的 Playground 进行编程是十分便利的，也非常有趣，因为这种开发体验是独一无二的，它让开发者与硬件的交互变得更加紧密。</p>
<p>让我们假设一个场景，如果我们要开发一个与 加速计、陀螺仪相关的 Demo！</p>
<p>通常我们需要在 Xcode 里面编写相关代码，注册开发者账号，连接真机，编译代码，等待安装，才能开始真正调试。想想这一连串的步骤就让人头大，不是么？</p>
<p>而现在，只需要在 Playground 上编写即可直接运行调试。</p>
<p>所以说了这么多，不妨把你吃灰已久的 iPad 拿出来把玩一下吧，相信你一定会爱上 Swift Playground 的！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 022 - Overriding self with a weak reference]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-022</guid><link>https://swiftsiqi.com/posts/Swift-Tips-022</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 23 Oct 2019 04:06:53 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903726_771197.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304903726_771197.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304903726_771197.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304903726_771197.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304903726_771197.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1202" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#22-overriding-self-with-a-weak-reference">Swift Tips 022 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>在处理逃逸闭包内部的逻辑时，我们通常会使用 <code>weak self</code> 的方式来避免循环引用。为了在闭包里面正确的使用 <code>self</code> 变量，我们需要通过可选绑定的方式将原先的 <code>self</code> 重新命名并使用。</p>
<p>在 Swift 4.2 之前，<code>self</code> 是全局保留关键字，所以如果在逃逸闭包中把 <code>self</code> 标记为 <code>weak</code> 后，还想继续使用 <code>self</code> 就需要使用 <code>将</code>self` 包起来：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">guard</span> <span class="kd">let</span> <span class="p">`</span><span class="kc">self</span><span class="p">`</span> <span class="p">=</span> <span class="kc">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<p>而在 Swift 4.2 之后，基于 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0079-upgrade-self-from-weak-to-strong.md#allow-using-optional-binding-to-upgrade-self-from-a-weak-to-strong-reference">Allow using optional binding to upgrade self from a weak to strong reference</a> 提案，可选绑定中的 <code>self</code> 不再作为保留关键字。我们完全可以光明正大的这么写了：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">guard</span> <span class="kd">let</span> <span class="nv">self</span> <span class="p">=</span> <span class="kc">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<h3>内存泄漏与闭包里的 self</h3>
<p>在开始下面的内容前，我们先回顾一下内存泄漏和闭包里的 sefl 这个话题。</p>
<p>假设我们有这样一段代码：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">doSomething</span><span class="p">(</span><span class="n">then</span><span class="p">:</span> <span class="p">{</span>
</div><div class="line">  <span class="c1">// do something else</span>
</div><div class="line"><span class="p">})</span>
</div></code></pre></div>
</div>
<p>由于 Swift 的内存管理机制，如果我们在闭包里面使用一些局部变量的话，闭包就会捕获这个变量，就像下面的代码一样：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">var</span> <span class="nv">dog</span> <span class="p">=</span> <span class="n">Dog</span><span class="p">()</span>
</div><div class="line"><span class="n">doSomething</span><span class="p">(</span><span class="n">then</span><span class="p">:</span> <span class="p">{</span>
</div><div class="line">  <span class="n">dog</span><span class="p">.</span><span class="n">bark</span><span class="p">()</span> <span class="c1">// dog must be captured so it will live long enough</span>
</div><div class="line"><span class="p">})</span>
</div></code></pre></div>
</div>
<p>Swift 编译器会自动管理 dog 的引用计数，这个 dog 会被 <code>doSomething</code> 的闭包 捕获，从而增加引用计数，即使跳出这段代码的作用域，dog 实例仍然会被 <code>doSomething</code> 的闭包所持有。</p>
<p>这种形式在上面的例子中似乎并无大碍，但有时候却会带来一些问题。最常见的一种情况就是循环引用。</p>
<p>试想一下：某个实例拥有一个闭包属性，而这个属性又会调用自身的一些实例方法或者属性时，它们就会产出如下图所示的引用关系。</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903709_087153.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1268/format,webp" type="image/webp"><img src="https://i.typlog.com/siqi/8304903709_087153.png" alt="image.png"loading="lazy" decoding="async" width="1268" height="660" /></picture></figure></div><p>根据前面的例子，我们知道只要闭包不被销毁，它就会一直持有 self，而另一边，闭包作为实例的一个属性，它们是同生死，共存亡！</p>
<p>你瞧，它们谁也无法释放对方……然后，内存泄漏了！</p>
<h3>避免循环引用</h3>
<p>为了避免循环引用，Swift 提供了捕获列表来让开发者决定如何持有相关变量。拿之前的 dog 代码举例说明，它就变成了如下的样式：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">dog</span> <span class="p">=</span> <span class="n">Dog</span><span class="p">()</span>
</div><div class="line"><span class="n">doSomething</span><span class="p">(</span><span class="n">then</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="n">dog</span><span class="p">]</span> <span class="k">in</span>
</div><div class="line">    <span class="c1">// dog is now Optional&lt;Dog&gt;</span>
</div><div class="line">    <span class="n">dog</span><span class="p">?.</span><span class="n">bark</span><span class="p">()</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>此时，当我们再次跳出这段代码的作用域后，闭包里 dog 将变为 nil。除非我们还在其他地方，通过一些途径强持有了 dog 实例。</p>
<p>同理，捕获列表也支持 self:</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">doSomething</span><span class="p">(</span><span class="n">then</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</div><div class="line">    <span class="kc">self</span><span class="p">?.</span><span class="n">doSomethingElse</span><span class="p">()</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>不过由于此时的 self 变成了一个可选类型，所以如果我们要区分不同状态下的代码行为，我们通常还会在闭包内部创建一个对 <code>weak self</code> 的强引用。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">doSomething</span><span class="p">(</span><span class="n">then</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</div><div class="line">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">strongSelf</span> <span class="p">=</span> <span class="kc">self</span> <span class="p">{</span> <span class="k">else</span> <span class="k">return</span> <span class="p">}</span>
</div><div class="line">    <span class="n">strongSelf</span><span class="p">.</span><span class="n">doSomethingElse</span><span class="p">()</span>
</div><div class="line"><span class="p">)</span>
</div></code></pre></div>
</div>
<p>在上面的这个例子中，我们确保了 <code>strongSelf</code> 是一定有值的，而且当 <code>self</code> 为 nil 的时候，不会执行剩下的代码。</p>
<h3>可选绑定里的 self</h3>
<p>虽然在许多 Swift 项目中能够看到 <code>strongSelf</code> 的写法，但开发者还是觉得这种方式有点不那么时髦，总想着要弄点什么新鲜玩意儿来！</p>
<p>果不其然，Swift 社区里的人发现了在闭包里还可以进行如下的操作：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">guard</span> <span class="kd">let</span> <span class="p">`</span><span class="kc">self</span><span class="p">`</span> <span class="p">=</span> <span class="kc">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<p>这种 ‘trick’ 的方式能够让我们统一 self 的写法，也让代码看起来变得更顺眼了一些。</p>
<p>但有意思的是，我们的 Chris，也就是 Swift 之父，亲口承认了这个所谓的“新特性”，其实是一个 Swift 编译器的 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0079-upgrade-self-from-weak-to-strong.md#relying-on-a-compiler-bug">bug</a> 。</p>
<p>但社区里的开发者并没有打算放弃努力！</p>
<p>终于，在 Swift 4.2 的时候，Swift 团队接受了开发者提出的相关 proposal 并提供了在可选绑定中不再将 self 作为保留关键字的特性。所以我们现在能够看到如下的写法:</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">guard</span> <span class="kd">let</span> <span class="nv">self</span> <span class="p">=</span> <span class="kc">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<p>当然取消了这个限制后也意味着 self 可能不一定是 self 了：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">var</span> <span class="nv">number</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span>
</div><div class="line"><span class="k">if</span> <span class="kd">let</span> <span class="nv">self</span> <span class="p">=</span> <span class="n">number</span> <span class="p">{</span>
</div><div class="line">    <span class="bp">print</span><span class="p">(</span><span class="kc">self</span><span class="p">)</span> <span class="c1">// 这里的 self 是 number：Int</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>所以，即使如此，还是希望大家在正确的地方将 self 作为可选绑定的变量名，免得造成其他的困扰。</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 021 - Using DispatchWorkItem]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-021</guid><link>https://swiftsiqi.com/posts/Swift-Tips-021</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 09 Oct 2019 04:02:41 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903844_239835.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304903844_239835.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304903844_239835.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304903844_239835.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304903844_239835.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="806" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#21-using-dispatchworkitem">Swift Tips 021 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>代码截图里，首先使用 DispatchWorkItem 创建了一个待执行任务，然后我们将该任务放在主队列中，声明该任务会在 1 秒后执行。最后一句代码不是必要的，它表示立即取消执行中的任务。</p>
<p>如果之前的代码执行正常的话，任务在执行完毕后就不会占用任何系统资源，但在某些情况下，例如第二句代码执行了 1 分钟还是没结束，这意味着主线程极有可能被阻塞了 1 分钟，为了不影响用户体验，我们极有可能会考虑停止当前任务。所以会需要使用 cancel 方法。写在这里只是为了表明我们可以在需要的时刻，调用并停止对应的任务。</p>
<h3>DispatchWorkItem 是什么</h3>
<p>早期的 GCD 可能会给使用者留下这么一个印象：一旦安排了一个任务就无法取消,需要操作 Operation 的 API 来取消任务。
如果有点忘了，我们用下面一段示例代码来回味一下，为了增加些许怀旧情结，这里的代码是用 objective-c 写的：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="bp">NSOperationQueue</span><span class="o">*</span> <span class="n">queue</span> <span class="p">=</span> <span class="p">[</span><span class="bp">NSOperationQueue</span> <span class="n">new</span><span class="p">];</span>
</div><div class="line"><span class="n">queue</span><span class="p">.</span><span class="n">maxConcurrentOperationCount</span> <span class="p">=</span> <span class="mi">1</span><span class="p">;</span> <span class="c1">// make it a serial queue</span>
</div><div class="line">
</div><div class="line"><span class="p">...</span>
</div><div class="line"><span class="p">[</span><span class="n">queue</span> <span class="n">addOperationWithBlock</span><span class="p">:...];</span> <span class="c1">// add operations to it</span>
</div><div class="line"><span class="p">...</span>
</div><div class="line">
</div><div class="line"><span class="c1">// Cleanup logic. At this point _do not_ add more operations to the queue</span>
</div><div class="line"><span class="n">queue</span><span class="p">.</span><span class="n">suspended</span> <span class="p">=</span> <span class="n">YES</span><span class="p">;</span> <span class="c1">// halts execution of the queue</span>
</div><div class="line"><span class="p">[</span><span class="n">queue</span> <span class="n">cancelAllOperations</span><span class="p">];</span> <span class="c1">// notify all pending operations to terminate</span>
</div><div class="line"><span class="n">queue</span><span class="p">.</span><span class="n">suspended</span> <span class="p">=</span> <span class="n">NO</span><span class="p">;</span> <span class="c1">// let it go.</span>
</div><div class="line"><span class="n">queue</span><span class="p">=</span><span class="kc">nil</span><span class="p">;</span> <span class="c1">// discard object</span>
</div></code></pre></div>
</div>
<p>不过这一现象在 iOS 8 之后发生了变化，Apple 提出了 DispatchWorkItem 这个概念和相关 API。</p>
<p>同样的功能，使用 DispatchWorkItem 后，我们只需要像代码截图里一样：调用 DispatchWorkItem 的实例方法 cancel 即可，这避免了一堆对 NSOperationQueue 的冗余操作。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="n">workItem</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span>
</div></code></pre></div>
</div>
<p>回到 DispatchWorkItem 上，它到底是个什么呢？通俗的来说，DispatchWorkItem 就是 GCD 里面常说的一段待执行的任务，更直白一点，它本质只是一个等待执行的代码块而已，可以在任意一个队列上被调用。</p>
<h3>DispatchWorkItem 怎么用</h3>
<p>DispatchWorkItem的初始化方法可以配置 Qos 和 DispatchWorkItemFlags，但是这两个参数都有默认参数，所以也可以只传入一个闭包也可以。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">workItem</span> <span class="p">=</span> <span class="n">DispatchWorkItem</span> <span class="p">{</span>
</div><div class="line">    <span class="c1">// Do something</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>除了代码截图里的 cancel 方法外，常见的方法还有：</p>
<ul>
<li><code>perform()</code> 方法：在队列中执行当前 DispatchWorkItem 的任务。</li>
<li><code>notify(queue:, execute:)</code> 方法：在当前任务完成后，通知其他队列中的 DispatchWorkItem 实例。</li>
<li><code>wait()</code> 方法：DispatchWorkItem 实例会阻塞当前线程直到任务完成。</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 020 - Combining a sequence of functions]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-003</guid><link>https://swiftsiqi.com/posts/Swift-Tips-003</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 05 Oct 2019 04:01:20 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304903929_105755.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304903929_105755.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304903929_105755.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304903929_105755.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304903929_105755.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="770" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#20-combining-a-sequence-of-functions">Swift Tips 020 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>代码截图里声明了一个 <code>+</code> 操作符，该操作符的两侧均为函数类型，且能够将 + 操作符的左参函数的返回值作为右参函数的入参。从而实现了一种类似“链式调用”的效果。</p>
<p>例如下面的代码：</p>
<div class="block-code"><pre><code>try (determineTarget + build + analyze + output)()</code></pre></div>
<p>等价于</p>
<div class="block-code"><pre><code>try (output( analyze ( build ( determineTarget ()))))</code></pre></div>
<h3>为什么能这么做</h3>
<p>运算符非常基础，大多数语言都将它们作为编译器（或解释器）的一部分进行处理。 但是 Swift 编译器并不对大多数操作符进行硬编码，而是将这部分工作留给了Swift 标准库，通过它来提供所有常见的操作符。</p>
<p>也正是这种微妙的差异为 Swift 打开了一扇通往“自由”的道路。在 Swift 中，我们不用被预定义的运算符所限制，可以自由地定义中缀、前缀、后缀和赋值运算符，这些自定义运算符在代码中可以像预定义的运算符一样使用，你甚至可以扩展已有的运算符。</p>
<p>说到底，运算符的本质只是一个函数而已。而在 Swift 的世界里，函数可是一等公民呀！</p>
<div class="blockquote"><blockquote><p>关于自定义运算符的更多内容，可阅读 The Swift Programming Language 一书中的 <a href="https://swiftgg.gitbook.io/swift/swift-jiao-cheng/26_advanced_operators#custom-operators">自定义运算符</a> 章节。</p>
</blockquote></div>
<h3>这样做的好处</h3>
<p>函数式编程在 Swift 领域里经常被提及，而今天的这个主题也与函数式编程息息相关。代码截图里定义的 <code>+</code> 操作符，很像函数式编程里提及的管道操作符（pipe operator）。</p>
<p>而同样功能的操作符，我们已经可以在 JavaScript，F# 里预置的操作符中看到（<code>|&gt;</code> 操作符）。</p>
<div class="blockquote"><blockquote><p>关于 JavaScript 和 F# 的管道操作符，可以阅读下面的资料：</p>
<ol>
<li>JavaScript 的管道操作符<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E7%AE%A1%E9%81%93%E6%93%8D%E4%BD%9C%E7%AC%A6">文档</a></li>
<li><a href="https://theburningmonk.com/2011/09/fsharp-pipe-forward-and-pipe-backward/">F# – Pipe Forward and Pipe Backward</a></li>
</ol>
</blockquote></div>
<p>这种类型的操作符允许我们以一种易读的方式去对函数进行链式调用。从另一个角度来看，管道操作符是单参数函数调用的语法糖，它允许我们这样执行一个调用(下面的例子使用的 JavaScript)：</p>
<div class="block-code"><pre><code>const double = (n) =&gt; n * 2;
const increment = (n) =&gt; n + 1;

// 没有用管道操作符
double(increment(double(5))); // 22

// 用上管道操作符之后
5 |&gt; double |&gt; increment |&gt; double; // 22</code></pre></div>
<p>虽然这种方式第一眼看上去有点懵，但熟悉了之后，在链式调用多个函数时，使用管道操作符还是改善了代码的可读性，毕竟从左到右的阅读方式更符合我们的认知。</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 019 - Chaining optionals with map() and flatMap()]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-019</guid><link>https://swiftsiqi.com/posts/Swift-Tips-019</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 04 Oct 2019 03:15:06 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304906702_475945.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304906702_475945.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304906702_475945.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304906702_475945.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304906702_475945.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1058" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#19-chaining-optionals-with-map-and-flatmap">Swift Tips 019 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>截图里 BEFORE 和 AFTER 在代码逻辑上完全一致，只是使用了两种不同的编码风格。前一种使用了常见的可选绑定，方法调用等手段，而后一种仅仅通过使用高阶函数就完成了所有的功能。</p>
<h3>Sequence 里的 map, flatMap 和 compactMap</h3>
<p>在开始话题之前，我们不妨先看看这三个函数在 Sequence 里的定义，</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">public</span> <span class="kd">func</span> <span class="nf">map</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="kc">_</span> <span class="n">transform</span><span class="p">:</span> <span class="p">(</span><span class="n">Element</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">T</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">T</span><span class="p">]</span>
</div><div class="line">
</div><div class="line"><span class="kd">public</span> <span class="kd">func</span> <span class="nf">flatMap</span><span class="p">&lt;</span><span class="n">SegmentOfResult</span><span class="p">&gt;(</span><span class="kc">_</span> <span class="n">transform</span><span class="p">:</span> <span class="p">(</span><span class="n">Element</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">SegmentOfResult</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">SegmentOfResult</span><span class="p">.</span><span class="n">Element</span><span class="p">]</span> <span class="k">where</span> <span class="n">SegmentOfResult</span> <span class="p">:</span> <span class="n">Sequence</span>
</div><div class="line">
</div><div class="line"><span class="kd">public</span> <span class="kd">func</span> <span class="nf">compactMap</span><span class="p">&lt;</span><span class="n">ElementOfResult</span><span class="p">&gt;(</span><span class="kc">_</span> <span class="n">transform</span><span class="p">:</span> <span class="p">(</span><span class="n">Element</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">ElementOfResult</span><span class="p">?)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">ElementOfResult</span><span class="p">]</span>
</div></code></pre></div>
</div>
<p>乍一看，它们都是接受一个名为 transform 闭包作为参数并且整个方法的返回值是一个数组。但仔细一看，这两个关键点在细节上又有着细微的不同。</p>
<h4>map</h4>
<p>map 对 Sequence 元素进行某种规则的转换，例如:</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">arr</span> <span class="p">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
</div><div class="line"><span class="c1">// arr = [1, 2, 4]</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">stringArr</span> <span class="p">=</span> <span class="n">arr</span><span class="p">.</span><span class="bp">map</span> <span class="p">{</span>
</div><div class="line">    <span class="s">&quot;No.&quot;</span> <span class="o">+</span> <span class="nb">String</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="c1">// stringArr = [&quot;No.1&quot;, &quot;No.2&quot;, &quot;No.4&quot;]</span>
</div></code></pre></div>
</div>
<h4>flatMap</h4>
<p>flatmap 里第一个函数闭包的定义是 <code>(Element) -&gt; SegmentOfResult</code>，并且这里 <code>SegmentOfResult</code> 被定义成 <code>SegmentOfResult : Sequence</code>，所以它是接受一个数组元素，然后输出一个 <code>SequenceType</code> 类型的元素的闭包。有趣的是，<code>flatMap</code> 最终执行的结果并不是 <code>SequenceType</code> 数组，而是 <code>Sequence</code> 内部元素组成的数组，即 <code>SegmentOfResult.Element</code>，可能文字读起来有点绕，我们来一段代码：</p>
<div class="block-code"><pre><code>let arr = [[1, 2, 3], [6, 5, 4]]
let flatArr = arr.flatMap {
    $0
}
// flatArr = [1, 2, 3, 6, 5, 4]</code></pre></div>
<p>在这个例子中，数组 arr 调用 <code>flatMap</code> 时，元素 <code>[1, 2, 3]</code> 和 <code>[6, 5, 4]</code> 分别被传入闭包中，又直接被作为结果返回。但是，最终的结果中，却是由这两个数组中的元素共同组成的新数组：<code>[1, 2, 3, 6, 5, 4]</code> 。</p>
<h4>compactMap</h4>
<p>如果在 <code>Sequence</code> 里仔细查看的话，我们还可以看到一个已经标注为废弃的同名 <code>flatMap</code> 的 API，它的替代者就是我们马上要介绍的 <code>compactMap</code> 。</p>
<div class="blockquote"><blockquote><p>Swift 4.1 之前存在 2 个两个 <code>flatMap</code> 函数，虽然它们都是用来降维的，但其中一个除了 <code>flat</code> 之外其实还有 <code>filter</code> 的作用，在使用时容易产生歧义，所以社区认为最好把第二个版本重新拆分出来，使用一个新的方法命名，就产生了这个提案 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md">SE-0187</a>。
最初这个提案用了 <code>filterMap</code> 这个名字，但后来经过讨论，就决定参考了 Ruby 的 <code>Array::compact</code> 方法，使用 <code>compactMap</code> 这个名字</p>
</blockquote></div>
<div class="block-code"><pre><code>public func flatMap&lt;ElementOfResult&gt;(_ transform: (Element) -&gt; ElementOfResult?) -&gt; [ElementOfResult]</code></pre></div>
<p>在这个函数里，闭包的定义变成了：<code>(Element) -&gt; ElementOfResult?</code>，返回值 <code>ElementOfResult</code> 不像 <code>flatMap</code> 那样要求是一个数组了，而变成了一个 Optional 的任意类型。而 <code>compactMap</code> 最终输出的数组结果，其实不是这个 <code>ElementOfResult?</code> 类型，而是这个 <code>ElementOfResult?</code> 类型解包之后，不为 <code>.None</code> 的元数数组：<code>[ElementOfResult]</code>。</p>
<p>用代码来总结一下它的功能：</p>
<div class="block-code"><pre><code>let arr: [Int?] = [1, 2, nil, 4, nil, 5]
let intArr = arr.flatMap { $0 }
// intArr = [1, 2, 4, 5]</code></pre></div>
<h3>Optional 里的 map, flatMap</h3>
<p>除了在 <code>Sequence</code> 协议下里使用 <code>map</code>，<code>flatMap</code>，在 <code>Optional</code> 里我们也能见到 <code>map</code>，<code>flatMap</code> 的身影。</p>
<div class="block-code"><pre><code>public enum Optional&lt;Wrapped&gt; : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)

    public func map&lt;U&gt;(_ transform: (Wrapped) throws -&gt; U) rethrows -&gt; U?
    public func flatMap&lt;U&gt;(_ transform: (Wrapped) throws -&gt; U?) rethrows -&gt; U?
}</code></pre></div>
<p>所以，对于一个 <code>Optional</code> 的变量来说，<code>map</code> 方法允许它再次修改自己的值，并且不必关心自己是否为 <code>.None</code>。例如：</p>
<div class="block-code"><pre><code>let a1: Int? = 3
let b1 = a1.map{ $0 * 2 }
// b1 = 6

let a2: Int? = nil
let b2 = a2.map{ $0 * 2 }
// b2 = nil</code></pre></div>
<p>相比于 <code>map</code> 而言，<code>flatMap</code> 能够处理闭包参数可能返回 <code>nil</code> 的情况。 例如：</p>
<div class="block-code"><pre><code>let s: String? = &quot;abc&quot;
let v = s.flatMap { (a: String) -&gt; Int? in
    return Int(a)
}</code></pre></div>
<h3>如何选择</h3>
<ul>
<li><p>在 Sequence 类型中，存在map，flatMap 和 compact 三种转换方法</p>
<ul>
<li><code>map</code> 可以将 Sequence 里的元素进行一次类型转换</li>
<li><code>flatMap</code> 等价于先 map 再 flatten（即数组降维）</li>
<li><code>compact</code> 用于去掉结果中的 nil</li>
</ul>
</li>
<li><p>在 Optional 类型里，存在map和flatMap</p>
<ul>
<li>当我们的输入是一个 Optional，同时我们需要在逻辑中处理这个 Optional 是否为 nil 时，就适合用 map 来替代原来的写法，使得代码更加简短。</li>
<li>当我们的闭包参数有可能返回 nil 的时候，就可以使用 Optional 的 <code>flatMap</code> 方法。</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 018 - Using self-executing closures for lazy properties]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-018</guid><link>https://swiftsiqi.com/posts/Swift-Tips-018</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Tue, 01 Oct 2019 03:12:41 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304906851_612291.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304906851_612291.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304906851_612291.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304906851_612291.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304906851_612291.png" alt="image.png"loading="lazy" decoding="async" width="1890" height="986" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#18-using-self-executing-closures-for-lazy-properties">Swift Tips 018 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>代码截图里的核心点是在于 StoreViewController 里的 collectionView 属性不仅是一个延时加载存储属性，还采用了闭包的方式初始化属性缺省值。</p>
<h3>lazy 关键字</h3>
<p>延时加载属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标识一个延时加载属性。</p>
<p>必须将延时加载属性声明成变量（使用 var 关键字），因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值，因此无法声明成延时加载。</p>
<h3>用闭包的方式初始化属性缺省值</h3>
<p>和其他属性一样，我们可以用自执行（self-executing）闭包来给 lazy 变量设定默认值，也就是用 <code>= { /* some code */ }()</code> 的方式替换掉 <code>= some code</code> 的方式。</p>
<p>需要注意的一点是，当属性是 lazy 时， 这意味着它的初始值暂时不会被计算，当需要计算的时候，<code>self</code> 已经完成初始化。这就是为什么你可以在那里使用 self ，这和非 lazy 属性正好相反：它的初始值在初始化阶段就被计算出来了。</p>
<h3>这样做的好处</h3>
<p>在说好处之前，我们先将代码还原成不用 lazy 关键字和自执行闭包的状态。你的代码大体可能如下：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">class</span> <span class="nc">StoreViewController</span><span class="p">:</span> <span class="bp">UIViewController</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">var</span> <span class="nv">collectionView</span><span class="p">:</span> <span class="bp">UICollectionView</span><span class="p">?</span>
</div><div class="line">
</div><div class="line">    <span class="kr">override</span> <span class="kd">func</span> <span class="nf">viewDidLoad</span><span class="p">()</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">super</span><span class="p">.</span><span class="n">viewDidLoad</span><span class="p">()</span>
</div><div class="line">        <span class="kd">let</span> <span class="nv">layout</span> <span class="p">=</span> <span class="bp">UICollectionViewFlowLayout</span><span class="p">()</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">collectionView</span> <span class="p">=</span> <span class="bp">UICollectionView</span><span class="p">(</span><span class="n">frame</span><span class="p">:</span> <span class="kc">self</span><span class="p">.</span><span class="n">view</span><span class="p">.</span><span class="n">bounds</span><span class="p">,</span> <span class="n">collectionViewLayout</span><span class="p">:</span> <span class="n">layout</span><span class="p">)</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">collectionView</span><span class="p">.</span><span class="n">delegate</span> <span class="p">=</span> <span class="kc">self</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">collectionView</span><span class="p">.</span><span class="n">dataSource</span> <span class="p">=</span> <span class="kc">self</span>
</div><div class="line">        <span class="k">if</span> <span class="kd">let</span> <span class="nv">subView</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">collectionView</span>  <span class="p">{</span>
</div><div class="line">            <span class="n">view</span><span class="p">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">subView</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>相比于原先的代码，viewDidLoad 里面的核心逻辑是向 view 里添加 子视图。但现在的这份代码在 viewDidLoad 整合了 collectionView 的初始化代码，除了代码结构上变得有些不那么整洁外，我们也可能无法保证 collectionView 只被初始化一次了。</p>
<p>所以用自执行闭包初始化 lazy 属性的方式，你觉得如何？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 017 - Speeding up Swift package tests]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-017</guid><link>https://swiftsiqi.com/posts/Swift-Tips-017</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Thu, 26 Sep 2019 03:11:10 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304906938_8785.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304906938_8785.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304906938_8785.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304906938_8785.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304906938_8785.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="554" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#17-speeding-up-swift-package-tests">Swift Tips 017 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>Swift Package Manager（Swift 包管理器，一般简称 SwiftPM 或者 SPM）是苹果官方提供的一个用于管理源代码分发的工具，旨在使分享代码和复用其他人的代码变得更加容易。该工具可以帮助我们编译和链接 Swift Packages，管理依赖关系、版本控制，以及支持灵活分发和协作（公开、私有、团队共享）等。</p>
<p>截图里的命令就是 SPM 的一则命令，用于运行 package 中的单元测试，后面的 –parallel 参数意味着单元测试可以并行执行。</p>
<h3>想知道更多关于 SPM 的使用方法</h3>
<p>除了 test 命令外，还有如下几个常用命令</p>
<ul>
<li><code>swift build</code>: 用于编译 package</li>
<li><code>swift package</code>: 在 package 中进行各种除编译/运行/测试之外的操作，如创建、编辑、更新、重置、修改编译选项/路径等</li>
<li><code>swift run</code>: 用于编译并运行一个可执行文件，该命令是在 Swift 4 中新增加的，详见这个提案，它相当于：</li>
</ul>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift build
</div><div class="line">$ .build/debug/myexecutable
</div></code></pre></div>
</div>
<p>此外，你可以在命令行中执行 <code>swift package --version</code> 查看当前 SwiftPM 的版本：</p>
<div class="block-code" data-language="shell"><div class="highlight"><pre><span></span><code><div class="line">$ swift package --version
</div><div class="line">Apple Swift Package Manager - Swift <span class="m">5</span>.0.0 <span class="o">(</span>swiftpm-14492.2<span class="o">)</span>
</div></code></pre></div>
</div>
<p>也可以执行 <code>swift package --help</code> 查看关于命令的更多帮助。</p>
<p>如果你对 SPM 还想了解更多，可以查阅官方<a href="https://swift.org/getting-started/#using-the-package-manager">使用示例</a>和<a href="https://github.com/apple/swift-package-manager/blob/master/Documentation/Usage.md">文档</a>。</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 016 - Avoiding mocking UserDefaults]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-016</guid><link>https://swiftsiqi.com/posts/Swift-Tips-016</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sat, 21 Sep 2019 03:08:54 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304907078_600759.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304907078_600759.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304907078_600759.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304907078_600759.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304907078_600759.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="986" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#16-avoiding-mocking-userdefaults">Swift Tips 016 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>代码截图里是一个关于模拟登录的单测用例，在这个测试用例中，测试的结果是与用户的 UserDefaults 设定相关联，所以需要在 <code>setup()</code> 方法中设置好初始状态。</p>
<p>我们可以看到代码里根据 <code>#file</code>（包含这个符号的文件的路径）创建了一个 UserDefaults 对象，并清除了 key 值为 <code>#file</code> 的 value，由此保证不会存有其他值。</p>
<p>在完成上述初始操作后，使用 userDefaults 对象初始化了与登录相关的控制类 LoginManager，并将其赋值给 manager 属性，以便后续使用。</p>
<h3>一些疑惑</h3>
<p>初看这段代码，你可能会疑惑 <code>UserDefaults(suiteName: #file)</code> 这段代码到底在干什么，为了展开这个话题，我们需要先解释 2 个问题：</p>
<ul>
<li>App Groups 是什么东西</li>
<li><code>UserDefaults(suiteName:)</code> 的入参是什么东西</li>
</ul>
<h4>App Groups 是什么东西</h4>
<p>App Groups 是 iOS 8 之后提供的新特性，主要用于同一个开发团队开发的 App 之间进行数据共享，这包括 App 和 Extension 之间共享同一份读写空间。同一个团队开发的多个应用之间如果能直接数据共享，将会大大提高用户体验。例如某公司的旗下有两个 App，当用户已经登录一个 App A 的情况下，再登录另一个 App B 时，B 不再需要繁琐的登录过程就可以直接使用 A 已经登录的信息。</p>
<p>另外 iOS 8 提供 Extension 功能之后，App Group 就显得尤为必要，例如 Containning app 没有运行，Extension 运行的情况下，Extension 和 Containning app 共享信息将是一个十分常见的需求。</p>
<div class="blockquote"><blockquote><p>关于如何开启和使用 App Groups 可以阅读文章：<a href="https://medium.com/ios-os-x-development/shared-user-defaults-in-ios-3f15cd2c9409">Shared User Defaults in iOS</a></p>
</blockquote></div>
<h4><code>UserDefaults(suiteName:)</code> 的入参是什么东西</h4>
<p>在 iOS 中，每个应用必须被沙盒化的，这就意味着应用之间是被隔离的，不能直接互相访问。而 UserDefaults 作为一种数据持久化解决方案，是能结合 App Groups 做到跨 App 之间访问的，那么它是怎么做到的呢？</p>
<p>这就要提到 UserDefault 的 Domain 类型，主要有五种，但要做到跨 App 之间的数据通信，我们仅需要关注 Application Domain 这一层级。</p>
<p>在沙盒机制的加成下，我们应该很容易理解一点，那就是每个 App 里的 UserDefault 文件，它的 Application Domain 是不一样的。这些文件的作用域通常是通过它们项目的 Bundle Identifier，或者是 App Group 中约定的 Suite Name 来区分的。就像我们在 A 项目里调用 <code>UserDefaults.standard</code> 的时候是不会返回 B 项目里的的 UserDefault 文件。</p>
<p>这样一来，我们再来看 <code>UserDefaults(suiteName:)</code> 这个 API，它的入参其实就是用来区分 Application Domain 的，以便开发者取得对应的 UserDefaults。</p>
<div class="blockquote"><blockquote><p>关于 UserDefault 作用域的更多内容可以阅读文章：<a href="https://medium.com/swift-india/userdefaults-under-the-hood-457461c8d262">UserDefaults under the hood</a></p>
</blockquote></div>
<h3>这样做的好处是什么</h3>
<p>笔者不才，我认为今天的例子里的唯一好处是，相比于调用 <code>UserDefaults.default</code>，<code>UserDefault(suiteName:)</code> 避免了直接操作原始文件带来的烦恼。</p>
<p>除了这些，还有哪些好处，聪明的你愿意分享出来么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 015 - Using variadic parameters]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-015</guid><link>https://swiftsiqi.com/posts/Swift-Tips-015</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 20 Sep 2019 03:06:33 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304907223_742352.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304907223_742352.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304907223_742352.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304907223_742352.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304907223_742352.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="986" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#15-using-variadic-parameters">Swift Tips 015 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>这段代码为 Canvas 类拓展了一个名为 <code>add(_ shapes: Shape...)</code> 的函数，由于不确定函数的入参个数，这里使用了可变参数作为入参，即 <code>Shape...</code>。</p>
<p>紧接着，我们定义了 <code>circle</code>, <code>lineA</code>, <code>lineB</code> 等三个图形，并将其添加到名为 <code>canvas</code> 的画布上，最后通过调用 <code>render</code> 方法将其绘制出来。</p>
<h3>可变参数是什么</h3>
<p>可变参数表示函数可以接受零个或多个值。当调用用可变参数的函数时，可变参数表示这个函数参数可以传入不确定数量的输入值。</p>
<p>开发者只需要在变量类型名后面加上 <code>...</code> 的方式来定义可变参数。可变参数的参数名称在函数体内部会被当做成包含此类型的数组。例如，一个叫做 <code>shapes</code> 的 <code>Shape...</code> 型可变参数，在函数体内可以当做一个叫 <code>shapes</code> 的 <code>[Shape]</code> 型的数组常量。</p>
<p>下面的这个函数用来计算一组任意长度数字的算术平均数：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">arithmeticMean</span><span class="p">(</span><span class="kc">_</span> <span class="n">numbers</span><span class="p">:</span> <span class="nb">Double</span><span class="p">...)</span> <span class="p">-&gt;</span> <span class="nb">Double</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">var</span> <span class="nv">total</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="mi">0</span>
</div><div class="line">    <span class="k">for</span> <span class="n">number</span> <span class="k">in</span> <span class="n">numbers</span> <span class="p">{</span>
</div><div class="line">        <span class="n">total</span> <span class="o">+=</span> <span class="n">number</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="k">return</span> <span class="n">total</span> <span class="o">/</span> <span class="nb">Double</span><span class="p">(</span><span class="n">numbers</span><span class="p">.</span><span class="bp">count</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="n">arithmeticMean</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</div><div class="line"><span class="c1">// 返回 3.0, 是这 5 个数的平均数。</span>
</div><div class="line"><span class="n">arithmeticMean</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mf">8.25</span><span class="p">,</span> <span class="mf">18.75</span><span class="p">)</span>
</div><div class="line"><span class="c1">// 返回 10.0, 是这 3 个数的平均数。</span>
</div></code></pre></div>
</div>
<h3>这样做的好处与弊端</h3>
<p>回到代码上来说，使用可变数组的好处究竟是什么，我们不妨创建一个不使用可变参数的 API：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">extension</span> <span class="nc">Canvas</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">add</span><span class="p">(</span><span class="kc">_</span> <span class="n">shapes</span><span class="p">:</span> <span class="n">Shape</span><span class="p">...)</span> <span class="p">{</span>
</div><div class="line">        <span class="n">shapes</span><span class="p">.</span><span class="n">forEach</span><span class="p">(</span><span class="n">add</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">anotherAdd</span><span class="p">(</span><span class="kc">_</span> <span class="n">shapes</span><span class="p">:</span> <span class="p">[</span><span class="n">Shape</span><span class="p">])</span> <span class="p">{</span>
</div><div class="line">        <span class="n">shapes</span><span class="p">.</span><span class="n">forEach</span><span class="p">(</span><span class="n">add</span><span class="p">)</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// ... 代码与截图相同</span>
</div><div class="line">
</div><div class="line"><span class="c1">// 调用可变参数的 API 版本</span>
</div><div class="line"><span class="n">canvas</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">circle</span><span class="p">,</span> <span class="n">lineA</span><span class="p">,</span> <span class="n">lineB</span><span class="p">)</span>
</div><div class="line"><span class="c1">// 调用数组参数的 API 版本</span>
</div><div class="line"><span class="n">canvas</span><span class="p">.</span><span class="n">anotherAdd</span><span class="p">([</span><span class="n">circle</span><span class="p">,</span> <span class="n">lineA</span><span class="p">,</span> <span class="n">lineB</span><span class="p">])</span>
</div></code></pre></div>
</div>
<p>在使用数组参数的 API 时，我们不得不在三个元素外面写一个 <code>[]</code> 来将其转换为数组，这种写法稍显冗余，尤其是只有一个元素的时候，<code>anotherAdd([cirle])</code> 的写法就显得很蹩脚。</p>
<p>当然这种写法也不是十全十美的，假设我们从某个 API 里获取了一组数据时（例如读取了 plist 里面的某个字段），返回值本身就是数组，此时我们还坚持使用可变参数的话，就变得十分麻烦了，但如果使用数组参数的话，代码将变得十分清晰。</p>
<p>如果你是 SDK 设计者的话，在遇到类似的场景时，不妨为相关 API 设计两个不同的版本 ，一个是可变参数，一个是相关类型的数组，这样从使用体验上一定会更好。</p>
<p>毕竟小孩子才做选择题，成年人当然是全都要！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 014 - Referring to enum cases with associated values as closures]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-014</guid><link>https://swiftsiqi.com/posts/Swift-Tips-014</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Mon, 16 Sep 2019 02:53:31 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304907997_263567.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304907997_263567.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304907997_263567.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304907997_263567.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304907997_263567.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="950" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#14-referring-to-enum-cases-with-associated-values-as-closures">Swift Tips 014 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>代码最开始定义一个名为 UnboxPath 的枚举类型，它有两个枚举值，一个成员值的名称叫做 key，具有 String 类型的关联值，另一个成员值的名称是 keyPath，具有 String 类型的关联值。</p>
<p>紧接着，代码里定义了一个名为 UserSchema 的结构体，它有三个存储属性，类型全部为 UnboxPath 并且设有默认值。</p>
<p>不过最有意思的，也是最特别的地方，则是这三个属性的默认值生成方式，它们是通过一个名为 key 的闭包完成的。</p>
<h3>为什么能这么做</h3>
<p>为什么 UserSchema 里的 UnboxPath 属性能通过 key 闭包实现呢，我们不妨看一下 key 闭包的定义：</p>
<p><a class="img-link" href="https://www.sketchk.xyz/2019/09/16/Swift-tips-014/02.jpg"><img src="https://www.sketchk.xyz/2019/09/16/Swift-tips-014/02.jpg" alt="02.png" /></a></p>
<p>我们发现 <code>UnboxPath.key</code> 这个枚举关联值变成了一个待执行的闭包，它的入参是 String 类型的实例，而返回值是 UnboxPath 类型的实例，</p>
<p>从某种角度来看，key 闭包此时就像是一种 <code>UnboxPath</code> 的“构造器”，不过这种构造器只能生成名称为 key 的枚举值。</p>
<p>就像代码截图里的标题所说一样，这里我们将枚举关联值当做闭包来使用，所以到这里，你的疑惑是不是得到了解答呢？</p>
<h3>这样做的好处</h3>
<p>将关联值当做闭包来使用的好处，目前能想到的好处主要有以下几点：</p>
<ol>
<li>简化代码，提升阅读体验</li>
<li>提供了一种生成特定枚举值的方法，方便使用</li>
<li>在迁移 Enum 类型时，能减少工作量</li>
</ol>
<p>前面两点不用多说，估计大家很容易能想到它的实际场景。</p>
<p>至于最后一点，我们不妨假设因为一些原因需要将 <code>UnboxPath</code> 升级为 <code>NewUnboxPath</code> 时，使用这种关联值的写法，能够帮我们避免一个个替换 <code>name</code>，<code>age</code>，<code>posts</code> 的类型，这时它与 <code>typealias</code> 起到的作用很相似。</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 013 - Using the === operator to compare objects by instance]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-013</guid><link>https://swiftsiqi.com/posts/Swift-Tips-013</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Sun, 15 Sep 2019 02:50:29 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908179_824301.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908179_824301.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908179_824301.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908179_824301.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908179_824301.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1022" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#13-using-the--operator-to-compare-objects-by-instance">Swift Tips 013 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>在代码截图中，我们看到 Enemy 通过 <code>InstanceEquatable</code> 拓展遵循了 <code>Equatable</code> 协议并重载了 <code>==</code> 运算符。声明了只有内存地址相等的状态下才符合 <code>==</code> 的定义，此时 <code>==</code> 与 <code>===</code> 的含义相同。</p>
<p>在这个前提下，调用 <code>contains</code> 函数的含义就变成了判断 player 摧毁的 ememy 中，是否包含目标对象引用的内存地址，而不是与目标对象内容相同的实例。</p>
<h3>=== 和 == 傻傻分不清楚</h3>
<p>简单来说，Swift 中提供了两种用于判等的操作符，一个是 <code>==</code> ，一个是 <code>===</code></p>
<p><code>==</code> 通常是用于判定两个对象的内容是否相同
<code>===</code> 通常是用于判定两个对象引用的是否为同一块内存地址。</p>
<h3>能说的更详细一点么</h3>
<p>对于类类型会存在多个实例指向同一个内存地址的情况，这是由于类类型本身是引用类型的缘故，类引用保存在 RTS （Run Time Stack） 上，而它们的实例保存在内存的堆上。</p>
<p>当我们使用 <code>==</code> 时，我们内心只是想验证两个实例是否相同，而不是验证两个实例是同一个实例。此时我们就需要提供一个验证两个实例相同的规则。</p>
<p>通常状态下，自定义类和结构体是没有默认的 <code>==</code> 和 <code>!=</code> 行为，我们需要让这些类型遵守 <code>Equatable</code> 协议并重载 <code>static func == (lhs:, rhs:) -&gt; Bool</code> 函数，举个例子</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">class</span> <span class="nc">Person</span> <span class="p">:</span> <span class="nb">Equatable</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">ssn</span><span class="p">:</span> <span class="nb">Int</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">name</span><span class="p">:</span> <span class="nb">String</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">ssn</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">ssn</span> <span class="p">=</span> <span class="n">ssn</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">name</span> <span class="p">=</span> <span class="n">name</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">static</span> <span class="kd">func</span> <span class="p">==</span> <span class="p">(</span><span class="n">lhs</span><span class="p">:</span> <span class="n">Person</span><span class="p">,</span> <span class="n">rhs</span><span class="p">:</span> <span class="n">Person</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="n">lhs</span><span class="p">.</span><span class="n">ssn</span> <span class="p">==</span> <span class="n">rhs</span><span class="p">.</span><span class="n">ssn</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>由于 SSN（social security number）是一个独一无二的标识符，就类似我们的身份证号，我们在判定 2 个对象是否相同时，是不需要关心名字的，不是么？</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">person1</span> <span class="p">=</span> <span class="n">Person</span><span class="p">(</span><span class="n">ssn</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="s">&quot;Bob&quot;</span><span class="p">)</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">person2</span> <span class="p">=</span> <span class="n">Person</span><span class="p">(</span><span class="n">ssn</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="s">&quot;Bob&quot;</span><span class="p">)</span>
</div><div class="line">
</div><div class="line"><span class="k">if</span> <span class="n">person1</span> <span class="p">==</span> <span class="n">person2</span> <span class="p">{</span>
</div><div class="line">   <span class="bp">print</span><span class="p">(</span><span class="s">&quot;the two instances are equal!&quot;</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>此时，即使 person1 和 person2 在堆上的地址不同，但由于他们的 ssn 相同，所以最终输出的还是 <code>the two instances are equal!</code></p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">if</span> <span class="n">person1</span> <span class="p">===</span> <span class="n">person2</span> <span class="p">{</span>
</div><div class="line">   <span class="c1">//It does not enter here</span>
</div><div class="line"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">   <span class="bp">print</span><span class="p">(</span><span class="s">&quot;the two instances are not identical!&quot;</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="p">===`</span> <span class="err">操作符检查的是引用的内存地址是否相同。由于</span> <span class="n">person1</span> <span class="err">和</span> <span class="n">person2</span> <span class="err">是完全</span> <span class="mi">2</span> <span class="err">个独立构造的实例，所以它们在堆上的地址，也就是内存地址是不一样的，所以输出的结果一定会是</span> <span class="p">`</span><span class="n">the</span> <span class="n">two</span> <span class="n">instances</span> <span class="n">are</span> <span class="n">not</span> <span class="n">identical</span><span class="p">!</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">person3</span> <span class="p">=</span> <span class="n">person1</span>
</div></code></pre></div>
</div>
<p>正如我们所说的那样，类类型是引用类型，上面这段代码将 person1 的引用赋值给了 person3，现在它们同时指向 person1 指向的内存地址。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">if</span> <span class="n">person3</span> <span class="p">===</span> <span class="n">person1</span> <span class="p">{</span>
</div><div class="line">   <span class="bp">print</span><span class="p">(</span><span class="s">&quot;the two instances are identical!&quot;</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>这里我想不用我太多解释，你也应该能猜到输出的结果了吧，如果还是不清楚，别担心，我们继续解释一下，这时候由于 person1 和 person3 指向的内存地址一样，也就是符号了 <code>===</code> 的定义，所以输出结果会是 <code>the two instances are identical!</code></p>
<p>通过这些小例子，你弄清楚 <code>==</code> 和 <code>===</code> 了么？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 012 - Calling initializers with dot syntax and passing them as closures]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-012</guid><link>https://swiftsiqi.com/posts/Swift-Tips-012</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Tue, 10 Sep 2019 02:47:08 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908376_638336.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908376_638336.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908376_638336.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908376_638336.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908376_638336.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="986" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#12-calling-initializers-with-dot-syntax-and-passing-them-as-closures">Swift Tips 012 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>今天的代码截图提到了 Swift 构造器的另一种使用方法，即用点语法（dot syntax）获取实例或者获取构造方法本身。</p>
<p>在获取实例上，使用点语法与我们常用的实例生成方式没有区别，例如下面的代码，<code>a</code> 和 <code>b</code> 都是 <code>Fahrenheit</code> 的一个实例，没有什么区别。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">a</span> <span class="p">=</span> <span class="n">Date</span><span class="p">()</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">b</span> <span class="p">=</span> <span class="n">Date</span><span class="p">.</span><span class="kd">init</span><span class="p">()</span>
</div></code></pre></div>
</div>
<p>而在获取构造方法上，使用点语法其实就是获取了一个与构造方法相同入参和返回值的闭包，例如下面代码中的 <code>a</code> 和 <code>Date.init</code> 表示的是一回事。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">a</span> <span class="p">=</span> <span class="p">{</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Date</span> <span class="k">in</span>
</div><div class="line">    <span class="k">return</span> <span class="n">Date</span><span class="p">()</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h3>为什么能使用点语法调用 init 方法</h3>
<p>Swift 里点语法这个语法糖能够帮助开发者直接调用某个类型或者类型实例下的方法和属性。</p>
<p>对于 init 方法而言，它虽然不是用 <code>func</code> 方式定义的，但它本质还是类型里的一个类方法。</p>
<p>所以用点语法直接调用 init 方法是没有任何问题的，只是我们通常会使用 <code>Date()</code> 的方式来创建实例。</p>
<h3>这样做的好处</h3>
<p>在代码截图里的场景下，如果不使用构造函数作为函数默认值的话，我们可能会将代码写成下面的形式：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">class</span> <span class="nc">AnotherLogger</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">storage</span><span class="p">:</span> <span class="n">LogStorage</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">dateProvider</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Date</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">storage</span><span class="p">:</span> <span class="n">LogStorage</span> <span class="p">=</span> <span class="n">LogStorage</span><span class="p">(),</span> <span class="n">dateProvider</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Date</span> <span class="p">=</span> <span class="p">{</span><span class="n">Date</span><span class="p">()})</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">storage</span> <span class="p">=</span> <span class="n">storage</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">dateProvider</span> <span class="p">=</span> <span class="n">dateProvider</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="kd">func</span> <span class="nf">log</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="n">Event</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="n">storage</span><span class="p">.</span><span class="n">store</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="n">event</span><span class="p">,</span> <span class="n">date</span><span class="p">:</span> <span class="n">dateProvider</span><span class="p">())</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>与代码截图里的写法对比，我们使用了 <code>{Date()}</code> 闭包作为默认参数，或许你觉得这么写也还行，但在实际开发过程中，这种用闭包做参数的写法并不一定是种优雅的写法，这里举一个例子：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// A: .init 的写法</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">AnotherLogger</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">storage</span><span class="p">:</span> <span class="n">LogStorage</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">dateProvider</span><span class="p">:</span> <span class="p">(</span><span class="n">TimeInterval</span><span class="p">,</span> <span class="n">Date</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Date</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">storage</span><span class="p">:</span> <span class="n">LogStorage</span> <span class="p">=</span> <span class="n">LogStorage</span><span class="p">(),</span> <span class="n">dateProvider</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">TimeInterval</span><span class="p">,</span> <span class="n">Date</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Date</span> <span class="p">=</span> <span class="n">Date</span><span class="p">.</span><span class="kd">init</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">storage</span> <span class="p">=</span> <span class="n">storage</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">dateProvider</span> <span class="p">=</span> <span class="n">dateProvider</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// B：闭包的写法</span>
</div><div class="line"><span class="kd">class</span> <span class="nc">AnotherLogger</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">storage</span><span class="p">:</span> <span class="n">LogStorage</span>
</div><div class="line">    <span class="kd">private</span> <span class="kd">let</span> <span class="nv">dateProvider</span><span class="p">:</span> <span class="p">(</span><span class="n">TimeInterval</span><span class="p">,</span> <span class="n">Date</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Date</span>
</div><div class="line">
</div><div class="line">    <span class="kd">init</span><span class="p">(</span><span class="n">storage</span><span class="p">:</span> <span class="n">LogStorage</span> <span class="p">=</span> <span class="n">LogStorage</span><span class="p">(),</span> <span class="n">dateProvider</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">TimeInterval</span><span class="p">,</span> <span class="n">Date</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Date</span> <span class="p">=</span> <span class="p">{(</span><span class="n">time</span><span class="p">:</span> <span class="n">TimeInterval</span><span class="p">,</span> <span class="n">date</span><span class="p">:</span> <span class="n">Date</span><span class="p">)</span> <span class="k">in</span>
</div><div class="line">        <span class="k">return</span> <span class="n">Date</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">timeInterval</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="n">since</span><span class="p">:</span> <span class="n">date</span><span class="p">)</span>
</div><div class="line">        <span class="p">})</span>
</div><div class="line">    <span class="p">{</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">storage</span> <span class="p">=</span> <span class="n">storage</span>
</div><div class="line">        <span class="kc">self</span><span class="p">.</span><span class="n">dateProvider</span> <span class="p">=</span> <span class="n">dateProvider</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>这么一看，是不是 <code>.init</code> 的语法还不错！</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 011 - Structuring UI tests as extensions on XCUIApplication]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-011</guid><link>https://swiftsiqi.com/posts/Swift-Tips-011</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 06 Sep 2019 02:44:31 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908537_493802.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908537_493802.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908537_493802.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908537_493802.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908537_493802.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1202" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#11-structuring-ui-tests-as-extensions-on-xcuiapplication">Swift Tips 011 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>今天的代码是在说我们可以将测试用例里的一部分代码写成 <code>XCUIApplication</code> 的扩展（例如 login，logout 和 goToCategories）， 这样会更好的突出测试用例的重点。</p>
<p>你也许会好奇，如果不将 login, logout 这样的代码抽离出去会怎么样呢？那么下面就是一段我“还原”的测试代码，不一定完全准确，经供参考：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">testLoggingInAndOut</span><span class="p">()</span> <span class="p">{</span>
</div><div class="line">    <span class="n">XCTAssertFalse</span><span class="p">(</span><span class="n">app</span><span class="p">.</span><span class="n">userIsLoggedIn</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">    <span class="n">app</span><span class="p">.</span><span class="n">launch</span><span class="p">()</span>
</div><div class="line">    <span class="c1">// login 相关</span>
</div><div class="line">    <span class="n">app</span><span class="p">.</span><span class="n">buttons</span><span class="p">[</span><span class="s">&quot;ShowLoginVC&quot;</span><span class="p">].</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">userNameTextField</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="n">textFields</span><span class="p">[</span><span class="s">&quot;用户名&quot;</span><span class="p">]</span>
</div><div class="line">    <span class="n">userNameTextField</span><span class="p">.</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="n">userNameTextField</span><span class="p">.</span><span class="n">typeText</span><span class="p">(</span><span class="s">&quot;SketchK&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">passwordTextField</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="n">textFields</span><span class="p">[</span><span class="s">&quot;密码&quot;</span><span class="p">]</span>
</div><div class="line">    <span class="n">passwordTextField</span><span class="p">.</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="n">passwordTextField</span><span class="p">.</span><span class="n">typeText</span><span class="p">(</span><span class="s">&quot;123&quot;</span><span class="p">)</span>
</div><div class="line">    <span class="n">app</span><span class="p">.</span><span class="n">buttons</span><span class="p">[</span><span class="s">&quot;Login&quot;</span><span class="p">].</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="n">XCTAssertTrue</span><span class="p">(</span><span class="n">app</span><span class="p">.</span><span class="n">userIsLoggedIn</span><span class="p">)</span>
</div><div class="line">
</div><div class="line">    <span class="c1">// logout 相关</span>
</div><div class="line">    <span class="n">app</span><span class="p">.</span><span class="n">buttons</span><span class="p">[</span><span class="s">&quot;ShowUserVC&quot;</span><span class="p">].</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="n">app</span><span class="p">.</span><span class="n">buttons</span><span class="p">[</span><span class="s">&quot;Logout&quot;</span><span class="p">].</span><span class="n">tap</span><span class="p">()</span>
</div><div class="line">    <span class="n">XCTAssertFalse</span><span class="p">(</span><span class="n">app</span><span class="p">.</span><span class="n">userIsLoggedIn</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>从这里，我们已经可以看出上面的代码在阅读体验上变差了很多，而且也增加了理解方面的成本。</p>
<p>所以 John Sundell 认为每次把这样的代码写在 Test Case 里面是不合适的，可以将这些代码可以做成 <code>XCUIApplication</code> 的 categories 来管理，从而让我们的 Test Case 看起来更加简洁，也更容易突出重点。</p>
<h3>关于测试的必要性</h3>
<p>我所在的公司很少会看到有开发人员写测试代码，一方面是因为业务压力大，留给开发人员写测试的时间几乎为零。另一个方面是测试的价值并没有被团队或者公司文化所接纳，团队更倾向于把剩下的问题交给 QA 来负责。</p>
<p>虽然我们目前不需要写任何测试代码，但不代表这种现状是 ok 的，我们的 app 里各个对象间的关系变得越来越复杂，一个小小的改动都有可能触发极其复杂的连锁反应。有时候为了保险起见，我们会无脑交给 QA 进行所谓的回归测试。这时候如果只有纯粹的手工测试，会面临两个问题：</p>
<ul>
<li>难以确定测试的边界</li>
<li>极大的测试人力耗费。</li>
</ul>
<p>可即使完成了这些回归测试，QA 也只能告诉你出了 bug ，不能告诉你问题的根源。而造成这些问题的根源，大多数情况下都是由于系统本身的复杂性，或者代码组织的不合理造成的。</p>
<p>我很认同这样一种说法：可测试的代码不一定是好代码，但坏代码几乎是不可能被测试的。深度耦合的代码，写他们的单元测试，难于上青天；但反过来，我们可以拿“可测试”为标准，不断的完善并重构代码，只要这样坚持下来，最终的代码质量都不会差到哪里去。</p>
<p>说了这么多，你对测试的必要性是怎么看待的呢？</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 010 - Avoiding default cases in switch statements]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-010</guid><link>https://swiftsiqi.com/posts/Swift-Tips-010</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Wed, 04 Sep 2019 02:43:02 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908627_852465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908627_852465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908627_852465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908627_852465.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908627_852465.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1058" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#10-avoiding-default-cases-in-switch-statements">Swift Tips 010 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>这段代码在说什么</h3>
<p>Swift 里的 <code>Switch</code> 语句不同于 Objective-C 里的 <code>Switch</code> 语句，在 Swift 中，Switch 语句必须是完备的。</p>
<p>这就是说，每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下，你可以使用默认（default）分支来涵盖其它所有没有对应的值，这个默认分支必须在 switch 语句的最后面。</p>
<p>当无法满足完备性时，编译器会提示错误。例如代码截图里的 <code>onboarding</code> 情况就没有被覆盖到，所以这时候编译器就会报错。</p>
<p>Swift 的这个特性让错误代码在开发期间就能被暴露出来，从而提升了运行时的稳定性。</p>
<h3>简化无意义的 default 分支代码</h3>
<p>虽然 Swift 这种强制你必须覆盖所有情况的写法提升了代码的健壮性，但是有时候还是会让人觉得有点闹心。</p>
<p>比如下面这段代码，因为 default 分支不会做任何事，但是又碍于编译器报错，你不能删掉 default 语句，只能在 default 后面写上一条无意义的代码，八成最后的代码会长成下面这个样子：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">name</span> <span class="p">=</span> <span class="s">&quot;SketchK&quot;</span>
</div><div class="line">
</div><div class="line"><span class="k">switch</span> <span class="n">name</span> <span class="p">{</span>
</div><div class="line"><span class="k">case</span> <span class="s">&quot;SketchK&quot;</span><span class="p">:</span> <span class="bp">print</span><span class="p">(</span><span class="s">&quot;Hello, SketchK&quot;</span><span class="p">)</span>
</div><div class="line"><span class="k">default</span><span class="p">:</span> <span class="bp">print</span><span class="p">(</span><span class="s">&quot;Do Nothing&quot;</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>那么有更简洁的写法么？答案是当然有，我们只需要在 <code>default:</code> 后面加上一组小括号即可。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="k">switch</span> <span class="n">name</span> <span class="p">{</span>
</div><div class="line"><span class="k">case</span> <span class="s">&quot;SketchK&quot;</span><span class="p">:</span> <span class="bp">print</span><span class="p">(</span><span class="s">&quot;Hello, SketchK&quot;</span><span class="p">)</span>
</div><div class="line"><span class="k">default</span><span class="p">:</span> <span class="p">()</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 009 - Using the guard statement in many different scopes]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-009</guid><link>https://swiftsiqi.com/posts/Swift-Tips-009</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Tue, 03 Sep 2019 02:41:47 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908705_037394.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908705_037394.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908705_037394.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908705_037394.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908705_037394.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="1526" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#9-using-the-guard-statement-in-many-different-scopes">Swift Tips 009 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>今天代码截图里想表达什么</h3>
<p>对于今天的代码而言，John Sundell 向我们展示了 <code>guard</code> 关键字的不同使用场景，在代码中，他利用 <code>guard</code> 完成了跳循环（<code>continue</code>），退循环（<code>break</code>），退函数（<code>return</code>），抛异常（<code>throw</code>），杀进程（<code>exit(1)</code>）的操作。</p>
<p>这里虽然 <code>guard !xxx</code> 的写法也解决了问题，但个人感觉在这里 <code>if-else</code> 的写法会更贴近人类的的直觉，读起来也会更通顺。</p>
<p>关于这点，我们已经无法考证 Joho Sundell 写下这段代码的原始想法，不过这里我们只需知道 <code>guard</code> 还有这些用法就好。</p>
<h3>guard 是什么</h3>
<p>看完了 John Sundell 的酷炫用法后，我们还是回到 guard 关键字上，它是 Swift 2 之后提出的新特性，它使用场景主要有三个</p>
<ol>
<li>突出代码主逻辑，避免过多的 <code>{}</code> 嵌套</li>
<li>通过 gurad 关键字进行可选值解包</li>
<li>对不期望的情况早做检查，使代码的可读性和维护性提升。</li>
</ol>
<p>例如下面的代码是用传统的 <code>if-else</code> 方式编写的</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">createPersonNoGuard</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="n">Person</span><span class="p">?</span> <span class="p">{</span>
</div><div class="line">    <span class="k">if</span> <span class="kd">let</span> <span class="nv">age</span> <span class="p">=</span> <span class="n">age</span><span class="p">,</span> <span class="kd">let</span> <span class="nv">name</span> <span class="p">=</span> <span class="n">name</span><span class="p">,</span> <span class="n">name</span><span class="p">.</span><span class="n">characters</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">age</span><span class="p">.</span><span class="n">characters</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
</div><div class="line">        <span class="k">if</span> <span class="kd">let</span> <span class="nv">ageFormatted</span> <span class="p">=</span> <span class="nb">Int</span><span class="p">(</span><span class="n">age</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">            <span class="k">return</span> <span class="n">Person</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="n">age</span><span class="p">:</span> <span class="n">ageFormatted</span><span class="p">)</span>
</div><div class="line">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">            <span class="k">return</span> <span class="kc">nil</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">        <span class="k">return</span> <span class="kc">nil</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>而这段代码是利用 guard 改造后的代码。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">createPerson</span><span class="p">()</span> <span class="kr">throws</span> <span class="p">-&gt;</span> <span class="n">Person</span> <span class="p">{</span>
</div><div class="line">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">age</span> <span class="p">=</span> <span class="n">age</span><span class="p">,</span> <span class="kd">let</span> <span class="nv">name</span> <span class="p">=</span> <span class="n">name</span><span class="p">,</span> <span class="n">name</span><span class="p">.</span><span class="n">characters</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">age</span><span class="p">.</span><span class="n">characters</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">            <span class="k">throw</span> <span class="n">InputError</span><span class="p">.</span><span class="n">InputMissing</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="k">guard</span> <span class="kd">let</span> <span class="nv">ageFormatted</span> <span class="p">=</span> <span class="nb">Int</span><span class="p">(</span><span class="n">age</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
</div><div class="line">        <span class="k">throw</span> <span class="n">InputError</span><span class="p">.</span><span class="n">AgeIncorrect</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">
</div><div class="line">    <span class="k">return</span> <span class="n">Person</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="n">age</span><span class="p">:</span> <span class="n">ageFormatted</span><span class="p">)</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>关于 guard 的一些最佳实践欢迎阅读以下 2 篇文章</p>
<ul>
<li><a href="https://www.natashatherobot.com/swift-guard-better-than-if/">Swift 2.0: Why Guard is Better than If</a></li>
<li><a href="https://radex.io/swift/guard/">When (not) to use guard</a></li>
</ul>
<h3>关于管理逻辑流分叉的探讨</h3>
<p>上一节提到了 guard 能够帮助开发者突出代码主逻辑，避免过多的 <code>{}</code> 嵌套，我们不妨举个例子，下面这段代码是我们实际开发中可能遇到的场景：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">foo1</span><span class="p">(</span><span class="kc">_</span> <span class="n">a</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">b</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">c</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">d</span><span class="p">:</span><span class="nb">Bool</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">    <span class="k">if</span><span class="p">(</span><span class="n">a</span><span class="o">&amp;&amp;</span><span class="n">b</span><span class="p">){</span>
</div><div class="line">        <span class="k">if</span> <span class="n">c</span> <span class="p">{</span>
</div><div class="line">            <span class="k">if</span> <span class="n">d</span> <span class="p">{</span>
</div><div class="line">                <span class="c1">// do something</span>
</div><div class="line">            <span class="p">}</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="c1">// do something</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>不知道你看到这样的层级嵌套时，有什么想法，我确实已经头大了，所以在没有 guard 之前，有人尝试用这种方式来提升代码的可读性：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">foo2</span><span class="p">(</span><span class="kc">_</span> <span class="n">a</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">b</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">c</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">d</span><span class="p">:</span><span class="nb">Bool</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">    <span class="k">while</span> <span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">        <span class="k">if</span> <span class="o">!</span><span class="p">(</span><span class="n">a</span> <span class="o">&amp;&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">            <span class="k">break</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="k">if</span> <span class="o">!</span><span class="n">c</span> <span class="p">{</span>
</div><div class="line">            <span class="k">break</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="k">if</span> <span class="o">!</span><span class="n">d</span> <span class="p">{</span>
</div><div class="line">            <span class="k">break</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="c1">// do something</span>
</div><div class="line">        <span class="k">break</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="c1">// do something</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>这里我们将嵌套的层级变得更加扁平化，从逻辑梳理的角度上来看是变得更加易读了，但是 <code>while(true)</code> 的写法总是有点别扭，不过不要捉急，Swift 有嵌套函数，我们还可以优化：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">func</span> <span class="nf">foo3</span><span class="p">(</span><span class="kc">_</span> <span class="n">a</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">b</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">c</span><span class="p">:</span><span class="nb">Bool</span><span class="p">,</span> <span class="kc">_</span> <span class="n">d</span><span class="p">:</span><span class="nb">Bool</span><span class="p">)</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">func</span> <span class="nf">should</span><span class="p">(){</span>
</div><div class="line">        <span class="k">if</span> <span class="o">!</span><span class="p">(</span><span class="n">a</span> <span class="o">&amp;&amp;</span> <span class="n">b</span><span class="p">){</span>
</div><div class="line">            <span class="k">return</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="k">if</span> <span class="o">!</span><span class="n">c</span> <span class="p">{</span>
</div><div class="line">            <span class="k">return</span>
</div><div class="line">        <span class="p">}</span>
</div><div class="line">        <span class="c1">// do something</span>
</div><div class="line">    <span class="p">}</span>
</div><div class="line">    <span class="n">should</span><span class="p">()</span>
</div><div class="line">    <span class="c1">// do something</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>即使平级的 <code>if else</code> 和嵌套的 <code>if else</code> 能改善一些体验，但都不如 <code>guard</code> 优雅，不是么？</p>
<p>这里需要多说两句的是，在一些语言中，例如 Ruby，有 <code>unless</code> 语句，本质上是 if 的相反情况（reverse if），即作用域内的代码只有在传递进来的条件被判断为 false 的时候执行。</p>
<p>Swift 中的 <code>guard</code>，虽然有一些类似，但是它们是不同的东西。<code>guard</code> 不是通常意义上的分支语义。它特别强调，在某些期望的条件不满足时，提前退出。</p>
<p>虽然在一些情况下，你可以将 <code>guard</code> 强行掰弯，当做 reverse if 来使用，但是，这可能会背离 <code>guard</code> 的初衷！使用 <code>if..else</code> 语句或者考虑将代码分割成多个函数。</p>
]]></content:encoded></item><item><title><![CDATA[Swift Tips 008 - Passing functions & operators as closures]]></title><guid>https://swiftsiqi.com/posts/Swift-Tips-008</guid><link>https://swiftsiqi.com/posts/Swift-Tips-008</link><dc:creator><![CDATA[张思琦]]></dc:creator><pubDate>Fri, 30 Aug 2019 02:40:06 +0000</pubDate><content:encoded><![CDATA[<p>每天了解一点不一样的 Swift 小知识</p>
<h2>代码截图</h2>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908827_453678.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908827_453678.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908827_453678.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908827_453678.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908827_453678.png" alt="image.png"loading="lazy" decoding="async" width="2048" height="590" /></picture></figure></div><div class="blockquote"><blockquote><p>代码出处: <a href="https://github.com/JohnSundell/SwiftTips#8-passing-functions--operators-as-closures">Swift Tips 008 by John Sundell</a></p>
</blockquote></div>
<h2>小笔记</h2>
<h3>为什么只用传递一个 <code>&lt;</code> 就实现了功能</h3>
<p>与 Objective-C 不同的是，<code>&lt;</code> 在 Swift 中是一个独立的函数，与其他的函数一样，类似 <code>&lt;</code> 这样的操作符也拥有函数参数和函数返回值。</p>
<p>我们看一下 <code>&lt;</code> 的定义</p>
<div class="photo"><figure><picture><source srcset="https://i.typlog.com/siqi/8304908814_902788.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800/format,webp 1x, https://i.typlog.com/siqi/8304908814_902788.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600/format,webp 2x" media="(min-width: 800px)" type="image/webp"><source srcset="https://i.typlog.com/siqi/8304908814_902788.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_800 1x, https://i.typlog.com/siqi/8304908814_902788.png?x-oss-process=image/auto-orient,1/interlace,1/quality,q_90/resize,m_lfit,w_1600 2x" media="(min-width: 800px)"><img src="https://i.typlog.com/siqi/8304908814_902788.png" alt="image.png"loading="lazy" decoding="async" width="1642" height="948" /></picture></figure></div><p>它其实本质就是一个返回值为 <code>(lhs: Self, rhs: Self) -&gt; Bool</code> 的函数，所以将今天的代码进行如下改造：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">ascending</span><span class="p">:</span> <span class="p">(</span><span class="nb">Int</span><span class="p">,</span> <span class="nb">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="p">=</span> <span class="p">{</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="k">return</span> <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">}</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">sorted</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">(</span><span class="n">by</span><span class="p">:</span> <span class="n">ascending</span><span class="p">)</span>
</div></code></pre></div>
</div>
<p>这段代码里 ascending 扮演的功能与 <code>&lt;</code> 完全一样，只是用了一个更容易理解的方式写代码。</p>
<p>当然你也可以这么理解 <code>&lt;</code></p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sorted</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">(</span><span class="n">by</span><span class="p">:</span> <span class="p">{</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="k">return</span> <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">})</span>
</div></code></pre></div>
</div>
<p>这种把函数当做入参的方式，在 Swift 的一些高阶函数使用中经常可以遇到，应该是需要了解的一个知识点。</p>
<p>这里推两篇拓展阅读：<a href="https://www.varvet.com/blog/higher-order-functions-in-swift/">HIGHER-ORDER FUNCTIONS IN SWIFT</a> 和 <a href="https://useyourloaf.com/blog/swift-guide-to-map-filter-reduce/">Swift Guide to Map Filter Reduce</a></p>
<h3>我好像还看到过 <code>array.sorted{ $0 &lt; $1 }</code> 的写法</h3>
<p><code>array.sorted{ $0 &lt; $1 }</code> 和 <code>array.sorted(by: &lt;)</code> 本质上没有任何区别，即运算结果一致，只是利用了不同的 Swift 语言特性，使其在最终的展示层面出现了一些变化。</p>
<p>这里我们来分析下代码是如何一步步变成 <code>array.sorted{ $0 &lt; $1 }</code> 的。其中涉及的知识点主要有 4 个</p>
<ol>
<li>隐式返回函数</li>
<li>尾随闭包</li>
<li>类型推断</li>
<li>参数名称缩写</li>
</ol>
<p>sorted 最原始的代码形式应该是这样的：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedA</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">(</span><span class="n">by</span><span class="p">:</span> <span class="p">{</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="k">return</span> <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">})</span>
</div></code></pre></div>
</div>
<h4>隐式返回的函数</h4>
<p>隐式返回的函数是指：如果一个函数的整个函数体是一个单行表达式，那么就可以隐式地返回这个表达式,也就是说我们可以省略像 <code>return</code> 这样的关键字。</p>
<p>结合原始代码的实际情况，它将变成如下的形式：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedB</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">(</span><span class="n">by</span><span class="p">:</span> <span class="p">{</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">})</span>
</div></code></pre></div>
</div>
<h4>尾随闭包</h4>
<p>尾随闭包是一个书写在函数圆括号之后的闭包表达式，函数支持将其作为最后一个参数调用。在使用尾随闭包时，你不用写出它的参数标签。</p>
<p>结合上面的代码，我们可以将其转换为如下的形式：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedC</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">(){</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>不过这还不最简洁的，如果闭包参数是传给函数的唯一参数，你还可以完全忽略括号。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedD</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">{</span> <span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> <span class="k">in</span>
</div><div class="line">    <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<h4>类型推断</h4>
<p>凭借着 Swift 强大的类型推断能力，上面的代码在还可以进一步简化成如下的形式。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedE</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">{</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span> <span class="k">in</span> <span class="n">lhs</span> <span class="o">&gt;</span> <span class="n">rhs</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<h4>参数名称缩写</h4>
<p>在 Swift 中，可以通过参数名称缩写而不是参数名字来引用参数</p>
<p>如果在闭包表达式中使用参数名称缩写，就可以在闭包定义中省略参数列表，并且对应参数名称缩写的类型会通过函数类型进行推断。<code>in</code> 关键字也同样可以被省略，此时闭包表达式完全由闭包函数体构成：</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">sortedF</span> <span class="p">=</span> <span class="n">array</span><span class="p">.</span><span class="bp">sorted</span><span class="p">{</span> <span class="nv">$0</span> <span class="o">&gt;</span> <span class="nv">$1</span> <span class="p">}</span>
</div></code></pre></div>
</div>
<p>拓展阅读: <a href="https://cocoacasts.com/shorthand-argument-names-in-swift">Shorthand Argument Names in Swift</a></p>
<h3>最后说点什么？</h3>
<p>Swift 的这些特性使其可以写出十分简洁的代码，让很多程序员爱不释手，但有时候过多的”美化”，反而增加了一些阅读门槛。</p>
<p>这里不妨举 2 个例子</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="kd">let</span> <span class="nv">value</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span> <span class="p">=</span> <span class="mi">1</span>
</div><div class="line">
</div><div class="line"><span class="c1">// Solution A</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">newValue</span> <span class="p">=</span> <span class="n">value</span><span class="p">.</span><span class="bp">map</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">}</span>
</div><div class="line">
</div><div class="line"><span class="c1">// Solution B</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">newValue</span><span class="p">:</span> <span class="nb">Int</span><span class="p">?</span>
</div><div class="line"><span class="k">if</span> <span class="kd">let</span> <span class="nv">value</span> <span class="p">=</span> <span class="n">value</span> <span class="p">{</span>
</div><div class="line">  <span class="n">newValue</span> <span class="p">=</span> <span class="n">value</span> <span class="o">+</span> <span class="mi">1</span>
</div><div class="line"><span class="p">}</span>
</div></code></pre></div>
</div>
<p>在这里，map 函数被用来处理可选值。</p>
<div class="block-code" data-language="swift"><div class="highlight"><pre><span></span><code><div class="line"><span class="c1">// Solution A</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">purpleView</span><span class="p">:</span> <span class="bp">UIView</span> <span class="p">=</span> <span class="p">{</span>
</div><div class="line">    <span class="nv">$0</span><span class="p">.</span><span class="n">backgroundColor</span> <span class="p">=</span> <span class="p">.</span><span class="n">purple</span>
</div><div class="line">    <span class="k">return</span> <span class="nv">$0</span>
</div><div class="line"><span class="p">}(</span><span class="bp">UIView</span><span class="p">())</span>
</div><div class="line">
</div><div class="line"><span class="c1">// Solution B</span>
</div><div class="line"><span class="kd">let</span> <span class="nv">purpleView</span><span class="p">:</span> <span class="bp">UIView</span> <span class="p">=</span> <span class="p">{</span>
</div><div class="line">    <span class="kd">let</span> <span class="nv">view</span> <span class="p">=</span> <span class="bp">UIView</span><span class="p">()</span>
</div><div class="line">    <span class="n">view</span><span class="p">.</span><span class="n">backgroundColor</span> <span class="p">=</span> <span class="p">.</span><span class="n">purple</span>
</div><div class="line">    <span class="k">return</span> <span class="n">view</span>
</div><div class="line"><span class="p">}()</span>
</div></code></pre></div>
</div>
<p>而这里利用了参数名称缩写的方式来简化闭包内部的实现。</p>
<p>以上代码到底是 Solution A 好还是 Solution B 好，我确实无法给出一个完美的答案，所以如何写出一份让所有人都觉得阅读体验良好的代码，或许会成为 Swifter 开发者甜蜜的烦恼吧。</p>
]]></content:encoded></item></channel></rss>