soranoba
soranoba Author of soranoba.net
programming

~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である.
おおよそのコードはここから確認できるが, 本質ではないので詳細は割愛する.

ここでポイントとなるのは, AsyncBlockOperationURLSessionのキューがともにmainQueueであるという点である.
OperationQueuemaxConcurrentOperationCountで指定した数までOperationを同時実行でき, 閾値に達すると実行中のOperationが終了するまで待つという挙動をする.
OperationQueue()で作成した場合はデフォルトで-1 (制限なし)だが, mainQueue1になっている.
つまり, このコードのOperation以下の2つでデッドロック状態になっているということだ.

  • AsyncBlockOperationの完了にはURLSessionDataTaskの実行が完了する必要があり
  • URLSessionDataTaskの実行が完了する為には, AsyncBlockOperationが完了している必要がある

これだけなら問題は単純なのだが, この問題はそんな簡単ではなかった. iOS14では動くのである.
apple/swift-corelibs-foundationを見ても, addOperationしているのでスタックトレースから見ることにした.

iOS14からはURLSessionのcompletionHandlerの実行形式が変わったようだ

iOS13 iOS14
iOS13のスタックトレース iOS14のスタックトレース


これはともに, completionHandlerの1行目で止めたときのスタックトレースである.
これを見ると, iOS13では-[NSBlockOperation main]が登場しているのに対し, iOS14ではdispatch_asyncで実行されているのが分かる. どうやら挙動が変わったらしい.

なお, テストでこの動作を再現しようとしたところ, iOS14でもiOS13と同様のスタックトレースになったので単純にOSによる差ではないようで, 謎が深まるばかりだった.
取り敢えず, 非同期のOperationを使う時はOperationQueue.mainを避けるようにしよう.

参考

この問題を試すためのコードは以下においてある.

(Updated: )

comments powered by Disqus