JavaFX 图像性能优化

标签 java image performance javafx

我正在使用 JavaFX 17 制作一个图片查看器应用程序。总而言之,该应用程序就像 Windows 照片/Windows 图片查看器。用户可以打开图片或文件夹。应用程序将显示给定的图片或给定文件夹中的第一张图片。我的应用程序将一次显示一张图片,用户可以使用可用的控件(下一张、上一张、最后一张和开头)导航图片。

我检查了以下线程以确保其足够优化:

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

没有 background loading ,应用程序不显示任何内容。即使导航控制状态因为知道有可用图片而改变。因此,单击下一步和上一步只会显示空白屏幕。使用后台加载时,仅加载第一个图像的少数部分。经过几次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);
        }
        
    }

}

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

最佳答案

问题是您一次性加载全尺寸的所有图像(需要大量内存),并将它们保存在 List<Pictures> 中。所以他们留在内存中。我尝试用你的代码加载 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 Activity (无法释放内存)。

graphs for cpu and heap usage without lazy loading

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

graphs for cpu and heap usage with lazy loading

关于JavaFX 图像性能优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71839473/

相关文章:

javascript - 缩小 JS/CSS 文件

python - 根据解析的文本将多个 bool 列添加到数据框 - python

java - 如何增加 Flink taskmanager.numberOfTaskSlots 以在没有 Flink 服务器的情况下运行它(在 IDE 或 fat jar 中)

java - 使用 JSON 模式在 Java 中创建和序列化数据

java - 为什么JDK7不允许方法局部内部类访问自己方法的局部变量

jquery - 使用 jquery 动态调整图像大小

java - 如何过滤掉同一个JVM内多个应用程序的Frame

javascript - 在图像完全加载之前使用 Javascript 获取图像尺寸

javascript - IE6 上的 removeAttr/addClass 问题

performance - 从大列表中计算所有差值的算法