自从blocks作为iOS 4的一部分被引入Objective-C以来,它们已经成为苹果平台上大多数现代api的重要组成部分。使用Block的惯例也随着closures被带入Swift,这是我们大多数人每天都在使用的语言特性。

但是,即使闭包被广泛使用,在使用它们时也要记住许多行为和注意事项。本周,让我们仔细看看闭包、捕获是如何工作的,以及一些使处理闭包更容易的技术。就让我们一探究竟吧!

The great escape

闭包有两种不同的变体——逃逸和非逃逸。当一个闭包进行逃逸(由@escaping参数属性标记)时,意味着它将以某种方式存储(要么作为属性,要么被另一个闭包捕获)。另一方面,非逃逸闭包不能存储,必须在使用时直接执行。

非逃逸闭包的一个例子是在集合上使用函数操作,例如forEach:

[1, 2, 3].forEach { number in
    ...
}

由于闭包将为集合的每个成员直接执行,因此不需要对其进行逃逸。

逃逸闭包主要出现在异步api中,比如DispatchQueue。例如,当你在调度一个异步闭包时,这个闭包将会逃逸:

DispatchQueue.main.async {
    ...
}

那么,有什么区别呢? 因为逃逸闭包将被存储,所以它们还需要存储定义它们的上下文。 当该上下文涉及到其他值或对象时,需要捕获这些值或对象,以便在闭包等待执行时不会消失。最常见的实际含义是在self上使用api时,这要求我们以某种方式显式地捕获self。

Capturing & retain cycles

由于转义闭包会自动捕获在闭包内使用的任何值或对象,因此它们是循环引用的常见来源。例如,当一个视图控制器被它的视图模型存储在一个闭包中:

class ListViewController: UITableViewController {
    private let viewModel: ListViewModel

    init(viewModel: ListViewModel) {
        self.viewModel = viewModel

        super.init(nibName: nil, bundle: nil)

        viewModel.observeNumberOfItemsChanged {
            // This will cause a retain cycle, since our view controller
            // retains its view model, which in turn retains the view
            // controller by capturing it in an escaping closure.
            self.tableView.reloadData()
        }
    }
}

对于这个问题的一个常见的解决方法,正如大多数使用闭包的人可能已经知道的那样,是弱地捕获self来打破retain循环:

viewModel.observeNumberOfItemsChanged { [weak self] in
    self?.tableView.reloadData()
}

Capturing the context instead of self

虽然上面的[weak self]解决方案在大多数情况下是伟大的,当你想要避免强捕获一个对象,它也有一些缺点。首先,它非常容易被忽略,因为编译器不会警告您任何潜在的循环引用。其次,当你必须从弱引用转换回强引用时,它可能会导致一些相当混乱的代码,像这样:

dataLoader.loadData(from: url) { [weak self] data in
    guard let strongSelf = self else {
        return
    }
    let model = try strongSelf.parser.parse(data, using: strongSelf.schema)
    strongSelf.titleLabel.text = model.title
    strongSelf.textLabel.text = model.text
}

捕获self的一个替代解决方案是捕获你在闭包中需要的单个对象。 这仍然可以让我们避免一个循环引用(因为像标签和模式这样的对象不存储闭包), 不需要做“weak/strong self dance”。下面是如何使用上下文元组实现这一点:

// We define a context tuple that contains all of our closure's dependencies
let context = (
    parser: parser,
    schema: schema,
    titleLabel: titleLabel,
    textLabel: textLabel
)

dataLoader.loadData(from: url) { data in
    // We can now use the context instead of having to capture 'self'
    let model = try context.parser.parse(data, using: context.schema)
    context.titleLabel.text = model.title
    context.textLabel.text = model.text
}

Arguments instead of capturing

捕获对象的另一种方法是将它们作为参数传递。这是我在为我的新游戏引擎Imagine引擎设计事件API时使用的一种技术,它允许你在使用闭包观察事件时传递一个观察者。这允许self被传入,它将被传入事件的闭包,而不必手动捕获它:

actor.events.moved.addObserver(self) { scene in
    ...
}

让我们回到最初的ListViewController示例,看看在观察它的视图模型时,我们如何实现完全相同的API。这样,我们甚至可以传入我们想要作为观察者重新加载的表视图,给我们一个非常好的调用站点,像这样:

viewModel.numberOfItemsChanged.addObserver(tableView) { tableView in
    tableView.reloadData()
}

为了实现上述目标,我们将使用一种非常类似于Imagine Engine事件系统的技术。 我们首先定义一个简单的事件类型,它可以附加观察闭包:

class Event {
    private var observers = [() -> Void]()
}

然后,我们将添加一个方法,该方法允许我们添加任何引用类型的观察者,以及一个在观察被触发后调用的闭包。这里有一个技巧,我们将把这个闭包传递到第二个闭包中,这个闭包弱地捕获了观察者,像这样:

func addObserver<T: AnyObject>(_ observer: T, using closure: @escaping (T) -> Void) {
    observers.append { [weak observer] in
        observer.map(closure)
    }
}

这使得我们只需要进行一次弱/强转换,而不会影响调用站点。最后,我们将添加一个触发器方法来触发事件:

func trigger() {
    for observer in observers {
        observer()
    }
}

现在我们可以回到ListViewModel,为numberOfItemsChanged添加一个事件,一旦它的条件满足,我们就会触发它,像这样:

class ListViewModel {
    let numberOfItemsChanged = Event()
    var items: [Item] { didSet { itemsDidChange(from: oldValue) } }

    private func itemsDidChange(from previousItems: [Item]) {
        if previousItems.count != items.count {
            numberOfItemsChanged.trigger()
        }
    }
}

似于上面的基于事件的API的最大优点是,它更难意外地引入retain循环,而且我们可以在代码中对任何类型的事件观察重用相同的实现。虽然上面的事件实现非常简单,而且缺乏像取消观察者注册这样的高级特性,但对于更简单的用例来说已经足够好了。

我们将在以后的博客文章中更详细地介绍基于事件的编程,您也可以查看Imagine Engine中使用的完整事件实现以获得更多细节。

Conclusion

闭包可以自动捕获在其内部使用的任何对象或值,这是一个非常棒的特性,也是使闭包易于使用的必要条件。然而,捕获也可能是bugs和 循环引用 的来源,并可能使代码变得更复杂和更难理解。

虽然我不建议在所有情况下都避免捕获,但我希望这篇文章能够提供一些替代方法来始终捕捉特定的 self。在某些情况下,采用经典的弱自捕获是最合适的解决方案,但对于其他情况,使用一些替代技术可以帮助您使基于闭包的代码更容易使用和维护。

原文链接