从DelphiXE4的例子分析FireMonkey ListBox效率问题.

DelphiXE4有一个Demo CustomListBox.
演示的是一个自定义Item的ListBox.其中有一个按钮,一次添加1000个Item.
尝试着把循环加到10000.乖乖,添加时慢得不得了,接近10秒才能添加完.我当初做光速搜索的时候几百万文件搜索都是100毫秒内解决的.差距怎么这么大.
结果是经过调整,添加10000个Item的时间由10秒提高到了800毫秒,不要太过分哦.

查看了一下Demo中的Button2Click中的代码.
改回到1000次循环,因为AQTime中执行比较慢,10000次循环等不起.

使用AQTime工具分析了一下时间.最后Button2Click一共用了0.71秒,而其中TListBoxItemData.SetBitmap就占了0.46秒.而TListBoxItemData.SetBitmap中的TBitmap.Assign 就占了0.46秒….最后发现最耗时的地方在于TCanvasD2D.DoMapBitmap中.于是打开FMX目录中FMX.Canvas.D2D.pas,定位到这个函数,发现写的中规中矩,连个循环都没有,代码又不多.只是调用了几个DX10的D2D的相关API,应该是这几个API的效率较差.

尝试着在主Form单元加入GlobalUseDirect2D := False;关闭D2D,那么FMX在渲染界面的时候启用的就是GDIPlus.
循环10000次的时间由10秒缩减到3秒.
罪魁祸首在于D2D的API速度.

给大家例子下载FireMonkey

再分析,发现Button2Click往下调用最耗时的是TControl.RefreshInheritedCursorForChildren;

这个函数做什么的我也没细看,估计是刷新Cursor的.但是它在每个TControl.DoAddObject中都会被调用到.把FMX.Controls单元拷贝到当前工程目录下,准备修改.
因为这个CustomListBox的例子中批量添加已经调用了BeginUpdate和EndUpdate,在这两段中间其实不需要刷新Cursor的.只要在EndUpdate的时候刷新一下即可.
所以修改:

procedure TControl.RefreshInheritedCursorForChildren;
var
  ChildControl: TControl;
begin
  if not IsUpdating then //isUpdating的时候不需要刷新光标
    if Controls.Count > 0 then
      for ChildControl in Controls do
        if ChildControl.Cursor = crDefault then
          ChildControl.RefreshInheritedCursor;
end;

procedure TControl.EndUpdate;
var
  I: Integer;
begin
  if IsUpdating then
  begin
    if Assigned(FControls) then
      for I := 0 to FControls.Count - 1 do
        FControls[I].EndUpdate;
    Dec(FUpdating);
    if not IsUpdating then
    begin
      Realign;
      RefreshInheritedCursorForChildren; //统一在Endupdate的时候调用一次就够了
    end;
  end;
end;

再尝试编译,发现代码10000次循环代码时间已经变为1秒了.


接着分析,发现最费时代码变成了IndexOf了.因为随着Items数量增加,IndexOf的耗时也就是指数增长.AQTime看虽然IndexOf并不是最耗时的,但是因为现在Item只有1000.如果Item有10000个的话就不是10倍关系了.
那么看代码:

procedure TfrmCustomList.Button2Click(Sender: TObject);
var
  Item: TListBoxItem;
begin
  // create custom item
  Item := TListBoxItem.Create(nil);
  Item.Parent := ListBox1;
  Item.StyleLookup := 'CustomItem';
  Item.Text := 'item ' + IntToStr(Item.Index); // set filename 这里用到了IndexOf
  if Odd(Item.Index) then      //这里用到了IndexOf
    Item.ItemData.Bitmap := Image1.Bitmap // set thumbnail
  else
    Item.ItemData.Bitmap := Image2.Bitmap; // set thumbnail
  Item.StylesData['resolution'] := '1024x768 px'; // set size
  Item.StylesData['depth'] := '32 bit';
  Item.StylesData['visible'] := true; // set Checkbox value
  Item.StylesData['visible.OnChange'] := TValue.From<TNotifyEvent>(DoVisibleChange); // set OnChange value
  Item.StylesData['info.OnClick'] := TValue.From<TNotifyEvent>(DoInfoClick); // set OnClick value
end;

把代码修改为:

procedure TfrmCustomList.Button2Click(Sender: TObject);
var
  Item: TListBoxItem;
  Index : Integer;
begin
  // create custom item
  Item := TListBoxItem.Create(nil);
  Item.Parent := ListBox1;
  Index := ListBox1.Count - 1;
  Item.StyleLookup := 'CustomItem';
  Item.Text := 'item ' + IntToStr(index); // set filename 这里用到了IndexOf
  if Odd(index) then      //这里用到了IndexOf
    Item.ItemData.Bitmap := Image1.Bitmap // set thumbnail
  else
    Item.ItemData.Bitmap := Image2.Bitmap; // set thumbnail
  Item.StylesData['resolution'] := '1024x768 px'; // set size
  Item.StylesData['depth'] := '32 bit';
  Item.StylesData['visible'] := true; // set Checkbox value
  Item.StylesData['visible.OnChange'] := TValue.From<TNotifyEvent>(DoVisibleChange); // set OnChange value
  Item.StylesData['info.OnClick'] := TValue.From<TNotifyEvent>(DoInfoClick); // set OnClick value
end;

再看结果,10000次循环变为800毫秒.虽然还是有点慢.但是毕竟可以勉强接受了.

总结:
1.D2D的BitMap的map处理上,比GDIPlus慢很多有,10秒减到3秒,(这个也可能是我显卡不行吧,没有在其他计算机上试过).D2D正常应该是把图都放到显存中,然后处理.
FireMonkey的ListBox这种场景下有多个自定义的图,每次都要创建D2D对象,把用户图片从内存传到显存.所以D2D不太适合这种场景.
2.这个是FireMonkey框架的设计问题.TControl.RefreshInheritedCursorForChildren被调用的次数太多.没判断BeginUpdate不需要刷新的情况
3.和Demo的代码有关,和FireMonkey的实现都有关,IndexOf不该这样频繁的调用.或者说ListItem的GetIndex方法不应该依赖IndexOf.否则数量非常多的时候频繁调用效率有问题.
4.ListBox设计的问题.导致添加大量Items的时候必然慢.因为每个Item都要事先Create,比如有一百万Item就要事先创建一百万次.其实这里可以设计一种模仿Windows的ListView的Virtual方式或者Android的ListView的方式.事先不创建这些Item,只调用ListBox的SetCount即可,然后可以给Listbox一个OnCreateItem方法,需要显示哪些Item就调用OnCreateItem来创建.这样既节约内存,又可以做到高速.或许易博龙认为这是个比较少的需求,所以留给我们自己来实现.

此条目发表在Delphi, 未分类分类目录。将固定链接加入收藏夹。

从DelphiXE4的例子分析FireMonkey ListBox效率问题.》有3条回应

  1. 匿名说:

    太牛了!!!!

  2. sWZ说:

    遇到大量添加的还是自己管理吧。速度远比系统管理要快的多得多得多。
    目前Firemonkey只是为了跨平台,优化是以后的事情。慢慢来吧,编译器和语言的硬伤才是delphi目前最大的问题

  3. 匿名说:

    非常好,尤其最后一条引入 lazy loading 是提高效率的核心和关键

评论已关闭。