无法在HTTP POST文件上传(Android)中获取进度

17

我正在开发一款Android应用,它可以让用户将文件上传到像Twitpic等服务中。POST上传不使用任何外部库,工作得很好。我的唯一问题是,由于所有上传操作都在我收到响应时完成,而不是在写入输出流时完成,因此无法获取任何进度。

以下是我所做的:

URL url = new URL(urlString); 
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 
conn.setDoInput(true); 
conn.setDoOutput(true); 
conn.setUseCaches(false); 
conn.setRequestMethod("POST"); 
conn.setRequestProperty("Connection", "Keep-Alive"); 
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); 
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());

然后我将表单数据写入dos,这在当前情况下并不重要。之后我会将文件数据本身写入(从“in”中读取,它是我想发送的数据的InputStream):

while ((bytesAvailable = in.available()) > 0)
{ 
 bufferSize = Math.min(bytesAvailable, maxBufferSize);
 byte[] buffer = new byte[bufferSize];
 bytesRead = in.read(buffer, 0, bufferSize);
 dos.write(buffer, 0, bytesRead);
} 

之后,我发送多部分表单数据来表示文件结束。然后,我关闭流:

in.close(); 
dos.flush(); 
dos.close(); 

一切都运行得非常完美,到目前为止没有任何问题。然而,我的问题是,无论文件大小如何,整个过程到这一点为止需要大约一到两秒钟时间。

似乎只有当我读取响应时上传才会发生:

DataInputStream inStream = new DataInputStream(conn.getInputStream());

上传的时间取决于文件的大小和网络连接速度,可能需要几秒钟或几分钟。 我的问题是: 1)为什么我把字节写入“dos”时上传没有发生? 2)如何获取进度以便在上传过程中显示进度对话框?

/编辑: 1)我在头部设置了Content-Length,这使问题略有改变,但并没有解决它: 现在整个内容在最后一个字节写入流之后上传。因此,这并没有改变无法抓取进度的情况,因为数据再次一次性写入。 2)我尝试了Apache HttpClient v4中的MultipartEntity。在那里,您根本没有OutputStream,因为在执行请求时会写入所有数据。因此,还没有抓取进度的方法。

是否有其他人有任何其他想法,如何在multipart / form上传中抓取进度?


也许这个答案会给你一些指引:https://dev59.com/iXVC5IYBdhLWcg3wjx5d#470047 - pableu
@pableu:我已经调查过了。问题是:Android SDK 中没有 RequestEntity 。它是 Apache HttpClient 3.1 的一部分,Android 不再使用它了。现在,我甚至尝试加载新的 4.0.1 库,但我看不到在这里获取进度的可能性。 还有其他想法吗? - Manuel
4个回答

20

最近几天我尝试了很多次,我认为我已经找到了回答最初问题的答案:

使用HttpURLConnection无法获取进度,因为Android中存在一个错误/异常行为: http://code.google.com/p/android/issues/detail?id=3164#c6

它将在Froyo发布后得到修复,但这不是等待的唯一选择...

所以,我找到的唯一其他选项是使用Apache HttpClient。 papleu链接的答案 是正确的,但它指的是通用Java。那里使用的类在Android中已不再可用(HttpClient 3.1曾经是SDK的一部分)。因此,您可以将HttpClient 4的库添加到项目中(特别是apache-mime4j-0.6.jar和httpmime-4.0.1.jar),然后结合第一个答案(Tuler)和最后一个答案(Hamy)使用:

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;

public class CountingMultipartEntity extends MultipartEntity {

    private final ProgressListener listener;

    public CountingMultipartEntity(final ProgressListener listener) {
        super();
        this.listener = listener;
    }

    public CountingMultipartEntity(final HttpMultipartMode mode, final ProgressListener listener) {
        super(mode);
        this.listener = listener;
    }

    public CountingMultipartEntity(HttpMultipartMode mode, final String boundary,
            final Charset charset, final ProgressListener listener) {
        super(mode, boundary, charset);
        this.listener = listener;
    }

    @Override
    public void writeTo(final OutputStream outstream) throws IOException {
        super.writeTo(new CountingOutputStream(outstream, this.listener));
    }

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

    public static class CountingOutputStream extends FilterOutputStream {

        private final ProgressListener listener;
        private long transferred;

        public CountingOutputStream(final OutputStream out,
                final ProgressListener listener) {
            super(out);
            this.listener = listener;
            this.transferred = 0;
        }


        public void write(byte[] b, int off, int len) throws IOException {
            out.write(b, off, len);
            this.transferred += len;
            this.listener.transferred(this.transferred);
        }

        public void write(int b) throws IOException {
            out.write(b);
            this.transferred++;
            this.listener.transferred(this.transferred);
        }
    }
}
这在使用 HttpClient 4 时有效,但缺点是我的apk现在的大小为235 kb(当我使用在我的问题中描述的多部分上传时,它只有90 kb),更糟糕的是,安装后的应用程序大小为735 kb(之前约为170 kb)。 真的很糟糕。只是为了在上传过程中获取进度,应用程序大小现在比以前大了4倍以上。

1
关于 .apk 和安装大小,你有没有在 SourceForge 上查看过 ProGuard?它可以将你的应用程序缩小到原来的大小甚至更小 :) - Tobias
1
这是一个非常棒的解决方案!非常感谢您的发布。 - Mark G.
您不再需要apache-mime4j-0.6.jar,因为从版本4.1 alpha 2开始,已经移除了对apache-mime4j-0.6.jar的依赖。 - CSchulz
它无法正确处理InputStreamBody。字节数与原始文件不匹配。 - CSchulz

1

试一下,它可行。

connection = (HttpURLConnection) url_stripped.openConnection();
    connection.setRequestMethod("PUT");
    String boundary = "---------------------------boundary";
    String tail = "\r\n--" + boundary + "--\r\n";
    connection.addRequestProperty("Content-Type", "image/jpeg");
    connection.setRequestProperty("Connection", "Keep-Alive");
    connection.setRequestProperty("Content-Length", ""
            + file.length());
    connection.setDoOutput(true);

    String metadataPart = "--"
            + boundary
            + "\r\n"
            + "Content-Disposition: form-data; name=\"metadata\"\r\n\r\n"
            + "" + "\r\n";

    String fileHeader1 = "--"
            + boundary
            + "\r\n"
            + "Content-Disposition: form-data; name=\"uploadfile\"; filename=\""
            + fileName + "\"\r\n"
            + "Content-Type: application/octet-stream\r\n"
            + "Content-Transfer-Encoding: binary\r\n";

    long fileLength = file.length() + tail.length();
    String fileHeader2 = "Content-length: " + fileLength + "\r\n";
    String fileHeader = fileHeader1 + fileHeader2 + "\r\n";
    String stringData = metadataPart + fileHeader;

    long requestLength = stringData.length() + fileLength;
    connection.setRequestProperty("Content-length", ""
            + requestLength);
    connection.setFixedLengthStreamingMode((int) requestLength);
    connection.connect();

    DataOutputStream out = new DataOutputStream(
            connection.getOutputStream());
    out.writeBytes(stringData);
    out.flush();

    int progress = 0;
    int bytesRead = 0;
    byte buf[] = new byte[1024];
    BufferedInputStream bufInput = new BufferedInputStream(
            new FileInputStream(file));
    while ((bytesRead = bufInput.read(buf)) != -1) {
        // write output
        out.write(buf, 0, bytesRead);
        out.flush();
        progress += bytesRead;
        // update progress bar
        publishProgress(progress);
    }

    // Write closing boundary and close stream
    out.writeBytes(tail);
    out.flush();
    out.close();

    // Get server response
    BufferedReader reader = new BufferedReader(
            new InputStreamReader(connection.getInputStream()));
    String line = "";
    StringBuilder builder = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        builder.append(line);
    }

参考资料:http://delimitry.blogspot.in/2011/08/android-upload-progress.html


0

可以从DefaultHttpClient获取进度。事实上,它保持在行中。

response = defaultHttpClient.execute( httpput, context );

直到数据完全发送并不意味着您无法抓取进度。也就是说,执行此行时HttpContext会发生变化,因此如果您通过不同的线程进行访问,可能会观察到其中的更改。您需要的是ConnAdapter。它包含嵌入式的HttpConnectionMetrics。

final AbstractClientConnAdapter connAdapter = (AbstractClientConnAdapter) context.getAttribute(ExecutionContext.HTTP_CONNECTION);
final HttpConnectionMetrics metrics = connAdapter.getMetrics();
int bytesSent = (int)metrics.getSentBytesCount();

如果您定期检查此内容,您将会观察到进展。一定要正确处理线程,并且所有的try/catch都要有保护。特别是,在Context不再有效(IllegalStateException)的情况下处理,这种情况发生在Put被取消或完成时。


0

请使用WatchedInputStream。

它非常好用,方便易操作。

  1. 使用WatchedInputStream来包装你的输入流。
  2. watchedStream.setListener

    watchedStream.setListener(new WatchedInputStream.Listener() {

    public void notify(int read) { // 做一些更新UI的事情。
    } }


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