【问题标题】:JavaFX Image Performance OptimizationJavaFX 图像性能优化
【发布时间】:2022-06-14 15:49:16
【问题描述】:

我正在使用 JavaFX 17 制作图片查看器应用程序。总而言之,该应用程序类似于 Windows Photo / Windows Picture Viewer。用户可以打开图片或文件夹。应用程序将显示给定图片或给定文件夹中的第一张图片。我的应用程序将一次显示一张图片,用户可以使用可用控件(下一张、上一张、最后一张和开始)浏览图片。

我检查了以下线程以确保它已经足够优化:

但是,我发现我的代码在处理 200 张图片时存在问题,每个图片大小约为 1~2 MB。

没有background loading,应用程序不会显示任何内容。即使导航控制状态因为知道有可用的图片而改变。所以,点击next & prev只会显示一个空白屏幕。使用背景加载时,仅加载第一张图片中的一小部分。几次next控制后,突然又变成空白了。

这是我最小的、可重现的示例:

package com.swardana.mcve.image;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * JavaFX App
 */
public class App extends Application {

    @Override
    public void start(Stage stage) {
        var view = new View();
        var path = Paths.get("Path/to/many/images");
        var storage = new Storage(new PictureSource(path));
        storage.setOnSucceeded(eh -> view.exhibit(storage.getValue()));
        Executors.newSingleThreadExecutor().submit(storage);
        var scene = new Scene(view, 640, 480);
        scene.addEventFilter(KeyEvent.KEY_PRESSED, eh -> {
            switch (eh.getCode()) {
                case RIGHT:
                    view.next();
                    break;
                case DOWN:
                    view.last();
                    break;
                case LEFT:
                    view.prev();
                    break;
                case UP:
                    view.beginning();
                    break;    
                default:
                    throw new AssertionError();
            }
        });
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

    public class Picture {

        private final String name;
        private final Image image;

        public Picture(final String name, final Path src) throws IOException {
            this(name, new Image(src.toUri().toURL().toExternalForm(), true));
        }

        public Picture(final String name, final Image img) {
            this.name = name;
            this.image = img;
        }

        public final String name() {
            return this.name;
        }

        public final Image image() {
            return this.image;
        }

    }

    public class PictureSource {

        private final Path source;

        public PictureSource(final Path src) {
            this.source = src;
        }

        public final List<Picture> pictures() {
            var dir = this.source.toString();
            final List<Picture> pictures = new ArrayList<>();
            try (var stream = Files.newDirectoryStream(this.source, "*.{png,PNG,JPG,jpg,JPEG,jpeg,GIF,gif,BMP,bmp}")) {
                for (final var path : stream) {
                    var picName = path.getFileName().toString();
                    pictures.add(
                        new Picture(picName, path)
                    );
                }
                return pictures;
            } catch (final IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
    
    public class Storage extends Task<List<Picture>> {
        private final PictureSource source;

        public Storage(final PictureSource src) {
            this.source = src;
        }

        @Override
        protected final List<Picture> call() throws Exception {
            return this.source.pictures();
        }
    }
    
    public class View extends VBox {
        private final ImageView image;
        private List<Picture> pictures;
        private int lastIdx;
        private int index;
        
        public View() {
            this.image = new ImageView();
            this.initGraphics();
        }
        
        // This method to accept value from the `Storage`.
        public void exhibit(final List<Picture> pics) {
           this.pictures = pics;
           this.index = 0;
           this.lastIdx = pics.size();
           this.onChange();
        }
        
        public void next() {
            if (this.index != this.lastIdx - 1) {
                this.index++;
                this.onChange();
            }
        }
        
        public void prev() {
            if (this.index != 0) {
                this.index--;
                this.onChange();
            }
        }
        
        public void last() {
            this.index = this.lastIdx - 1;
            this.onChange();
        }
        
        public void beginning() {
            this.index = 0;
            this.onChange();
        }

        // Whenever the action change, update the image from pictures.
        public void onChange() {
            this.image.setImage(this.pictures.get(this.index).image());
        }
        
        private void initGraphics() {
            this.getChildren().add(this.image);
        }
        
    }

}

非常感谢任何帮助和建议。

【问题讨论】:

  • minimal reproducible example 请注意 M (忽略所有的绒毛 - 只是一个简单的类来加载图像以及如何在后台线程上使用它)
  • @kleopatra 我已更新我的答案以删除所有绒毛并提供最少的可重复示例。
  • hmm .. 无法重现问题(不过可能是图像)。注意:我认为,不需要您的存储,无论如何它都会立即返回 - 加载是由图像本身完成的,也就是说您有许多您不(想要)控制的后台线程。因此,您可能正在尝试导航到尚未完全加载的图像 - 不确定在这种情况下系统应该做什么(文档状态“显示占位符”,而我看到一个空白视图,直到图像已完全加载 - 检查其进度属性)
  • 是的,我的代码在处理 200 项以下的图像时没有任何问题。但是,当达到 200 张图像时,它会显示这种行为。可能需要查看有关如何解决此问题的其他选项
  • 尝试使用GridView

标签: java image performance javafx


【解决方案1】:

问题是您一次加载所有图像的全尺寸(它需要大量内存)并将它们保存在List&lt;Pictures&gt; 中,因此它们保留在内存中。我尝试用你的代码加载 100 张大图片,第 10 张图片我得到了OutOfMemoryError: Java heap space(使用的堆大小约为 2.5GB)。

我找到了两种可能的解决方案:

  1. 调整图像大小。
  2. 按需加载图片(惰性)。

将图像大小调整为 800 像素宽度可将使用的堆减少到 600MB。为此我更改了Picture 的类构造函数。

public Picture(final String name, final Path src) throws IOException {
    this(name, new Image(src.toUri().toURL().toExternalForm(), 800, 0, true, true, true));
}

如果仅在需要时才加载图像,则最常使用的堆大小约为 250MB,有几次跳跃到 500MB。 同样,我更改了 Picture 类的构造函数并引入了一个新字段 imageUrl,因此 Path 刚刚转换为 URL string 并且没有创建 Image 对象。

private final String imageUrl;

public Picture(final String name, final Path src) throws IOException {
    this(name, src.toUri().toURL().toExternalForm());
}

public Picture(final String name, final String imageUrl) {
    this.name = name;
    this.imageUrl = imageUrl;
}

image() 方法现在不返回预加载图像,而是按需加载图像并同步执行。

public final Image image() {
    return new Image(imageUrl);
}

对于一个包含 850 张图片的文件夹,我得到了这个:

在创建Picture 时加载图像并将它们全部保存在List 中会消耗大量内存和 GC 活动(无法释放内存)。

graphs for cpu and heap usage without lazy loading

通过延迟加载,我得到了这些图表。

graphs for cpu and heap usage with lazy loading

【讨论】:

  • 调整图像大小是不可能的,因为需要按原样显示图像质量。实际上,我考虑过延迟加载。但是,由于我的用户可以在图片之间切换,因此每次创建一个新图像不会增加内存消耗,而不是一开始就将所有内容都加载为图像?
  • 是的。我对包含 850 张图像的文件夹进行了测试,并编辑了答案,现在您可以在图表上看到资源消耗。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-08
  • 1970-01-01
  • 2019-06-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多