最近一直為自己制作的相冊軟件(http://www.tonixsoft.com/ultraalbum/index.php?lang=chs)打開大文件時速度慢而郁悶,我以前的做法是先用TFileStream打開一個文件,然后在其中找到其中的數(shù)據(jù)段,把其中內(nèi)容復(fù)制給一個TMemoryStream,之所以要再將它復(fù)制給一個獨(dú)立的TMemoryStream是因?yàn)椋罄m(xù)處理的一個文件型數(shù)據(jù)庫組件必須接受一整個TStream,作為其存儲媒介,整個過程簡直慢得無法忍受。
之所以速度慢,是有兩方面的原因:
1。用TFileStream打開文件,操作系統(tǒng)在打開文件后會為文件生成內(nèi)存鏡像,文件一大,那么開辟空間以及內(nèi)存拷貝的工作就會變得極為緩慢。
2。將TFileStream中的一部分再復(fù)制給TMemoryStream,這個復(fù)制過程會開辟新的內(nèi)存再進(jìn)行復(fù)制,理所當(dāng)然內(nèi)存大了,復(fù)制時間也會變長。
我決心針對目前我所遇到的問題,再寫一個文件讀取類,目前就叫TFastFileStream吧,它必須從TStream繼承而來,這樣才能和其它組件方便地結(jié)合起來。
首先,要解決的是打開大文件慢的問題,對于這個,使用MapViewOfFile(),將文件直接當(dāng)作內(nèi)存鏡像來訪問就可以了,關(guān)于MapViewOfFile(),以及文件內(nèi)存鏡像,可以參考這篇文章: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;
最后,被做成鏡像的數(shù)據(jù)就存放在FMemory中了,然后,覆蓋TStream的Read()方法,當(dāng)外部需要取得數(shù)據(jù)時,讓它到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;
這段函數(shù)主要還是模仿TCustomMemoryStream中的同名方法來寫的,但是有一點(diǎn)比較奇怪,當(dāng)我使用Delphi自己的內(nèi)存拷貝函數(shù)Move()時,程序總是會訪問到非法地址,所以只好改為用API函數(shù)CopyMemory()了。
另外,需要實(shí)現(xiàn)的函數(shù)還有:
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;
這樣,一套完整的文件讀取機(jī)制就有了。
由于復(fù)雜度的關(guān)系,我沒有實(shí)現(xiàn)文件保存機(jī)制,感興趣的朋友請自己實(shí)現(xiàn)吧。
接下去,需要解決的是如何將目前用到的兩個Stream的復(fù)制操作進(jìn)行優(yōu)化,開始想到的辦法是,建立一個新的Stream類,它在從別的Stream復(fù)制出數(shù)據(jù)時,不新開內(nèi)存,而是將內(nèi)部的內(nèi)存指針指向源Stream內(nèi)的數(shù)據(jù)塊中的某一段,但是這樣一來,這個Stream類只有在源Stream的生存期內(nèi)才可用,關(guān)系變得似乎有些混亂了。
后來,忽然又想到另一個辦法,其實(shí)對于外部類來說(即我用到的文件型數(shù)據(jù)庫組件),它只是使用Read(),Seek()等方法來訪問數(shù)據(jù)的,那么我只要用一些欺騙的方法,讓內(nèi)部類返回給外部的只是其內(nèi)部數(shù)據(jù)中的某一段就可以了。
對于我的程序來說,在找到我要的數(shù)據(jù)的位置后,對其設(shè)置一個虛擬的數(shù)據(jù)范圍,在以后的外部訪問時,都返回該虛擬數(shù)據(jù)范圍內(nèi)的數(shù)據(jù)。這樣一來,只需要在原TFastFileStream的基礎(chǔ)上進(jìn)行一定的改造就可以了。
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;
好了,到此為止改造就結(jié)束了,最后換上這個新寫的FileStream類,一試,速度果然是驚人的快啊,原來打開一個近30MB的文件,使用兩個Stream類,需要約40秒,改成新的TFastFileStream后,只需要一個類就搞定,時間在5秒以內(nèi),哈哈,果然爽阿!
如果需要這個類的完整代碼,可以寫信聯(lián)系我:
tonyki[at]citiz.net
新聞熱點(diǎn)
疑難解答
圖片精選