用于进度报告的InputStream或Reader包装器

12

因此,我正在将文件数据提供给一个使用 Reader 的API,并且我希望有一种报告进度的方法。

似乎编写一个FilterInputStream实现来包装FileInputStream、跟踪已读取的字节数与总文件大小之间的差异并触发一些事件(或调用某个update()方法)来报告分数进度应该很简单。

(或者,它可以报告绝对已读取的字节数,其他人可以做数学计算——在其他流媒体情况下可能更加普遍有用。)

我知道我以前见过这个,甚至可能以前做过这个,但是我找不到这个代码,而且我懒得再写一遍。有人手头有这个吗?或者有人可以建议更好的方法吗?


一年多(和一点点)后...

我基于下面Adamski的答案实现了一个解决方案,并且它有效,但经过几个月的使用后,我不建议使用它。当你有很多更新时,触发/处理不必要的进度事件成为巨大的代价。基本计数机制很好,但最好让关心进度的人轮询,而不是推送给他们。

(如果您知道总大小,则可以尝试仅在每个> 1%的更改时触发事件或其他内容,但这并不值得麻烦。而且通常情况下,你不知道总大小。)

5个回答

14

这是一个相当基本的实现,当读取更多字节时会触发 PropertyChangeEvent。一些注意事项:

  • 该类不支持 markreset 操作,尽管这些操作很容易添加。
  • 该类不检查总读取字节数是否超过预期的最大字节数,尽管在显示进度时客户端代码总是可以处理这个情况。
  • 我没有测试过这段代码。

代码:

public class ProgressInputStream extends FilterInputStream {
    private final PropertyChangeSupport propertyChangeSupport;
    private final long maxNumBytes;
    private volatile long totalNumBytesRead;

    public ProgressInputStream(InputStream in, long maxNumBytes) {
        super(in);
        this.propertyChangeSupport = new PropertyChangeSupport(this);
        this.maxNumBytes = maxNumBytes;
    }

    public long getMaxNumBytes() {
        return maxNumBytes;
    }

    public long getTotalNumBytesRead() {
        return totalNumBytesRead;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }

    @Override
    public int read() throws IOException {
        int b = super.read();
        updateProgress(1);
        return b;
    }

    @Override
    public int read(byte[] b) throws IOException {
        return (int)updateProgress(super.read(b));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return (int)updateProgress(super.read(b, off, len));
    }

    @Override
    public long skip(long n) throws IOException {
        return updateProgress(super.skip(n));
    }

    @Override
    public void mark(int readlimit) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void reset() throws IOException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean markSupported() {
        return false;
    }

    private long updateProgress(long numBytesRead) {
        if (numBytesRead > 0) {
            long oldTotalNumBytesRead = this.totalNumBytesRead;
            this.totalNumBytesRead += numBytesRead;
            propertyChangeSupport.firePropertyChange("totalNumBytesRead", oldTotalNumBytesRead, this.totalNumBytesRead);
        }

        return numBytesRead;
    }
}

还不错。我可能会忘记使用skip()。 :) - David Moles
@David:老实说,实现跳过操作意味着引入令人讨厌的(int)转换,因此如果您知道您不需要它,我也会在这里抛出UnsupportedOperationException。 - Adamski
1
此外,您需要检查 super.read() 是否返回负数(即数据结束)。 - PhoneixS
7
我认为那里还有一个错误。read(byte[]b)的实现应该只是return read(b, 0, b.length);不需要进度更新)。在当前的实现中,调用read(byte[]b)会导致读取的字节数被计算两次,因为FilterInputStream#read(byte[]b)的实现直接调用了read(byte[]b,int off,int len) - Marco13

6

Guavacom.google.common.io包可以帮助你一些。下面的代码没有被编译和测试过,但应该能让你找到正确的方向。

long total = file1.length();
long progress = 0;
final OutputStream out = new FileOutputStream(file2);
boolean success = false;
try {
  ByteStreams.readBytes(Files.newInputStreamSupplier(file1),
      new ByteProcessor<Void>() {
        public boolean processBytes(byte[] buffer, int offset, int length)
            throws IOException {
          out.write(buffer, offset, length);
          progress += length;
          updateProgressBar((double) progress / total);
          // or only update it periodically, if you prefer
        }
        public Void getResult() {
          return null;
        }
      });
  success = true;
} finally {
  Closeables.close(out, !success);
}

这可能看起来是很多代码,但我认为这是最少的代码。注意其他回答这个问题的人并没有给出完整的代码示例,所以很难进行比较。

你知道用URL而不是文件完成这个任务的可靠方法吗?让我困扰的是找到URL内容长度的方法。如此所示,如果使用gzip压缩,则似乎无法知道流的大小。http://android-developers.blogspot.fr/2011/09/androids-http-clients.html - Snicolas
这个评论难道不应该附加在那个答案上吗? - Kevin Bourrillion

5
Adamski的答案是正确的,但存在一个小错误。覆盖的read(byte[] b)方法通过超类调用read(byte[] b, int off, int len)方法。
因此,每次读取操作都会调用updateProgress(long numBytesRead)两次,当总文件已读取完毕后,您将得到一个大小为文件大小两倍的numBytesRead

不覆盖read(byte[] b)方法可以解决这个问题。


6
这应该是对@Adamski答案的评论,而不是一个独立的答案,除非你在你的答案中放置了更正后的代码。 - PhoneixS

1

如果您正在构建GUI应用程序,那么总会有ProgressMonitorInputStream。如果没有涉及GUI,则按照您所描述的方式包装InputStream是易如反掌的,并且比在此处发布问题花费更少的时间。


是的,我看了那个。它是一个GUI应用程序,但它相当复杂,进度报告不仅仅是弹出一个标准的ProgressMonitor。包装InputStream所需的时间比发布问题要少,但我就永远不知道是否有人有更好的主意了。 - David Moles

0

补充一下@Kevin Bourillion的答案,这种技术也可以应用于网络内容(防止两次读取流:一次读取大小,一次读取内容):

        final HttpURLConnection httpURLConnection = (HttpURLConnection) new URL( url ).openConnection();
        InputSupplier< InputStream > supplier = new InputSupplier< InputStream >() {

            public InputStream getInput() throws IOException {
                return httpURLConnection.getInputStream();
            }
        };
        long total = httpURLConnection.getContentLength();
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ByteStreams.readBytes( supplier, new ProgressByteProcessor( bos, total ) );

其中 ProgressByteProcessor 是一个内部类:

public class ProgressByteProcessor implements ByteProcessor< Void > {

    private OutputStream bos;
    private long progress;
    private long total;

    public ProgressByteProcessor( OutputStream bos, long total ) {
        this.bos = bos;
        this.total = total;
    }

    public boolean processBytes( byte[] buffer, int offset, int length ) throws IOException {
        bos.write( buffer, offset, length );
        progress += length - offset;
        publishProgress( (float) progress / total );
        return true;
    }

    public Void getResult() {
        return null;
    }
}

PS:这不适用于使用gzipped压缩的urlconnections。 - Snicolas

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