RetrofitのファイルDLでハマった話
原因をはっきり検証したわけでなく体感的なメモに近いのですが、対応していて面白かったので、適当に業務に関する部分のコード削りながら、こっちに置きます。
- Retrofitの通信処理をバックグラウンドに指定して、InputStream型のレスポンスの処理をメインスレッドに指定したらandroid.os.NetworkOnMainThreadException
- レスポンスボディのログを吐いた場合はエラーが出ないが、レスポンスボディのログを吐かないとエラー
- おそらくInputStreamを受け取った時点では全部のデータをサーバから受け取っていなくて、バッファーに読み込むときに足りなくなったらサーバからデータを追加で読み込んでいるとかでは
- ログを吐いた場合エラーがでないのはログを吐くために一旦データをすべて読み込んでいるため?
- 同じような原因で、読み込めたバイト数を指定せずにFileOutputStream#writeメソッドを呼んでいたらサーバ上のファイルサイズとDLしたファイルサイズがずれた
- binaryのjpegをutf-8のJSONで読み込んで画像が途中からばけた
上記をふまえた注意点のコメントが入った雑なコードを置いておきます。
// Kotlinです apiClient.downloadItemFile(itemId) .map { response -> var downloadFile = writeResponseStreamToFile("/absolute/to/path", response) downloadFile // InputStreamを扱っている部分もバックグラウンドで処理されるようにobserveOnの置き所に注意 }.map { downloadFile -> downloadFile?.let { insertContentProvider("fileName", "/absolute/to/path") } // ここはもしかしたらメインスレッドでもいいかも } . subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( ... ) // ApiClient#downloadItemFile()のインターフェイス @Streaming @GET("/items/{itemId}/download") fun downloadItemFile( @Path("itemId") itemId: Int, // RetrofitでJSON以外のファイルを扱う場合はContentTypeヘッダの設定を行うこと @Header("Content-Type") contentType: String = "image/jpg; charset=binary" ): Observable<Response> // absoluteFilePathで渡されたパスのファイルへ、Responseのバイト列を書き込む private fun writeResponseStreamToFile(absoluteFilePath: String, response: Response) : String? { var inputStream : InputStream? = null var outputStream : FileOutputStream? = null try { if (response.status == 200) { inputStream = response.body.`in`() var buff = kotlin.ByteArray(4096) val fileSize = response.body.length() var downloadedSize = 0L var readByte = 0 outputStream = FileOutputStream(absoluteFilePath) while(downloadedSize < fileSize) { // InputStreamにすべてのデータがのっていない場合は、 // ネットワーク上から追加でファイルを読み込みながらbuffにバイト列を書き込むので // 書き込めるバイト数は不定になるので注意 readByte = inputStream.read(buff) if (readByte == -1) { break } outputStream.write(buff, 0, readByte) // バイト数はちゃんと指定しましょう } outputStream.flush() } } catch (e: IOException) { e.printStackTrace() } finally { inputStream?.close() outputStream?.close() return absoluteFilePath } } // fileNameで渡されたファイル名のファイルをContentProviderに登録 private fun insertContentProvider(fileName: String, absoluteFilePath: String) { var values = ContentValues() val contentResolver = mContext.contentResolver values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") values.put(MediaStore.MediaColumns.TITLE, fileName) values.put("_data", absoluteFilePath); contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) }