0%

s28948876-2

之前看过《乔布斯传》,印象最深刻的是乔布斯的粗鲁和暴脾气,以及他的“现实扭曲力场”,很难相信有那么多极富才华的人会在这样一个人手下干活。而看完《成为乔布斯》,才理解了乔布斯为什么会成为乔布斯,在被苹果放逐多年之后,乔布斯为何又能上演王者归来,再次带领苹果改变世界。

乔布斯首先是个天才,因此得以年少成名,21岁就与沃兹创立了苹果公司,制造了世界最早商业化的个人电脑,开启了个人电脑时代。但是在公司上升阶段,乔布斯年少轻狂,自我主义等等弱点开始暴露出来,使他和公司其他管理层的关系逐渐恶化,加上乔布斯后来主导的Apple III、Lisa、Macintosh等电脑机型的销量不理想,最终使苹果的董事们抛弃了乔布斯。

之后乔布斯离开苹果,创立了NeXT公司,主打高校市场的工作站。尽管NeXT起步阶段获得众多高校和投资人的亲睐和投资,但乔布斯自大狂傲的性格最终还是把事情搞砸了。原本计划18个月推出的产品,一直拖到三年后才推出,而且价格远远高于市场预期,而且直到一年后才卖出去一台,此时距离乔布斯创立NeXT公司已经过去4年了。

乔布斯精心打造的NeXT未能如他所愿,而无心插柳收购的皮克斯动画制作室却最终获得意外成功,从1986年收购皮克斯,到后来皮克斯与迪士尼合作的3D动画电影《玩具总动员》上影,在电脑领域几乎已经被人遗忘的乔布斯才重新找回自尊,而更重要的是,皮克斯的成功经验让乔布斯学习到了重要的管理技巧:

“看着团队成员因为合作而变成更好的自己,我想这对史蒂夫触动很大,”拉塞特(皮克斯的首席创意官)说道,“我认为这是他重回苹果后最关键的变化之一。他能以更开放、更包容的态度来看待别人的才华,从别人的才华中得到鼓舞,同时激励别人完成他自己无法完成的伟大事业。”

皮克斯的动画都有一个基本套路,主人公一开始自大高傲,看不起普通人,因而遭遇挫折陷入困境,之后在某些善良人的帮助下,主人公逐渐学会了善良、勇敢、智慧等品质,克服了自身的性格弱点,最终得到救赎,完成目标。

虽然乔布斯的性格并没有真正改变,但他已经懂得如何有效利用自己的性格优势来达到自己的目的,并克服自己的性格缺点,不让它影响项目的发展。

后来,出现了不少乔布斯的模仿者。对此,比尔盖茨有一句一种见血的评语:“很多想模仿史蒂夫的人只学会了他混蛋的那一面,却学不会他作为天才的那一面。”

简介:

我在瓣读中使用了Realm做数据的持久化存储,所以把自己的一些实践体验分享一下。

Realm 的身世,大家可以在官网上查看,当初我是看了一遍 Realm 官网上的介绍,就很快决定了选用它,它第一眼吸引的特点有以下几点:

1、原生就支持 Swift,有 RealmSwift
2、多平台、可视化支持,同时支持iOS和Androi**d,还有一个Realm Brower应用,可以查看数据库中的内容。
3、使用简单方便,它不是基于Sqlite的封装,而是完全自己开发了一个数据库,完全没有那些sqlite语句,基本上看一眼示例代码就知道怎么用了。

基本操作

下面我们就来看一下 Realm 的一些基本操作

创建数据库

1
2
3
4
5
6
7
8
// 默认配置
let realm = try! Realm()

// 自定义配置
let url = URL(fileURLWithPath: RLMRealmPathForFile("evernoteData.realm"), isDirectory: false)
var config = Realm.Configuration(fileURL: url)
config.schemaVersion = realm.configuration.schemaVersion
let evernoteRealm = try! Realm(configuration: config)

add update delete query

需要存储到 Realm 数据库中的 Model 都必须继承自 RealmSwift 自定义的一个类: Object, 自持 Swift 的 nullability 特性,支持一对一和一对多的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import RealmSwift

// Dog model
class Dog: Object {
@objc dynamic var name = ""
@objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
@objc dynamic var name = ""
@objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
let dogs = List<Dog>()
}

还可以给 Model 设置主键primaryKey,默认值defaultPropertyValues,忽略的属性ignoredProperties,必要属性requiredProperties,索引indexedProperties。

比较有用的是主键和索引,最好是给每个 Model 都设置主键,这样便于查询和更新,一开始的时候瓣读上的图书评分model没有设置主键,结果所有的书都用了同一个评分,导致评分都显示一样了。

要将一个 Model 添加进数据库中也很简单:

1
2
3
4
5
6
7

let dog = Dog()
let realm = try! Realm()
try! realm.write() {
realm.add(dog)
}

更新 删除 Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

let dog = Dog()
let realm = try! Realm()
try! realm.write() {
realm.add(dog, update: true)
}

try! realm.write() {
dog.name = "ww"
dog.setValue("hh", forKeyPath: "name")
}

// 删除
try! realm.write() {
realm.delete(dog)
realm.deleteAll()
}

查询:

1
2
3
4
5
6
7
8
9
10
11
12

let dogs = realm.objects(Dog.self) // retrieves all Dogs from the default Realm

// Query using a predicate string
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")

// Query using an NSPredicate
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)

// Sorting
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")

Realm 中的所有查询都是懒加载,当你执行上面这些代码的时候,真正的数据并不会加载到内存中,只有当 Model 的属性被真正访问的时候,它才回被加载。而且查询出来的结果是实时更新的。当数据发生变化时不需要再重新查询一遍。

通知:

查询出来的结果可以添加通知,监听数据库的变化,来改变UI:

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
29
30
31
32
33
34
35
36
37
38

class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil

override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")

// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

deinit {
notificationToken?.invalidate()
}
}

数据库的迁移

因为瓣读是不断在增加新的功能,所以数据也是不断增加的,这就需要经常迁移数据库,而Realm要做迁移也非常简单,新增删除表和字段,都不需要做额外的操作,只有修改了字段的时候才需要做手动的迁移,这是瓣读数据库迁移的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
migration.enumerateObjects(ofType: DBCollection.className()) { oldObject, newObject in
if let newObject = newObject, let oldObject = oldObject, let oldId = oldObject["id"] as? Int {
newObject["id"] = String(oldId)
}
}
}
})
Realm.Configuration.defaultConfiguration = config

多线程操作

Realm 是一个MVCC数据库,MVCC是Multiversion concurrency control,多版本并发控制。采用的方法是,每一个连接的线程都会数据在一个特定时刻的快照,采用的是和Git一样的源文件管理算法,每一个线程都有一个特定的数据库版本,就像Git的分支。

因此,Realm 是不允许多个线程访问同一个数据库实例的,必须要每个线程都单独创建一个数据库实例,但线程runloop循环开始的时候,线程中的数据库实例能够自动刷新获取最新的数据库数据,也可以通过执行Realm.refresh() 或者 Realm.commitWrite() 方法来手动刷新。

对于需要跨线程访问的 Model, 需要用 ThreadSafeReference 来包一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

let person = Person(name: "Jane")
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "background").async {
autoreleasepool {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
try! realm.write {
person.name = "Jane Doe"
}
}
}

如果想要深入了解 Realm 的多线程处理机制,可以查看这篇官方文章:数据库的设计:深入理解Realm的多线程处理机制

缺点

官方列了很多缺点:一些类名、属性名的长度,属性值的大小等等,影响比较大的缺点应该就是 Model 有很多限制:

只能继承自 Object
不能重写 Setter 和 getter 方法

类的继承,也基本不能使用多态特性,子、父类不能互相转换,不能对多个类同时进行查询,也没有多类容器。
而且最蛋疼的一点是,属性不支持基本的数组和字典,有些属性其实就是一个字符串数组,但因为不支持数组,只能为一个字符串新建一个Model,里面只有一个字符串属性。

该文发布在少数派:为了更高效地在豆瓣写读书笔记,我开发了瓣读APP

瓣读APP是一款豆瓣读书笔记客户端,能帮你快速搜索、标记图书,更高效地写读书笔记,支持在笔记中插入原文、图片、设置笔记为仅自己可见,支持文字识别插入笔记、将笔记转成长图分享。

App Store下载

瓣读的起源

瓣读APP可以说是我独立开发的第一款完整的APP,最早在2015年就完成了一个初级版本,并上架到App Store。但当时只是作为一个练手项目,APP各方面都不成熟,上架大概半年之后就下架了,直到最近才重新开发了一个新版本,就是现在的瓣读。

开发瓣读的初衷很简单,就是为了更方便地写读书笔记。我个人是豆瓣的深度用户,平时看书也都会在豆瓣上标记,也想在豆瓣上写读书笔记,但多次尝试发现,在豆瓣上写读书笔记非常麻烦,而且还缺失功能:

  1. 写读书笔记的入口非常深。在豆瓣APP里写读书笔记,至少要有5次点击,期间还有滑动操作。写读书笔记一个很重要地体验,应该是不打断阅读节奏,心有所感时,立即就能记笔记。
  2. 在豆瓣APP里写笔记不能插入原文。我相信大部分人写读书笔记很大一部分是摘抄原文,而在豆瓣APP里没有了插入原文这一功能。虽然pc断有,但pc端相比app,写笔记更麻烦。

基于以上两点我个人在实际中遇到的痛点,我再次决定开发一个豆瓣读书笔记应用,于是有了瓣读的诞生。

随处可见的写笔记入口

鉴于最大的一个痛点是写笔记的入口太深,我在瓣读APP里几乎所有可能会有写笔记需求的地方都添加了写笔记的入口,包括笔记列表页、图书列表页、图书详情页,再加上3D Touch快捷操作,总共有四个入口,让用户在任何地方都能一键写笔记。

dfbce71d5c67b0da536927752c54be7

文字识别插入原文

要解决第二个痛点,在笔记编辑页面增加插入原文的功能就可以,这点很简单,但我还想把它做得更高效。在豆瓣上查看用户写的笔记会发现,很多用户都是拍一张照片就是一篇笔记了,比打字抄原文方便多了。既然如此,那我干脆就直接集成文字识别的功能,用户拍照就能自动识别成文字,并作为原文插入。为了跟方便地拍照记笔记,文字识别也有快捷入口,在3D Touch和发现tab写都可以一键拍照识别文字。

4e9bdd87ab5b2a33ea090c367da91079

笔记仅自己可见

笔记仅自己可见,目前是瓣读的一项独家功能,包括豆瓣官方,以及其它一些豆瓣读书笔记客户端都没有这项功能。

无论是在豆瓣app,还是在豆瓣网站上,写的笔记都是公开的,所有人都可以看到,但对于笔记来说,有一些是私密的,并不想给其它人看到。豆瓣的API中,已经有笔记仅自己可见的相关接口了,但官方产品并没有提供这项功能。

在瓣读APP中,写笔记时选中右上角的“仅自己可见”就可以。仅自己可见的笔记依然会同步到豆瓣,但不会公开,只有自己能看到。

onlyshowme

关于收费

自己付出劳动开发的产品,自然希望能获得一定的收入。苹果App Store的付费方式有应用付费和应用内购。现在比较流行的是应用免费+内购解锁功能的方式。

内购又分为4种,分别是:

  • 消耗型:购买后只能使用一次,下次还得再次购买,典型的就是内购充值。
  • 非消耗型:只需要购买一次,永久有效,典型的就是内购解锁高级功能。
  • 自动续期订阅:有时效性,并会自动续订,典型的如订阅会员、期刊。
  • 非续费订阅: 有时效性,但不会自动续订,这类比较少。

瓣读最适合的应该是第二种非消耗型,一次购买永久解锁所有功能。但因为瓣读的文字识别用的是百度的接口,是按次数收费的,使用的次数越多,费用就越多,因此如果是一次购买,无限使用的话,一旦人数过多,就有可能会入不敷出。因此,参照萝卜书摘等应用的做法,目前仅针对文字识别功能需要购买文字识别次数,其它功能都是免费的。

后续开发计划

目前还只是完成了基础功能的开发,后续计划将会增加笔记同步到印象笔记、笔记导出、读书计划、读书统计等功能。

App Store下载

Swift 4最重大的一个变化就是增加了一个Codable协议,解决了在Swift中进行字典<->模型转换的问题。

在OC中,因为有runtime,我们能够比较方便的做字典转模型的操作,而在Swift中,没有了runtime,很难对字典转模型做自动操作,虽然网上现在也有一些第三方库,但要么使用繁琐,要么使用的是苹果不太推荐的做法。现在苹果推出了Codable协议,彻底解决了这个问题。

简介

Codable协议是EncodableDecodable协议的组合,看命名就可以知道,它们其中一个负责编码,一个负责解码。Swift中的一些基本类型:String,Int,Double,Date,Data,URL都已经遵循了这个协议。而我们自定义的类型想要遵循这个协议也非常简单,不需要写任何多余的代码:

1
2
3
4
5
6

struct Person: Codable {
let name: String
let age: Int?
}

只要一个类型中的属性都是 Codable 类型,那么这个类型就可以遵循 Codable 协议:

1
2
3
4
5
6

struct Book: Codable {
let price: Double
let author: Person
}

Swift自带的 ArrayDictionary 也可以遵循 Codable 协议,只要里面的元素都是 codable 类型:

1
2
3
4
5
6

struct Book: Codable {
let price: Double
let authors: [Person]
}

自动编码、解码

默认Coding Key

一个类型只要遵循了Codable协议,就能利用PropertyListEncoderJSONEncoder 进行自动解码和编码了,默认的Coding Key就是属性名:

1
2
3
4
5
6
7

let dic = ["name": "lijun", "age": 30] as [String : Any]
let data = try! JSONSerialization.data(withJSONObject: dic, options: [])
let person = try? JSONDecoder().decode(Person.self, from: data)

// person.name == "lijun", person.age == 30

自定义Coding Key

大部分情况,从API的字段命名规则和app端是不一样的,所以需要使用自定义的Coding Key。这也很方便,只需要在类型内部写一个 CodingKeys 枚举,并遵循CodingKes协议即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

struct Person: Codable {
let name: String
let age: Int?

enum CodingKeys: String, CodingKey {
case name = "title"
case age
}
}

let dic = ["title": "lijun", "age": 30] as [String : Any]
let data = try! JSONSerialization.data(withJSONObject: dic, options: [])
let person = try? JSONDecoder().decode(Person.self, from: data)

手动编码、解码

有时候,API返回的数据结构并不一定能和模型的属性一一对上,比如可能只需要某个字段里的某一个字段,这时候就可以自己手动实现Decode协议:

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
29

struct Person {
let name: String
let age: Int?

enum CodingKeys: String, CodingKey {
case name = "title"
case extraInfo = "extra_info"
}

enum ExtraInfoKeys: String, CodingKey {
case age
}
}

extension Person: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)

let extraInfo = try values.nestedContainer(keyedBy: ExtraInfoKeys.self, forKey: .extraInfo)
age = try? extraInfo.decode(Int.self, forKey: .age)
}
}

let dic = ["title": "lijun", "extra_info": ["age": 30]] as [String : Any]
let data = try! JSONSerialization.data(withJSONObject: dic, options: [])
let penson = try? JSONDecoder().decode(Person.self, from: data)

手动实现Encodable协议同理:

1
2
3
4
5
6
7
8
9
10
extension Person: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)

var extraInfo = container.nestedContainer(keyedBy: ExtraInfoKeys.self, forKey: .extraInfo)
try? extraInfo.encode(age, forKey: .age)
}
}

参考资料

本文为《代码整洁之道(Clean Code)》一书的读书笔记。这本书是以Java开发为例,但一些基本的原则是通用的。

书中以Java为例,从命名、函数、类、注释、单元测试、格式、错误处理并发编程等各方面详细介绍了如何让代码变得整洁。整洁的代码有很多特征,其中最重要的,也是一眼能够看出来的,就是整洁的代码都是短小的。诚然短小的代码并不一定都整洁,但整洁的代码一定是短小的,它每份文件的代码量是短小的,每个类是短小的,每个函数是短小的,每个注释是短小的,每个单元测试同样也是短小的。

短小就意味着代码没有多余的内容,意味着代码结构清晰,意味着更好的可读性。

当然短小是有前提的,就是所有的代码命名都是有意义的,类名、函数名、变量名都是有意义的,要在有意义的前提下,尽量做到短小。

有意义的命名

一个有意义的命名有以下特点:

  • 名副其实
  • 避免误导
  • 做有意义的区分,比如同样两只狗,不能叫dog1、dog2这样无意义的区分,可以叫smallDog、bigDog这种有意义。
  • 使用读得出来的名称
  • 使用可搜索的名称
  • 每个概念对应一个词
  • 添加有意义的语境,避免添加无用的语境

类应该短小

关于类的第一条规则是类应该短小。第二条规则是还要更短小。

单一职责原则(SRP:Single responsibility principle)

要想类变得短小,首先类不能拥有太多权责,而应该遵循单一权责原则。职责是指类变化的原因,单一职责原则就是指一个类应该有且只有一个修改的原因。

遵循单一职责原则的类,它拥有的属性和方法会比多职责类要少。这是让类变得短小的根本方法。

系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装了一个职责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

高内聚性

内聚是指一个模块内部各成员之间相互关联的程度。高内聚性,则意味着类内部的各个成员互相依赖、互相聚合的程度高。如果一个类中每个变量都被每个方法所使用,说明这个类具有最大的内聚性。

一个类应该是高内聚、低耦合的。内部互相依赖,结合一个整体,外部不会有依赖关系。当类的某个实体变量只被少数方法使用,则意味着这个类正在丧失内聚性,应该考虑对其进行拆分了。因此保持类的高内聚性,会得到许多短小的类。

函数应该短小

和类一样,函数的第一规则是短小,第二规则是还要更短小。

只做一件事件

和类一样,要让函数短小,根本方法是要让一个函数只做一件事件。

每个函数一个抽象层级

要确保函数只做一件事,函数中的语句应该都在同一抽象层级上。我感觉这是做到函数整洁的一个很重要的规则。一个函数内部如果混杂着不同层级的代码,往往会导致代码的混乱,不同层级的代码在函数中纠缠。

无副作用

副作用是一种谎言。函数只承诺做一件事,但往往在内部会对类的变量做一些改动。这很容易导致内部一些函数调用的顺序依赖,造成必须按某种特定的顺序调用函数,换一种顺序就会出现bug。

此外,函数还应该使用描述性的名称,宁愿让名称长一点,也要将函数所做的事描述清楚。另外,函数参数应该越少越好。

注释要短小

最好的注释是没有注释

只有在我们无法用代码表达意图时才需要注释,注释很多时候是为了弥补失败的代码。注释会撒谎,而代码永远是真实的,我们应该尽量让代码来表达一切。除了提供代码之外的信息需要注释外,其它的注释都应该避免。

代码之外的信息包括:

  • 法律信息
  • 对代码意图的解释
  • 警示
  • TODO注释

单元测试的短小

单元测试要保持短小,最根本的方法是遵循TDD的三定律:

  • 在编写不能通过的单元测试前,不可编写生产代码。
  • 只编写刚好无法通过的单元测试,编译错误也算不通过。
  • 只可编写刚好足以通过当前失败测试的生产代码。

遵循了这三条规则,我们所写的所有测试代码都是刚好能够导致测试无法通过的代码,没有重复的测试代码。

此外,和每个函数只做一件事一样,每个测试函数也应该只测试一个测试条件。

由于Swift是一门静态的、强类型的语言,因此Swift编译器能够在编译阶段就对代码做大量优化,本文将探讨Swift编译器会对代码做哪些优化,我们该如何利用编译器的优化特性写出更高效的Swift代码。

Premature optimization is the root of all evil.
—–by Donald Knut

显式和隐式性能优化

只有当你发现了性能问题,并找到了原因之后,才应该开始做性能优化。

性能优化有两种类型:

  • 显式的
  • 隐式的

显式的性能优化直接指向低效的代码,这类优化通常要求对代码做重大的修改,比如换一种更加高效的算法。这通常会降低代码的可读性。使用更多的内存做缓存也能提高性能。

隐式的性能优化则需要借助语言特性来提高性能。它通常不需要对代码做重大修改,对代码可读性也没有负面影响,有时甚至能提高可读性。它被称为隐式是因为你能在代码的任何地方使用它,但过一段时间之后你又不会感觉到它的存在。

这里主要将探讨如何利用Swift的语言特性来做隐式的性能优化。

使用常量代替变量

相比于变量,使用常量拥有更高的性能。因为常量能清晰地告诉编译器,这个变量的值将不会被改变,编译器就会对它做内联优化,并不会为它分配内存空间,这样既能节省内存空间,也能提高运行速度。

方法调用

两种主要的方法调用类型:

  • 静态: 静态方法绑定是指当你调用一个方法时,编译器知道你确实是在调用这个类的这个方法。
  • 动态: 动态方法绑定在方法和对象之间是一种弱绑定。当你调用一个对象的某个方法时,无法保证这个对象一定就能执行这个方法。

Objective-C就是一种动态类型语言,并且拥有动态运行时。它采用消息发送机制来调用方法。当你调用一个方法时,实际上是发送了一个消息给这个对象,这个对象接受到这个消息后,必须先检查一下它是否实现了这个方法。

而Swift使用的是静态方法绑定,它拥有一个虚方法表,记录了一个类型拥有的所有函数指针。当在Swift中调用一个方法时,能够直接拿到这个方法的内存地址,然后执行它。这样就不需要去检查是否实现了这个方法。

全局函数(Global function)和静态方法(Static method)的执行速度是最快的。

类方法(Class method)分为两种情况,一种是不能被重写时,它用于和全局函数和静态方法一样的执行效率,一种是能够被重写时,当方法调用者在编译时是不确定的,那编译器就不能对它做优化,它的执行效率就会稍差。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SuperClass {
class func test() {
print("super class test")
}
}

class SubClass: SuperClass {
override class func test() {
print("sub class test")
}
}

let instance = arc4random() % 2 == 0 ? SuperClass.self : SubClass.self
instance.test()

如果给类方法或者给类加上final修饰词,表明这个类或方法不能被继承重写,那这个类方法也将拥有和全局函数、静态方法一样的性能。

实例方法和类方法相似,因为有继承重写的因素,编译器能否对实例方法进行优化,取决于编译器能否确切地知道实例方法的具体调用者。

Intelligent code

因为Swift是一门静态的、强类型的语言,因此它会尝试去移除所有不必要的代码执行,比如那些没有被使用过的代码,或者对应用没有任何实际影响的代码。

1
2
3
4
5
6
7
8

class Test {
func doNothing() { }
}

Test().doNothing()
Test().doNothing()

在这段代码中,doNothing方法没有做任何事,Swift编译器就会自动移除Test().doNothing()这两句代码。

到OC就没有这种优化,因为OC可以在运行时可以改变一个方法的实际调用和实现。

但是Swift编译器不能自动忽略print方法,所以我们在程序里通过print打log时,可以采用只有在debug模式下执行print方法的方式,这样在release模式下,编译器就能忽略print方法了。

Using nonoptimizable constants

在swift中,有些类型在类中使用时,不能和其它类型一样拥有上面的优化特性,即使是使用了常量。这些类型有:

  • String
  • Array
  • Custom Class objects
  • Closures
  • Set
  • Dictionary

以上几种类型因为要暴露给OC使用,内部会维持一个OC的相应类型,比如Sting对应NSString。这样就拥有了一些OC的运行时特性,因此编译器无法自动忽略它们。

但如果是在struct中使用这些类型,那这些类型就不会暴露给OC使用,编译器就又能为它们做优化了。

一些提高代码速度的方法

  1. 使用final
  2. 使用@inline(__always)是函数始终编译成内联函数
  3. 使用不可变的值类型
  4. 避免在Swift代码中使用OC类型
  5. 避免暴露Swift代码给OC使用
  6. 避免使用OC的动态特性

苹果为了保准UITableView视图的性能,使用了cell的重用机制,cell可以通过重用标示符(reusableIdentifier)进行复用,默认的注册cell和获取cell的方法中,需要传入一个字符串作重用标示符。但这种方式很容易出错,而且使用起来也相当别扭,一种普遍的解决方式,就是直接只用类名作为重用标示符:

1
2
3
4
tableview.registerClass(UITableViewCell.self, forCellReuseIdentifier: String(describing: UITableViewCell.self))

tableview.dequeueReusableCellWithIdentifier(String(describing: UITableViewCell.self))

但这种写法依然颇为繁琐,每次都要传入一个类,并把它转化成字符串。所幸,借助Swift的泛型特性,我们可以有更加优雅的实现方式。

使用协议

使用泛型来优化 TableView Cells 的使用体验这篇文章中,作者详细介绍了如何通过协议+泛型的方式,优化TableView Cells 的使用体验。具体的做法很简单,首先声明了一个协议,提供并默认实现了一个reuseIdentifier静态属性:

1
2
3
4
5
6
7
8
9
10
11

protocol Reusable {
static var reuseIdentifier: String { get }
}

extension Reusable {
static var reuseIdentifier: String {
return String(describing: self)
}
}

然后为UITableView提供一个注册和获取重用cell的方法:

1
2
3
4
5
6
7
8
9
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) where T: Reusable {
self.register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
}

func dequeueReusableCell<T: UITableViewCell>(indexPath: IndexPath) -> T where T: Reusable {
return self.dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
}
}

这样只要cell遵守了Reusable协议,就可以通过上面两个方法注册复用cell了。具体的代码和使用,请阅读原文:使用泛型来优化 TableView Cells 的使用体验

这种方式的确是比原生方法方便了不少,但依然有两个缺点:

  1. 采用这种方式的cell必须遵循Reusable协议,虽然我们可以通过让UITableViewCell遵守这个协议的方式,避免每个UITableViewCell子cell都写一遍,但依然还有不便之处。
  2. 这种方式产生的reusableIdentifier是固定的,就是类名,因此不适用于一个cell有不同的样式,需要不同的reusableIdentifier的情况。

实际上使用协议做一层封装完全没必要,直接通过扩展增加方法就可以了。

通过方法扩展

我们在注册、复用cell的时候,最根本的是需要有一个reusableIdentifier,上述方法是在一个协议中提供了产生reusableIdentifier的默认实现,因此需要遵循协议,而且无法对reusableIdentifier进行自定义。其实如果我们只是想要一个类名作为reusableIdentifier,根本不需要任何协议或结构体等多余的类型,只需要将上面我们为UITableView扩展的两个个方法中,ReuseIdentifier的获取方式改成String(describing: T.self)即可:

1
2
3
4
5
6
7
8
9
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) {
self.register(T.self, forCellReuseIdentifier: String(describing: T.self))
}

func dequeueReusableCell<T: UITableViewCell>(indexPath: IndexPath) -> T {
return self.dequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath) as! T
}
}

但这种方式还是有第二种缺点,无法自定义reusableIdentifier,要做到这点,也很简单,在上述两个方法中加上一个参数即可,然后给参数提供一个默认值。

1
2
3
4
5
6
7
8
9
extension UITableView {
func register<T: UITableViewCell>(_: T.Type, reuseIdentifier: String = String(describing: T.self)) {
self.register(T.self, forCellReuseIdentifier: reuseIdentifier)
}

func dequeueReusableCell<T: UITableViewCell>(indexPath: IndexPath, reuseIdentifier: String = String(describing: T.self)) -> T {
return self.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T
}
}

在实际项目中,我们会碰到一个view需要有不同的布局样式的情况,这种情况,我们可能会分成几个view来写,或者在一个view中通过if...else..来适配不同的布局。前一种方式会造成代码浪费,后一种方式造成一堆if...else...,降低代码质量。在Swift中,通过使用协议和结构体,可以将一个view中的视图和布局功能解耦,让一个view可以便捷地使用不同的布局,一个布局可以便捷地运用到不同的view上。

阅读全文 »

当我们通过协议类型来创建一个变量时,这个变量会被包装到一个叫做存在容器的盒子里,这个存在容器除了变量本身的数据外,还会存储一些其它的数据,因此它占用的内存会变大,相应的也会损失些性能。

阅读全文 »

在Swift 2发布时,苹果将Swift定义为一门面向协议编程的语言,协议(Protocol)在Swift中被赋予了更加强大、灵活的功能。相比于Objective-C的协议,Swift的协议不仅可以被用做代理,也可以用作对接口的抽象,代码的复用。这里主要介绍下协议的扩展与协议的动态、静态派发。

阅读全文 »