红鱼儿在其博客中发表的一篇文章中引述了官方QC中的一处缺陷报告,谈到了 TCustomMultiResBitmap.ItemByScale 的效率问题,并提供了一个优化。不过我觉得它的优化有点太偷懒了,而且一旦不匹配,效率还不如原来的实现。所以我简单审视了下 ItemByScale 的代码,实际上它的效率问题在于过度优化造成了负优化。如果在提供的图片特别多(比如成百上千个),它进行一遍比较并排序还是有意义的,但现实是我们每个程序中实际使用的图数量是有限的,一般只有几个,用排序后再比较的算法明显是过度的优化反而降低了效率(我也没少干这事:-:)。
说了人家那么多,不提供一个自己的实现好象说不过去。实际上我想提供一个带缓存的版本,后来觉得先不得了,提供一个简单改进版本吧,这个版本在测试的几种情况下都明显优于原来的实现,一般是在4-10倍左右。
function TCustomMultiResBitmap.ItemByScale(const AScale: Single; const ExactMatch: Boolean; const IncludeEmpty: Boolean): TCustomBitmapItem; var LScale, LCurrentScale: Single; I, LCount: Integer; //Added by swish LDelta,LMinDelta: Single; AItem: TCustomBitmapItem; //Comment by swish // LScaleArray: TArray<Single>; begin LScale := RoundTo(AScale, TCustomBitmapItem.ScaleRange); Result := nil; if not ExactMatch then begin //Replaced by swish LCount:=-1; LMinDelta:=MaxInt; for I := 0 to Count - 1 do begin AItem:=Items[I]; if (not IncludeEmpty) and AItem.IsEmpty then Continue; LDelta:=AItem.Scale-LScale; if LDelta<0 then LDelta:=-LDelta; if LDelta<LMinDelta then begin LMinDelta:=LDelta; LCount:=I; if (LMinDelta=0) or ((LMinDelta>0) and (LMinDelta<TEpsilon.Scale)) then Break end; end; if LCount>=0 then Result:=Items[LCount]; Exit; //Follow is origin codes // LScaleArray := ScaleArray(IncludeEmpty); // LCount := Length(LScaleArray); // if LCount > 0 then // begin // I := -1; // repeat // Inc(I) // until (LScale < LScaleArray[I]) or (I >= LCount - 1); // if I > 0 then // begin // LCurrentScale := LScaleArray[I - 1] + (LScaleArray[I] - LScaleArray[I - 1]) / 5; // if LScale <= LCurrentScale then // Dec(I); // end; // LScale := LScaleArray[I]; // end; end; for I := 0 to Count - 1 do if SameValue(LScale, Items[I].Scale, TEpsilon.Scale) and (IncludeEmpty or (not Items[I].IsEmpty)) then begin Result := Items[I]; Exit; end end;
使用新的 TCustomMultiResBitmap 的方法参考红鱼儿的原博客,我就不多说了。实际上,如果进一步优化的话,官方应该将SameValue 改成内联,另外,象 Items[I] 这种本质上对函数的调整还是应该缓存到一个变量里。真正彻底的优化最好就是在维护不同分辨率图片时,内部按缩放比例进行排序,这样在这里就可以直接二分找到最接近缩放比的图片了,不过那个留给官方去改吧。说不定官方有更好的算法呢。