~iOS13でURLSessionのcompletionHandlerが実行されない問題への対応
久しぶりに面白いバグに遭遇した.
iOS14ではURLSession.dataTask(with:completionHandler:)のcompletionHandlerが実行されるが, それ以前のOSでは実行されないというものだ.
それも全ての箇所ではなく, 1箇所だけ何故か常に実行されない.
OperationQueue.mainでOperationが実行中は他のOperationが実行されなくなる
今回問題が発生したコードは以下のような構造をしていた. もちろん, 実際のコードはこんなシンプルではなかったが.
class Service {
private let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
func execute() {
OperationQueue.main.addOperation(AsyncBlockOperation { done in
let task = self.session.dataTask(with: URLRequest(url: URL(string: "https://soranoba.net")!)) { _, _, _ in
done()
}
task.resume()
})
}
}
このコードのAsyncBlockOperationは引数のblockを実行するまで完了とみなさないBlockOperationである.
おおよそのコードはここから確認できるが, 本質ではないので詳細は割愛する.
ここでポイントとなるのは, AsyncBlockOperationとURLSessionのキューがともにmainQueueであるという点である.
OperationQueueはmaxConcurrentOperationCountで指定した数までOperationを同時実行でき, 閾値に達すると実行中のOperationが終了するまで待つという挙動をする.
OperationQueue()で作成した場合はデフォルトで-1 (制限なし)だが, mainQueueは1になっている.
つまり, このコードのOperation以下の2つでデッドロック状態になっているということだ.
AsyncBlockOperationの完了にはURLSessionDataTaskの実行が完了する必要がありURLSessionDataTaskの実行が完了する為には,AsyncBlockOperationが完了している必要がある
これだけなら問題は単純なのだが, この問題はそんな簡単ではなかった. iOS14では動くのである.
apple/swift-corelibs-foundationを見ても, addOperationしているのでスタックトレースから見ることにした.
iOS14からはURLSessionのcompletionHandlerの実行形式が変わったようだ
| iOS13 | iOS14 |
|---|---|
![]() |
![]() |
これはともに, completionHandlerの1行目で止めたときのスタックトレースである.
これを見ると, iOS13では-[NSBlockOperation main]が登場しているのに対し, iOS14ではdispatch_asyncで実行されているのが分かる. どうやら挙動が変わったらしい.
なお, テストでこの動作を再現しようとしたところ, iOS14でもiOS13と同様のスタックトレースになったので単純にOSによる差ではないようで, 謎が深まるばかりだった.
取り敢えず, 非同期のOperationを使う時はOperationQueue.mainを避けるようにしよう.
参考
この問題を試すためのコードは以下においてある.
記事が気に入ったらチップを送ることができます!
You can give me a cup of coffee :)
Kyash ID: soranoba
Amazon: Wish List
GitHub Sponsor: github.com/sponsors/soranoba
PayPal.Me: paypal.me/soranoba
(Updated: )

