Swift 5 创建和使用 Framework, XCFramework 从入门到精通 John 易筋 ARTS 打卡 Week 42
1. Algorithm: 每周至少做一个 LeetCode 的算法题
笔者的文章:
LeetCode 全集请参考:LeetCode Github 大全
题目
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
注意:本题与主站 239 题相同:https://leetcode-cn.com/problems/sliding-window-maximum/
解题
2. Review: 阅读并点评至少一篇英文技术文章
笔者的文章:
翻译:Swift 5创建和使用Framework, XCFramework 从入门到精通
说明
了解如何构建 iOS 框架,该框架可让您在应用程序之间共享代码,模块化代码或将其分发为第三方库。
更新说明:本教程已由 Emad Ghorbaninia 更新到 iOS 14,Xcode 12 和 Swift 5.3。原始教程由 Sam Davies 撰写。
您是否曾经想在两个应用程序之间共享大量代码,或者想与其他开发人员共享程序的一部分?
也许您想对代码进行模块化,就像 iOS SDK 通过功能将其 API 分开一样。或者,您可能希望像流行的第三方库一样分发代码。
在本教程中,您将把在为iOS创建自定义CalendarControl中开发的CalendarControl提取到一个单独的可重用框架中。一路上,您将:
为 CalendarControl 创建一个新框架 Framework。
迁移现有代码。
将所有内容导入回应用程序。
构建一个二进制框架 XCFramework。
将其打包为超级便携式 Swift 软件包。
为您的 Swift 软件包设置一个存储库并发布它。
待您完成时,该应用程序将像以前一样使用您开发的便携式 XCFramework 进行操作!
入门
使用本教程顶部或底部的“下载材料”按钮,下载入门项目和最终项目。查找入门项目文件夹。找到 1-RWCalendarPickerUI 并打开 RWCalendarPicker.xcodeproj。
RWCalendarPicker 是一款类似于提醒的清单应用程序,可让用户创建任务并设置截止日期。
构建并运行以了解其工作原理。
查看 RWCalendarPicker 中的文件,以熟悉该项目。CalendarControl 的代码分为几个类:
Day.swift:一个数据模型,其中包含每个日对象的数据。
MonthMetadata.swift:几个月的数据模型。
CalendarDateCollectionViewCell.swift:使用集合视图中的单元格显示一个月中的几天。每个单元格都在此处自定义。
CalendarPickerFooterView.swift:允许用户选择不同的月份。
CalendarPickerHeaderView.swift:显示当前月份和年份,让用户关闭选择器并显示工作日标签。
CalendarPickerViewController.swift:日历的主体,所有相关视图在此合并。
CalendarPicker 非常方便。在此应用程序之外的多个应用程序中使用它会不会很好?救援框架 Framework!
什么是框架?
框架是独立的,可重用的代码和资源块,您可以将其导入许多应用程序中。您甚至可以在 iOS,tvOS,watchOS 和 macOS 应用程序之间共享它们。与 Swift 的访问控制结合使用时,框架可帮助定义代码模块之间强大的,可测试的接口。
框架具有三个主要目的:
封装代码
模块化代码
重用代码
如果您使用其他语言编程,则可能听说过 node modules, packages, gems or jars。Frameworks 与 Apple 生态系统中的框架相同。iOS SDK 中常见框架的一些示例包括 Foundation,UIKit,SwiftUI,CloudKit 和 Combine。
用 Swift 的话来说,模块是分发在一起的一组编译后的代码。框架是模块的一种,而应用程序是另一种。
注意:如果您想了解有关框架的更多信息,请阅读什么是框架?。
创建框架
苹果在 Xcode 6 中引入了 Cocoa Touch Framework,最近又将其更改为 Framework。创建框架从未如此简单。首先,您将为框架创建项目。
在 Xcode 中,选择 File▸New▸Project…。然后选择 iOS▸框架和库▸框架。
单击下一步。然后将产品名称设置为 CalendarControl。使用您自己的组织名称和组织标识符。
单击下一步。在文件选择器中,选择在 2-Framework 创建项目。然后点击创建。
现在您有一个项目,尽管很无聊,但却创建了一个框架!
将源代码添加到框架
您当前的状态是一个没有代码的框架。这和不加糖的纯巧克力一样诱人。在本节中,您将通过将现有文件添加到框架中来介绍代码。
从 RWCalendarPicker 源目录中,将 Day.swift 拖动 到 Xcode 中的 CalendarControl 项目中。确保根据需要选中“复制项目”,以便将文件复制到新项目中,而不是添加引用。框架需要自己的代码(而不是引用)才能独立。
仔细检查 Day.swift 在 CalendarControl 中是否具有目标成员身份,以确保它出现在最终框架中。通过选择此文件并确保在“文件”检查器的“目标成员资格”区域中选择了 CalendarControl 来进行验证。
现在,按照上述步骤将这些文件添加到您的项目中:
MonthMetadata.swift
CalendarDateCollectionViewCell.swift
CalendarPickerFooterView.swift
CalendarPickerHeaderView.swift
CalendarPickerViewController.swift
>注意:将类划分到各自的文件夹组中并不是绝对必要的,但这是组织代码的良好实践。
在“项目”导航器中选择您的项目,然后在“目标”中选择“ CalendarControl”。打开构建设置。然后将“构建要分发的库”设置为“是”。这将产生一个模块接口文件,当有人跳转到 Xcode 中的模块定义时,该文件将显示您的公共 API 。
生成框架项目。确保您成功完成构建,没有构建警告或错误。
向项目添加框架
关闭 CalendarControl。返回到 RWCalendarPicker。现在,您已经将日历框架组装到框架中,在主项目中不再需要迁移的文件。删除以下文件。在确认对话框中选择移至垃圾箱。
Day.swift
MonthMetadata.swift
CalendarDateCollectionViewCell.swift
CalendarPickerFooterView.swift
CalendarPickerHeaderView.swift
CalendarPickerViewController.swift
生成项目。您将看到几个可预测的错误,其中 Xcode 抱怨不知道到底 CalendarPickerViewController 是什么。具体来说,您会在范围错误消息中看到“找不到 CalendarPickerViewController”。
您将通过添加 CalendarControl 框架项目来解决这些问题。
嵌入您的二进制文件
现在,右键单击项目导航器中的根 RWCalendarPicker 节点。单击添加文件到“ RWCalendarPicker”。
在文件选择器中,导航到 2-Framework / CalendarControl 并选择 CalendarControl.xcodeproj。然后单击“添加”将其添加为子项目。
注意:并非必须将框架项目添加到应用程序项目中。您可以添加 CalendarControl.framework 输出。
但是,将项目组合在一起可以更轻松地同时开发框架和应用程序。您对框架项目所做的任何更改都会自动传播到应用程序。这也使 Xcode 更容易解析路径并知道何时重建项目。
生成并运行。您会看到相同的编译错误!
即使两个项目现在在一起,RWCalendarPicker 仍然没有得到 CalendarControl。就像他们坐在同一个房间里一样,但是 RWCalendarPicker 无法看到新框架。
请按照以下步骤解决问题:
将方案更改为 CalendarControl 并进行构建。
然后,展开 CalendarControl 项目以查看“产品”文件夹。
在其下面查找 CalendarControl.framework。该文件是框架项目的输出,该项目打包了二进制代码,标头,资源和元数据。
然后选择顶层 RWCalendarPicker 节点以打开项目编辑器。
单击 RWCalendarPicker 目标。然后转到“常规”选项卡。
向下滚动到“框架,库和嵌入式内容”部分。
拖动 CalendarControl.framework 从产品文件夹的 CalendarControl.xcodeproj 到这一部分。
您已在 Frameworks,Libraries 和 Embedded Content 中为该框架添加了一个条目。
现在,该应用程序知道了该框架以及在何处可以找到它。这样就足够了吧?
切换到 RWCalendarPicker 方案并构建项目。更多相同的错误。
您错过了框架开发的重要部分:访问控制。
访问控制
尽管框架是项目的一部分,但是项目的代码对此并不了解。眼不见,心不烦。
转到 ItemDetailViewController.swift,并将以下行添加到文件顶部的导入列表中:
至关重要的是,此包含仍然不能解决构建错误,因为 Swift 使用访问控制来让您确定结构是否对其他文件或模块可见。
默认情况下,Swift 使所有内容 internal—仅在其自己的模块中可见。
要恢复该应用程序的功能,您必须更新 CalendarControl 类上的访问控制。
尽管有点乏味,但是更新访问控制的过程通过隐藏不想出现在框架外部的代码来提高模块化。您可以通过使某些函数不具有访问修饰符或显式声明它们来实现此目的 internal。
Swift 具有五个级别的访问控制。创建自己的框架时,请遵循以下经验法则:
Open and public 开放和公开:适用于应用或其他框架(例如自定义视图)调用的代码。
Internal 内部:用于在框架内的函数和类之间使用的代码,例如该视图中的自定义层。
Fileprivate:用于单个文件中使用的代码,例如用于计算布局高度的辅助函数。
Private 私有:用于封闭声明中使用的代码,例如单个文件的 class 块以及该声明在同一文件中的扩展名。
当 CalendarControl 成为 RWCalendarPicker 应用程序的一部分时,内部访问就不成问题。现在,它在一个单独的模块中,您必须将其公开,以供应用使用。您将在下一部分中进行操作。
注:如果您想了解更多关于访问控制和理解之间的区别 open 和 public,看一看的访问控制文件。
更新框架访问级别
打开 CalendarPickerViewController.swift。通过将 public 关键字添加到类定义中来公开类,如下所示:
现在 CalendarPickerViewController 对导入 CalendarControl 框架的任何应用程序文件都是可见的。
接下来,将 public 关键字添加到:
CalendarPickerViewController.init(baseDate:selectedDateChanged:)
CalendarPickerViewController.init(coder:)
CalendarPickerViewController.viewDidLoad()
CalendarPickerViewController.viewWillTransition(to:with:)
CalendarPickerViewController.collectionView(_:numberOfItemsInSection:)
CalendarPickerViewController.collectionView(_:cellForItemAt:)
CalendarPickerViewController.collectionView(_:didSelectItemAt:)
CalendarPickerViewController.collectionView(_:layout:sizeForItemAt:)
>注意:您可能想知道为什么必须声明 init 为 public。Apple 在其访问控制文档中解释了访问控制的这一点和其他一些要点。
生成并运行。现在,您获得了 CalendarControl。
恭喜你!现在,您有了一个有效的独立框架以及使用该框架的应用程序!
发布 XCFramework
您可能在 WWDC 2019 期间听说过 XCFramework。是的,您是对的:这是可以使用 Xcode 生成的二进制框架的名称。
在 2019 年之前,您只有一个机会来制作自己的二进制框架:通用静态库(Universal Static Library),也称为 Fat Framework。
为了支持多种架构,例如模拟器和设备,您必须将它们组合到 fat 框架中的一个库下。但是,在本文之后,您的框架不必再胖了。
归档您的框架
在本节中,您将与老朋友 Terminal 一起工作。oo!
打开您的终端,并使用以下命令导航到 framework 文件夹。或者,您可以在 cd 命令后将项目文件夹拖到终端:
接下来,开始为以下目标归档您的框架:
iOS
Simulator
macOS
从 iOS 开始。在终端中输入以下命令:
该命令将使用以下列表作为输入来生成您的框架的存档:
-scheme CalendarControl:它将使用此方案进行归档。
-configuration Release:它将使用发布配置进行构建。
-destination'generic / platform = iOS':这是架构类型。
-archivePath:它将归档文件以给定名称保存到该文件夹路径中。
SKIP_INSTALL:设置为 NO 即可将框架安装到存档中。
BUILDLIBRARIESFOR_DISTRIBUTION:确保构建了用于分发的库并创建了接口文件。
接下来,目标模拟器。通过将此命令添加到终端来进行归档:
这些命令选项与 iOS 的选项相同,不同之处在于:
-destination'generic / platform = iOS Simulator':在此处设置体系结构类型。
-archivePath:这会将存档生成到具有给定名称的文件夹路径中。
最后,为 macOS 生成一个新的存档。在终端中添加以下命令:
除了以下区别外,此命令与其他命令相同:
-destination'platform = macOS,arch = x86_64,variant = Mac Catalyst':这是您指示体系结构类型的位置。
-archivePath:这会将存档生成到具有给定名称的文件夹路径中。
正如您在 finder 和下面的屏幕快照中所看到的,您从框架中生成了三个不同的存档文件。
生成 XCFramework
现在,制作二进制框架 XCFramework。将以下命令添加到终端:
此命令使用之前生成的档案将 XCFramework 添加到 build 文件夹。
检查构建文件夹以查看 XCFramework 包含的内容。
嘘!现在,您已经完成了第一个 XCFramework。
将 XCFramework 集成到您的项目中
现在是时候从 RWCalendarPicker 项目中删除 CalendarControl 引用了,就像这样:
这样做是因为您不再需要访问框架的源代码。将 XCFramework 拖到项目目标的“框架,库和嵌入式内容”部分:
生成并运行。您将可以访问与以前相同的类,但是这次您的框架只是二进制的。
将 XCFramework 作为 Swift 软件包分发
在 WWDC 2020 上,Apple 宣布您可以轻松地在 Swift Packages 中分发 XCFramework。那不是很棒吗?
注意:如果您不熟悉 Swift Packages 或 Swift Package Manager,可以通过阅读iOS的Swift Package Manager来了解更多信息。
您应该具有用于分发 XCFramework 的 Swift 软件包。您将在下一部分中创建一个。然后,您可以通过在 GitHub 上发布框架来共享您的框架。
准备 Swift 包
在入门项目文件中,您已经有一个简单的 Swift 软件包。导航到/ starter / 3-SwiftPackage / CalendarControl 并打开 Package.swift
此类是您的 Swift 软件包的清单。您需要对其进行修改,以使 CalendarControl 成为 Swift 软件包。
请按照下列步骤,并用正确的值填充清单:
Platforms:
此代码指示它可以在哪些平台上运行。
Product
这些是包装提供的产品。这些可以是库(您可以将其导入其他 Swift 项目中的代码)或可执行文件(可以由操作系统运行)的代码。一个产品是可以对其他包导出到使用的目标。
Targets
目标是独立构建的代码模块。在这里,您可以为 XCFramework 添加路径。
注意:清单的第一行必须包含格式化的注释,该注释告诉 Swift 软件包管理器构建软件包所需的最低 Swift 编译器版本。
最后,将 XCFramework 添加到项目中并将其放在 Sources 下:
恭喜你!您的 Swift 软件包已准备好分发。
大二进制和计算校验和
尽管并非如此 CalendarControl,但特别庞大的框架将需要格外小心。如果您想了解更多有关处理较大的二进制文件的信息,请打开下面的扰流板。
如果您有一个大型的二进制框架(如 Apple 所建议的),则不应将其添加到存储库中。相反,您可以将其上传到主机,并在清单目标中添加一个 URL,如下所示:
在这里,您有三个输入:
名称:这是您的包裹名称。
URL:您的 XCFramework 压缩文件 URL。
校验和:您的 Swift 软件包将确保框架 zip 文件与通过检查此校验和所生成的压缩文件相同。
请按照以下步骤计算校验和:
压缩您的框架,然后将 zip 文件复制到 Swift Package 文件夹中。
在终端中,导航到 Swift Package 文件夹。
运行以下命令并生成校验和。
您的 XCFramework 校验和在这里。您可以将此复制到清单中。
发布您的 Swift 软件包
Xcode 使发布软件包变得容易。接下来,您将发布您创建的 CalendarControl Swift 软件包。
使用 Xcode,打开 Swift Package 项目。然后从菜单栏中选择 Source Control▸New Git Repositories…。
这将在您的计算机上创建一个 Git 存储库,并首次提交您的代码。
必须先将其公开发布,然后才能发布您的软件包供他人使用。最简单的方法是发布到 GitHub。
如果您没有 GitHub 帐户,则可以在github.com上免费创建一个。然后,如果还没有这样做,请通过在 Xcode 菜单中选择 Xcode▸Preferences 将 GitHub 帐户添加到 Xcode 。选择帐户。
现在,单击+添加一个新帐户。选择 GitHub 并按要求填写您的凭据。
打开源代码管理导航器,然后选择 CalendarControl 程序包。然后,通过右键单击或按住 Control 键单击打开上下文菜单。
接下来,选择 New“ CalendarControl” Remote…。您可以将可见性更改为“私人”或接受默认设置。然后点击创建。
这会在 GitHub 上创建一个新的存储库,并自动为您推送代码。
接下来,通过为包创建标签来设置框架的版本。在上下文菜单中,选择“标记主要…”。将其标记为 1.0.0 版,然后点击创建。
最后,从 Xcode 菜单栏中选择 Source Control▸Push…。确保选择了包括标签。然后单击“推”。
这会将标签推送到 SwiftPM 可以读取它的 GitHub。您的软件包的 1.0.0 版现已发布。:]
使用 Swift 包
现在是时候在 RWCalendarPicker 项目中使用您的 Swift 软件包了。
再次打开 RWCalendarPicker。从项目目标中删除导入的 CalendarControl 框架:
接下来,您将学习如何使用 Swift Package Manager 将 Swift Package 添加到项目中。
使用 SPM 添加程序包依赖性
选择文件▸斯威夫特包▸添加包依赖...。粘贴 Git 存储库 URL。单击下一步。
根据您的 GitHub 设置,您可能需要在此处验证 SSH 密钥。然后,在“规则”下,确保为版本 1.0.0 选择了“至下一专业”。单击下一步。
如果您想了解有关主要和次要版本控制的更多信息,请访问 semver.org。Xcode 提取包后,请确保已选择 CalendarControl 产品并将其添加到 RWCalendarPicker 目标。然后选择完成。
生成并运行。确保一切都像以前一样运行。
极好的!现在,您有一个在项目内部使用的远程 Swift 软件包。
然后去哪儿?
您可以使用本教程顶部或底部的“下载材料”按钮下载项目的完整版本。
在本教程中,您学习了如何:
从 RWCalendarPicker 中提取 CalendarControl 并创建一个新框架。
在 RWCalendarPicker 中使用 CalendarControl 框架。
建立一个二进制框架,并在 RWCalendarPicker 中使用该 XCFramework。
发布一个 Swift 软件包,并在 RWCalendarPicker 中使用它。
干得好!
从这里,观看有关此主题的 WWDC 2019 和 2020 视频:
#pic_center =600x
参考
https://www.raywenderlich.com/17753301-creating-a-framework-for-ios
https://developer.apple.com/documentation/xcode/creatingastandaloneswiftpackagewithxcode
3. Tips: 学习至少一个技术技巧
笔者的文章:
Swift 5 UIStackView中添加自动换行的Label
说明
实现 UIStackView 中添加可以自动换行的 Label
思路分析:
view 中添加 UIStackView,UIStackView 添加 Label;
UIStackView 的 constraint 为上,左,右;
Label 的 constraint 为左,右;Label 的
numberOfLine = 0
Demo 代码如下:
参考
https://stackoverflow.com/questions/34386528/multiline-label-in-uistackview
4. Share: 分享一篇有观点和思考的技术文章
笔者的文章:
讲师:邱岳
1. 什么是数据分析,以及为什么要数据分析
通过对数据进行整理加工获得信息和知识,从而了解产品的生存情况、发现潜在机会、引导和支撑产品与运营决策、验证策略效果。
数据是互联网产品经理的福音,也是改变互联网产品经理与传统软件产品经理 / 消费产品经理工作性质的核心元素。<font color='red'>实时 - 精确 - 完善 - 结构化 </font>
用数据而非直觉:避免决策视角狭窄、避免无依据决策。<font color='red'>让决策变宽、变扎实</font>
立场和逻辑之争永无止境,数据加逻辑聊天 一招毙命。
2. 几个与数据分析有关的故事
半小时之内撤下对用户转化率有影响的特性;
Google 的 41 种蓝色和 $200 Million 的收益
按钮颜色也能带来收益区别?
3. 数据分析的常见工具
<font color='red'>GoogleAnalytics</font> / MixPanel / Growing IO / Sensors Data / 友盟
小程序:小程序数据助手 / 阿拉丁
Tableau / Excel / Python / <font color='red'>Google Sheets</font> / SQL
R / MatLab ......
重新认识<font color='red'>Excel</font>几个关键词: Excel 公式、透视表、VBA、Google Script、Python Excel
4. 数据分析的执行过程
数据规划(设定目标)
数据埋点 / 记录 (技术支持与实现)
数据收集与整理(原始数据 --> 结构化数据)
数据统计与分析(从数据 --> 信息 / 知识)
结论或行动(信息 --> 决策 / 行动)
5. 数据规划 --> 设定目标
反应产品 / 功能的运转情况
☞ 选定关键指标,每天盯住
寻找流程瓶颈点或新的产品机会
☞ 行为路径,流量漏斗
提供产品、运营决策支撑
☞ 进行 A / B 测试
☞ 设定策略水位数值(双十一晚上大促的时候,发大红包,促成交易。根据数据给指定用户发红包)
对具体的产品运营策略提供效果追踪
4.1 如何选关键指标
小心虚荣指标 (比如某个大 V 的推广)
与利益相关者的价值有关系吗?(产品经理看功能的使用频率,投资人或老板看 GMV)
这个数字变好就等于产品变好吗?(某个功能多弹了一个界面,使 PV 增加)
大家能交流吗?(关注指标的变化)
提前发现问题还是滞后发现问题?(用户试用频率降低,就说明产品开始走下坡路)
有信息吗?能撬动吗?(比如把用户拉回来,用优质的文章吸引用户)
5. 数据埋点
一般说到数据埋点,都指的是行为数据的收集;
本质上是一个带了各种各样线索的记录请求,是无状态的;
数据埋点需求,与其描述埋点的方案,不如告诉 BI 或工程师你想干什么;
大部分的数据埋点需求基于事件(页面访问也算事件),列出想要的时间列表和分类即可;
很难一次埋点,就全埋对,从小事开始埋,比如一个按钮,然后逐渐研究更大的故事;
若公司存在完善的埋点和数据收集流程与规范,以上都不算。
6. 数据收集与整理
将原始的异构数据,整理为可以进行筛选 / 统计 / 处理的结构化数据;
这里的大部分工作都是数据工程师 / 数据工具完成的,这一步产品经理介入不多;
6.1 怎么收集竞争对手的数据? 怎么收集行业数据?
注册用户 id
Job Description
竞争对手的员工过来面试
报告:
7. 数据统计与分析
指标:某一件事情的度量数值,比如用户量、访问量、访问时长、转化率等,通常与具体业务的中间目标或最终目标直接相关。
维度:对指标进行不同细分的方式,比如用户量可以分为新用户、老用户、安卓用户、iOS 用户、付费用户免费用户等,不同的细分方式,代表了不同的分析角度。
8. 结论或行动
数据分析要指向一个目标,信息 / 知识 / 决策 / 原则;
【目标】是数据分析的起点,也是终点;
对外:数据报告、数据洞察、业务策略结论;
对内:行业认知、做事的经验和原则;
9. 数据分析的思路和框架
偏向以用户、行为为核心:用户是谁、从哪里来、到哪里去;
偏向流量:流量成本、变现效率、可持续性;
偏向卖货:流量、转化、毛利;
偏向 Saas: 客户分类、留存与激活、收入规模与效率;
平台型产品:各角色利益博弈、货币化率;
10. 数据分析对象
用户属性数据:一组画像;
用户行为数据:一张地图;
业务数据:一堆图标;
财务数据:钱;
行业数据:几个数字;
宏观数据;
11. 数据分析常见指标与释义
用户属性数据 - 技术参数、地理位置、年龄、性别、地区......
用户行为数据 - PV、UV、(PV/UV)、VV、UPV、DAU、MAU、(DAU/MAU)、WAU、AAC、MAC、WAC、CTR、留存、来源、访问时长......
业务数据 - (跟你相关)如:课程数量、训练营数量、订单数、发帖量、包裹数......
财务数据 - GMV、ARPU、LTV、客单价、复购率、转化率、Take Rate......
行业数据 - TAM、CAC、TAC......
宏观数据 - 行业时长规模、GDP 占比。
GMV 包括了下单但未付款的数据,下单但是退货的也算。行业惯例。
12. 数据分析常见指标与释义
最重要的是,有你自己的指标定义,而不是一个业界通行的【虚荣指标】。
所有的指标应该在脑海中组成一张【数据大图】,知道它们之间的关系;
让数据成为自己的语言,用数据重构直觉,而不是用情绪和好恶来构建直觉;
13. 不是作业的作业
PDD 的财报,对比京东和阿里,关注 AAC、MAC、MAU、GMV、Take Rate.
财报查看券商,sec --> https://en.wikipedia.org/wiki/U.S.Securitiesand_Exchange_Commission
版权声明: 本文为 InfoQ 作者【John(易筋)】的原创文章。
原文链接:【http://xie.infoq.cn/article/38a800650b100239810976c85】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论