如何清理FMX动态ListView中Bitmap的内存占用?
FMX Dynamic ListView Bitmap Memory Leak When Clearing Items
兄弟,我太懂这个坑了!FMX的动态ListView加载带Bitmap的项时,内存泄漏是真的头疼,我之前也踩过一模一样的坑。你的问题核心是**TListItemImage里的Bitmap没有被正确释放**,哪怕调用了Items.Clear,要么是异步加载的线程还在偷偷赋值,要么是ListView内部的引用没切断。
先给你拆解原因:
- 你的
GetIcon用异步线程加载图片,当你调用Clear时,可能有些线程还在跑,之后会把Bitmap赋值给已经被清理的ListItem,导致这些Bitmap永远飘在内存里。 - 直接调用
Items.Clear不会自动释放每个ListItem里的Bitmap——FMX的动态ListView对项的对象有缓存机制,不会主动销毁Bitmap对象。 - 你之前把Bitmap设为
nil没用,因为TListItemImage.Bitmap属性不会自动释放旧对象;直接释放项会崩溃,因为ListView还持有这些项的引用。
现在给你一步步的解决方案:
1. 先管好异步加载的线程(最关键!)
首先要把所有加载图片的线程管起来,清理ListView前先停止这些线程,避免它们后续乱赋值。
首先在你的TPluginInstaller类里加个线程列表:
private FImageLoadThreads: TThreadList<TThread>;
然后在类的构造和析构里初始化/清理这个列表:
constructor TPluginInstaller.Create(AOwner: TComponent); begin inherited; FImageLoadThreads := TThreadList<TThread>.Create; end; destructor TPluginInstaller.Destroy; var Thread: TThread; begin // 程序退出时清理所有剩余线程 for Thread in FImageLoadThreads.LockList do begin Thread.Terminate; Thread.WaitFor; Thread.Free; end; FImageLoadThreads.UnlockList; FImageLoadThreads.Free; inherited; end;
接下来修改你的GetIcon方法,把线程加入列表,并且在线程结束时移除,同时确保Bitmap的所有权正确转移:
procedure TPluginInstaller.GetIcon(const aURL: string; aIcon: TListItemImage); var LoadThread: TThread; begin if aURL = '' then begin aIcon.ImageIndex := 3; Exit; end; LoadThread := TThread.CreateAnonymousThread( procedure var imgStream: TMemoryStream; LocalBitmap: TBitmap; begin // 先检查线程是否被终止,避免无效操作 if TThread.CurrentThread.Terminated then Exit; imgStream := TMemoryStream.Create; try if TThread.CurrentThread.Terminated then Exit; TDownloadURL.DownloadRawBytes(aURL, imgStream); if TThread.CurrentThread.Terminated then Exit; LocalBitmap := TBitmap.CreateFromStream(imgStream); try TThread.Synchronize(nil, procedure begin if not TThread.CurrentThread.Terminated then begin // 先释放旧的Bitmap!这步之前你没做 aIcon.Bitmap.Free; aIcon.Bitmap := LocalBitmap; LocalBitmap := nil; // 把所有权转给aIcon,避免重复释放 end; end); finally LocalBitmap.Free; // 只有当所有权没转出去时才释放 end; finally imgStream.Free; // 线程结束后从列表移除 FImageLoadThreads.Remove(LoadThread); end; end); // 线程结束后自动释放 LoadThread.OnTerminate := procedure(Sender: TObject) begin TThread(Sender).Free; end; // 把线程加入管理列表 FImageLoadThreads.Add(LoadThread); LoadThread.Start; end;
2. 清理ListView时手动释放Bitmap
在调用Items.Clear之前,先遍历所有项,手动释放每个TListItemImage的Bitmap,切断引用:
修改你Load方法里的同步部分:
TThread.Synchronize(nil, procedure var I: Integer; aItem: TListViewItem; aIcon: TListItemImage; begin spnedtPage.Max := fLastPage; spnedtPage.Value := fPage; lblPageMax.Text := ' of ' + fLastPage.ToString; lvPluginInstaller.BeginUpdate; try // 第一步:先停掉所有正在加载图片的线程 var ThreadList := FImageLoadThreads.LockList; try for var Thread in ThreadList do begin Thread.Terminate; Thread.WaitFor; end; ThreadList.Clear; finally FImageLoadThreads.UnlockList; end; // 第二步:手动释放每个项的Bitmap for I := lvPluginInstaller.Items.Count - 1 downto 0 do begin aItem := lvPluginInstaller.Items[I]; aIcon := aItem.Objects.FindObjectT<TListItemImage>('Icon'); if Assigned(aIcon) then begin aIcon.Bitmap.Free; aIcon.Bitmap := nil; end; // 如果你还有其他带Bitmap的TListItemImage(比如其他图标),这里也要一起释放 end; // 第三步:再调用Clear lvPluginInstaller.Items.Clear; // 后续添加新项的代码... finally lvPluginInstaller.EndUpdate; end; end);
为什么这样就能解决?
- 先停掉所有加载线程,避免后续给已经被清理的ListItem赋值Bitmap,杜绝“幽灵Bitmap”。
- 手动释放每个Bitmap,确保内存被立刻回收,而不是等着GC或者ListView的缓存机制。
- 转移Bitmap的所有权,避免重复释放或者内存泄漏。
按这个方法改完后,你再测试一下,重复加载和清理应该就不会内存持续上涨了。
内容的提问来源于stack exchange,提问作者Adriaan




