如何使用FileChannels的transferFrom()方法来监控进度(JProgressBar)?

15

我需要一些关于 JProgressBar 组件的帮助。我的程序使用 java.nio FileChannels 将文件从一个位置复制到另一个位置。实际的复制方法是 transferFrom()

我现在有两个问题:

  1. 如何监控 FileChannels 的传输进度?所有我找到的教程都使用传统的 java.io InputStreams 并在循环遍历 inputstream 的同时增加进度 int。

  2. 我的复制方法(FileChannel 方法)封装在一个单独的方法中,该方法由迭代源和目标文件夹的其他方法调用,然后为每个文件调用 FileChannel 方法。

如何为整个复制机制实现 ProgressBar ?

好吧,我应该早点阅读 FAQ,所以我想我需要编辑我的初始帖子而不是评论答案,对吗?

这是我到目前为止所做的事情。 就像 jambjo 建议的那样(顺便说一句,谢谢),transferFrom() 方法现在被循环了。 BTW:有什么首选的块大小吗?或者像 EJP 说的那样,它取决于我的进度条的粒度?

这是我的代码片段:

while (position < size) {
 position += destination.transferFrom(source, position, chunkSize);
 current =  (position/size)*100;
 System.out.println(current);
}

很不幸,在循环内部'current'的值一直保持在0,我不知道为什么。 我是不是漏掉了什么?

再次感谢jambjo!非常感谢您的建议! 现在单个文件的进度监视已经运作良好,让我们来解决我的第二个问题。


我想,不,我必须监视不止一个文件,而是一大堆文件的进度。 我的主要复制方法遍历各个目录并通过调用实际传输方法来复制适当的文件。 因此,复制方法不会传输文件,它们只是为实际传输方法选择文件。


如果位置和大小是整数类型(int或long),则整数除法position/size将始终为0,如果size大于position。 (int)(100.*position/size)可能会达到您想要的效果。 - jarnbjo
3个回答

28

我意识到我在这里重新启动了一个非常古老的线程,但我今天在谷歌搜索中发现了它,所以...

如果你想要监控进度,最好像EJP建议的那样让系统处理块大小,这样它可以优化传输。 监视的方法是编写一个包装器类ReadableByteChannel,用于在每次调用其read方法时传递进度消息。 这里有一个例子:

package download.progress.example;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

public class DownloadProgressExample {
    public static void main( String[] args ) {
        new Downloader( "/tmp/foo.mp3", "http://example.com/bar.mp3" );
    }

    private interface RBCWrapperDelegate {
        // The RBCWrapperDelegate receives rbcProgressCallback() messages
        // from the read loop.  It is passed the progress as a percentage
        // if known, or -1.0 to indicate indeterminate progress.
        // 
        // This callback hangs the read loop so a smart implementation will
        // spend the least amount of time possible here before returning.
        // 
        // One possible implementation is to push the progress message
        // atomically onto a queue managed by a secondary thread then
        // wake that thread up.  The queue manager thread then updates
        // the user interface progress bar.  This lets the read loop
        // continue as fast as possible.
        public void rbcProgressCallback( RBCWrapper rbc, double progress );
    }

    private static final class Downloader implements RBCWrapperDelegate {
        public Downloader( String localPath, String remoteURL ) {
            FileOutputStream        fos;
            ReadableByteChannel     rbc;
            URL                     url;

            try {
                url = new URL( remoteURL );
                rbc = new RBCWrapper( Channels.newChannel( url.openStream() ), contentLength( url ), this );
                fos = new FileOutputStream( localPath );
                fos.getChannel().transferFrom( rbc, 0, Long.MAX_VALUE );
            } catch ( Exception e ) {
                System.err.println( "Uh oh: " + e.getMessage() );
            }
        }

        public void rbcProgressCallback( RBCWrapper rbc, double progress ) {
            System.out.println( String.format( "download progress %d bytes received, %.02f%%", rbc.getReadSoFar(), progress ) );
        }

        private int contentLength( URL url ) {
            HttpURLConnection           connection;
            int                         contentLength = -1;

            try {
                HttpURLConnection.setFollowRedirects( false );

                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod( "HEAD" );

                contentLength = connection.getContentLength();
            } catch ( Exception e ) { }

            return contentLength;
        }
    }

    private static final class RBCWrapper implements ReadableByteChannel {
        private RBCWrapperDelegate              delegate;
        private long                            expectedSize;
        private ReadableByteChannel             rbc;
        private long                            readSoFar;

        RBCWrapper( ReadableByteChannel rbc, long expectedSize, RBCWrapperDelegate delegate ) {
            this.delegate = delegate;
            this.expectedSize = expectedSize;
            this.rbc = rbc;
        }

        public void close() throws IOException { rbc.close(); }
        public long getReadSoFar() { return readSoFar; }
        public boolean isOpen() { return rbc.isOpen(); }

        public int read( ByteBuffer bb ) throws IOException {
            int                     n;
            double                  progress;

            if ( ( n = rbc.read( bb ) ) > 0 ) {
                readSoFar += n;
                progress = expectedSize > 0 ? (double) readSoFar / (double) expectedSize * 100.0 : -1.0;
                delegate.rbcProgressCallback( this, progress );
            }

            return n;
        }
    }
}

3
非常好。是的,这个帖子已经很旧了,但我通过谷歌搜索也发现了它,你的解决方案真的很有用。 - Al-Khwarizmi
2
这个方法可以工作,但请注意,如果您使用一个包装的ReadableByteChannel替换FileChannel作为FileChannel#transferFrom(...)中源通道的话,您将绕过API实现中的潜在优化。例如,Oracle的JVM使用FileChannel.map来访问源通道中的数据,而不是使用ReadableByteChannel.read - jarnbjo

7

无法监控单个transferFrom的进度,但由于您可以传递偏移量和长度参数,因此可以在其周围实现自己的循环,并在适当大小的数据块之间更新进度条。


1
是的,但这个解决方案会影响NIO API的性能,不是吗?在这种情况下,使用它没有意义。Peter可能应该使用IO API。 - Maxbester
@Maxbester:如果块的大小合理(较大的块更好),性能差异应该可以忽略不计。 - jarnbjo
你可以在自己的线程中执行“transferFrom”,然后从主(阻塞)线程监视它的进度... 这应该是两全其美的最佳选择。 - k_g

2

虽然这完全与首先使用transferTo()的目的毫无关系,该目的是尽可能将复制交给内核。你要么想这样做,要么想看到进展。你必须选择。至少你必须选择在进度显示中需要多少细节。


您是正确的,FileChannel 中转移方法的主要优势是在尽可能低的级别上进行复制,但是一次性复制整个文件应该比分段复制表现更好的原因并不充分。 - jarnbjo
2
是的,有的,这就是API存在的原因。如果内核不必重新调度进程、重新映射内存等,它可以进行更高效的复制。块越大越好。 - user207421

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