• 关于大图片加载的速度太慢导致界面卡顿的问题


    Delphi 盒子论坛上有人提出这个问题。我写了一大段回复,结果提示有非法字符,没法提交。所以把回复帖到这里来。

    原帖地址:盒子论坛 v2.1

    原文内容如下:

    本人菜鸟一枚。LoadFromFile载入大图会造成UI卡顿,之前在Win平台使用PostMessage方法解决,请问在安卓平台解决载入大图卡顿的方法。
    我尝试使用匿名线程的方式,网上查询会造成阻塞。求解决办法。
     

    1. procedure TMainForm.ListBoxItem13Click(Sender: TObject);
    2. var
    3.   SearchField: string// 要查找的字段名
    4.   SearchValue: string// 要查找的字段值
    5.   ai: TAniIndicator;
    6. begin
    7.   TabControl3.TabIndex := 0;
    8.   Text1.Text := '规划图';
    9.   MainTab2CiyeTab.ExecuteTarget(self);
    10.   SearchField := 'name';
    11.   SearchValue := '%' + '规划图' + '%';
    12.   ImageViewer1.Bitmap:=nil;
    13.   if ImageViewer1.Bitmap.IsEmpty then
    14.   begin
    15.   TThread.CreateAnonymousThread(
    16.     procedure()
    17.     begin
    18.       try
    19.         TThread.Synchronize(TThread.CurrentThread,
    20.           procedure()
    21.           begin
    22.           ai := TAniIndicator.Create(ImageViewer1);
    23.           ai.Parent := ImageViewer1;
    24.           ai.Style := TAniIndicatorStyle.Circular;
    25.           ai.StyleLookup := 'aniindicatorstyle';
    26.           ai.Height := 50;
    27.           ai.Width := 50;
    28.           ai.Position.X := (ImageViewer1.Width - ai.Width) / 2;
    29.           ai.Position.Y := (ImageViewer1.Height - ai.Height) / 2;
    30.           ai.Visible := true;
    31.           ai.Enabled := true;
    32.           end);
    33.           sleep(10);
    34.           TThread.Synchronize(TThread.CurrentThread,
    35.           procedure()
    36.           begin
    37.           if System.SysUtils.FileExists(TPath.Combine(TPath.GetPublicPath,'规划图')then
    38.           begin
    39. //          Application.ProcessMessages;
    40.           ImageViewer1.Bitmap.LoadFromFile
    41.           (TPath.Combine(TPath.GetPublicPath, '规划图.jpg'))
    42.           end
    43.           else
    44.           begin
    45.           TThread.Synchronize(TThread.CurrentThread,
    46.           procedure()
    47.           begin
    48.           Toast('照片载入中...');
    49.           with FDQuery3 do
    50.           begin
    51.           close; // 先关闭数据模块中的Query
    52.           Sql.Clear; // 清空Query中的SQL值
    53.           Sql.Add('select * from  heliushuixitu where ' + SearchField
    54.           + ' like ''%' + SearchValue + '%''');
    55.           Open();
    56.           TBlobField(FieldByName('image'))
    57.           .SaveToFile(TPath.Combine(TPath.GetPublicPath,'规划图.jpg'));
    58. //          Application.ProcessMessages;
    59.           ImageViewer1.Bitmap.LoadFromFile
    60.           (TPath.Combine(TPath.GetPublicPath, '规划图.jpg'));
    61.           end;
    62.           end);
    63.           end;
    64.           end);
    65.       finally
    66.         ai.Enabled := False;
    67.         ai.Visible := False;
    68.         ai.Free;
    69.       end;
    70.       sleep(1000);
    71.     end).start;
    72.   end;
    73.   ToolButtion.Visible := true;
    74.   ToolShareButton.Visible := true;
    75. end;

    我的回复如下:

    首先看原文的这段代码:

    1. TThread.Synchronize(TThread.CurrentThread,
    2. procedure()
    3. begin
    4. if System.SysUtils.FileExists(TPath.Combine(TPath.GetPublicPath,'规划图')) then
    5. begin
    6. // Application.ProcessMessages;
    7. ImageViewer1.Bitmap.LoadFromFile
    8. (TPath.Combine(TPath.GetPublicPath, '规划图.jpg'))
    9. end
    10. end

    这段代码,是把从文件加载图片到 ImageViewer 的过程,放进主进程执行。那么,你又何必用线程?

    如果 ImageViewer1.Bitmap.LoadFromFile 因为文件大导致很慢导致 UI 卡,那么,你这段代码,一样卡。因为你这段代码和线程没有任何关系。

    如果 ImageViewer1.Bitmap.LoadFromFile 因为文件大导致很慢,你执行 ProcessMessage 也没有任何意义。ProcessMessage 通常是用于在主线程里面有个循环很耗时,比如循环执行10万次,那么,在循环里面,每执行一次,执行一次 ProcessMessage 让主线程可以处理一下 Windows 的消息让界面不会不响应鼠标键盘等等,让界面不要看起来像是卡住死掉一样。

    如果是非常耗时的代码,不想让界面卡住,那耗时的代码用线程来执行而不是用主线程来执行,是比较好的。比如上面我说的那种10万次的循环。

    但是,一旦要动界面元素,比如 ImageViewer1 那么,必须用主线程。用线程去操作界面元素,是会出问题的。

    因此,这里唯一可以用线程去做事的地方就是加载文件,以及把文件变成 Bitmap;

    所以,你可以试试分两步:


    1. 用线程加载文件;比如 MemoryStream.LoadFromFile;把文件加载进内存;你可以在 LoadFromFile 的前面和后面加上 GetTickCount 看看前后用了多少时间;


    2. 把内存数据变成 Bitmap,比如用 Bitmap.LoadFromStream;同样,你可以加上 GetTickCount 看看这句话会消耗多少时间;

    最后,你可以在主线程里面,使用 ImageViewer1.Bitmap.Assign(YourBitMap) 的方式,把加载到内存里面的 bitmap 显示出来。这句话因为是操作界面元素,必须放到主线程里面。同样,你也可以用 GetTickCount 看看这句话需要多少时间。

    最后,也就是 3 楼说的,你可以在线程里面,直接用一个内存变量 TBitmap 加载文件,而不是我上面说的分两步加载。你也可以看看这样做需要多少时间。

    总结一下:


    1. 加载文件数据进内存如果很耗时,这个代码如果在主线程执行,就会把主线程卡住。那么这个加载的代码,可以用线程来执行,避免卡住界面。

    2. 最终要显示到界面上,比如把图片的内存数据赋值给 TImage 或者 TImageViewer,因为会影响界面变动,必须要使用主线程来操作。

    3. 所以,假设时间消耗比较多是在第一步,那么你把第一步放进线程,会解决界面卡住的问题;如果时间消耗比较多是在第二步,因为第二步必须在主线程执行,如果它消耗时间导致界面被卡住,那就没有任何办法了。

    BTW:


    上面第三条,我说没有办法了。其实办法还是有的,那就是:
    1. 不改动界面的情况下,在内存里面,把图片缩小;这个缩小比例的代码,可以用线程来执行;


    2. 显示缩小的图片,肯定比显示非常大的图片,耗时更短。


    3. 你甚至可以把图片缩小为几个档次,然后界面首先加载最小的图片;然后再逐渐加载更大的图片。假设你的界面非常大,就会看到图片一开始很模糊,逐渐变清楚。如果你的界面尺寸没有多大,实际上你也没必要显示一个非常大的图片,你只需要显示这个图片的缩小版本就好了。这就好比,你把一个像素为 1万X1万的图片,显示到手机的小屏幕上,和显示像素为1千 X 1千的图片,在手机屏幕上看,清晰度是没有区别的。因此你没有必要显示那个像素是 1万 X 1万的大图片。因此,你可以先用线程把它缩小后,再用主线程去显示。这样就解决了主线程显示一个非常大的图片可能会比较耗时的问题。

  • 相关阅读:
    《网络协议》07. 其他协议
    node.js
    [附源码]计算机毕业设计JAVA购买车票系统
    直流充电桩测试仪的作用
    JDK动态代理
    动手测起来!搭载全自研数据库内核,我们将性能提升了20%
    Eclipse Theia:Eclipse的继承者?
    微软广告和网络服务CEO承认OpenAI的Sora将加入Copilot,但需要一些时间
    C#通过C++操作共享内存和Python通讯[C#调用exe不显示窗口]
    gitlab+jenkins+k8s实现持续集成springboot+springcloud
  • 原文地址:https://blog.csdn.net/pcplayer/article/details/126396870