首页 > 解决方案 > JavaFX listview 使用线程在自定义单元格中加载错误的图像

问题描述

我在我的第一个 JavaFX 项目上工作,并且对 listview 自定义单元工厂有问题。这是我的代码

package ir.sadeghpro.instagram.cell;

import com.ibm.icu.util.PersianCalendar;
import ir.sadeghpro.insta.client.Comment;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;

import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

public class DischargeComment extends ListCell<Comment> {

    @FXML
    private AnchorPane pane;

    @FXML
    private TextFlow lblComment;

    @FXML
    private Label lblDate;

    @FXML
    private Label lblTime;

    @FXML
    private Hyperlink lblUsername;

    @FXML
    private ImageView img;


    public static String search = "";
    private FXMLLoader mLLoader;
    private static Map<String, Image> images = new HashMap<>();

    @Override
    protected void updateItem(Comment item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null) {

            setText(null);
            setGraphic(null);

        } else {
            if (mLLoader == null) {
                mLLoader = new FXMLLoader(getClass().getClassLoader().getResource("cell/discharge_comment.fxml"));
                mLLoader.setController(this);

                try {
                    mLLoader.load();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }

            ObservableList<Node> children = lblComment.getChildren();
            lblComment.setTextAlignment(TextAlignment.JUSTIFY);
            children.clear();
            if (!search.isEmpty() && item.getText().contains(search)) {
                int lastIndex = 0;
                for (int index = item.getText().indexOf(search); index >= 0; index = item.getText().indexOf(search, index + 1)) {
                    Text text = new Text(item.getText().substring(lastIndex, index));
                    text.setTextAlignment(TextAlignment.LEFT);
                    children.add(text);
                    text = new Text(item.getText().substring(index, index + search.length()));
                    text.setTextAlignment(TextAlignment.LEFT);
                    text.setFill(Color.RED);
                    children.add(text);
                    lastIndex = index + search.length();
                }
                if (lastIndex < item.getText().length()) {
                    Text text = new Text(item.getText().substring(lastIndex));
                    text.setTextAlignment(TextAlignment.LEFT);
                    children.add(text);
                }
            } else {
                children.add(new Text(item.getText()));
            }
            PersianCalendar persianCalendar = new PersianCalendar();
            persianCalendar.setTimeInMillis(item.getTimestamp() * 1000L);
            lblDate.setText(persianCalendar.get(Calendar.YEAR) + "/" + (persianCalendar.get(Calendar.MONTH) + 1) + "/" + persianCalendar.get(Calendar.DAY_OF_MONTH));
            lblTime.setText(persianCalendar.get(Calendar.HOUR) + ":" + persianCalendar.get(Calendar.MINUTE));
            lblUsername.setText(item.getOwnerUsername());
            Image image;
            if ((image = images.get(item.getOwnerId())) == null) {
                img.setImage(null);
                new Thread(() -> {
                    Image image1 = new Image(item.getOwnerProfilePicUrl());
                    images.put(item.getOwnerId(), image1);
                    Platform.runLater(() -> img.setImage(image1));
                }).start();
            } else {
                img.setImage(image);
            }
            Circle clip = new Circle(25, 25, 25);
            img.setClip(clip);
            lblUsername.setOnMouseClicked(e->{
                try {
                    Desktop.getDesktop().browse(new URI("https://www.instagram.com/" + item.getOwnerUsername()));
                } catch (IOException | URISyntaxException exception) {
                    exception.printStackTrace();
                }
            });

            setText(null);
            setGraphic(pane);
            setHeight(Region.USE_COMPUTED_SIZE);
        }
    }
}

我的问题在第 107-114 行。在这一行中,如果用户的图像在我下载它并添加到 hashmap 图像之前没有下载,接下来将它添加到查看,它工作正常但是当滚动列表快速时可能会下载 100 个图像,因为在下载图像后的线程中添加到单元格 ImageView 甚至单元格消失并且不再显示,例如我在索引 10 中有单元格 X,在索引 25 中有单元格 Y,如果我快速滚动 X 显示在 Y 单元格中的一些时间图像

对不起,如果我没有解释清楚,因为这是我的第一个 JavaFX 项目

标签: javamultithreadinglistviewjavafx

解决方案


private static Map<String, Image> images = new HashMap<>();

...

Image image;
if ((image = images.get(item.getOwnerId())) == null) {
    img.setImage(null);
    new Thread(() -> {
        Image image1 = new Image(item.getOwnerProfilePicUrl());
        images.put(item.getOwnerId(), image1);
        Platform.runLater(() -> img.setImage(image1));
    }).start();
} else {
    img.setImage(image);
}

缓存图像是个好主意,但这样做是错误的。您在不同的线程上加载并插入图像到地图。由于您不同步访问,因此无法保证两个线程以相同的方式查看地图。还考虑到Image提供异步加载 a 的方式这一事实,实际上Image并不需要自己创建线程。
此外,对于大量数据,您可能希望删除 GUI 当前未使用的图像。使用SoftReferences 将是一个好主意。
然而,主要问题是缺乏同步。如果你滚动得足够快,多个线程可能会为同一个单元格加载不同的图像,而你不知道最后一个启动的是否是最后一个执行的Platform.runLater. 来自同一来源的多个图像可以并行加载。
也没有办法重用你的缓存。如果应用程序的其他部分需要图像,则无法以这种方式重用它们。

我的建议:

...

import java.awt.Desktop; // importing more classes from awt than neccessary could result in problems
...

public class DischargeComment extends ListCell<Comment> {

    ...

    /**
     * Constructor to pass external cache
     * @param cache 
     */
    public DischargeComment(Map<String, SoftReference<Image>> cache) {
        if (cache == null) {
            throw new IllegalArgumentException();
        }
        this.cache = cache;
    }

    /**
     * constructor using the default cache
     */
    public DischargeComment() {
        this(getDefaultCache());
    }

    private final Map<String, SoftReference<Image>> cache;

    private static Map<String, SoftReference<Image>> defaultCache;

    private static final URL FXML_URL = DischargeComment.class.getResource("cell/discharge_comment.fxml");

    public static Map<String, SoftReference<Image>> getDefaultCache() {
        if (defaultCache == null) {
            defaultCache = new HashMap<>();
        }
        return defaultCache;
    }

    public static String search = "";
    private boolean loaded = false; // no need for a reference to fxmlloader here

    @Override
    protected void updateItem(Comment item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else {
            if (!loaded) {
                FXMLLoader mLLoader = new FXMLLoader(FXML_URL);
                mLLoader.setController(this);

                try {
                    mLLoader.load();
                    img.setClip(new Circle(25, 25, 25));
                    loaded = true;
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }

            ...

            // use single access here
            // also use url as key
            cache.compute(item.getOwnerProfilePicUrl(), (key, value) -> {
                Image image = null;
                if (value != null) {
                    image = value.get();
                }
                if (image == null) {
                    image = new Image(key, true); // load image in background
                    value = new SoftReference<>(image);
                }
                img.setImage(image);
                return value;
            });

            lblUsername.setOnMouseClicked(e->{
                try {
                    Desktop.getDesktop().browse(new URI("https://www.instagram.com/" + item.getOwnerUsername()));
                } catch (IOException | URISyntaxException exception) {
                    exception.printStackTrace();
                }
            });

            setText(null);
            setGraphic(pane);
            setPrefHeight(Region.USE_COMPUTED_SIZE); // don't set height to -1
        }
    }
}

推荐阅读