~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: )

