公司產(chǎn)品是基于android研發(fā)的機(jī)頂盒,有一個(gè)功能是可以將保存在sd卡中的日志文件復(fù)制到插入盒子的U盤(pán)中,以供傳閱。測(cè)試發(fā)現(xiàn),當(dāng)界面提示導(dǎo)出完畢后迅速拔掉U盤(pán),則有很大概率導(dǎo)出的文件大小為0kb(文件存在)。而當(dāng)界面提示導(dǎo)出完畢后等待約5~6秒鐘再拔出,日志文件大小正常。
該問(wèn)題給我的直觀感覺(jué)是目標(biāo)文件創(chuàng)建了,但是沒(méi)有寫(xiě)入內(nèi)容。然而通過(guò)調(diào)試信息發(fā)現(xiàn),經(jīng)過(guò)API寫(xiě)入操作后,目標(biāo)文件創(chuàng)建并且文件大小是正確的,但是快速拔出U盤(pán)時(shí),內(nèi)容還沒(méi)來(lái)得及真正寫(xiě)入到文件。
這個(gè)問(wèn)題最終解決了,問(wèn)題在于文件流寫(xiě)入與存儲(chǔ)設(shè)備的同步,特此記錄一下。
Java復(fù)制文件的4種方式
詳見(jiàn):java復(fù)制文件的4種方式
使用FileStreams復(fù)制
使用FileChannel復(fù)制
使用Commons IO復(fù)制
使用Java7的Files類復(fù)制
使用FileStreams復(fù)制
其中,產(chǎn)品原有代碼的日志復(fù)制采用最常見(jiàn)的FileStream方法,代碼如下:
private static void copyFileUsingFileStreams(File source, File dest)
throws IOException {
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(source);
output = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
output.flush();//注釋1:FileOutputStream.flush()實(shí)際是多余的
}
} finally {
input.close();
output.close();
}
}
對(duì)FileOutputStream.flush()方法的誤解
如上節(jié)代碼注釋1所示,在為FileOutputStream寫(xiě)入數(shù)據(jù)后調(diào)用了flush(),試圖將緩沖區(qū)中的字節(jié)全部寫(xiě)入文件。但查看flush()源碼發(fā)現(xiàn),F(xiàn)ileOutputStream并沒(méi)有實(shí)現(xiàn)這個(gè)方法,因而調(diào)用的實(shí)際是其父類OutputStream.flush(),但也只是一個(gè)空方法:
/**
* Flushes this stream. Implementations of this method should ensure that
* any buffered data is written out. This implementation does nothing.
*
* @throws IOException
* if an error occurs while flushing this stream.
*/
public void flush() throws IOException {
/* empty */
}
也就是說(shuō)FileOutputStream.flush()方法沒(méi)有任何作用,只有BufferedOutputStream這類實(shí)現(xiàn)了緩存區(qū)的讀寫(xiě)流的flush()才有作用。
可以為FileOutputStream接上BufferedOutputStream實(shí)現(xiàn)緩存區(qū)的讀寫(xiě):
FileOutputStream fos = new FileOutputStream("/sdcard/a.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(...);
bos.flush();
但是即使如此,快速拔掉U盤(pán)仍然會(huì)導(dǎo)致導(dǎo)出文件大小為0。走投無(wú)路了,因此嘗試其他的文件復(fù)制操作。
使用FileChannel復(fù)制
FileChannel是一個(gè)連接到文件的通道,可以通過(guò)文件通道讀寫(xiě)文件。據(jù)說(shuō)比文件流復(fù)制的速度更快。
private static void copyFileUsingFileChannels(File source, File dest) throws IOException {
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
} finally {
inputChannel.close();
outputChannel.close();
}
}
使用該方法復(fù)制文件到U盤(pán),并不能解決快速拔出U盤(pán)導(dǎo)致文件大小為0的問(wèn)題。但通過(guò)這個(gè)實(shí)踐,更讓我確定并不是FileStream或FileChannel代碼的問(wèn)題,而是數(shù)據(jù)因?yàn)槟撤N原因,從API調(diào)用寫(xiě)入方法到真正寫(xiě)入U(xiǎn)盤(pán)存在一個(gè)延遲。于是,我嘗試找FileChannel是否有關(guān)于“立即寫(xiě)入”之類的方法,發(fā)現(xiàn)了:
/**
* Requests that all updates to this channel are committed to the storage
* device.
* @param metadata
* {@code true} if the file metadata should be flushed in
* addition to the file content, {@code false} otherwise.
* @throws ClosedChannelException
* if this channel is already closed.
* @throws IOException
* if another I/O error occurs.
*/
public abstract void force(boolean metadata) throws IOException;
force()方法令該通道的所有修改提交到存儲(chǔ)設(shè)備。果然,在transferFrom()后之后增加該方法的調(diào)用,文件大小為0的問(wèn)題解決。文章JAVA NIO系列教程(七) FILECHANNEL 對(duì)force()方法的說(shuō)明是:
FileChannel.force()方法將通道里尚未寫(xiě)入磁盤(pán)的數(shù)據(jù)強(qiáng)制寫(xiě)到磁盤(pán)上。出于性能方面的考慮,操作系統(tǒng)會(huì)將數(shù)據(jù)緩存在內(nèi)存中,所以無(wú)法保證寫(xiě)入到FileChannel里的數(shù)據(jù)一定會(huì)即時(shí)寫(xiě)到磁盤(pán)上。要保證這一點(diǎn),需要調(diào)用force()方法。
FileOutputStream flush操作時(shí)有時(shí)無(wú)效的解決辦法
FileChannel有force()方法可以強(qiáng)制寫(xiě)入數(shù)據(jù),難道FileOutputStream.flush()不是這類的方法嗎?那難道文件流方式就沒(méi)辦法達(dá)到一樣的效果嗎?當(dāng)然可以!一篇文章解決了我的疑惑:FileOutputStream flush操作時(shí)有時(shí)無(wú)效的解決辦法
flush 的常規(guī)協(xié)定是:如果此輸出流的實(shí)現(xiàn)已經(jīng)緩沖了以前寫(xiě)入的任何字節(jié),則調(diào)用此方法指示應(yīng)將這些字節(jié)立即寫(xiě)入它們預(yù)期的目標(biāo)。如果此流的預(yù)期目標(biāo)是由基礎(chǔ)操作系統(tǒng)提供的一個(gè)抽象(如一個(gè)文件),則刷新此流只能保證將以前寫(xiě)入到流的字節(jié)傳遞給操作系統(tǒng)進(jìn)行寫(xiě)入,但不保證能將這些字節(jié)實(shí)際寫(xiě)入到物理設(shè)備(如磁盤(pán)驅(qū)動(dòng)器)
正如前文所說(shuō),F(xiàn)ileOutputStream的flush()實(shí)際沒(méi)有作用,該文章此處可能需要勘誤,但不影響我們的理解。
正如我猜測(cè)的,API調(diào)用了Stream或Channel的寫(xiě)入方法,只是寫(xiě)入給了操作系統(tǒng)(如android),但是操作系統(tǒng)什么時(shí)候?qū)懭氲轿募到y(tǒng)(如U盤(pán)),我們并不知道。從目前的現(xiàn)象看,至少有5~6秒的延遲。
該文章也給出了FileDescriptor.sync()來(lái)解決這個(gè)問(wèn)題:
FileDescriptor.sync()強(qiáng)制所有系統(tǒng)緩沖區(qū)與基礎(chǔ)設(shè)備同步。該方法在此 FileDescriptor 的所有修改數(shù)據(jù)和屬性都寫(xiě)入相關(guān)設(shè)備后返回。特別是,如果此 FileDescriptor 引用物理存儲(chǔ)介質(zhì),比如文件系統(tǒng)中的文件,則一直要等到將與此 FileDesecriptor 有關(guān)的緩沖區(qū)的所有內(nèi)存中修改副本寫(xiě)入物理介質(zhì)中,sync 方法才會(huì)返回。 sync 方法由要求物理存儲(chǔ)(比例文件)處于某種已知狀態(tài)下的代碼使用。例如,提供簡(jiǎn)單事務(wù)處理設(shè)施的類可以使用 sync 來(lái)確保某個(gè)文件所有由給定事務(wù)造成的更改都記錄在存儲(chǔ)介質(zhì)上。 sync 只影響此 FileDescriptor 的緩沖區(qū)下游。如果正通過(guò)應(yīng)用程序(例如,通過(guò)一個(gè) BufferedOutputStream 對(duì)象)實(shí)現(xiàn)內(nèi)存緩沖,那么必須在數(shù)據(jù)受 sync 影響之前將這些緩沖區(qū)刷新,并轉(zhuǎn)到 FileDescriptor 中(例如,通過(guò)調(diào)用 OutputStream.flush)。
在文件流上使用該方法:
private static void copyFileUsingFileStreamsSync(File source, File dest)
throws IOException {
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(source);
output = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
output.getFD().sync();//寫(xiě)入后同步
} finally {
input.close();
output.close();
}
}
事實(shí)證明,sync后的文件大小恢復(fù)正常。以下是復(fù)制文件后快速拔出U盤(pán)的測(cè)試,其中單數(shù)序號(hào)進(jìn)行了sync,偶數(shù)序號(hào)未sync。
總結(jié)
OutputStream的flush()沒(méi)有實(shí)際實(shí)現(xiàn),只有部分子類重寫(xiě)了該方法,如BufferedOutputStream
flush()的作用在于將以前寫(xiě)入到流的字節(jié)傳遞給操作系統(tǒng)進(jìn)行寫(xiě)入,但不保證操作系統(tǒng)馬上將這些字節(jié)實(shí)際寫(xiě)入到物理設(shè)備(如磁盤(pán)驅(qū)動(dòng)器)
文件流可以使用FileOutputStream.getFD().sync()將文件流的修改同步到存儲(chǔ)設(shè)備
文件通道可以使用FileChannel.force()令該通道的所有修改提交到存儲(chǔ)設(shè)備