为何在主线程调用多Block的BlockOperation start()时,Block不都在主线程执行?
这个问题其实和BlockOperation的内部设计直接相关,咱们一步步拆解:
1. 单个Block的情况(第一个测试总是通过的原因)
当BlockOperation只包含一个执行Block时,它的start()方法会同步地在当前调用线程执行这个Block。所以你在主线程调用start(),这个Block自然就在主线程跑,断言永远通过。
2. 多个Block的情况(第二个测试偶尔失败的原因)
一旦你通过addExecutionBlock(_:)添加了额外的Block,BlockOperation的行为就变了:它的设计目标是并行执行多个Block,所以会自动把这些额外的Block提交到系统全局并发队列(也就是后台线程)去执行——哪怕你是在主线程调用start(),或者把整个BlockOperation加到OperationQueue.main里。
这里要注意:OperationQueue.main是串行队列,它只会保证BlockOperation本身的start()方法在主线程触发,但BlockOperation内部的多Block并行逻辑会绕开这个队列,自己用全局并发队列调度额外的任务,这就是为什么第二个Block偶尔会跑在后台线程,导致断言失败。
3. 解决方法:让所有Block都在主线程执行
如果你需要所有执行Block都严格在主线程运行,有两种靠谱的方式:
方式一:在每个Execution Block里手动指定主线程
直接在每个Block内部把任务派发到主线程,确保执行环境正确:
func test_callStartOnMainThread_executeTwoBlockOnMainThread() { let asyncExpectation = expectation(description: "Async block executed") asyncExpectation.expectedFulfillmentCount = 2 let blockOper = BlockOperation { XCTAssertTrue(Thread.isMainThread, "Expect first block was executed on Main Thread") asyncExpectation.fulfill() } blockOper.addExecutionBlock { // 手动切换到主线程执行 DispatchQueue.main.sync { XCTAssertTrue(Thread.isMainThread, "Expect second block was executed on Main Thread") asyncExpectation.fulfill() } } OperationQueue.main.addOperation(blockOper) wait(for: [asyncExpectation], timeout: 2.0) }
这里用sync是为了保证Block的执行完成后再继续,避免测试期望提前等待。
方式二:放弃BlockOperation的多Block特性,直接用主队列添加单个Operation
如果不需要并行执行,直接把每个任务作为单独的Operation加到OperationQueue.main,这样所有任务都会串行在主线程执行:
func test_callStartOnMainThread_executeTwoBlockOnMainThread() { let asyncExpectation = expectation(description: "Async block executed") asyncExpectation.expectedFulfillmentCount = 2 let mainQueue = OperationQueue.main mainQueue.addOperation { XCTAssertTrue(Thread.isMainThread, "Expect first block was executed on Main Thread") asyncExpectation.fulfill() } mainQueue.addOperation { XCTAssertTrue(Thread.isMainThread, "Expect second block was executed on Main Thread") asyncExpectation.fulfill() } wait(for: [asyncExpectation], timeout: 2.0) }
总结
BlockOperation的多Block并行是设计默认行为,它优先利用全局并发队列来提升执行效率,不会继承调用线程的执行环境。如果需要所有任务都在主线程,必须手动控制每个任务的执行线程,或者改用单个Operation的方式。
内容的提问来源于stack exchange,提问作者Gulya Boiko

