在异步代码的上下文中,办理应用程序的内存往往特别扎手,由于跟着时刻的推移,通常需求捕获和保存各种目标和值,以便履行和处理咱们的异步调用。

虽然Swift相对较新的async/await语法的确使许多异步操作更简略编写,但在办理此类异步代码所触及的各种使命和目标的内存时,它依然需求咱们非常小心。

隐性捕获

async/await(以及咱们从同步上下文调用此类代码时需求用于包装此类代码Task类型)的一个风趣方面是,当咱们的异步代码履行时,目标和值通常怎么被隐式捕获

例如,假定咱们正在开发一个DocumentViewController,它下载并显现从给定URL下载的Document。为了在视图控制器即将显现给用户时懒洋洋地履行咱们的下载,咱们在视图控制器的viewWillAppear办法中启动该操作,然后咱们要么烘托下载的文档,要么显现遇到的任何过错——像这样:

class DocumentViewController: UIViewController {
    private let documentURL: URL
    private let urlSession: URLSession
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task {
            do {
                let (data, _) = try await urlSession.data(from: documentURL)
                let decoder = JSONDecoder()
                let document = try decoder.decode(Document.self, from: data)
                renderDocument(document)
            } catch {
                showErrorView(for: error)
            }
        }
    }
    private func renderDocument(_ document: Document) {
        ...
    }
    private func showErrorView(for error: Error) {
        ...
    }
}

现在,假如咱们快速检查上面的代码,好像没有任何目标捕获。究竟,异步捕获传统上只发生在转义闭包中,这反过来又要求咱们在访问此类闭包中的本地特点或办法时一直清晰引证self(当self引证类实例时)。

因此,咱们或许会期望,假如咱们开端显现咱们的DocumentViewController,但在下载完结之前脱离它,一旦没有外部代码(例如其parentUINavigationController)坚持对它的强烈引证,它将被成功重新分配。但事实并非如此。

这是由于上述隐式捕获发生在咱们创立Task或运用await等候异步调用结果时。Task中运用的任何目标将主动保存,直到该使命完结(或失利),包含咱们引证其任何成员时,就像咱们上面所做的那样。

在许多情况下,这种行为实践上或许不是问题,而且或许不会导致任何实践的内存走漏,由于一切捕获的目标在捕获使命完结后终究都会被开释。但是,假定咱们预计DocumentViewController下载的文档或许适当大,假如用户在不同屏幕之间快速导航,咱们不期望多个视图控制器(及其下载操作)保存在内存中。

处理这类问题的经典办法是履行weak self捕获,该捕获通常在捕获闭包自身中伴跟着guard-let self表达式,以便将弱引证转换为强引证,然后能够在闭包的代码中运用:

class DocumentViewController: UIViewController {
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task { [weak self] in
            guard let self = self else { return }
            do {
                let (data, _) = try await self.urlSession.data(
                    from: self.documentURL
                )
                let decoder = JSONDecoder()
                let document = try decoder.decode(Document.self, from: data)
                self.renderDocument(document)
            } catch {
                self.showErrorView(for: error)
            }
        }
    }
    ...
}

不幸的是,在这种情况下,这行不通,由于当咱们的异步URLSession调用暂停时,咱们的本地self引证仍将被保存,直到咱们一切闭包的代码完结运转(就像函数中的局部变量被保存到该范围退出停止)。

因此,假如咱们真的想弱地捕捉自我,那么咱们有必要在整个封闭过程中一直运用这种弱的self参阅。为了更简略地运用咱们的urlSessiondocumentURL特点,咱们能够独自捕获这些特点,由于这样做不会阻止咱们的视图控制器自身被开释:

class DocumentViewController: UIViewController {
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task { [weak self, urlSession, documentURL] in
            do {
                let (data, _) = try await urlSession.data(from: documentURL)
                let decoder = JSONDecoder()
                let document = try decoder.decode(Document.self, from: data)
                self?.renderDocument(document)
            } catch {
                self?.showErrorView(for: error)
            }
        }
    }
    ...
}

好消息是,跟着上述内容的到位,假如在下载完结之前终究被辞退,咱们的视图控制器现在将成功分配。

但是,这并不意味着其使命将主动撤销。在这种情况下,这或许不是问题,但假如咱们的网络调用导致某种副作用(如数据库更新),那么即使在咱们的视图控制器被开释后,该代码仍将运转,这或许会导致过错或意外行为。

撤销使命

一旦咱们的DocumentViewController内存不足,确保任何正在进行的下载使命的确会被撤销的一种办法是存储对该使命的引证,然后在咱们的视图控制器被解除分配时调用其cancel办法:

class DocumentViewController: UIViewController {
    private let documentURL: URL
    private let urlSession: URLSession
    private var loadingTask: Task<Void, Never>?
    ...
    deinit {
    loadingTask?.cancel()
}
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadingTask = Task { [weak self, urlSession, documentURL] in
            ...
        }
    }
    ...
}

现在一切都按预期作业,一旦被封闭,咱们一切视图控制器的内存和异步状况都会主动清理——但咱们的代码在这个过程中也变得适当复杂。有必要为每个履行异步使命的视图控制器编写一切内存办理代码将适当繁琐,这乃至或许让咱们置疑async/await是否真的比组合、委托或闭包等技能给咱们带来任何真实的优点。

谢天谢地,还有另一种办法能够完结上述模式,它不触及那么多的代码和复杂性。由于该惯例是长时间运转的async办法在被撤销时抛出过错,一旦咱们的视图控制器即将被封闭,咱们能够简略地撤销loadingTask——这将使咱们的使命抛出过错,退出并开释其一切捕获的目标(包含self)。这样,咱们不再需求弱地捕获self,或做任何其他类型的手动内存办理作业——给咱们以下完结:

class DocumentViewController: UIViewController {
    private let documentURL: URL
    private let urlSession: URLSession
    private var loadingTask: Task<Void, Never>?
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadingTask = Task {
            do {
                let (data, _) = try await urlSession.data(from: documentURL)
                let decoder = JSONDecoder()
                let document = try decoder.decode(Document.self, from: data)
                renderDocument(document)
            } catch {
                showErrorView(for: error)
            }
        }
    }
    override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    loadingTask?.cancel()
}
    ...
}

请注意,当咱们的使命被撤销时,咱们的showErrorView办法现在仍将被调用(由于将抛出过错,而且此时self仍保存在内存中)。但是,就功能而言,额定的办法调用应该完全能够忽略不计。

长时间调查

一旦咱们开端运用async/await来设置某种异步序列或流的长时间运转调查,上述内存办理技能应该变得更加重要。例如,在这里,咱们让UserListViewController调查UserList类,以便在更改User模型数组后重新加载其表视图数据:

class UserList: ObservableObject {
    @Published private(set) var users: [User]
    ...
}
class UserListViewController: UIViewController {
    private let list: UserList
    private lazy var tableView = UITableView()
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task {
            for await users in list.$users.values {
                updateTableView(withUsers: users)
            }
        }
    }
    private func updateTableView(withUsers users: [User]) {
        ...
    }
}

请注意,上述完结现在不包含咱们之前在DocumentViewController中完结的任何使命撤销逻辑,在这种情况下,这实践上会导致内存走漏。原因是(与咱们之前的Document加载使命不同),咱们的UserList调查使命将无限期地运转,由于它正在迭代基于Publisher的异步序列,该序列无法抛出过错或以任何其他方式完结。

好消息是,咱们能够运用与之前完全相同的技能轻松修复上述内存走漏,以防止咱们的DocumentViewController保存在内存中——也就是说,一旦咱们的视图控制器即将消失,就能够撤销咱们的调查使命:

class UserListViewController: UIViewController {
    private let list: UserList
    private lazy var tableView = UITableView()
    private var observationTask: Task<Void, Never>?
    ...
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        observationTask = Task {
            for await users in list.$users.values {
                updateTableView(withUsers: users)
            }
        }
    }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        observationTask?.cancel()
    }
    ...
}

请注意,在这种情况下,在deinit中履行上述撤销是行不通的,由于咱们正在处理实践的内存走漏——这意味着除非咱们打破调查使命的无休止循环,不然永远不会调用deinit

结论

起初,Taskasync/await等技能好像使异步、与内存相关的问题成为曩昔,但不幸的是,在履行各种async标记调用时,咱们依然有必要小心怎么捕获和保存目标。虽然实践的内存走漏和保存周期或许不像运用组合或闭包之类的东西时那么简略遇到,但咱们依然有必要确保咱们的目标和使命的办理方式使咱们的代码健壮且易于维护。