盡管Java提供了java.io.File這樣一個操作文件的類,但并沒有提供一個復(fù)制文件的方法。
然而,當我們需要對磁盤上的文件進行處理的時候,這是一個很重要的方法。在這個時候我們往往不得不自己實現(xiàn)這樣一個完成文件復(fù)制操作的方法。下面將會介紹4種常見的文件復(fù)制的實現(xiàn),并比較下它們的性能。
使用FileStream
能找到的最常見經(jīng)典例子。從文件A的輸入流讀取一批字節(jié),寫到文件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);
}
}
如你所見,這種實現(xiàn)方式需要多次讀取數(shù)據(jù),再寫入將數(shù)據(jù)寫入,因此受限于我們提供的buffer的大小,他的效率有點一般。
使用FileChannel
Java NIO類庫里引入了一個叫transferFrom的方法,文檔里說這是一個會比FileStream方式更快的復(fù)制操作。
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)方法用于文件復(fù)制,如果項目里使用到了這個類庫,使用這個方法是個不錯的選擇。它的內(nèi)部也是使用Java NIO的FileChannel實現(xiàn)的。
private static void copyFileUsingApacheCommonsIO(File source, File dest) throws IOException {
FileUtils.copyFile(source, dest);
}
使用Java 7 的Files類
如果對Java 7 的使用有經(jīng)驗的話,那應(yīng)該接觸過Files這個工具類。
private static void copyFileUsingJava7Files(File source, File dest) throws IOException {
Files.copy(source.toPath(), dest.toPath());
}
性能
由于項目里沒用到Apache Common IO,Android僅支持Java 7的語法特性,因此我只測試了前兩種,數(shù)據(jù)如下:
(這里更正下,Android支持Java 7的語法特性,但核心類庫的支持并不完整,比如上文提及的
Files類、awt包等就屬于不支持的范圍。)
復(fù)制文件,大小2M
| buffer大小 | 耗時 |
|---|---|
| 512 | 455 |
| 1024 | 238 |
| 2048 | 131 |
| 4096 | 82 |
| 8192 | 46 |
| 16384 | 36 |
| 32768 | 30 |
| 65536 | 29 |
| 131072 | 26 |
| NIO方式 | 31 |
復(fù)制文件,大小4.5M
| buffer大小 | 耗時 |
|---|---|
| 512 | 937 |
| 1024 | 523 |
| 2048 | 315 |
| 4096 | 155 |
| 8192 | 104 |
| 16384 | 108 |
| 32768 | 83 |
| 65536 | 74 |
| 131072 | 79 |
| NIO方式 | 75 |
復(fù)制文件,大小8M
| buffer大小 | 耗時 |
|---|---|
| 512 | 1774 |
| 1024 | 942 |
| 2048 | 488 |
| 4096 | 311 |
| 8192 | 225 |
| 16384 | 169 |
| 32768 | 154 |
| 65536 | 129 |
| 131072 | 121 |
| NIO方式 | 108 |
復(fù)制文件,大小161M
| buffer大小 | 耗時 |
|---|---|
| 512 | 38561 |
| 1024 | 19994 |
| 2048 | 10747 |
| 4096 | 5500 |
| 8192 | 3857 |
| 16384 | 3327 |
| 32768 | 3201 |
| 65536 | 3288 |
| 131072 | 3281 |
| NIO方式 | 3266 |
結(jié)論
從數(shù)據(jù)上可以看出,使用FileStream的方式,復(fù)制的效率跟我們的buffer大小取值關(guān)系很大,這無疑加大了我們使用它進行文件復(fù)制的負擔。
而NIO的方式則不然,無論是小文件、還是大文件,它的效率都跟我們測試FileStream的最好水平相當!
因此,把FileStream這種老舊的實現(xiàn)方式從項目里挪走吧,是時候用上FileChannel了。
2016年06月16日 14:04 更新
附錄:FileChannel的內(nèi)部實現(xiàn)
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大小,繼續(xù)寫入,直到完全寫完。
需要注意到的是,buffer并不是我們平時使用的byte數(shù)組,而是MappedByteBuffer對象,這是java nio引入的文件內(nèi)存映射方案,讀寫性能很高。
Written with StackEdit.