合理的使用 Swift 中的 final 类

你上一次在 Swift 中继承一个类是什么时候?而且这个类是你创建的但不是 Cocoa 体系中的一部分。在 protocol 扩展和一般的 extension 扩展存在的情况下,你多久继承一次非 Cocoa 类型的 class ?

如果你的答案在 0% 和 5% 之间,你是相当具有代表性的。在 Swift 的类型体系中,引用类型不再使用继承来紧密耦合了。

进一步来说,你多久创建一次类和子类带着这样的意图:他们将来会被继承,通过一个特定模块之外的 API 客户端。(假设,当然,你不是 Apple,并且你没有在写 view 和 controller)。

当继承成为例外而不是规则,是时候考虑默认创建 final 类?或者将模块封装为 internal-by-default 更好?这样公开类在原模块外就不能被继承。

辩论目前如火如荼的展开在 Swift 进度列表中,关于这如何进行,以及是否应该这么做,是否类应该被设计为强制调用父类以重写方法(需要上层调用)。

John McCall 写道:


我们目前的意图是,公开的继承和重写将被锁定为默认,但是内部的继承和重写不会。我认为这达到了平衡,此外,这与一般语言的代码演进是一致的,通过以下几种方式将促进 “无副作用” 迅速发展:

(1) 避免手工管理的障碍,当你正在 hack 一个模块最初的实现代码,但是
(2) 不会让最初的实现代码在内部隐源,并且二进制兼容允许模块之外的代码,以及
(3) 提供良好的语言工具,来逐渐的把那些最初的原型接口构建为更强的内部抽象。

所有默认行为的硬限制都维系在模块边界上,因为我们假定,当你之前犯了一个错误的决定,这能直接的在模块内部修复任何问题。

所以,okay,默认一个类是可继承的,并且不是真的这样设计的,现在模块中有一些子类,这导致了一些问题。只要没人改变缺省情况 (他们本可以无所顾忌的在任何情况下做,但如果只需要一个外部的子类,那就不太可能去做),所有这些子类仍是模块内的,你仍然有自由的控制权,以纠正最初的错误设计。


Joe Groff 的想法:


强大的子类化能力要求有意识的设计,就像 API 设计的其他所有方面一样。


Jordan Rose:


关于此有趣的事情是,遗漏的错误:没有考虑是否一个类应该是 final ,这比另一种选择更糟糕。这里忽略一下优化,一个开始是 final 的类,之后就无法更改;这无法改变这个类目前是如何使用的,对于许多库发展问题,这是最好的答案: 缺省情况应该是安全的,类的设计者可以选择以后更进取。


HEXO 自动部署

用 hexo 有一阵子了,以前一直都是在本机做 generationdeploy,看到 github 有 webhooks 这样方便的API,所以就来给 HEXO 加个自动部署吧!

自动部署是非常酷的,如果你不想在本机做冗长的 generationdeploy 的话。自动部署的情况下只需要在客户端向仓库传递源码即可,剩下的都由 server 来自动完成。

hexo 默认会带一个 gitignore,所以用 git 保存 hexo 的源码非常的方便。

所以我的部署方案是:源码放在 github 私有仓,站点挂在公开仓库的 gh-pages 分支上。

需要 Server端与客户端 配置好 git、nodejs、hexo-cli 即可。 (一定要尽量保持版本一致性)

如下是客户端部署的脚本:

1
2
3
4
5
6
7
8
9
#!/bin/bash

echo -e "\033[32m [AUTO DEPLOY] git commit... \033[0m"
d=`date +%x-%T`
git add .
git commit -m "auto deploy at "${d}
echo -e "\033[32m [AUTO DEPLOY] git push... \033[0m"
git push origin master
echo -e "\033[32m [AUTO DEPLOY] git push finish! \033[0m"

记得加上执行权限 chmod +x deploy.sh.

如下是 server端部署的脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

echo -e "\033[32m [AUTO SYNC] sync hexo start \033[0m"
cd /root/Blog/
echo -e "\033[32m [AUTO SYNC] git pull... \033[0m"
git pull origin master
echo -e "\033[32m [AUTO DEPLOY] hexo generate... \033[0m"
hexo g
echo -e "\033[32m [AUTO DEPLOY] hexo deploy... \033[0m"
hexo d
echo -e "\033[32m [AUTO DEPLOY] deploy hexo finish \033[0m"

同样,加上执行权限 chmod +x deploy.sh.

然后用 nodejs 搭一个简易的 server 用来接收 github 发来的 push 通知,这里利用的是github-webhook-handler,具体可以读它的文档,如下即是文档中的示例稍加改动后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var http = require('http')
var exec = require('child_process').exec;
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/webhook', secret: '********' });

http.createServer(function (req, res) {
handler(req, res, function (err) {
res.statusCode = 404;
res.end('no such location');
});
}).listen(7777);

handler.on('error', function (err) {
console.error('Error:', err.message);
});

handler.on('push', function (event) {
console.log('Received a push event for %s to %s',
event.payload.repository.name,
event.payload.ref);
exec('/root/Blog/sync.sh', function(err, stdout, stderr){
if(err) {
console.log('sync server err: ' + stderr);
} else {
console.log(stdout);
}
});
});

当然,这里可以启用forever来让这段代码在后台一直执行。

1
2
npm install -g forever
forever start server.js

当写完内容,想发布的时候只要执行./deploy.sh即可。

That`s it!

参考

ShangHai


在上海逛了三天,感谢SwiftGG的大家,稳重的PPT、古怪精灵的Cee、萌萌哒蛋蛋、胖胖的帮主、不正经的小青、万人迷的Sai 还有星星老师、小狗老师 ……

非常有幸能认识你们,大家在一起聊天、聚餐真的很开心。

KVO的缺陷


最近用了一些KVO,发现KVO本身有不少缺点,如下列举一二:

糟糕的API

KVO只有一个方法,也就是说很多时候不得不写一大坨代码来处理监听。如下是一个示例,代码如下:

1
2
3
4
5
6
7
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _view && [keyPath isEqualToString:NSStringFromSelector(@selector(property))]) { // 确保KVO只响应需要的对象的属性
...
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; //父类也有可能需要监听
}
}

父类和子类中同时存在KVO,有坑

父类和子类监听了同一个对象的同一个参数时,很容易出现父类在dealloc中remove了一次,子类又remove了一次的情况下。

依赖KVC,这种硬编码的方式,编译期无法检查错误

如上的例子为了防止这一点使用了NSStringFromSelector来防止非法的属性,但是这只适用于单层的keyPath。

比如想监听一个vc的持有的scrollView的属性就不能这么做了。

参考:

Key-Value Observing Done Right

浅谈iOS中的闭包(三) - ARC 对 Block 做了什么?


在 OC 中,Block 有如下三种类型:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

如下是具体的描述

  • _NSConcreteGlobalBlock 是全局的静态block,不会访问任何外部变量。这种不捕捉外界变量的block是不需要内存管理的,这种block不存在于Heap或是Stack而是作为代码片段存在,类似于C函数。

  • _NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。

  • _NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。

在 MRC 下 block 在创建时是 stack 对象,如果我们需要在离开当前函数仍能够使用我们创建的 block 。我们就需要把它拷贝到堆上以便进行以引用计数为基础的内存管理。

例子

Global

1
2
3
4
5
6
- (void) viewDidLoad {
NSLog(@"%@",^{
int temp = 10;
printf("%d\n",temp);
});
}

Stack

1
2
3
4
5
6
- (void) viewDidLoad {
int temp = 10;
NSLog(@"%@",^{
printf("%d\n",temp);
});
}

Heap

1
2
3
4
5
6
- (void) viewDidLoad {
int temp = 10;
NSLog(@"%@",[^{
printf("%d\n",temp);
} copy]);
}

ARC做了什么?

文档里是这么说的:

With the exception of retains done as part of initializing a strong parameter variable or reading a weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

即在 ARC 下创建的 block 仍然是 _NSConcreteStackBlock 类型,当 block 被引用或返回时,ARC 帮助我们完成了 copy 和内存管理的工作。这种 block 变成了 _NSConcreteMallocBlock 类型。

浅谈iOS中的内存管理(五) - ARC中的Trick


以前一直都以为 ARC 只是做了在编译期帮助开发者寻找合适的位置插入诸如 release autorelease这样的事情,不过看了一些资料以后,发现 ARC 似乎并不仅仅在编译期有优化,甚至在运行时期也有一些优化,优化的目的之一就是巧妙地跳过调用autorelease方法。这里做一下总结。

正如前面几篇文章提到的那样,MRC 时代,如果一个方法创建了对象,然后 return 这个对象,那就需要手动调用对象的 autorelease 方法,以实现 非自己生成的对象自己也能持有 这样的需求。以前一直都以为编译器无非是在编译期帮我们找到了合适的位置插入 autorelease,但是现在看来,并不是这么简单,ARC 似乎更聪明一些。

ARC 中有一个非常巧妙地运行时的优化方案用来跳过某些情况下 autorelease机制的调用,如下是大致的流程:

当方法全部基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 替代 MRC 下的 autorelease。在 MRC 下想要 retain 的时候,ARC 会调用 objc_retainAutoreleasedReturnValue()

在调用 objc_autoreleaseReturnValue()时,会在栈上查询 return address 以确定 return value 是否会被传给 objc_retainAutoreleasedReturnValue。如果没传,说明返回值不能直接从提供方发送给接受方,这时就会调用 autorelease。反之,如果返回值能顺利地从提供方传送给接收方,那么就会跳过 autorelease 并修改 return address 以跳过 retain 的过程。

Code Zen


我想,代码的精神所在,大概就是把 immutable/duplicate 的东西封装,把 mutable 的东西解耦。

封装的东西可以是 class 可以是 model 也可以是 Block 这样的代码段。

怎么才能寻找到被封装的内容?索引咯。

对于字典,索引是 Key ;对于数组,索引是下标;对于更广泛的内容,索引是指针。

或许可以这样说,比如OC中:用类和对象封装父类指针和方法列表,用映射来找到方法的代码片段。

浅谈iOS中的闭包(二) - Copy Or Not?


昨天突然被提及到了一个问题:Block的作为属性,应该是copy 还是 strong?(当然,原问题不是这个,出于某些原因吧。)

很多人第一反应都是 copy 但是,我似乎在苹果的Tips上看到过,似乎strong也可以,所以只描述了一下堆栈拷贝流程,没敢确切的回答,回来翻了一下文档,果然,在 ARC 下都是可以的。

ARC 下,之所以 Block 还使用 copy 是从 MRC 遗留下来的,在 MRC 中,方法内部的 Block 实在栈区的,使用 copy 可以把它放到堆区。其实在 ARC 下写不写都OK。因为编译器自动对 Block 进行了 copy 操作。(Swift 中变成了 逃逸性闭包与非逃逸性闭包,可以参考我以前的文章。) 我猜也许好多人还坚持要在 ARC 下写 copy 可能是在时刻提醒自己吧。这样就会不犯二去手动拷贝属性值。

property 中 的NSString, NSArray, NSDictionary 使用 copy 比较常见,因为他们有对应的 mutable,为了确保这些不可变对象不会乱改变,所以应该在设置新属性的时候 copy 一份。

浅谈iOS中的内存管理(四) - Swift中的ARC


昨晚跟张瑞鹏谈起了Rust语言,其实一直对Rust语言充满了敬畏。Google了一下Rust vs Swift似乎也没什么靠谱的回答,可能二者目前区别不是很大。不过Quora上的一个回答引发了我的兴趣。

这里撇开Swift中的即时编译、交互式编程,只关注内存管理这方面。(Chris Lattner在他的博客中也提到这两者特性只是他个人的一时兴起。)

Troy Harvey说Rust的所有权系统(Ownership system)可以100%的防止内存泄露的发生,而Swift中使用的自动引用计数 98%可以杜绝内存泄露的发生(由于存在循环强引用)。

这里就来探索Swift中的ARC与此前OC中的ARC发生了哪些改变?这里把重点放在如何在Swift中解决循环强引用。

无主引用 (unowned reference)

在之前的三篇文章中,可以得知,在OC中weak可以用来破除两个强引用的循环引用。在Swift中,则又多了一个无主引用(unowned reference)。

弱引用(weak reference)和无主引用(unowned reference)都允许循环引用中的某一个实例引用另外一个实例而不保持强引用,这样实例就能够互相引用而不会产生循环强引用的副作用。

那么弱引用(weak reference)和无主引用(unowned reference)在Swift中又有什么区别呢?一般来说,对于在生命周期中会变为nil的实例使用弱引用(weak reference)。对于在初始化赋值后再也不会变为nil的实例,使用无主引用(unowned reference)。

无主引用永远都是有值的,这与弱引用不同。因此,无主引用总是被定义为非可选类型。可以在声明前加入unowned关键字以表示一个无主引用。

由于无主引用是非可选类型,所以不需要在使用时将其展开。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为nil,因为非可选类型不允许被赋值为nil

文档中的一个例子如下:

如下定义了CustomerCreditCard两个类,用于表示银行的客户和客户的信用卡。这两个类中,每一个都将另一个类的实例作为自身的属性。在这个数据模型中,有一点与其他循环强引用的例子不同:一个客户可能有或者没有信用卡,但是一张信用卡总是对应着一个确定的客户。为了表示这种关系,Customer类有一个可选类型的card属性,但是CreditCard类有一个非可选类型的customer属性。

由于信用卡总是对应着一个客户,因此将customer属性定义为无主引用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Customer {
let name: String
var card: CreditCard?
init (name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized.")
}
}

class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit{
print("Card #\(number) is being deinitialized.")
}
}

在这个模型中,Customer实例持有对CreditCard实例的强引用, 而CreditCard实例持有对Customer实例的无主引用。

还有可能存在另一种场景:在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

如下定义了CountryCity两个类,在这个模型中,每个城市必须属于一个国家,每个国家必须有一个首都城市。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String){
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}

class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}

Country的构造函数调用了City的构造函数。然而只有Country的实例完全初始化完后, Country的构造函
数才能把self传给City的构造函数。为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将CountrycapitalCity属性声明为隐式解析可选类型的属性。这表示像其他可选类型一样,capitalCity属性的默认值为nil,但是不需要展开它的值就能访问它。

由于capitalCity默认值为nil,一旦Country的实例在构造函数中给name属性赋值后,整个初始化过程就完成了。这代表一旦name属性被赋值后,Country的构造函数就能引用并传递隐式的selfCountry的构造函数在赋值capitalCity时,就能将self作为参数传递给City的构造函数。

以上的意义在于你可以通过一条语句同时创建CountryCity的实例,而不产生循环强引用,并且capitalCity的属性能被直接访问,而不需要通过感叹号来展开它的可选值。

闭包引起的循环引用

当一个闭包被赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时,也会出现循环引用。

如下的例子用来展示一个由闭包引起的循环引用的发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name:String, text:String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized.")
}
}

这个例子中闭包在其闭包体内使用了self(引用了self.nameself.tex t),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。

Note: 虽然闭包多次使用了self,但它只捕获HTMLElement实例的一个强引用。

Swift 中对此提供了一中优雅的解决方案叫做闭包捕获列表(closuer capture list)。

定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。

捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,可以声明每个捕
获的引用为弱引用或无主引用。

Note: Swift 有如下要求:只要是在闭包内使用的成员,就要用self.someProperty或者self.someMethod() (而不只是somePropertysomeMethod)。这提醒你可能会一不小心就捕获了self

于是,我们可以改造此前的asHTML闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name:String, text:String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized.")
}
}

捕获列表常见的语法如下:

1
2
3
4
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}

如果闭包没有指明参数列表或者返回类型,则会通过上下文推断,那么可以把捕获列表和关键字in放在闭包最开始的地方:

1
2
3
4
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}

Swift中的结构体与类


在学C++时,老师经常会问的一个问题就是:类和结构体有什么区别啊?

在学操作系统之前,这两者似乎除了一些默认权限上的不同也没有什么太明显的区别。

今天我们来聊聊在Swift中的结构体和类,Swift中的类和结构体的运用有着明显的区别,通过一番探赜,体会到了为什么Swift是一门更安全的语言。

(如下的讨论只针对Swift中的类和结构体,其他语言中不一定完全适用。)

共性与不同

结构体和类有很多共性,在C语言中之所以也可以使用面向对象的编程思想就是因为结构体和函数指针的存在。在Swift中它们之间有如下共性:

  • 都可以定义属性用于存储值
  • 都可以使用下标来提供值访问
  • 都可以定义方法
  • 都可以定义构造器
  • 都可以写extension
  • 都可以实现协议

不过,似乎类的功能更多一些,比如

  • 类可以使用继承
  • 引用计数允许一个类的多次引用
  • 类的实例可以释放其分配的资源
  • 类的实例可以在运行时得知其类型

在OC中,NSStringNSArrayNSDictionary类型都是类类型,而Swift中的StringArrayDictionary都是结构体。

一般来说结构体和枚举都是值类型,而类是引用类型。也就是说,在OC中诸如NSString这些类型作为参数被传入时不会发生值拷贝,而是传递现有实例的引用,而在Swift中它们的值会被拷贝。当然Swift的文档中也提到了:Swift在幕后只在绝对必要的情况下才会做出值拷贝。Swift 会管理所有的值拷贝以确保性能最优化,所以没必要回避赋值以保证性能最优化。

可能你会问,为什么要这么做?参考了一下stackoverflow里面的讨论。由于struct是值类型,所以有着不可变的特性。而 Swift 恰恰追求的是线程安全,不可变性极大的有助于线程安全。这也是Swift在方便性与安全性上的一种妥协。

所以,至此也不难理解为什么Swift官网中关于Swift Feature的描述中第一条就是Safe。简单的来说就是:一时的方便可能会为今后的维护留下隐患。

用途

结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。所以如何选择就成为了一个问题。参考了Apple官方的文档以及他人的资料,总结如下:

当符合一条或多条以下条件时,建议使用结构体:

  • 该数据结构用来封装少量简单的数据值。
  • 该数据结构的实例在被传递时,明确的需要使用值传递。
  • 该数据结构中所存储的值类型属性也应该被拷贝而不是引用传递时。
  • 该数据结构不需要继承另一个已有类型的属性。

举例来说,以下情况适合使用结构体:

  • 几何形状,封装width height属性。
  • 三位坐标系内的一点,封装x y z属性。

除此外,大多数情况下自定义的数据结构的构造都应该是类。