delphi windows内存映射
<p>delphi windows内存映射</p><p><strong>使用内存映射文件读写大文件</strong></p>
<p>几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。使用字符串变量的方法不仅会加重内存的负担,而且会Unicode和ASCII码的转换会把你弄得焦头烂额。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,比I/O读写要快20倍,所谓I/O操作不是对外围设备直接进行操作,而是对设备与cpu连接的接口电路的操作。而映射文件的方法就是对磁盘直接进行操作。</p>
<p><strong>内存映射为什么速度快?</strong></p>
<p>使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。</p>
<p><strong>在多个进程之间共享数据</strong></p>
<p>如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。</p>
<p><strong>内存映射流程</strong></p>
<p>首先要通过<strong>CreateFile()</strong>来创建或<strong>打开一个文件内核对象</strong>,这个对象标识了磁盘上将要用作内存映射文件的文件。在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大的物理存储空间还需要通过<strong>CreateFileMapping()</strong>来<strong>创建一个文件映射内核对象</strong>以告诉系统文件的尺寸以及访问文件的方式。<br> CreateFileMapping()在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由<strong>MapViewOfFile()</strong>负责通过系统的管理而<strong>将文件映射对象的全部或部分映射到进程地址空间</strong>,实际上相当于加载文件中指定的数据到内存中。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过<strong>UnmapViewOfFile()</strong>完成从进程的地址空间<strong>撤消文件数据的映像</strong>、通过<strong>CloseHandle()关闭前面创建的文件映射对象和文件对象</strong>。</p>
<p><strong>内存映射处理巨型文件</strong></p>
<p>十几GB乃至几十GB容量的巨型文件,而一个32位进程所拥有的虚拟地址空间只有2^32 = 4GB,显然不能一次将文件映像全部映射进来。对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。这需要对上面的一般流程进行适当的更改:<br> 1)映射从文件开头的映像;<br> 2)对该映像进行访问;<br> 3)取消此映像;<br> 4)映射一个从文件中的一个更深的位移开始的新映像;<br> 5)重复步骤2,直到访问完全部的文件数据。</p>
<p><strong>处理大文件例子</strong></p>
<p>平时很少使用大文件的内存映射,碰巧遇到了这样的要求,所以把过程记录下来,当给各位一个引子吧,因为应用不算复杂,可能有考虑不到的地方,欢迎交流。<br>对于一些小文件,用普通的文件流就可以很好的解决,可是对于超大文件,比如2G或者更多,文件流就不行了,所以要使用API的内存映射的相关方法,即使是内存映射,也不能一次映射全部文件的大小,所以必须采取分块映射,每次处理一小部分。<br>先来看几个函数<br>CreateFile :打开文件<br>GetFileSize : 获取文件尺寸<br>CreateFileMapping :创建映射<br>MapViewOfFile :映射文件<br>看MapViewOfFile的帮助,他的最后两个参数都需要是页面粒度的整数倍,一般机器的页面粒度为64k(65536字节),而我们实际操作中,一般都不是这样规矩的,任意位置,任意长度都是可能的,所以就要做一些处理。<br>本例的任务是从一个长度列表中(FInfoList),依次读取长度值,然后到另外一个大文件(FSourceFileName)中去顺序读取指定长度的数据,如果是小文件,这个就好办了,一次读到文件流中,然后依次读取就是了,大数对于大文件,就需要不断改变映射的位置,来取得我们想要的数据。<br>本例中显示先通过GetSystemInfo来获取页面粒度,然后以10倍的页面粒度为一个映射数据块,在for循环中,会判断已经读取的长度(totallen)加上即将读取的长度,是否在本次映射范围之内(10倍的页面粒度),如果在就继续读取,如果超出了,就要记下剩下的数据,然后重新映射下一块内存,并将记录下的剩余数据合并到新读取的数据中,有点绕啊(可能是我的想法太绕了),下面列出代码。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">procedure TGetDataThread.DoGetData;
var
FFile_Handle:THandle;
FFile_Map:THandle;
list:TStringList;
p:PChar;
i,interval:Integer;
begin
try
totallen := 0;
offset := 0;
tstream := TMemoryStream.Create;
stream := TMemoryStream.Create;
list := TStringList.Create;
//获取系统信息
GetSystemInfo(sysinfo);
//页面分配粒度大小
blocksize := sysinfo.dwAllocationGranularity;
//打开文件
FFile_Handle := CreateFile(PChar(FSourceFileName),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if FFile_Handle = INVALID_HANDLE_VALUE then Exit;
//获取文件尺寸
filesize := GetFileSize(FFile_Handle,nil);
//创建映射
FFile_Map := CreateFileMapping(FFile_Handle,nil,PAGE_READONLY,0,0,nil);
if FFile_Map = 0 then Exit;
//此处我们已10倍blocksize为一个数据块来映射,如果文件尺寸小于10倍blocksize,则直接映射整个文件长度
if filesize div blocksize > 10 then
readlen := 10*blocksize
else
readlen := filesize;
for i := 0 to FInfoList.Count - 1 do
begin
list.Delimiter := ':';
list.DelimitedText := FInfoList.Strings;
//取得长度,我这里做了解析,因为我存储的信息为 a:b:c 这种类型,所以以:号分隔
len := StrToInt(list.Strings);
interval := StrToInt(list.Strings);
if (i = 0) or (totallen+len >=readlen) then
begin
//如果已读取的长度加上即将要读取的长度大于 10倍blocksize,那么我们要保留之前映射末尾的内容,以便和新映射的内容合并
if i > 0 then
begin
offset := offset + readlen;
//写入临时流
tstream.Write(p^,readlen-totallen);
tstream.Position := 0;
end;
//如果未读取的数据长度已经不够一个分配粒度,那么就直接映射剩下的长度
if filesize-offset < blocksize then
readlen := filesize-offset;
//映射,p是指向映射区域的指针
//注意这里第三个参数,一直设为0,这个值要根据实际情况设置
p := PChar(MapViewOfFile(FFile_Map,FILE_MAP_READ,0,offset,readlen));
end;
//如果临时流中有数据,需要合并
if tstream.Size > 0 then
begin
//把临时流数据copy过来
stream.CopyFrom(tstream,tstream.Size);
//然后在末尾写入新数据,合并完成
stream.Write(p^,len-tstream.Size);
totallen := len-tstream.Size;
//移动指针的位置,指向下一个数据的开始
Inc(p,len-tstream.Size);
tstream.Clear;
end
else
begin
stream.Write(p^,len);
totallen := totallen + len;
Inc(p,len);
end;
stream.Position := 0;
//将流保存成文件
stream.SaveToFile(IntToStr(i)+'.txt');
stream.Clear;
end;
finally
stream.Free;
tstream.Free;
CloseHandle(FFile_Handle);
CloseHandle(FFile_Map);
end;
end;
</pre>
</div>
<p> <strong>如何将一整个文件读入内存,文件大小有64M</strong></p>
<p></p>
<div class="cnblogs_code">
<pre>function FastReadFile(FileName: <span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">): Integer;
</span><span style="color: rgba(0, 0, 255, 1)">const</span><span style="color: rgba(0, 0, 0, 1)">
PAGE_SIZE </span>= <span style="color: rgba(128, 0, 128, 1)">4</span> * <span style="color: rgba(128, 0, 128, 1)">1024</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">映射块大小不易过大,尽量以4k对齐</span>
<span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)">
hFile: THandle;
szHigh,szLow: DWORD;
szFile,ps: Int64;
hMap: THandle;
hData: Pointer;
dwSize: Cardinal;
begin
Result :</span>= -<span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">;
hFile :</span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
hMap :</span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
hData :</span>=<span style="color: rgba(0, 0, 0, 1)"> nil;
szHigh :</span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">try</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">打开已存在的文件,获得文件句柄</span>
hFile :=<span style="color: rgba(0, 0, 0, 1)"> CreateFile(PChar(FileName),GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ,
nil,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,</span><span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">if</span> hFile = <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> then
begin
Result :</span>=<span style="color: rgba(0, 0, 0, 1)"> GetLastError;
Exit;
end;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">获取文件大小</span>
hMap := <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
hData :</span>=<span style="color: rgba(0, 0, 0, 1)"> nil;
szHigh :</span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
szLow :</span>=<span style="color: rgba(0, 0, 0, 1)"> GetFileSize(hFile,@szHigh);
szFile :</span>= szLow or (szHigh shl <span style="color: rgba(128, 0, 128, 1)">32</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">创建映射句柄</span>
hMap :=<span style="color: rgba(0, 0, 0, 1)"> CreateFileMapping(hFile, nil, PAGE_READWRITE, szHigh, szLow, nil);
</span><span style="color: rgba(0, 0, 255, 1)">if</span> hMap = <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> then
begin
Result :</span>=<span style="color: rgba(0, 0, 0, 1)"> GetLastError;
Exit;
end;
ps :</span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">文件可能比较大,分块进行映射</span>
<span style="color: rgba(0, 0, 255, 1)">while</span> ps < szFile <span style="color: rgba(0, 0, 255, 1)">do</span><span style="color: rgba(0, 0, 0, 1)">
begin
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">计算映射大小及位置</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> szFile - ps ><span style="color: rgba(0, 0, 0, 1)"> PAGE_SIZE then
dwSize :</span>=<span style="color: rgba(0, 0, 0, 1)"> PAGE_SIZE
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">
dwSize :</span>= szFile -<span style="color: rgba(0, 0, 0, 1)"> ps;
szLow:</span>=<span style="color: rgba(0, 0, 0, 1)"> ps and $FFFFFFFF;
szHigh :</span>= ps shr <span style="color: rgba(128, 0, 128, 1)">32</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">进行映射</span>
hData :=<span style="color: rgba(0, 0, 0, 1)"> MapViewOfFile(hMap,FILE_MAP_ALL_ACCESS,szHigh,szLow,dwSize);
</span><span style="color: rgba(0, 0, 255, 1)">if</span> hData =<span style="color: rgba(0, 0, 0, 1)"> nil then
Break;
</span><span style="color: rgba(0, 0, 255, 1)">try</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">此时文件偏移ps处的数据通过hData即可读取到,块大小为dwSize
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">以下加上你读取的代码,可以做一个回调函数
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">比如你要当前位置的数据(取文件数)拷到指定内存处 CopyMemory(目标地址指针,hData,dwSize);
</span><span style="color: rgba(0, 128, 0, 1)">//
</span> <span style="color: rgba(0, 0, 255, 1)">finally</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">移动文件偏移位置</span>
ps := ps +<span style="color: rgba(0, 0, 0, 1)"> dwSize;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">释放映射块</span>
<span style="color: rgba(0, 0, 0, 1)"> UnmapViewOfFile(hData);
hData :</span>=<span style="color: rgba(0, 0, 0, 1)"> nil;
end;
end;
</span><span style="color: rgba(0, 0, 255, 1)">finally</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">释放必要资源</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> hData <><span style="color: rgba(0, 0, 0, 1)"> nil then
UnmapViewOfFile(hData);
</span><span style="color: rgba(0, 0, 255, 1)">if</span> hMap <> <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> then
CloseHandle(hMap);
</span><span style="color: rgba(0, 0, 255, 1)">if</span> hFile <> <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> then
CloseHandle(hFile);
end;
end;</span></pre>
</div>
<p>Delphi内存映射文件例子</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">unitFileMap;
interface
uses
Windows,Messages,SysUtils,Classes,Graphics,Controls,Forms,StdCtrls,Dialogs;
type
TFileMap=class(TComponent)
private
FMapHandle:THandle; //内存映射文件句柄
FMutexHandle:THandle; //互斥句柄
FMapName:string; //内存映射对象
FSynchMessage:string; //同步消息
FMapStrings:TStringList; //存储映射文件信息
FSize:DWord; //映射文件大小
FMessageID:DWord; //注册的消息号
FMapPointer:PChar; //映射文件的数据区指针
FLocked:Boolean; //锁定
FIsMapOpen:Boolean; //文件是否打开
FExistsAlready:Boolean; //是否已经建立过映射文件
FReading:Boolean; //是否正在读取内存文件数据
FAutoSynch:Boolean; //是否同步
FOnChange:TNotifyEvent; //当内存数据区内容改变时
FFormHandle:Hwnd; //存储本窗口的窗口句柄
FPNewWndHandler:Pointer;
FPOldWndHandler:Pointer;
procedureSetMapName(Value:string);
procedureSetMapStrings(Value:TStringList);
procedureSetSize(Value:DWord);
procedureSetAutoSynch(Value:Boolean);
procedureEnterCriticalSection;
procedureLeaveCriticalSection;
procedureMapStringsChange(Sender:TObject);
procedureNewWndProc(varFMessage:TMessage);
public
constructorCreate(AOwner:TComponent);override;
destructorDestroy;override;
procedureOpenMap;
procedureCloseMap;
procedureReadMap;
procedureWriteMap;
propertyExistsAlready:BooleanreadFExistsAlready;
propertyIsMapOpen:BooleanreadFIsMapOpen;
published
propertyMaxSize:DWordreadFSizewriteSetSize;
propertyAutoSynchronize:BooleanreadFAutoSynchwriteSetAutoSynch;
propertyMapName:stringreadFMapNamewriteSetMapName;
propertyMapStrings:TStringListreadFMapStringswriteSetMapStrings;
propertyOnChange:TNotifyEventreadFOnChangewriteFOnChange;
end;
implementation
constructorTFileMap.Create(AOwner:TComponent);
begin
inheritedCreate(AOwner);
FAutoSynch:=True;
FSize:=4096;
FReading:=False;
FMapStrings:=TStringList.Create;
FMapStrings.OnChange:=MapStringsChange;
FMapName:='Unique&Commonname';
FSynchMessage:=FMapName+'Synch-Now';
ifAOwnerisTFormthen
begin
FFormHandle:=(AOwnerasTForm).Handle;
FPOldWndHandler:=Ptr(GetWindowLong(FFormHandle,GWL_wNDPROC));
FPNewWndHandler:=MakeObjectInstance(NewWndProc);
ifFPNewWndHandler=nilthen
raiseException.Create('超出资源');
SetWindowLong(FFormHandle,GWL_WNDPROC,Longint(FPNewWndHandler));
end
elseraiseException.Create('组件的所有者应该是TForm');
end;
destructorTFileMap.Destroy;
begin
CloseMap;
SetWindowLong(FFormHandle,GWL_WNDPROC,Longint(FPOldWndHandler));
ifFPNewWndHandler<>nilthen
FreeObjectInstance(FPNewWndHandler);
FMapStrings.Free;
FMapStrings:=nil;
inheriteddestroy;
end;
procedureTFileMap.OpenMap;
var
TempMessage:arrayofChar;
begin
if(FMapHandle=0)and(FMapPointer=nil)then
begin
FExistsAlready:=False;
FMapHandle:=CreateFileMapping($FFFFFFFF,nil,PAGE_READWRITE,0,FSize,PChar(FMapName));
if(FMapHandle=INVALID_HANDLE_VALUE)or(FMapHandle=0)then
raiseException.Create('创建文件映射对象失败!')
else
begin
if(FMapHandle<>0)and(GetLastError=ERROR_ALREADY_EXISTS)then
FExistsAlready:=True;//如果已经建立的话,就设它为TRUE;
FMapPointer:=MapViewOfFile(FMapHandle,FILE_MAP_ALL_ACCESS,0,0,0);
ifFMapPointer=nilthen
raiseException.Create('映射文件的视图到进程的地址空间失败')
else
begin
StrPCopy(TempMessage,FSynchMessage);
FMessageID:=RegisterWindowMessage(TempMessage);
ifFMessageID=0then
raiseException.Create('注册消息失败')
end
end;
FMutexHandle:=Windows.CreateMutex(nil,False,PChar(FMapName+'.Mtx'));
ifFMutexHandle=0then
raiseException.Create('创建互斥对象失败');
FIsMapOpen:=True;
ifFExistsAlreadythen//判断内存文件映射是否已打开
ReadMap
else
WriteMap;
end;
end;
procedureTFileMap.CloseMap;
begin
ifFIsMapOpenthen
begin
ifFMutexHandle<>0then
begin
CloseHandle(FMutexHandle);
FMutexHandle:=0;
end;
ifFMapPointer<>nilthen
begin
UnMapViewOfFile(FMapPointer);
FMapPointer:=nil;
end;
ifFMapHandle<>0then
begin
CloseHandle(FMapHandle);
FMapHandle:=0;
end;
FIsMapOpen:=False;
end;
end;
procedureTFileMap.ReadMap;
begin
FReading:=True;
if(FMapPointer<>nil)thenFMapStrings.SetText(FMapPointer);
end;
procedureTFileMap.WriteMap;
var
StringsPointer:PChar;
HandleCounter:integer;
SendToHandle:HWnd;
begin
ifFMapPointer<>nilthen
begin
StringsPointer:=FMapStrings.GetText;
EnterCriticalSection;
ifStrLen(StringsPointer)+1<=FSize
thenSystem.Move(StringsPointer^,FMapPointer^,StrLen(StringsPointer)+1)
else
raiseException.Create('写字符串失败,字符串太大!');
LeaveCriticalSection;
SendMessage(HWND_BROADCAST,FMessageID,FFormHandle,0);
StrDispose(StringsPointer);
end;
end;
procedureTFileMap.MapStringsChange(Sender:TObject);
begin
ifFReadingandAssigned(FOnChange)then
FOnChange(Self)
elseif(notFReading)andFIsMapOpenandFAutoSynchthen
WriteMap;
end;
procedureTFileMap.SetMapName(Value:string);
begin
if(FMapName<>Value)and(FMapHandle=0)and(Length(Value)<246)then
begin
FMapName:=Value;
FSynchMessage:=FMapName+'Synch-Now';
end;
end;
procedureTFileMap.SetMapStrings(Value:TStringList);
begin
ifValue.Text<>FMapStrings.Textthen
begin
ifLength(Value.Text)<=FSizethen
FMapStrings.Assign(Value)
else
raiseException.Create('写入值太大');
end;
end;
procedureTFileMap.SetSize(Value:DWord);
var
StringsPointer:PChar;
begin
if(FSize<>Value)and(FMapHandle=0)then
begin
StringsPointer:=FMapStrings.GetText;
if(Value<StrLen(StringsPointer)+1)then
FSize:=StrLen(StringsPointer)+1
elseFSize:=Value;
ifFSize<32thenFSize:=32;
StrDispose(StringsPointer);
end;
end;
procedureTFileMap.SetAutoSynch(Value:Boolean);
begin
ifFAutoSynch<>Valuethen
begin
FAutoSynch:=Value;
ifFAutoSynchandFIsMapOpenthenWriteMap;
end;
end;
procedureTFileMap.EnterCriticalSection;
begin
if (FMutexHandle<>0)andnotFLockedthen
begin
FLocked:=(WaitForSingleObject(FMutexHandle,INFINITE)=WAIT_OBJECT_0);
end;
end;
procedureTFileMap.LeaveCriticalSection;
begin
if(FMutexHandle<>0)andFLockedthen
begin
ReleaseMutex(FMutexHandle);
FLocked:=False;
end;
end;
//消息捕获过程
procedureTFileMap.NewWndProc(varFMessage:TMessage);
begin
withFMessagedo
begin
ifFIsMapOpen
if(Msg=FMessageID)and(WParam<>FFormHandle)then
ReadMap;
Result:=CallWindowProc(FPOldWndHandler,FFormHandle,Msg,wParam,lParam);
end;
end;end.
</pre>
</div>
<p> </p>
<p> </p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:{咏南中间件},转载请注明原文链接:https://www.cnblogs.com/hnxxcxg/p/14103406.html</p><br><br>
来源:https://www.cnblogs.com/hnxxcxg/p/14103406.html
頁:
[1]