使用OKHTTP下载二进制文件

86

我在我的Android应用程序中使用OKHTTP客户端进行网络通信。

这个例子展示了如何上传二进制文件。我想知道如何使用OKHTTP客户端获取二进制文件下载的输入流。

以下是该示例的清单:

public class InputStreamRequestBody extends RequestBody {

    private InputStream inputStream;
    private MediaType mediaType;

    public static RequestBody create(final MediaType mediaType, 
            final InputStream inputStream) {
        return new InputStreamRequestBody(inputStream, mediaType);
    }

    private InputStreamRequestBody(InputStream inputStream, MediaType mediaType) {
        this.inputStream = inputStream;
        this.mediaType = mediaType;
    }

    @Override
    public MediaType contentType() {
        return mediaType;
    }

    @Override
    public long contentLength() {
        try {
            return inputStream.available();
        } catch (IOException e) {
            return 0;
        }
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        Source source = null;
        try {
            source = Okio.source(inputStream);
            sink.writeAll(source);
        } finally {
            Util.closeQuietly(source);
        }
    }
}

简单的GET请求的当前代码如下:

OkHttpClient client = new OkHttpClient();
request = new Request.Builder().url("URL string here")
                    .addHeader("X-CSRFToken", csrftoken)
                    .addHeader("Content-Type", "application/json")
                    .build();
response = getClient().newCall(request).execute();

现在我该如何将响应转换为 InputStream 。与 Apache HTTP Client 的响应类似,对于 OkHttp 响应,可以使用以下内容:

InputStream is = response.getEntity().getContent();

编辑

接受下面的答案。 我修改后的代码:

request = new Request.Builder().url(urlString).build();
response = getClient().newCall(request).execute();

InputStream is = response.body().byteStream();

BufferedInputStream input = new BufferedInputStream(is);
OutputStream output = new FileOutputStream(file);

byte[] data = new byte[1024];

long total = 0;

while ((count = input.read(data)) != -1) {
    total += count;
    output.write(data, 0, count);
}

output.flush();
output.close();
input.close();

请检查我的修改后的答案,我正在等待您的反馈。 - Nadir Belhaj
很高兴它对你有用,伙计 :D - Nadir Belhaj
顺便提一下,如果请求中出现ioexception并且HttpEngine设置为重试,则您的InputStreamRequestBody将无法正常工作。请参见https://github.com/square/okhttp/blob/master/okhttp/src/main/java/com/squareup/okhttp/Call.java第202行,writeTo在while循环中被调用。这将导致错误。(我在从“content://”inputstream上传时偶然发现了这个问题) - njzk2
11个回答

216

就我来说,我建议使用okio中的response.body().source()(因为OkHttp已经原生支持它),以便更轻松地处理下载文件时可能出现的大量数据。

@Override
public void onResponse(Call call, Response response) throws IOException {
    File downloadedFile = new File(context.getCacheDir(), filename);
    BufferedSink sink = Okio.buffer(Okio.sink(downloadedFile));
    sink.writeAll(response.body().source());
    sink.close();
}

以下是从文档中摘取的与InputStream相比的一些优势:

该接口在功能上等同于InputStream。 当使用异构数据消耗时,InputStream需要多层处理:使用DataInputStream处理原始值,BufferedInputStream进行缓冲,使用InputStreamReader处理字符串。这个类对所有这些都使用BufferedSource。 Source避免了不可能实现的available()方法。相反,调用者会指定所需的字节数。

Source省略了InputStream跟踪的不安全组合的标记和重置状态,而是由调用者仅仅缓冲他们需要的内容。

当实现一个source时,你不需要担心单字节读取方法,因为它很难高效地实现,并且返回257种可能的值之一。

而且,source具有更强大的跳过方法:BufferedSource.skip(long)不会过早返回。


24
由于不会存在任何无谓的响应数据复制,这种方法也是最快的。 - Jesse Wilson
2
如何使用这种方法处理进度? - zella
1
@zella 只需使用 write(Source source, long byteCount) 和一个循环即可。当然,您需要从 body 中获取 contentLength 以确保读取正确数量的字节。在每个循环中触发一个事件。 - kiddouk
5
解决了!可用的代码:while ((source.read(fileSink.buffer(), 2048)) != -1)。该代码意思是从source中读取数据,然后将其写入到fileSink的缓冲区中,并重复执行这个动作,直到source.read的返回值为-1。 - zella
1
@IgorGanapolsky,确实如此,但是您必须实现Zip压缩程序才能在后续获取对存档内文件的访问权限。 - kiddouk
显示剩余3条评论

40

从OKHTTP获取ByteStream

我一直在阅读 OkHttp 的文档,您需要按照以下步骤进行:

使用此方法:

response.body().byteStream(),它将返回一个InputStream

这样,您就可以简单地使用BufferedReader或任何其他替代方法。

OkHttpClient client = new OkHttpClient();
request = new Request.Builder().url("URL string here")
                     .addHeader("X-CSRFToken", csrftoken)
                     .addHeader("Content-Type", "application/json")
                     .build();
response = getClient().newCall(request).execute();

InputStream in = response.body().byteStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String result, line = reader.readLine();
result = line;
while((line = reader.readLine()) != null) {
    result += line;
}
System.out.println(result);
response.body().close();

3
你提到的文档示例适用于请求简单数据或网页,但是对于下载二进制文件,我需要使用BufferedInputStream吗?这样我就可以将字节存储在缓冲区中,并将它们写入FileOutputStream以保存在本地存储中了。 - pratsJ
1
编辑二进制解决方案 - Nadir Belhaj
1
OKHTTP响应没有像Apache HTTP Client那样的getEntity()方法。在问题中进行了编辑。 - pratsJ
已编辑,附上终极答案 ;) - Nadir Belhaj
15
不要使用BufferedReader处理二进制数据;这会通过不必要的字节到字符解码导致数据损坏。 - Jesse Wilson
为什么要 close() 响应体? - IgorGanapolsky

13

基于源代码 "okio",最佳的下载选项

private void download(@NonNull String url, @NonNull File destFile) throws IOException {
    Request request = new Request.Builder().url(url).build();
    Response response = okHttpClient.newCall(request).execute();
    ResponseBody body = response.body();
    long contentLength = body.contentLength();
    BufferedSource source = body.source();

    BufferedSink sink = Okio.buffer(Okio.sink(destFile));
    Buffer sinkBuffer = sink.buffer();

    long totalBytesRead = 0;
    int bufferSize = 8 * 1024;
    for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1; ) {
        sink.emit();
        totalBytesRead += bytesRead;
        int progress = (int) ((totalBytesRead * 100) / contentLength);
        publishProgress(progress);
    }
    sink.flush();
    sink.close();
    source.close();
}

编写 downloadFile 方法 - Jemshit Iskenderov
我无法获取body.contentLength()的值...它总是-1。 - H Raval
3
不确定但可能需要将flush()close()方法放在finally块中,以确保任何异常情况下仍然能够刷新和关闭它们。 - Joshua Pinter
1
注意:sink.buffer()已被弃用,应使用sink.getBuffer()(Java)或sink.buffer(Kotlin)代替:https://square.github.io/okio/changelog/#version-210 - Joshua Pinter

10

这是我在使用Okhttp + Okio库时,在每个块下载后发布下载进度的方法:

public static final int DOWNLOAD_CHUNK_SIZE = 2048; //Same as Okio Segment.SIZE

try {
        Request request = new Request.Builder().url(uri.toString()).build();

        Response response = client.newCall(request).execute();
        ResponseBody body = response.body();
        long contentLength = body.contentLength();
        BufferedSource source = body.source();

        File file = new File(getDownloadPathFrom(uri));
        BufferedSink sink = Okio.buffer(Okio.sink(file));

        long totalRead = 0;
        long read = 0;
        while ((read = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
            totalRead += read;
            int progress = (int) ((totalRead * 100) / contentLength);
            publishProgress(progress);
        }
        sink.writeAll(source);
        sink.flush();
        sink.close();
        publishProgress(FileInfo.FULL);
} catch (IOException e) {
        publishProgress(FileInfo.CODE_DOWNLOAD_ERROR);
        Logger.reportException(e);
}

getDownloadPathFrom 是做什么的? - MBehtemam
请编写缺失的方法。 - Jemshit Iskenderov
2
我无法获取body.contentLength()的值...它总是-1。 - H Raval
5
应将 while (read = (source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) { 改为 while ((read = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {。修改后的代码意思不变,只是更加准确和易于理解,它会在每次读取数据时检查读取操作是否成功,并且只要读取的字节数不等于-1,循环就会一直执行。 - Matthieu Harlé
1
如果你在while循环中不调用sink.emit();,那么将会使用大量的内存。 - manfcas
显示剩余2条评论

6
更好的解决方案是使用OkHttpClient,示例如下:
OkHttpClient client = new OkHttpClient();

            Request request = new Request.Builder()
                    .url("http://publicobject.com/helloworld.txt")
                    .build();



            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    e.printStackTrace();
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {

                    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

//                    Headers responseHeaders = response.headers();
//                    for (int i = 0; i < responseHeaders.size(); i++) {
//                        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
//                    }
//                    System.out.println(response.body().string());

                    InputStream in = response.body().byteStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    String result, line = reader.readLine();
                    result = line;
                    while((line = reader.readLine()) != null) {
                        result += line;
                    }
                    System.out.println(result);


                }
            });

我无法获取body.contentLength()的值...它总是-1。 - H Raval
1
@HRaval 这不是一个 okhttp 的问题,这只是意味着服务器没有发送“Content-Length”头信息。 - Sergei Voitovich
@Joolah 如何将该文件存储在内部存储器中? - Hardik Parmar

5

基于kiddouk答案的Kotlin版本

 val request = Request.Builder().url(url).build()
 val response = OkHttpClient().newCall(request).execute()
 val downloadedFile = File(cacheDir, filename)
 val sink: BufferedSink = downloadedFile.sink().buffer()
 sink.writeAll(response.body!!.source())
 sink.close()

3

根据KiddoUK在2015年的回答(https://dev59.com/FV8e5IYBdhLWcg3wS465#29012988),更新Kotlin代码如下:

val sourceBytes = response.body().source()
val sink: BufferedSink = File(downloadLocationFilePath).sink().buffer()
sink.writeAll(sourceBytes)
sink.close()

并且添加进度监控:

val sourceBytes = response.body().source()
val sink: BufferedSink = File(downloadLocationFilePath).sink().buffer()

var totalRead: Long = 0
var lastRead: Long
while (sourceBytes
          .read(sink.buffer, 8L * 1024)
          .also { lastRead = it } != -1L 
) {
    totalRead += lastRead
    sink.emitCompleteSegments()
    // Call your listener/callback here with the totalRead value        
}

sink.writeAll(sourceBytes)
sink.close()

1
我在这里附上我的解决方案:
fun downloadFile(url: String, file: File) : Completable {
    val request = Request.Builder().url(url).build()
    return Completable.fromPublisher<Boolean> { p ->
        OkHttpClient.Builder().build().newCall(request).enqueue(object: Callback {
            override fun onFailure(call: Call, e: IOException) {
                p.onError(e)
            }

            override fun onResponse(call: Call, response: Response) {
                val sink = Okio.buffer(Okio.sink(file))
                sink.writeAll(response.body()!!.source())
                sink.close()
                p.onComplete()
            }

        })
    }
}

重构客户端是非常重要的,特别是当你使用多种中间件或者要在json格式中编码数据时。你需要找到一个解决方案,不将响应编码为其他格式。请注意,保留HTML标签。

1

以下是一种快速的方法。

  • client - OkHttpClient
  • url - 您希望下载的URL
  • destination - 您希望保存的位置
    fun download(url: String, destination: File) = destination.outputStream().use { output ->
        client.newCall(Request.Builder().url(url).build())
            .execute().body!!.byteStream().use { input ->
                input.copyTo(output)
            }
    }

1
如果您想将下载的字节写入最新版 Android 上的共享存储,您需要手头有Uri而不是File实例。以下是如何将Uri转换为OutputStream的方法:
fun Uri?.toOutputStream(context: Context)
        : OutputStream? {
    if (this == null) {
        return null
    }

    fun createAssetFileDescriptor() = try {
        context.contentResolver.openAssetFileDescriptor(this, "w")
    } catch (e: FileNotFoundException) {
        null
    }

    fun createParcelFileDescriptor() = try {
        context.contentResolver.openFileDescriptor(this, "w")
    } catch (e: FileNotFoundException) {
        null
    }

    /** scheme://<authority>/<path>/<id> */
    if (scheme.equals(ContentResolver.SCHEME_FILE)) {
        /** - If AssetFileDescriptor is used, it always writes 0B.
         * - (FileOutputStream | ParcelFileDescriptor.AutoCloseOutputStream) works both for app-specific + shared storage
         * - If throws "FileNotFoundException: open failed: EACCES (Permission denied)" on Android 10+, use android:requestLegacyExternalStorage="true" on manifest and turnOff/turnOn "write_external_storage" permission on phone settings. Better use Content Uri on Android 10+ */
        return try {
            FileOutputStream(toFile())
        } catch (e: Throwable) {
            null
        }

    } else if (scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
        // i think you can't write to asset inside apk
        return null

    } else {
        // content URI (MediaStore)
        if (authority == android.provider.MediaStore.AUTHORITY) {
            return try {
                context.contentResolver.openOutputStream(this, "w")
            } catch (e: Throwable) {
                null
            }
        } else {
            // content URI (provider), not tested
            return try {
                val assetFileDescriptor = createAssetFileDescriptor()
                if (assetFileDescriptor != null) {
                    AssetFileDescriptor.AutoCloseOutputStream(assetFileDescriptor)
                } else {
                    val parcelFileDescriptor = createParcelFileDescriptor()
                    if (parcelFileDescriptor != null) {
                        ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
                    } else {
                        null
                    }
                }
            } catch (e: Throwable) {
                null
            }
        }
    }
}

一旦您拥有了OutputStream,其余部分与其他答案类似。更多关于sink/sourceemit/flush的内容:
// create Request
val request = Request.Builder()
            .method("GET", null)
            .url(url)
            .build()

// API call function
fun apiCall(): Response? {
    return try {
        client.newCall(request).execute()
    } catch (error: Throwable) {
        null
    }
}

// execute API call
var response: Response? = apiCall()
// your retry logic if request failed (response==null)

// if failed, return
if (response == null || !response.isSuccessful) {
    return
}

// response.body
val body: ResponseBody? = response!!.body
if (body == null) {
    response.closeQuietly()
    return
}

// outputStream
val outputStream = destinationUri.toOutputStream(appContext)
if (outputStream == null) {
    response.closeQuietly() // calls body.close
    return
}
val bufferedSink: BufferedSink = outputStream!!.sink().buffer()
val outputBuffer: Buffer = bufferedSink.buffer

// inputStream
val bufferedSource = body!!.source()
val contentLength = body.contentLength()

// write
var totalBytesRead: Long = 0
var toBeFlushedBytesRead: Long = 0
val BUFFER_SIZE = 8 * 1024L // KB
val FLUSH_THRESHOLD = 200 * 1024L // KB
var bytesRead: Long = bufferedSource.read(outputBuffer, BUFFER_SIZE)
var lastProgress: Int = -1
while (bytesRead != -1L) {
    // emit/flush
    totalBytesRead += bytesRead
    toBeFlushedBytesRead += bytesRead
    bufferedSink.emitCompleteSegments()
    if (toBeFlushedBytesRead >= FLUSH_THRESHOLD) {
        toBeFlushedBytesRead = 0L
        bufferedSink.flush()
    }

    // write
    bytesRead = bufferedSource.read(outputBuffer, BUFFER_SIZE)

    // progress
    if (contentLength != -1L) {
        val progress = (totalBytesRead * 100 / contentLength).toInt()
        if (progress != lastProgress) {
            lastProgress = progress
            // update UI (using Flow/Observable/Callback)
        }
    }
}

bufferedSink.flush()

// close resources
outputStream.closeQuietly()
bufferedSink.closeQuietly()
bufferedSource.closeQuietly()
body.closeQuietly()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接