我正在使用 JavaFX 17 制作一个图片查看器应用程序。总而言之,该应用程序就像 Windows 照片/Windows 图片查看器。用户可以打开图片或文件夹。应用程序将显示给定的图片或给定文件夹中的第一张图片。我的应用程序将一次显示一张图片,用户可以使用可用的控件(下一张、上一张、最后一张和开头)导航图片。
我检查了以下线程以确保其足够优化:
- JavaFx Images in Gridpane slowing down performance drastically
- What is the best way to display millions of images in Java?
但是,我发现我的代码在处理 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)。
我找到了两种可能的解决方案:
- 调整图像大小。
- 按需加载图像(惰性)。
将图像大小调整为 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
通过延迟加载,我得到了这些图表。
关于JavaFX 图像性能优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71839473/