You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:55:10