尽管Java提供了java.io.File
这样一个操作文件的类,但并没有提供一个复制文件的方法。
然而,当我们需要对磁盘上的文件进行处理的时候,这是一个很重要的方法。在这个时候我们往往不得不自己实现这样一个完成文件复制操作的方法。下面将会介绍4种常见的文件复制的实现,并比较下它们的性能。
使用FileStream
能找到的最常见经典例子。从文件A的输入流读取一批字节,写到文件B的输出流。
public static void copy(String from, String to, int bufferSize) {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(new File(from));
out = new FileOutputStream(new File(to));
byte[] buffer = new byte[bufferSize];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
} catch (Exception e) {
Log.w(TAG + ":copy", "error occur while copy", e);
} finally {
safelyClose(TAG + ":copy", in);
safelyClose(TAG + ":copy", out);
}
}
如你所见,这种实现方式需要多次读取数据,再写入将数据写入,因此受限于我们提供的buffer的大小,他的效率有点一般。
使用FileChannel
Java NIO类库里引入了一个叫transferFrom
的方法,文档里说这是一个会比FileStream
方式更快的复制操作。
public static void copyNio(String from, String to) {
FileChannel input = null;
FileChannel output = null;
try {
input = new FileInputStream(new File(from)).getChannel();
output = new FileOutputStream(new File(to)).getChannel();
output.transferFrom(input, 0, input.size());
} catch (Exception e) {
Log.w(TAG + "copyNio", "error occur while copy", e);
} finally {
safelyClose(TAG + "copyNio", input);
safelyClose(TAG + "copyNio", output);
}
}
使用Apache Commons IO
Appache Commons IO 提供了一个FileUtils.copyFile(File from, File to)
方法用于文件复制,如果项目里使用到了这个类库,使用这个方法是个不错的选择。它的内部也是使用Java NIO的FileChannel
实现的。
private static void copyFileUsingApacheCommonsIO(File source, File dest) throws IOException {
FileUtils.copyFile(source, dest);
}
使用Java 7 的Files类
如果对Java 7 的使用有经验的话,那应该接触过Files
这个工具类。
private static void copyFileUsingJava7Files(File source, File dest) throws IOException {
Files.copy(source.toPath(), dest.toPath());
}
性能
由于项目里没用到Apache Common IO
,Android仅支持Java 7
的语法特性,因此我只测试了前两种,数据如下:
(这里更正下,Android支持Java 7的语法特性,但核心类库的支持并不完整,比如上文提及的
Files
类、awt
包等就属于不支持的范围。)
复制文件,大小2M
buffer大小 | 耗时 |
---|---|
512 | 455 |
1024 | 238 |
2048 | 131 |
4096 | 82 |
8192 | 46 |
16384 | 36 |
32768 | 30 |
65536 | 29 |
131072 | 26 |
NIO方式 | 31 |
复制文件,大小4.5M
buffer大小 | 耗时 |
---|---|
512 | 937 |
1024 | 523 |
2048 | 315 |
4096 | 155 |
8192 | 104 |
16384 | 108 |
32768 | 83 |
65536 | 74 |
131072 | 79 |
NIO方式 | 75 |
复制文件,大小8M
buffer大小 | 耗时 |
---|---|
512 | 1774 |
1024 | 942 |
2048 | 488 |
4096 | 311 |
8192 | 225 |
16384 | 169 |
32768 | 154 |
65536 | 129 |
131072 | 121 |
NIO方式 | 108 |
复制文件,大小161M
buffer大小 | 耗时 |
---|---|
512 | 38561 |
1024 | 19994 |
2048 | 10747 |
4096 | 5500 |
8192 | 3857 |
16384 | 3327 |
32768 | 3201 |
65536 | 3288 |
131072 | 3281 |
NIO方式 | 3266 |
结论
从数据上可以看出,使用FileStream
的方式,复制的效率跟我们的buffer大小取值关系很大,这无疑加大了我们使用它进行文件复制的负担。
而NIO的方式则不然,无论是小文件、还是大文件,它的效率都跟我们测试FileStream
的最好水平相当!
因此,把FileStream
这种老旧的实现方式从项目里挪走吧,是时候用上FileChannel
了。
2016年06月16日 14:04 更新
附录:FileChannel的内部实现
private long transferFromFileChannel(FileChannelImpl input, long start, long length) throws IOException {
if(!input.readable) {
throw new NonReadableChannelException();
} else {
Object lock = input.positionLock;
synchronized(lock) {
long position = input.position();
long total = Math.min(length, input.size() - position);
long remain = total;
long current = position;
long bufferSize;
while(remain > 0L) {
bufferSize = Math.min(remain, 8388608L);
MappedByteBuffer buffer = input.map(MapMode.READ_ONLY, current, bufferSize);
try {
long written = (long)this.write(buffer, start);
assert written > 0L;
current += written;
start += written;
remain -= written;
} catch (IOException e) {
if(remain != total) {
break;
}
throw e;
} finally {
unmap(buffer);
}
}
bufferSize = total - remain;
input.position(position + bufferSize);
return bufferSize;
}
}
}
大致就是从输入里映射一部分作为buffer,写到输出去,Buffer的大小最大为8388608,如果剩余的文件长度小于这个值,则用剩余文件长度的大小为buffer大小,继续写入,直到完全写完。
需要注意到的是,buffer并不是我们平时使用的byte数组,而是MappedByteBuffer
对象,这是java nio
引入的文件内存映射方案,读写性能很高。
Written with StackEdit.
网友评论