今回はiOS7以降におけるバックグラウンドダウンロードの仕組みについて基礎とハマりどころなどを紹介します。
バックグラウンドダウンロードというのは、データのダウンロードが始まった後にユーザーが他のアプリを起動したり、端末をスリープさせたりしても、裏でダウンロードをおこない続けることです。
データのダウンロードをあまり意識せず、待つ時間を有効に利用できます。ユーザーにとっては非常に優しい機能ですので、是非試してみてください。
また、実際にバックグラウンドダウンロード機能を実装するにあたって、よくはまるところも共有したいと思います。
今回はじめてiOSのダウンロード機能に触れる方もいるかもしれませんので、ここでまず簡単なおさらいをします(以下のコードはすべてObjective-Cベースですが、Swiftでも基本的に扱うAPIは同じです)。
バックグラウンドダウンロードはiOS7で追加されたNSURLSession
がサポートする機能です。ただし、backgroundSessionConfiguration
で作ったsessionの利用が前提です。
データはNSURLSessionDownloadTask
を通してダウンロードします。
iOS Developer Library - Using NSURLSession
NSURLSession
に関するプログラミングガイド
簡単なサンプルや仕様の説明が書いてあります。まずこれを読んでください。
※そのページの左側のリンクからいろんなページにとんで、さらに資料を入手できます。
はまった時にここで解決策を探したり、開発チームに質問したりするのがおすすめです。
WWDC2014 - What's New in Foundation Networking
結構前の発表ですが、意外と役に立ちます。(後述)
※画面の左下の「Resource」ボタンを押して、PDFをダウンロードするのがおすすめです。
バックグラウンドで約1GBのデータをダウンロードすることです。
コードを組む前に、まず考えてなければならないことがいくつかあります。
正直、自分は最初にプログラミングガイドしか読んでいなかったので、何となくいけそうな設計を出しました。
NSURLSessionDownloadTask
を二つずつ作って、前のダウンロードが終わった時に新たなNSURLSessionDownloadTask
を作るという循環でリストをどんどん消化上記の案に基づいて実装しましたが、見事にほぼ全部の地雷を踏みました。
まず、こちらのフローを説明します。
DownloadManager
を定義し、必要なdelegateを継承します。- (id)init
NSURLSession
を生成[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:]
を使用することCreateDownloadTask:(NSString *)sUrl
NSURLSessionDownloadTask
を作成uuid
をファイル名としてtaskDescription
に設定[task resume]
を呼んで、ダウンロードを開始- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
taskDescription
から取得NSURLSessionDownloadTask
を作成(CreateDownloadTask
を呼ぶ)- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
から呼ばれるcompletionHandler
を呼ぶ必要がある一見正しそうに見えますし、実際に実行してみると確かにダウンロードがおこなわれます。
でも何か変だな、と気付きました
何回もテストしてわかったこと: アプリがバックグラウンドに行った時に
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
NSURLSessionConfiguration
の中身を掘り下げてdiscretionaryというパラメーターを発見(つまりバックグラウンドで開始したデータ転送は常にシステムがコントロールすることが分かった)もう一度、設計案へ立ち戻ってまずいポイントを抽出します
NSURLSessionDownloadTask
を二つずつ作って、前のダウンロードが終わった時に新たなNSURLSessionDownloadTask
を作るという循環でリストをどんどん消化 ❌バックグラウンドでタスクを作るループではダウンロード速度の面で実用性が乏しい点と、ダウンロード済みデータを回収する仕組みを改めて考えざるを得ない場面になりました。さらに、アプリが途中でクラッシュした場合、次回は未ダウンロードのパッケージのみをダウンロードする必要があります。
NSURLSessionDownloadTask
を二つずつ作って、前のダウンロードが終わった時に新たなNSURLSessionDownloadTask
を作るという循環でリストをどんどん消化変更点:
init()
の中でcheckPrevious
を呼ぶ-(void)onAppWillEnterBackground
-(void)checkPrevious
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
が呼ばれるので、ここで未完成のタスクを削除する(必要に応じて処理を追加してください)パッケージ10個でテストした限りでは特に問題ありませんでした。
が、また問題が発生しました
パッケージ数を500個まで増やしてテストした場合、
-(void)onAppWillEnterBackground
の処理は制限時間内に終わらず、アプリがOSに落とされる。(Xcodeに繋いで実行している場合は警告が出力されるだけで、落とされない)backgroundTask
に回して落ちないようにしたら、今度はコールバックがおかしくなって失敗その後に調べた結果、ここの返答にAppleのデベロッパーがこう書いています。「バックグラウンドセッションは相対的に少数の大きいサイズのファイルのための設計です。」
また、WWDC2014 - What's New in Foundation Networkingからダウンロードした707_whats_new_in_foundation_networking.pdfの中身を読むと、こんなアンチパターンと推奨の使い方が書いてあります。
ここで、もう一度最初の設計案を見てみます
NSURLSessionDownloadTask
を二つずつ作って、前のダウンロードが終わった時に新たなNSURLSessionDownloadTask
を作るという循環でリストをどんどん消化 ❌まさか見事にアンチパターンを全部踏んだのでは?
NSURLSessionDownloadTask
を二つずつ作って、前のダウンロードが終わった時に新たなNSURLSessionDownloadTask
を作るという循環でリストをどんどん消化バックグラウンドダウンロードは実に便利かつユーザーに優しい機能なので、多少実装・検証コストかかっても入れたほうがいいと思います。実は今回一つの課題が残っています。クラッシュから再開した時に途中までのタスクをキャンセルせずに途中からダウンロードするいわばRangeDownloadの対応です。いつか対応しようと思っています。
バックグラウンドダウンロードを取り入れる際には以下の方針に沿ってやりましょう
Kyo Shinyuu
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。