FileOutputStream flush操作時(shí)有時(shí)無(wú)效的解決辦法

公司產(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è)備

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容