从 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)文件

使用 Java 从 Github 下载二进制文件

是否可以从 JSP 下载二进制文件?

我可以从 iTunes Connect 下载我自己的二进制文件吗?

如何下载iOS App的原始文件?

从 Apple App Store 下载的应用程序与从 XCode 加载的应用程序之间的二进制文件是不是存在差异?