Swift Playground串行队列调度后弱引用变为nil问题排查
刚帮你理清这个问题的来龙去脉啦,咱们一步步拆解:
问题现象
在Swift Playground中调用test_recursive_serial()函数时,会出现循环关联的classC对象的delegate莫名变为nil的情况;但如果直接把函数内的代码放到Playground全局作用域执行,问题就不会出现。另外要注意,必须添加PlaygroundPage.current.needsIndefiniteExecution = true才能让异步任务正常运行。
你的测试代码如下:
import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true let serialQueue = DispatchQueue(label: "myQueue"); public protocol my_protocol:class { func do_something(ofValue:Int,completion:((Int) -> Void)) } public class classA:my_protocol { public let some_value:Int; public init(value:Int){ self.some_value = value; } public func do_something(ofValue:Int,completion:((Int) -> Void)) { print("A:\(some_value) in current thread \(Thread.current) is executing \(Thread.current.isExecuting)"); if self.some_value == ofValue { completion(ofValue); } } } public class classB { public weak var jump_to_C:classC?; public var value:Int = 0; } public class classC { weak var delegate:my_protocol?{ willSet { if (newValue == nil) { print("target set to nil") } else { print("target set to delegate") } } } var someB:classB? public func do_something_else() { print(self.delegate!) } public func do_another(withValue:Int,completion:((Int) -> Void)) { } public func run(completion:@escaping ((Int) -> Void)) { print("\(self.someB?.value)"); assert(self.delegate != nil, "not here"); if let obj = someB?.jump_to_C, obj !== self { someB?.value += 1; print("\(someB!)") usleep(10000); if let value = someB?.value, value > 100 { completion(someB!.value); } else { serialQueue.async { print("lauching...") obj.run(completion: completion); } } }else{ print("pointing to self or nil...\(someB)") } } } public func test_recursive_serial() { let my_a = classA(value:100); let arrayC:[classC] = (0..<10).map { (i) -> classC in let c = classC(); c.delegate = my_a; return c; } let arrayB:[classB] = (0..<10).map { (i) -> classB in let b = classB(); let ii = (i + 1 >= 10) ? 0 : i + 1; b.jump_to_C = arrayC[ii] return b; } arrayC.forEach { (cc) in cc.someB = arrayB[Int(arc4random())%arrayB.count]; } arrayC.first!.run() { (value) in print("done!"); } } test_recursive_serial()
问题根源
这个问题的核心是Swift ARC的自动释放机制:
- 当你调用
test_recursive_serial()时,函数内部创建的my_a、arrayC、arrayB都是局部变量,函数执行完毕后,这些变量没有被任何外部对象持有,ARC会立即释放它们 classC的delegate是弱引用(weak),当被引用的classA对象被释放后,delegate会自动变成nil- 而如果直接在Playground全局作用域执行代码,这些对象属于全局变量,会被Playground环境持有,所以不会被提前释放,也就不会出现
delegate为nil的问题
解决办法
临时快速修复:持有局部对象
可以把函数内创建的对象赋值给全局变量,让它们在函数执行后依然被持有:
// 全局变量用于持有对象 var globalMyA: classA? var globalArrayC: [classC]? var globalArrayB: [classB]? public func test_recursive_serial() { let my_a = classA(value:100); let arrayC:[classC] = (0..<10).map { (i) -> classC in let c = classC(); c.delegate = my_a; return c; } let arrayB:[classB] = (0..<10).map { (i) -> classB in let b = classB(); let ii = (i + 1 >= 10) ? 0 : i + 1; b.jump_to_C = arrayC[ii] return b; } arrayC.forEach { (cc) in cc.someB = arrayB[Int(arc4random())%arrayB.count]; } // 赋值给全局变量,避免被ARC释放 globalMyA = my_a globalArrayC = arrayC globalArrayB = arrayB arrayC.first!.run() { (value) in print("done!"); } }
更规范的Playground异步任务处理方案
使用XCTest的期望等待机制配合通知,既能确保异步任务执行期间对象被持有,又能让代码更整洁:
import PlaygroundSupport import XCTest PlaygroundPage.current.needsIndefiniteExecution = true func waitForNotificationNamed(_ notificationName: String, timeout: TimeInterval = 5.0) -> Bool { let expectation = XCTNSNotificationExpectation(name: NSNotification.Name(rawValue: notificationName)) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) return result == .completed } // 复用你的protocol和class定义:my_protocol, classA, classB, classC public func test_recursive_serial() { let taskCompletedNotification = "TaskCompleted" let my_a = classA(value:100); let arrayC:[classC] = (0..<10).map { (i) -> classC in let c = classC(); c.delegate = my_a; return c; } let arrayB:[classB] = (0..<10).map { (i) -> classB in let b = classB(); let ii = (i + 1 >= 10) ? 0 : i + 1; b.jump_to_C = arrayC[ii] return b; } arrayC.forEach { (cc) in cc.someB = arrayB[Int(arc4random())%arrayB.count]; } arrayC.first!.run() { (value) in print("done!"); // 任务完成时发送通知 NotificationCenter.default.post(name: NSNotification.Name(rawValue: taskCompletedNotification), object: nil) } // 等待任务完成,确保对象在等待期间被持有 XCTAssert(waitForNotificationNamed(taskCompletedNotification, timeout: 90)) } test_recursive_serial()
这个方案的优势在于:
- 通过
XCTWaiter等待异步任务完成的通知,避免了全局变量的滥用 - 函数执行期间,
my_a、arrayC、arrayB会被闭包和等待机制间接持有,直到任务完成才会被释放 - 配合
needsIndefiniteExecution = true保证Playground不会提前终止
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

