最近一直為自己制作的相冊軟件(http://www.tonixsoft.com/ultraalbum/index.PHP?lang=chs)打開大文件時速度慢而郁悶,我以前的做法是先用TFileStream打開一個文件,然後在其中找到其中的數據段,把其中內容復制給一個TMemoryStream,之所以要再將它復制給一個獨立的TMemoryStream是因為,後續處理的一個文件型數據庫組件必須接受一整個TStream,作為其存儲媒介,整個過程簡直慢得無法忍受。
之所以速度慢,是有兩方面的原因:
1。用TFileStream打開文件,操作系統在打開文件後會為文件生成內存鏡像,文件一大,那麼開辟空間以及內存拷貝的工作就會變得極為緩慢。
2。將TFileStream中的一部分再復制給TMemoryStream,這個復制過程會開辟新的內存再進行復制,理所當然內存大了,復制時間也會變長。
我決心針對目前我所遇到的問題,再寫一個文件讀取類,目前就叫TFastFileStream吧,它必須從TStream繼承而來,這樣才能和其它組件方便地結合起來。
首先,要解決的是打開大文件慢的問題,對於這個,使用MapViewOfFile(),將文件直接當作內存鏡像來訪問就可以了,關於MapVIEwOfFile(),以及文件內存鏡像,可以參考這篇文章:http://www.vccode.com/file_show.PHP?id=2409
Delphi下建立文件鏡像的方法為:
constructor TFastFileStream.Create(const AFileName:String);
var
FileSizeHigh:LongWord;
begin
FFileHandle:=CreateFile(PChar(AFileName),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if FFileHandle=INVALID_HANDLE_VALUE then begin
raise FastFileStreamException.Create('Error when open file');
end;
FSize:=GetFileSize(FFileHandle,@FileSizeHigh);
if FSize=INVALID_FILE_SIZE then begin
raise FastFileStreamException.Create('Error when get file size');
end;
FMappingHandle:=CreateFileMapping(FFileHandle,nil,PAGE_READONLY,0,0,nil);
if FMappingHandle=0 then begin
raise FastFileStreamException.Create('Error when mapping file');
end;
FMemory:=MapVIEwOfFile(FMappingHandle,FILE_MAP_READ,0,0,0);
if FMemory=nil then begin
raise FastFileStreamException.Create('Error when map vIEw of file');
end;
end;
最後,被做成鏡像的數據就存放在FMemory中了,然後,覆蓋TStream的Read()方法,當外部需要取得數據時,讓它到FMemory中去取:
function TFastFileStream.Read(var Buffer;Count:LongInt):LongInt;
begin
if (FPosition >= 0) and (Count >= 0) then
begin
Result := FSize - FPosition;
if Result > 0 then
begin
if Result > Count then Result := Count;
//Move(Pointer(Longint(FMemory) + FPosition)^, Buffer, Result);
CopyMemory(Pointer(@Buffer),Pointer(LongInt(FMemory)+FPosition),Result);
Inc(FPosition, Result);
Exit;
end;
end;
Result := 0;
end;
這段函數主要還是模仿TCustomMemoryStream中的同名方法來寫的,但是有一點比較奇怪,當我使用Delphi自己的內存拷貝函數Move()時,程序總是會訪問到非法地址,所以只好改為用API函數CopyMemory()了。
另外,需要實現的函數還有:
function TFastFileStream.GetSize():Int64;
begin
result:=FSize;
end;
function TFastFileStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
case Ord(Origin) of
soFromBeginning: FPosition := Offset;
soFromCurrent: Inc(FPosition, Offset);
soFromEnd: FPosition := FSize + Offset;
end;
Result := FPosition;
end;
這樣,一套完整的文件讀取機制就有了。
由於復雜度的關系,我沒有實現文件保存機制,感興趣的朋友請自己實現吧。
接下去,需要解決的是如何將目前用到的兩個Stream的復制操作進行優化,開始想到的辦法是,建立一個新的Stream類,它在從別的Stream復制出數據時,不新開內存,而是將內部的內存指針指向源Stream內的數據塊中的某一段,但是這樣一來,這個Stream類只有在源Stream的生存期內才可用,關系變得似乎有些混亂了。
後來,忽然又想到另一個辦法,其實對於外部類來說(即我用到的文件型數據庫組件),它只是使用Read(),Seek()等方法來訪問數據的,那麼我只要用一些欺騙的方法,讓內部類返回給外部的只是其內部數據中的某一段就可以了。
對於我的程序來說,在找到我要的數據的位置後,對其設置一個虛擬的數據范圍,在以後的外部訪問時,都返回該虛擬數據范圍內的數據。這樣一來,只需要在原TFastFileStream的基礎上進行一定的改造就可以了。
procedure TFastFileStream.SetUseableMemory(const StartPos:Int64;const Size:Int64);
begin
FUseableStartPos:=StartPos;
FSize:=Size;
end;
function TFastFileStream.Read(var Buffer;Count:LongInt):LongInt;
begin
...
CopyMemory(Pointer(@Buffer),Pointer(LongInt(FMemory)+FUseableStartPos+FPosition),Result);
...
end;
好了,到此為止改造就結束了,最後換上這個新寫的FileStream類,一試,速度果然是驚人的快啊,原來打開一個近30MB的文件,使用兩個Stream類,需要約40秒,改成新的TFastFileStream後,只需要一個類就搞定,時間在5秒以內,哈哈,果然爽阿!
如果需要這個類的完整代碼,可以寫信聯系我:
[email protected]