第五章——结构体与类(内存)

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

值类型的内存管理非常简单,值类型变量的内存会被自动分配,然后自动回收。使用值类型永远不会导致循环引用问题。

Swift使用自动引用计数(ARC)来管理类对象的内存。规则如下:

  1. 创建一个新的引用指向对象时,对象引用计数加1
  2. 变量超出作用域时,引用计数减1
  3. 如果引用计数为0,变量被释放

使用ARC后,Swift的内存管理机制看上去和使用垃圾回收(GC)机制有些类似,但实际上两者完全不同。任何使用ARC的语言(不错,比如OC)都难以避免循环引用的问题。这在编程时要格外小心。比如下面这个例子:

class View {
var window: Window
init(window: Window) {
self.window = window
}
}

class Window {
var rootView: View?
}

var window: Window? = Window()
var view: View? = View(window: window!)
window?.rootView = view
view = nil
window = nil
复制代码

因为windowview互相持有对方,所以把他们设置为nil后,引用计数依然为1。所以谁也没能释放掉。

弱引用

为了解决循环引用问题,我们可以使用弱引用和无主引用。关键字分别是weakunowned。弱引用不会增加对象的引用计数,如果对象被销毁,那弱引用自动变为nil。所以,弱引用只能对可选类型使用。比如Windows类可以这样重写:

class Window {
weak var rootView: View?
}
复制代码

这样一来,window变量不会引用rootView,所以当view置为nil时,对象就被释放了。一旦view被释放,window的引用计数也会降为零。这就是使用弱引用打破引用循环的例子。

无主引用

如果不想让对象变成可选类型,我们就只能选择无主引用了。比如View类可以这样重写:

class View {
unowned var window: Window
init(window: Window) {
self.window = window
}
}
复制代码

无主引用也不会增加对象的引用计数,它总是假设这个引用是存在的。一旦引用不存在了,就会导致程序崩溃。所以使用无助引用一定要慎重,要确保在自己的生命周期内这个引用总是存在。

类和结构体实战

选择值类型还是引用类型取决于我们面对什么样的具体问题以及涉及到的数据的类型。接下来通过一个具体例子来进行对比。需求是把存款从一个银行转存到另一个银行。我们使用三种方法实现:

  1. 使用类
  2. 使用结构体和纯函数
  3. 使用结构体和inout参数

由于每个账户都有身份标识,所以很自然的想法是用类来抽象银行账户:

typealias USDCents = Int

class Account {
var funds: USDCents = 0
init(funds: USDCents) {
self.funds = funds
}
}
复制代码

每个账户的初始存款都是0,接下来新建两个账户:

let alice = Account(funds: 100)
let bob = Account(funds: 0)
复制代码

转存函数很容易实现,它的返回值为Bool类型表示转存是否成功。如果余额不足,则转存失败:

func transfer(amount: USDCents, source: Account, destination: Account) -> Bool {
guard source.funds >= amount else { return false }

source.funds -= amount;
destination.funds += amount

return true
}
复制代码

调用方法很简单,被传入到方法的参数alicebob会直接被修改:

transfer(50, source: alice, destination: bob)
复制代码

这个代码简单易懂,不过唯一的缺点是:它不是线程安全的,如果这段代码运行在多线程环境下,就很有可能出问题。所以必须保证只能有一个线程运行这个方法。

纯结构体

用结构体也可以抽象一个银行账户,它的定义和之前类似,不过初始化方法可以不写,系统会自动生成:

struct Account {
var funds: USDCents
}
复制代码

由于结构体具有值语义,transfer函数就比较难实现了,因为函数中只能处理参数的副本:

func transfer(amount: USDCents, var source: Account, var destination: Account) -> (source: Account, destination: Account)? {
guard source.funds >= amount else { return nil }

source.funds -= amount;
destination.funds += amount

return (source, destination)
}
复制代码

这样写虽然麻烦,但也有好处,因为结构体具有值语义,所以一看就知道Account参数不会在函数中被修改,也不会有两个线程同时修改同一个结构体变量。

我们需要一个可变的数据结构(比如数组)持有所有的Account结构体,这就是编程中所说的“单一数据源”原则(SSOT)。可以这样更新这个数据源:

if let (newAlice, newBob) = transfer(50, source: alice, destination: bob) {
// 更新数据源
}
复制代码

对数据源的更新也要确保是串行的。

结构体和inout

最后一个例子是使用结构体和带有inout参数的函数,这样参数在函数内部会被复制一份,做完修改后再赋值给函数外的变量。函数内对参数的修改是线程安全的。

func transfer(amount: USDCents, inout source: Account, inout destination: Account) -> Bool {
guard source.funds >= amount else { return false }

source.funds -= amount;
destination.funds += amount
return true
}
复制代码

尽管调用函数的语法看上去和C语言的指针传递非常类似,但依然要强调一下参数其实还是按值传递而不是按引用传递的。参数在函数内部被修改,函数返回时修改后的参数会覆盖函数外面的原来的参数:

var alice = Account(funds: 100) //要标记成var
var bob = Account(funds: 0)

transfer(50, source: &alice, destination: &bob)
复制代码

这种写法吸收了前两种方案的优点,它在函数内部保证了数据一致性,同时又能像第一种方案一样简单易用。

闭包和内存

(注):个人认为原书这一节分析的太浅了,Swift闭包和循环引用管理的具体分析可以参考我的这篇文章的后半部分:OC与Swift闭包对比总结

闭包通过强引用来截获外部变量,不过这也导致了潜在的循环引用问题。一个常见的例子就是对象A强引用了对象B,对象B强引用了一个回调函数,回调函数内部又强引用了对象A。举个具体例子,ViewController有一个XML解析器,解析器有一个回调函数,回调函数又强引用了ViewController,它们之间的关系如下图所示:

用代码表示一下:

class XMLReader {
var onTagOpen: (tagName: String) -> ()
var onTagClose: (tagName: String) -> ()

init(url: NSURL) {
onTagOpen = { _ in }
onTagClose = { _ in }
}

deinit { print("reader deinit") }
}

class Controller {
let reader: XMLReader = XMLReader(url: NSURL())
var tags: [String] = []

deinit { print("controller deinit")}
}
复制代码

Controller中有一个tags数组,存储了所有它读取到的标签。循环发生在viewDidLoad函数中:

func viewDidLoad() {
viewController.reader.onTagOpen = {
self.tags.append($0)
}
}
复制代码

打破引用循环有三种方法:

  1. Controller中的reader标记为weak。不过这样做会导致没有任何变量强引用XMLReader对象,它一旦被创建就会立刻释放。
  2. XMLReader中的onTagOpen闭包标记为weak。不过闭包不能被标记为weak,所以这种方法也行不通。即使这样可行,在使用XMLReader时就要考虑到这一点,并在一定程度上手动管理闭包。
  3. 使用捕获列表确保闭包不会强引用Controller,在这个例子中,这是唯一的解决方案。

捕获列表

在这个例子中,ViewController的生命周期肯定长于XMLReader,也就是说XMLReader创建时,ViewController已经创建了;XMLReader销毁时,ViewController不一定销毁。这种情况下我们可以使用无主引用unowned

func viewDidLoad() {
viewController.reader.onTagOpen = { [unowned self] in
self.tags.append($0)
}
}
复制代码

如果选择弱引用,闭包中的self会变成可选类型,代码就会变成这样:

self?.tags.append($0)
复制代码

我们可以在捕获列表中创建新的变量,比如创建一个弱引用变量mReader,指向XML解析器:

func viewDidLoad() {
viewController.reader.onTagOpen = { [unowned self, weak myReader = self.reader] tag in
self.tags.append(tag)
if tag == "stop" {
myReader?.stop()
}
}
}
复制代码

在捕获列表中创建的变量,仅在闭包内有效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值