首先,从答案开始,然后我将解释为什么这里有一个真正的问题,然后我将努力解决为什么这个真正的问题,而不仅仅是一种"让编译器高兴"的方法."
答案是:
- 将授权控制器标记为
@MainActor
signInAnonymouslyIfNecessary
分到nonisolated
分
signInAnonymouslyIfNecessary
个任务:
@MainActor final class AuthController {
nonisolated func signInAnonymouslyIfNecessary() {
Task { @MainActor in
/// ...
}
}
}
这是为什么
UINavigationController(和扩展AuthFormNavC)是@MainActor
.但是,不能保证deinit
会在MainActor上被调用.(事实证明,through a great deal of work from the UIKit team,非常有可能在MainActor上调用UI View和UI ViewController,但这没有明确promise ,并且通常不是真的.
这意味着警告是正确的.在任意线程上访问self.authController
无效.这是一个长期存在的真实原因,当试图删除deinit
中的KVO观察时,这就是为什么我们需要像PMKVObserver这样的包.
从创建authController
的上下文(即MainActor)以外的上下文调用authController.signInAnonymouslyIfNecessary()
也是无效的.AuthController不是Scrum(并且可能不是线程安全的).
我说所有这些都是为了指出,这个代码实际上从来都不是合法的,在一般情况下(在UIKit的魔力之外),它是现实世界崩溃的一个长期原因,Swift Concurrency在这里帮助消除.
但我们该怎么办?
首先,很明显,AuthController打算成为MainActor,所以应该这样标记:
@MainActor class AuthController {
第二,虽然在deinit
中延长self
的生命周期 是非法的,但延长self
财产的生命周期 肯定不是非法的.这里有一个直接的方法来做到这一点(尽管有一个更安全的方法,我将得到下一步).
// This is for illustration, see below for a safer solution
deinit {
// Do not overlook the `[authController]` here!
Task { @MainActor [authController] in
authController.signInAnonymouslyIfNecessary()
}
}
我必须强调逮捕名单的重要性.这使得authController
成为Task内部的本地复制变量,避免捕获self
.如果不包含这个捕获将崩溃,编译器不会对此发出警告.列表中已经讨论过几次了,至少有一个关于它的FB.我也打开了一个bug report.好消息是,捕获deinit
中的self
将可靠地崩溃,而不仅仅是未定义的行为.Swift checks the retain count在deinit
的末尾以确保self
没有逃脱.
因此,将事情打包在一个任务中解决了问题,但它也带来了新的头痛.我们必须非常小心我们的捕获名单.deinit
个代码通常没有得到很好的测试,最小的错误都可能导致程序崩溃.如果我们能说"deinit
个任务中没有任务,它们太危险了."我们可以.
保持AuthController为@MainActor
,我们可以说从任何使用nonisolated
的线程调用signInAnonymouslyIfNecessary
是安全的:
@MainActor final class AuthController {
nonisolated func signInAnonymouslyIfNecessary() {
Task { @MainActor in
/// This block will likely capture `self`. That's good.
/// There is no reason for a `weak` dance here; there's no cycle.
/// We want `self` to live long enough to finish the job.
/// ...
}
}
}
有了这个,你的deinit
是好的.即使您不控制AuthController,您也可以扩展它以提供一个调用Task中实际方法的nonisolated
方法.
作为一项规则,应该避免deinit
,特别是对于任何重要的事情.它总是有许多棘手的角落情况,一直是挑战性的使用可靠,even in the ObjC days with -dealloc
.Swift并发只是expose 了许多长期存在的问题.正如@ Sweeper所指出的,这个特殊的情况可能应该在viewDidDisappear
中完成,这可能是你真正想要的行为,或者是响应模型的改变,而不是依赖于视图控制器的内存管理的副作用.但这也是一个重要的问题,一般情况值得探讨.
下面是一个简化版本的代码,可以用来玩,看看什么有效和不有效:
@MainActor class Service {
func doThings() {}
}
@MainActor class Controller {
let service: Service
init(service: Service) { self.service = service }
func printMe() { print("Controller lives") }
deinit {
print("Controller deinit")
// As written, this crashes. Adding [service] here will fix it.
Task { @MainActor in
service.doThings()
}
}
}
let service = Service()
do {
let controller = Controller(service: service)
controller.printMe()
}