从 OKHTTP 下载二进制文件
Posted
技术标签:
【中文标题】从 OKHTTP 下载二进制文件【英文标题】:Download binary file from OKHTTP 【发布时间】:2014-11-11 15:14:21 【问题描述】:我在我的 android 应用程序中使用 OKHTTP 客户端进行联网。
This 示例显示如何上传二进制文件。我想知道如何使用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);
简单获取请求的当前代码是:
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();
【问题讨论】:
检查我编辑的答案我正在等待您的反馈 很高兴它对你有用:D 附带说明,如果请求中存在 ioexception 并且 HttpEngine 设置为重试,您的 InputStreamRequestBody 将无法工作。请参阅github.com/square/okhttp/blob/master/okhttp/src/main/java/com/… 第 202 行,在 while 循环中调用 writeTo。它会导致错误。 (我在从content://
输入流上传时偶然发现了这一点)
【参考方案1】:
对于它的价值,我会推荐 response.body().source()
和 okio(因为 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 跟踪的 unsafe-to-compose 标记和重置状态;调用者只是缓冲他们需要的东西。
在实现源代码时,您不必担心难以高效实现并返回 257 个可能值之一的单字节读取方法。
而且 source 有一个更强大的跳过方法:BufferedSource.skip(long) 不会过早返回。
【讨论】:
这种方法也是最快的,因为不会有任何不必要的响应数据副本。 如何处理这种方法的进展? @zella 只需使用 write(Source source, long byteCount) 和一个循环。当然,您需要从正文中获取 contentLength 以确保您读取的字节数正确。在每个循环中触发一个事件。 解决了!工作代码:while ((source.read(fileSink.buffer(), 2048)) != -1)
@IgorGanapolsky 确实如此,但如果您希望访问存档中的文件,则必须实现 Zip 放气器以获取所需的所有文件。【参考方案2】:
从 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();
【讨论】:
您在文档中提到的示例适用于具有简单数据或网页的请求,但对于下载二进制文件,我不需要缓冲输入流吗?这样我就可以获取缓冲区中的字节并将它们写入 Fileoutputstream 以保存在本地存储中? 用二进制解决方案编辑 OKHTTP 响应没有像 Apache HTTP 客户端那样的 getEntity() 方法。在问题中进行了编辑 用最终答案编辑;) 不要对二进制数据使用BufferedReader
;它将被不必要的字节到字符解码损坏。【参考方案3】:
下载的最佳选择(基于源代码“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方法 我无法获取 body.contentLength()...它总是 -1 不是肯定的,但您可能希望将flush()
和 close()
方法放在 finally
块中,以确保任何异常仍会刷新并关闭它们。【参考方案4】:
这就是我在每次块下载后发布下载进度时使用 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
做什么?
请写下缺少的方法
我无法获取 body.contentLength()...它总是 -1
while (read = (source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1)
应该是while ((read = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1)
如果你不在while
循环中调用sink.emit();
,你会使用大量的内存。【参考方案5】:
更好的解决方案是使用 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 @HRaval 这不是 okhttp 问题,这仅仅意味着服务器没有发送“Content-Length”标头 @Joolah 如何将该文件存储在内部存储中?【参考方案6】:基于 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()
【讨论】:
【参考方案7】:如果您尝试在最新的 Android 上将下载的字节写入 Shared Storage,您应该拥有 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
后,rest 与其他答案类似。更多关于sink/source和emit/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()
【讨论】:
【参考方案8】:更新 Kotlin 以匹配 KiddoUK 2015 年的回答 (https://***.com/a/29012988/413127):
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()
【讨论】:
以上是关于从 OKHTTP 下载二进制文件的主要内容,如果未能解决你的问题,请参考以下文章
JAVA - 从网络服务器下载二进制文件(例如 PDF)文件