9 篇博文 含有标签「Xcode」
查看所有标签利用 Debug Memory Graph 检测内测泄漏
前言
平常我们都会用 Instrument 的 Leaks / Allocations 或其他一些开源库进行内存泄露的排查,但它们都存在各种问题和不便,
在这个 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。
今天介绍一种简单直接的检测内测泄漏的方法:Debug Memory Graph
就是这货:

正文
我最近的项目中,退出登录后(跳转到登录页),发现首页控制器没有被销毁,依旧能接收通知。
退出登录代码:
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Login" bundle:[NSBundle mainBundle]];
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = [storyboard instantiateViewControllerWithIdentifier:@"LoginVC"];
很明显发生了循环引用导致的内测泄漏。
接下来就使用 Debug Memory Graph 来查看内测泄漏了。
运行程序
首先启动 Xcode 运行程序。
Debug Memory Graph

点击 Debug Memory Graph 按钮后,可以看到红框内的是当前内存中存在的对象。其中,绿色的就是视图控制器。
这样,我们随时都可以查看内测中存在的对象,换句话说,就是可以通过观察 Memory Graph 查看内测泄漏。
调试你的App
继续运行你的程序

然后对App进行调试、push、pop 操作,再次点击 Debug Memory Graph 按钮。那些该释放而依旧在内测中的 控制器 或 对象 就能一一找出来了。
接下来,只要进入对应的控制器找到内测泄漏的代码就OK了,一般是Block里引用了 self,改为 weakSelf 就解决了。
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
WS(weakSelf)
sView.btnBlock = ^(NSInteger idx){
[weakSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:idx] withRowAnimation:UITableViewRowAnimationAutomatic];
};
结语
就这样,利用 Debug Memory Graph,可以简单快速的检测内测泄漏。
一般由两个对象循环引用的内测泄漏是比较好发现的,如果是由三个及其三个以上的对象形成的大的循环引用,就会比较难排查了。
iTunes Connect 构建版本不显示
前言
今天新项目上架,在Xcode打包上传到App Store后,在iTunes Connect构建版本中居然找不到上传的App...
解决
从iOS10开始,苹果更加注重对用于隐私的保护,App 里边如果需要访问用户隐私,必须要做描述,所以要在 plist 文件中添加描述。
而这三个基础描述是必须添加的:
-
麦克风权限:
Privacy - Microphone Usage Description是否允许此App使用你的麦克风? -
相机权限:
Privacy - Camera Usage Description是否允许此App使用你的相机? -
相册权限:
Privacy - Photo Library Usage Description是否允许此App访问你的媒体资料库?
其他的权限可以根据自己 APP 的情况来添加。
添加完权限之后然后继续提交 App 就可以了。
若还是找不到,返回 plist 文件中,删除之前的权限,重新添加一下,有可能你哪不小心添加的权限末尾有空格,或者字段不对。
End
Xcode9 无线调试功能
iOS自动打包
利用xcode的命令行工具
xcdeobulid进行项目的编译打包,生成ipa包,并上传到fir
现在网上的自动打包教程几乎都还是xcodebuild + xcrun的方式先生成.app包 再生成.ipa包,结果弄了一整天硬是没成功~
后来发现PackageApplication is deprecated,悲剧。然后手动压缩的 .ipa包因为签名问题无法装到手机上。
后来用了archive + -exportArchive终于可以了~
正文
Xcodebuild
xcodebuild 的使用可以用 man xcodebuild查看。
查看项目详情
# cd 项目主目录
xcodebuild -list
输出项目的信息
Information about project "StackGameSceneKit":
Targets:
StackGameSceneKit
StackGameSceneKitTests
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
StackGameSceneKit
要留意 Configurations,Schemes这两个属性。
自动打包流程
生成 archive
生成archive的命令是 xcodebuild archive
xcodebuild archive -workspace ${project_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${build_configuration} \
-archivePath ${export_archive_path}
-
参数一:项目类型,,如果是混合项目 workspace 就用
-workspace,如果是 project 就用-project -
-scheme:项目名,上面xcodebuild -list中的Schemes -
-configuration:编译类型,在configuration选择,Debug或者Release -
-archivePath:生成 archive 包的路径,需要精确到xx/xx.archive
首先需要创建一个AdHocExportOptions.plist文件
导出ipa包
导出.ipa包经常会出现错误,在ruby2.4.0版本中会报错,所以请使用其他版本的ruby,最初的原因是使用了 ruby2.4.0 进行编译时出现的错误。
解决方法是低版本的 ruby 进行编译,如使用系统版本:rvm use system。后面升级macOS系统(10.12.5)后发现 ruby2.4.0 能成功 导出ipa包了。
导出ipa包使用命令:xcodebuild -exportArchive
xcodebuild -exportArchive \
-archivePath ${export_archive_path} \
-exportPath ${export_ipa_path} \
-exportOptionsPlist ${ExportOptionsPlistPath}
archivePath:上面生成 archive 的路径-exportPath:导出 ipa包 的路径exportOptionsPlist:导出 ipa包 类型,需要指定格式的plist文件,分别是AppStroe、AdHoc、Enterprise,如下图

选择这三个类别需要分别创建三个plist文件:
-
AdHocExportOptionsPlist.plist
-
AppStoreExportOptionsPlist.plist
-
EnterpriseExportOptionsPlist.plist
上传到 Fir
将项目上传到 Fir
下载 fir 命令行工具
gem install fir-cli
获取 fir 的 API Token(右上角)

上传
fir publish "ipa_Path" -T "firApiToken"
自动打包脚本
再次提醒,请不要使用 ruby 2.4.0 运行该脚本!,若在 ruby 2.4.0 下编译失败,请切换低版本的ruby。
切换完毕记得重新安装 fir 命令行工具。
脚本我fork了 jkpang 的脚本进行修改,添加了自动上传到 fir 的功能。
使用方法在Github上有详细介绍。
GitHub:<https://github.com/qiubaiying/iOSAutoArchiveScript>
利用 自定义终端指令 简化打包过程
以zsh为例:
open ~/.zshrc
添加自定义命令 cd + sh
alias mybuild='cd 项目地址/iOSAutoArchiveScript/ && sh 项目地址/iOSAutoArchiveScript/iOSAutoArchiveScript.sh'
这样打开终端输入mybuild,就可以轻松实现一键打包上传了
CocoaPods 安装和使用
最近换了新机器,重新搭建了开发环境,其中当然包括 CocoaPods。
装完顺便更新下 CocoaPods 安装文档。
正文
安装
CocoaPods 是用 ruby 实现的,要想使用它首先需要有 ruby 的环境。
升级ruby
查看ruby版本 $ ruby -v
ruby 2.0.0p648 (2015-12-16 revision 53162) [universal.x86_64-darwin16]
CocoaPods需要2.2.2版本及以上的,我们先升级ruby。
使用 rvm 安装 ruby
curl -L get.rvm.io | bash -s stable source ~/.bashrc source ~/.bash_profile
切换 ruby 源
ruby 下载源使用亚马逊的云服务被墙了,切换国内的 ruby-china源 (<https://ruby.taobao.org/>已经停止维护,详情查看公告):
$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
$ gem sources -l
*** CURRENT SOURCES ***
https://gems.ruby-china.org
安装并切换 ruby
这里不建议安装最新的 2.4.0 版本,因为次版本的 ruby,在xcodebuild 自动打包时,会出现问题! 所以退一步,安装 2.3.3版本~
rvm install 2.3.3 --disable-binary
rvm use 2.3.3 --default
到此ruby升级完毕.
有关RVM的使用可以看这篇 RVM 使用指南
安装CocoaPods
- 安装
sudo gem install -n /usr/local/bin cocoapods
- 升级版本库
pod setup
这里需要下载版本库(非常庞大),需要等很久
Receiving objects: 72% (865815/1197150), 150.07 MiB | 190.00 KiB/s
或者直接从其他装有cocoapod的电脑中拷贝~/.cocoapods到你的用户目录,然后再 pod setup会节省不少时间
使用
创建 podfile 文件
绝大多数人创建podfile都是用 vim Podfile 命令
其实pod 已经提供了创建 podfile 文件的命令,在工程目录下
pod init
将会自动生成 podfile 文件,并且为你写好了格式,稍做修改就能使用
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'projectName' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for projectName
target 'projectNameTests' do
inherit! :search_paths
# Pods for testing
end
target 'projectNameUITests' do
inherit! :search_paths
# Pods for testing
end
end
其中的
target 'projectNameTests' do
inherit! :search_paths
# Pods for testing
end
target 'projectNameUITests' do
inherit! :search_paths
# Pods for testing
end
是指定在单元测试和UI测试时导入的测试框架,若没有使用测试框架可以删除。
修改iOS版本,添加Alamofire库
# Uncomment the next line to define a global platform for your project
# platform :ios, '8.0'
target 'projectName' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for projectName
pod 'Alamofire', '~> 4.4'
end
加载代码库
使用下面的命令,直接在本地版本库中查找对应的代码库信息,不升级版本库,节省时间
pod install --verbose --no-repo-update
若找不到库,再使用下面的命令
pod install
版本号
对版本号的操作除了指定与不指定,你还可以做其他操作:
\>0.1高于0.1的任何版本\>=0.1版本0.1和任何更高版本<0.1低于0.1的任何版本<=0.1版本0.1和任何较低的版本〜>0.1.2版本 0.1.2的版本到0.2 ,不包括0.2。 这个基于你指定的版本号的最后一个部分。这个例子等效于>= 0.1.2并且<0.2.0,并且始终是你指定范围内的最新版本
结语
关于CocoaPods的安装和使用就这样简单的介绍完了,至于更多使用的方法(平时也用不到~)你可以用下面命令查看
$ pod
若对 CocoaPods 的个人仓库感兴趣,也可以看看我的这两篇博客
强化 swift 中的 print
在 Swift 中,最简单的输出方法就是使用 print(),在我们关心的地方输出字符串和值。
当程序变得非常复杂的时候,我们可能会输出很多内容,而想在其中寻找到我们希望的输出其实并不容易。我们往往需要更好更精确的输出,这包括输出这个 log 的文件,调用的行号以及所处的方法名字等等。
在 Swift 中,编译器为我们准备了几个很有用的编译符号,它们分别是:
| 符号 | 类型 | 描述 |
|---|---|---|
| #file | String | 包含这个符号的文件的路径 |
| #line | Int | 符号出现处的行号 |
| #column | Int | 符号出现处的列 |
| #function | String | 包含这个符号的方法名字 |
有了上面的这些编译符号,我们就可以自定义一个输出函数:printm
public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) {
print("[\((filename as NSString).lastPathComponent) \(line) \(function)]\n",items)
}
因为输出是一个很消耗性能的操作,所以在releass环境下需要将输出函数去掉,将上面的函数换成:
#if DEBUG
public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) {
print("[\((filename as NSString).lastPathComponent) \(line) \(function)]\n",items)
}
#else
public func printm(items: Any..., filename: String = #file, function: String = #function, line: Int = #line) { }
#endif
参考:
- 《LOG 输出》 - 王巍 (@ONEVCAT)
Swift 3.1 的新变化「译」

Xcode 8.3 和 Swift 3.1 现在已经发布了(3/28)!
可以通过 AppStore 或 Apple Developer 进行下载

Xcode 8.3 优化了 Objective-C 与 Swift 混编项目的编译速度.
Swift 3.1 版本包含一些期待已久的 Swift package manager 功能和语法本身的改进。
如果您没有密切关注 Swift Evolution 进程,请继续阅读 - 本文非常适合您!
在本文中,我将强调Swift 3.1中最重要的变化,这将对您的代码产生重大影响。我们来吧!😃
开始
Swift 3.1与Swift 3.0源代码兼容,因此如果您已经使用Xcode 中的 Edit \ Convert \ To Current Swift Syntax ... 将项目迁移到Swift 3.0,新功能将不会破坏您的代码。不过,苹果已经在Xcode 8.3中支持Swift 2.3。所以如果你还没有从Swift 2.3迁移,现在是时候这样做了!
在下面的部分,您会看到链接的标签,如[SE-0001]。这些是 Swift Evolution 提案号码。我已经列出了每个提案的链接,以便您可以发现每个特定更改的完整详细信息。我建议您尝试在Playground上验证新的功能,以便更好地了解所有更改的内容。
Note:如果你想了解 swift 3.0 中的新功能,可以看这篇文章。
语法改进
首先,我们来看看这个版本中的语法改进,包括关于数值类型的可失败构造器(Failable Initializers),新的序列函数等等。
可失败的数值转换构造器(Failable Numeric Conversion Initializers)
Swift 3.1 为所有数值类型 (Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float, Float80, Double) 添加了可失败构造器。
这个功能非常有用,例如,以安全、可恢复的方式处理外源松散类型数据的转换,下面来看 Student 的 JSON 数组的处理:
class Student {
let name: String
let grade: Int
init?(json: [String: Any]) {
guard let name = json["name"] as? String,
let gradeString = json["grade"] as? String,
let gradeDouble = Double(gradeString),
let grade = Int(exactly: gradeDouble) // <-- 3.1 的改动在这
else {
return nil
}
self.name = name
self.grade = grade
}
}
func makeStudents(with data: Data) -> [Student] {
guard let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
let jsonArray = json as? [[String: Any]] else {
return []
}
return jsonArray.flatMap(Student.init)
}
let rawStudents = "[{\"name\":\"Ray\", \"grade\":\"5.0\"}, {\"name\":\"Matt\", \"grade\":\"6\"},
{\"name\":\"Chris\", \"grade\":\"6.33\"}, {\"name\":\"Cosmin\", \"grade\":\"7\"},
{\"name\":\"Steven\", \"grade\":\"7.5\"}]"
let data = rawStudents.data(using: .utf8)!
let students = makeStudents(with: data)
dump(students) // [(name: "Ray", grade: 5), (name: "Matt", grade: 6), (name: "Cosmin", grade: 7)]
在 Student 类中使用了一个可失败构造器将 grade 属性从 Double 转变为 Int,像这样
let grade = Int(exactly: gradeDouble)
如果gradeDouble不是整数,例如6.33,它将失败。如果它可以用一个正确的表示Int,例如6.0,它将成功。
Note:虽然
throwing initializers可以用来替代failable initializers。但是使用failable initializers会更好,更符合人的思维。
新的序列函数(Sequence Functions)
swift3.1添加了两个新的标准库函数在 Sequence 协议中:prefix(while:)``和prefix(while:)[SE-0045]。
构造一个斐波纳契无限序列:
let fibonacci = sequence(state: (0, 1)) {
(state: inout (Int, Int)) -> Int? in
defer {state = (state.1, state.0 + state.1)}
return state.0
}
在Swift 3.0中,您只需指定迭代次数即可遍历fibonacci序列:
// Swift 3.0
for number in fibonacci.prefix(10) {
print(number) // 0 1 1 2 3 5 8 13 21 34
}
在swift 3.1中,您可以使用prefix(while:)和drop(while:)获得符合条件在两个给定值之间的序列中的所有元素,就像这样:
// Swift 3.1
let interval = fibonacci.prefix(while: {$0 < 1000}).drop(while: {$0 < 100})
for element in interval {
print(element) // 144 233 377 610 987
}
prefix(while:)返回满足某个谓词的最长子序列。它从序列的开头开始,并停在给定闭包返回false的第一个元素上。
drop(while:) 相反:它返回从给定关闭返回false的第一个元素开始的子序列,并在序列结尾完成。
Note:这种情况,可以使用尾随闭包的写法:
let interval = fibonacci.prefix{$0 < 1000}.drop{$0 < 100}
Concrete Constrained Extensions(姑且翻译为类的约束扩展吧)
Swift 3.1允许您扩展具有类型约束的通用类型。以前,你不能像这样扩展类型,因为约束必须是一个协议。我们来看一个例子。
例如,Ruby on Rails提供了一种isBlank检查用户输入的非常有用的方法。以下是在Swift 3.0中用 String 类型的扩展实现这个计算型属性:
// Swift 3.0
extension String {
var isBlank: Bool {
return trimmingCharacters(in: .whitespaces).isEmpty
}
}
let abc = " "
let def = "x"
abc.isBlank // true
def.isBlank // false
如果你希望isBlank计算型属性为一个可选值所用,在swift 3.0中,你将要这样做
// Swift 3.0
protocol StringProvider {
var string: String {get}
}
extension String: StringProvider {
var string: String {
return self
}
}
extension Optional where Wrapped: StringProvider {
var isBlank: Bool {
return self?.string.isBlank ?? true
}
}
let foo: String? = nil
let bar: String? = " "
let baz: String? = "x"
foo.isBlank // true
bar.isBlank // true
baz.isBlank // false
这创建了一个采用 String 的 StringProvider 协议而在你使用StringProvider扩展可选的 wrapped 类型时,添加isBlank方法。
Swift 3.1中,用来替代协议方法,扩展具体类型的方法像这样:
// Swift 3.1
extension Optional where Wrapped == String {
var isBlank: Bool {
return self?.isBlank ?? true
}
}
这就用更少的代码实现了和原先相同的功能~
泛型嵌套(Nested Generics)
Swift 3.1允许您将嵌套类型与泛型混合。作为一个练习,考虑这个(不是太疯狂)的例子。每当某个团队领导raywenderlich.com想在博客上发布一篇文章时,他会分配一批专门的开发人员来处理这个问题,以满足网站的高质量标准:
class Team<T> {
enum TeamType {
case swift
case iOS
case macOS
}
class BlogPost<T> {
enum BlogPostType {
case tutorial
case article
}
let title: T
let type: BlogPostType
let category: TeamType
let publishDate: Date
init(title: T, type: BlogPostType, category: TeamType, publishDate: Date) {
self.title = title
self.type = type
self.category = category
self.publishDate = publishDate
}
}
let type: TeamType
let author: T
let teamLead: T
let blogPost: BlogPost<T>
init(type: TeamType, author: T, teamLead: T, blogPost: BlogPost<T>) {
self.type = type
self.author = author
self.teamLead = teamLead
self.blogPost = blogPost
}
}
将BlogPost内部类嵌套在其对应的Team外部类中,并使两个类都通用。这是团队如何寻找我在网站上发布的教程和文章:
Team(type: .swift, author: "Cosmin Pupăză", teamLead: "Ray Fix",
blogPost: Team.BlogPost(title: "Pattern Matching", type: .tutorial,
category: .swift, publishDate: Date()))
Team(type: .swift, author: "Cosmin Pupăză", teamLead: "Ray Fix",
blogPost: Team.BlogPost(title: "What's New in Swift 3.1?", type: .article,
category: .swift, publishDate: Date()))
但实际上,在这种情况下,您可以简化该代码。如果嵌套的内部类型使用通用外部类型,那么它默认继承父类的类型。因此,您不需要如此声明:
class Team<T> {
// original code
class BlogPost {
// original code
}
// original code
let blogPost: BlogPost
init(type: TeamType, author: T, teamLead: T, blogPost: BlogPost) {
// original code
}
}
Note:如果您想了解更多关于Swift中的泛型,请阅读我们最近更新的Swift泛型入门的教程。
Swift版本的可用性
您可以使用**#if swift(>= N)** 静态构造来检查特定的Swift版本:
// Swift 3.0
#if swift(>=3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}
#elseif swift(>=3.0)
func intVersion(number: Double) -> Int {
return Int(number)
}
#endif
然而,当使用Swift标准库时,这种方法有一个主要缺点。它需要为每个受支持的旧语言版本编译标准库。这是因为当您以向后兼容模式运行Swift编译器时,例如您要使用Swift 3.0行为,则需要使用针对该特定兼容性版本编译的标准库版本。如果您使用版本3.1模式编译的,那么您根本就没有正确的代码
因此,@available除了现有平台版本 [SE-0141] 之外,Swift 3.1扩展了该属性以支持指定Swift版本号:
// Swift 3.1
@available(swift 3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}
@available(swift, introduced: 3.0, obsoleted: 3.1)
func intVersion(number: Double) -> Int {
return Int(number)
}
这个新功能提供了与intVersionSwift版本有关的方法相同的行为。但是,它只允许像标准库这样的库被编译一次。编译器然后简单地选择可用于所选择的给定兼容性版本的功能。
Note:注意:如果您想了解更多关于Swift 的
可用性属性( availability attributes),请参阅我们关于Swift中可用性属性的教程。
逃逸闭包(Escaping Closures)
在Swift 3.0 [ SE-0103 ] 中函数中的闭包的参数是默认是不逃逸的(non-escaping)。在Swift 3.1中,您可以使用新的函数withoutActuallyEscaping()将非逃逸闭包转换为临时逃逸。
func perform(_ f: () -> Void, simultaneouslyWith g: () -> Void,
on queue: DispatchQueue) {
withoutActuallyEscaping(f) { escapableF in // 1
withoutActuallyEscaping(g) { escapableG in
queue.async(execute: escapableF) // 2
queue.async(execute: escapableG)
queue.sync(flags: .barrier) {} // 3
} // 4
}
}
此函数同时加载两个闭包,然后在两个完成之后返回。
f与g进入函数后由非逃逸状态,分别转换为逃逸闭包:escapableF和escapableG。- async(execute:) 的调用需要逃逸闭包,我们在上面已经进行了转换。
- 通过运行
sync(flags: .barrier),您确保async(execute:)方法完全完成,稍后将不会调用闭包。 - 在范围内使用
escapableFandescapableG.
如果你存储临时逃离闭包(即真正逃脱)这将是一个Bug。未来版本的标准库可以检测这个陷阱,如果你试图调用它们。
Swift Package Manager 更新
啊,期待已久的 Swift Package Manage 的更新了!
可编辑软件包(Editable Packages)
Swift 3.1将可编辑软件包(editable packages)的概念添加到Swift软件包管理器 [ SE-0082 ]。
该swift package edit命令使用现有的Packages并将其转换为editable Packages。使用--end-edit命令将 package manager 还原回 规范解析的软件包(canonical resolved packag)。
版本固定(Version Pinning)
Swift 3.1 添加了版本固定的概念[ SE-0145 ]。该 pin 命令 固定一个或所有依赖关系如下所示:
$ swift package pin --all // 固定所有的依赖
$ swift package pin Foo // 固定 Foo 在当前的闭包
$ swift package pin Foo --version 1.2.3 // 固定 Foo 在 1.2.3 版本
使用unpin命令恢复到以前的包版本:
$ swift package unpin —all
$ swift package unpin Foo
Package manager 将每个依赖库的版本固定信息存储在 Package.pins 文件中。如果该文件不存在,则Package manager 会自动创建。
其他
swift package reset 命令将会把 Package 重置干净。
swift test --parallel 命令 执行测试。
其他改动
在 swift 3.1 中还有一些小改动
多重返回函数
C函数返回两次,例如vfork 和 vfork 已经不用了。他们以有趣的方式改变了程序的控制流程。所以 Swift 社区 已经禁止了该行为,以免导致编译错误。
自动链接失效(Disable Auto-Linking)
Swift Package Manager 禁用了在C语言 模块映射(module maps)中的自动链接的功能:
// Swift 3.0
module MyCLib {
header “foo.h"
link “MyCLib"
export *
}
// Swift 3.1
module MyCLib {
header “foo.h”
export *
}
结语
Swift 3.1改善了Swift 3.0的一些功能,为即将到来的Swift 4.0的大改动做准备。这些包括对泛型,正则表达式,更科学的String等方面的作出极大的改进。
如果你想了解更多,请转到 Swift standard library diffs 或者查看官方的的Swift CHANGELOG,您可以在其中阅读所有更改的信息。或者您可以使用它来了解 Swift 4.0 中的内容!






