使用OKHTTP跟踪分段文件上传的进度

39

我正在尝试实现一个进度条来指示多部分文件上传的进度。

我从这个答案的评论中了解到 - https://dev59.com/KGAf5IYBdhLWcg3wyVJ1#24285633,我必须包装传递给RequestBody的Sink并提供一个回调函数来跟踪移动的字节数。

我已创建了一个自定义的RequestBody,并使用CustomSink类包装了sink,但是通过调试我可以看到字节是由RealBufferedSink ln 44 写入的,而自定义的sink写方法仅运行一次,无法让我跟踪移动的字节数。

    private class CustomRequestBody extends RequestBody {

    MediaType contentType;
    byte[] content;

    private CustomRequestBody(final MediaType contentType, final byte[] content) {
        this.contentType = contentType;
        this.content = content;
    }

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

    @Override
    public long contentLength() {
        return content.length;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        CustomSink customSink = new CustomSink(sink);
        customSink.write(content);

    }
}


private class CustomSink implements BufferedSink {

    private static final String TAG = "CUSTOM_SINK";

    BufferedSink bufferedSink;

    private CustomSink(BufferedSink bufferedSink) {
        this.bufferedSink = bufferedSink;
    }

    @Override
    public void write(Buffer source, long byteCount) throws IOException {
        Log.d(TAG, "source size: " + source.size() + " bytecount" + byteCount);
        bufferedSink.write(source, byteCount);
    }

    @Override
    public void flush() throws IOException {
        bufferedSink.flush();
    }

    @Override
    public Timeout timeout() {
        return bufferedSink.timeout();
    }

    @Override
    public void close() throws IOException {
        bufferedSink.close();
    }

    @Override
    public Buffer buffer() {
        return bufferedSink.buffer();
    }

    @Override
    public BufferedSink write(ByteString byteString) throws IOException {
        return bufferedSink.write(byteString);
    }

    @Override
    public BufferedSink write(byte[] source) throws IOException {
        return bufferedSink.write(source);
    }

    @Override
    public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
        return bufferedSink.write(source, offset, byteCount);
    }

    @Override
    public long writeAll(Source source) throws IOException {
        return bufferedSink.writeAll(source);
    }

    @Override
    public BufferedSink writeUtf8(String string) throws IOException {
        return bufferedSink.writeUtf8(string);
    }

    @Override
    public BufferedSink writeString(String string, Charset charset) throws IOException {
        return bufferedSink.writeString(string, charset);
    }

    @Override
    public BufferedSink writeByte(int b) throws IOException {
        return bufferedSink.writeByte(b);
    }

    @Override
    public BufferedSink writeShort(int s) throws IOException {
        return bufferedSink.writeShort(s);
    }

    @Override
    public BufferedSink writeShortLe(int s) throws IOException {
        return bufferedSink.writeShortLe(s);
    }

    @Override
    public BufferedSink writeInt(int i) throws IOException {
        return bufferedSink.writeInt(i);
    }

    @Override
    public BufferedSink writeIntLe(int i) throws IOException {
        return bufferedSink.writeIntLe(i);
    }

    @Override
    public BufferedSink writeLong(long v) throws IOException {
        return bufferedSink.writeLong(v);
    }

    @Override
    public BufferedSink writeLongLe(long v) throws IOException {
        return bufferedSink.writeLongLe(v);
    }

    @Override
    public BufferedSink emitCompleteSegments() throws IOException {
        return bufferedSink.emitCompleteSegments();
    }

    @Override
    public OutputStream outputStream() {
        return bufferedSink.outputStream();
    }
}

有人有一个关于我如何处理这个的例子吗?

5个回答

70

你需要创建一个自定义的RequestBody并重写writeTo方法,在那里你必须将你的文件分段发送到下沉处理器中。非常重要的是,每个片段后都要刷新处理器,否则进度条会很快填满,而文件实际上并没有被发送到网络上,因为内容将保留在处理器(作为缓冲区)中。

public class CountingFileRequestBody extends RequestBody {

    private static final int SEGMENT_SIZE = 2048; // okio.Segment.SIZE

    private final File file;
    private final ProgressListener listener;
    private final String contentType;

    public CountingFileRequestBody(File file, String contentType, ProgressListener listener) {
        this.file = file;
        this.contentType = contentType;
        this.listener = listener;
    }

    @Override
    public long contentLength() {
        return file.length();
    }

    @Override
    public MediaType contentType() {
        return MediaType.parse(contentType);
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        Source source = null;
        try {
            source = Okio.source(file);
            long total = 0;
            long read;

            while ((read = source.read(sink.buffer(), SEGMENT_SIZE)) != -1) {
                total += read;
                sink.flush();
                this.listener.transferred(total);

            }
        } finally {
            Util.closeQuietly(source);
        }
    }

    public interface ProgressListener {
        void transferred(long num);
    }

}

你可以在我的 gist 中找到一个完整的实现,支持在 AdapterView 中显示进度并取消上传:https://gist.github.com/eduardb/dd2dc530afd37108e1ac


1
如果在慢速网络连接上传小文件,则似乎无法正常工作。请参见https://github.com/square/okhttp/issues/1078。这种情况有解决方案吗? - coalmee
@Edy Bolos 有没有办法将它与 RxJava 和 Observable 结合使用? - Sree
回答你的问题:一切都可以包装在Observable中 :) 但我得让别人来做。我的建议是,也许可以使用BehaviorSubject来发出UploadsHandler中的进度值。 - Eduard B.
2
正如之前的评论者所说,即使在网络连接不佳的情况下,小文件也不会显示进度。这是因为网络接口发送缓冲区太大,可以完全容纳整个文件,而okHttp会报告上传完成100%。使用此解决方案可以解决此问题。https://gist.github.com/slightfoot/00a26683ea68856ceb50e26c7d8a47d0 - Simon
3
你们能否明确一下“小”的具体含义,是指10MB还是2MB或者其他大小? - kinsley kajiva
我想创建一个通用的自定义请求体,以便可以发送一个POJO或文件。因此,我将使用toRequestBody扩展函数。你能帮忙处理吗? - Sagar Nayak

9
  • 我们只需要创建一个自定义的RequestBody,无需实现自定义的BufferedSink。我们可以分配Okio缓冲区来从图像文件读取,并将此缓冲区连接到接收器(sink)。

请参见下面的createCustomRequestBody函数示例。

public static RequestBody createCustomRequestBody(final MediaType contentType, final File file) {
    return new RequestBody() {
        @Override public MediaType contentType() {
            return contentType;
        }
        @Override public long contentLength() {
            return file.length();
        }
        @Override public void writeTo(BufferedSink sink) throws IOException {
            Source source = null;
            try {
                source = Okio.source(file);
                //sink.writeAll(source);
                Buffer buf = new Buffer();
                Long remaining = contentLength();
                for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
                    sink.write(buf, readCount);
                    Log.d(TAG, "source size: " + contentLength() + " remaining bytes: " + (remaining -= readCount));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
}
  • to use -

    .addPart(
        Headers.of("Content-Disposition", "form-data; name=\"image\""),
        createCustomRequestBody(MediaType.parse("image/png"), new File("test.jpg")))
    .build()
    

由于被接受的答案中所解释的原因,这个不会按预期工作。 “每个段落后都要清空缓存非常重要,否则您的进度条将很快填满,而实际上文件并没有被发送到网络上。” - lasec0203

3
这个东西非常好用!
Gradle
dependencies {
  compile 'io.github.lizhangqu:coreprogress:1.0.2'
}

//wrap your original request body with progress
RequestBody requestBody = ProgressHelper.withProgress(body, new ProgressUIListener()....} 

这里提供完整的示例代码: https://github.com/lizhangqu/CoreProgress


2

对于Kotlin用户来说,这可能会很有帮助。我在java.io.File上编写了一个扩展函数。

import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source
import java.io.File

fun File.asProgressRequestBody(
    contentType: MediaType? = null,
    onProgress: (percent: Float) -> Unit
): RequestBody {
    return object : RequestBody() {
        override fun contentType() = contentType

        override fun contentLength() = length()

        override fun writeTo(sink: BufferedSink) {
            source().use { source ->
                var total: Long = 0
                var read: Long = 0
                while (source.read(sink.buffer, 2048).apply {
                        read = this
                    } != -1L) {
                    total += read
                    sink.flush()
                    onProgress.invoke(
                        (total.toFloat() / length())*100
                    )
                }
            }


        }
    }
}

你可以像以下这样在Retrofit和OkHttp中使用它来创建多部分请求体:
fun file2MultiPartBody(myFile: File,key:String,onProgress:(percent:Float)->Unit): MultipartBody.Part {
    val requestBody = myFile.asProgressRequestBody("application/octet-stream".toMediaType(),onProgress)
    return MultipartBody.Part.createFormData(key, myFile.name, requestBody)
}

这个可以工作,但是日志显示在上传时它会在上传之前先运行一次,然后再次运行导致进度条完成一次,然后再次完成(当它实际上上传文件时)。 - DevinM

0

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - Adrian Mole

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