【问题标题】:Best way to repeatedly load/remove multiple images on screen在屏幕上重复加载/删除多个图像的最佳方法
【发布时间】:2017-10-24 13:20:12
【问题描述】:

我有一个任务执行器,它有 5 个外部面板,每个外部面板都是一个 JFrame,它有一个 innerPanelinnerpanel 是负责绘制图像的JPanel。我一遍又一遍地显示和删除许多图像,我的表现很差。

有人可以建议一种在不使用JFrame 的情况下加载和删除许多图像的方法吗?

我在屏幕的不同部分显示图像,有时持续时间极短50-1500ms。有时图像仅部分加载,而在其他情况下则根本不加载。

Main

public class Main {

    public static void main(String[] args) {

        new Main();
    }

    public Main() {
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(new OuterPanel(ScreenPosition.CENTER)), 0, 3, TimeUnit.SECONDS);
        scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(new OuterPanel(ScreenPosition.CENTER)), 0, 3, TimeUnit.SECONDS);
        scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(new OuterPanel(ScreenPosition.CENTER)), 0, 3, TimeUnit.SECONDS);
        scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(new OuterPanel(ScreenPosition.CENTER)), 0, 3, TimeUnit.SECONDS);
        scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(new OuterPanel(ScreenPosition.CENTER)), 0, 3, TimeUnit.SECONDS);
    }
}

ImageTask

public class ImageTask implements Runnable {

    private OuterPanel outerPanel;
    private int messageIndex;

    public ImageTask(OuterPanel outerPanel) {
        this.outerPanel = outerPanel;
    }

    @Override
    public void run() {
        outerPanel.setMessage(imagePath);
        outerPanel.setVisible(true);
        try {
            TimeUnit.MILLISECONDS.sleep(150);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        outerPanel.setVisible(false);
    }
}

OuterPanel

public class OuterPanel extends JFrame {

    private InnerPanel innerPanel;
    public static int height = 200;
    public static int width = 200;

    public OuterPanel(ScreenPosition screenPosition) {
        initComponents();

        setFocusable(false);
        setUndecorated(true);
        setBackground(new Color(0, 0, 0, 0));
        setAlwaysOnTop(true);
        setSize(SetScreenLocation.screenSize.width, SetScreenLocation.screenSize.height);
        setFocusableWindowState(false);
        setEnabled(false);
        pack();

        setLocation(0, 0);
    }

    private void initComponents() {
        innerPanel = new InnerPanel();
        add(innerPanel);
    }

    public void setMessage(Message message) {
        ImageIcon icon = IconFetch.getInstance().getIcon(message.getImagePath());
        if (icon != null) {
            Image img = IconFetch.getInstance().getScaledImage(icon.getImage(), width, height);
            innerPanel.setImage(img);
        }
    }

}

InnerPanel

public class InnerPanel extends JPanel {
    private String message;
    private Image img;

    public InnerPanel() {
        setOpaque(false);
        setPreferredSize(new Dimension(900, 450));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        if (img != null) {
            x = (this.getWidth() - img.getWidth(null)) / 2;
            y = (this.getHeight() - img.getHeight(null)) / 2;
            g2d.drawImage(img, x, y, this);
        }
    }

    public void setImage(Image image) {
        this.img = image;
        repaint();
    }
}

【问题讨论】:

  • 为了更好的帮助,请尽快发布正确的minimal reproducible example,这个缺少方法调用和main 方法,所以我们无法知道您是否将程序放在 EDT 上,如果您正在使用自己的线程或Swing TimerUtils Timer
  • 1) 获取示例图像的一种方法(如@Frakcool 所述)是热链接到在this Q&A 中看到的图像。 2) 见The Use of Multiple JFrames, Good/Bad Practice?
  • 我已经看到了,使用 JFrame 是一个问题,原因有很多。它创建了许多堆叠在一起的窗口,并导致性能问题。我还需要重复加载多张图像并将它们显示在屏幕的不同部分,有时需要很短的时间。我开始认为这不是 Java 可以做到的。
  • 提示:如果你想回复某人,你应该在他们的名字前添加@,例如@AndrewThompson。你为什么要创建新的JFrame 并在这么短的时间内显示它们?我认为最好创建一个JFrame,更改图像及其在屏幕上的位置,而不是每次都创建新图像以及使用Swing Timer 而不调用.sleep(...)
  • “我还需要重复加载多个图像并将它们显示在屏幕的不同部分” - 也许考虑某种缓存,加载图像可能会很耗时。 “我开始认为这不是 Java 可以完成的事情” - 我很确定它可以,但你可能需要注意你的资源管理,重新 -在可能的情况下使用元素,这可能意味着拥有一个不可见的帧池,进一步减少对象创建和销毁的开销。您还需要小心,因为 Swing 不是线程安全的

标签: java multithreading image swing jpanel


【解决方案1】:

所以,三件事是耗时的:

  1. 加载图片
  2. 首次创建/显示框架
  3. 垃圾回收

如果可以减少这些开销,就可以提高系统的性能

我看不到 IconFetch 的工作原理,但我强烈建议您这样做:

  1. 您使用ImageIO.read 加载图像,因为在图像完全加载之前它不会返回
  2. 您将结果缓存起来,这样您就不会一遍又一遍地重新加载同一张图片,这样做代价高昂。

这是一个非常简单的例子。你传递一个文件路径,它会将图像加载到缓存中,如果它还没有缓存,否则它会返回缓存的值

public enum ImagePool {
    INSTANCE;

    private Map<String, Image> images;

    private ImagePool() {
        images = new HashMap<>(25);
    }

    public synchronized Image grab(String name) throws IOException {
        Image image = images.get(name);
        if (image == null) {
            image = ImageIO.read(new File(name));
            images.put(name, image);
        }
        return image;
    }

}

第一次在屏幕上显示一个窗口是一个代价高昂的过程,而且由于系统开销,您并不总是知道窗口何时在屏幕上真正“可见”。

为此,您应该专注于将窗口缓存到某种池中并尽可能地重复使用它们,例如:

public enum FramePool {
    INSTANCE;

    private int minSize = 15;
    private int maxSize = 25;

    private List<ImageFrame> avaliable;

    private FramePool() {
        avaliable = new ArrayList<>(25);

        for (int index = 0; index < minSize; index++) {
            avaliable.add(new ImageFrame());
        }
    }

    public synchronized ImageFrame grab() {
        ImageFrame frame;
        System.out.println(avaliable.size());
        if (avaliable.isEmpty()) {
            System.out.println("Make new");
            frame = new ImageFrame();
        } else {
            frame = avaliable.remove(0);
        }

        return frame;
    }

    public synchronized void release(ImageFrame frame) {
        if (avaliable.size() < maxSize) {
            avaliable.add(frame);
        } else {
            System.out.println("Destory");
            frame.dispose();
        }
    }

    public class ImageFrame extends JFrame {

        private JLabel label;
        private int timeout = 150;

        public ImageFrame() throws HeadlessException {
            label = new JLabel();
            add(label);
            setUndecorated(true);
            setBackground(new Color(0, 0, 0, 0));
            setAlwaysOnTop(true);
            setFocusableWindowState(false);
            setFocusable(false);
            pack();

            addWindowListener(new WindowAdapter() {
                @Override
                public void windowOpened(WindowEvent e) {
                    System.out.println("Show me");
                    Timer timer = new Timer(timeout, new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            System.out.println("Hide me");
                            setVisible(false);
                            System.out.println("Return");
                            FramePool.INSTANCE.release(ImageFrame.this);
                        }
                    });
                    timer.start();
                }

            });
        }

        public void setTimeout(int timeout) {
            this.timeout = timeout;
        }

        public void setImage(Image image) {
            label.setIcon(new ImageIcon(image));
            pack();

            Rectangle bounds = new Rectangle(0, 0, 0, 0);

            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice gd = ge.getDefaultScreenDevice();

            GraphicsConfiguration gc = gd.getDefaultConfiguration();
            bounds = gc.getBounds();

            Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gc);

            bounds.x += insets.left;
            bounds.y += insets.top;
            bounds.width -= (insets.left + insets.right);
            bounds.height -= (insets.top + insets.bottom);

            int x = (int) (Math.random() * bounds.width);
            int y = (int) (Math.random() * bounds.height);

            if (x + getWidth() > bounds.width) {
                x = bounds.width - getWidth();
                if (x < 0) {
                    x = 0;
                }
            }
            if (y + getHeight() > bounds.height) {
                y = bounds.height - getHeight();
                if (y < 0) {
                    y = 0;
                }
            }

            setLocation(bounds.x + x, bounds.y + y);
        }

    }
}

最后,你设置任务...

public class ImageTask implements Runnable {

    private String name;
    private int timeout;

    public ImageTask(String name, int timeout) {
        this.name = name;
        this.timeout = timeout;
    }

    @Override
    public void run() {
        try {
            System.out.println(this);
            FramePool.ImageFrame frame = FramePool.INSTANCE.grab();
            frame.setImage(ImagePool.INSTANCE.grab(name));
            frame.setTimeout(timeout);
            frame.setVisible(true);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

现在,我采取了不同的方法。基本上,我只是将图像的名称传递给任务,然后任务利用帧和图像池来构建结果并将其显示在屏幕上。

对我来说,好处是隔离控制。要在屏幕上显示图像,我只需要知道ImageTask 和图像文件名即可。

为了我的测试,我从一个目录加载了一堆图像并显示它们。

ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
File[] files = path.listFiles((File pathname) -> pathname.getName().toLowerCase().endsWith(".png"));
for (File file : files) {
    scheduledExecutorService.scheduleWithFixedDelay(new ImageTask(file.getPath(), 250 + (int) (Math.random() * 1000)), 0, 3, TimeUnit.SECONDS);
}

一旦所有的对象创建过程顺利进行

这个想法有一些变化,例如,也许您不需要 FramePool 并且可以为每个 ImageTask 创建一个实例

【讨论】:

  • 我可以确认这是可行的。但是,我不明白您为什么要为每张图片添加一个新的ImageTask。为什么不创建一些任务(3-8)并更改其中的图像?我试图改变你的代码来做到这一点,但图像没有再次加载。我想在 5 个固定位置显示大约 50 张图像,我认为为此安排 50 个任务不是一个好主意。
  • “我认为为此安排 50 个任务不是一个好主意” - 为什么?它是一个单线程执行器,因此一次只会执行一个任务。即使您使用具有 3-5 个线程的池执行器,我仍然没有看到问题
  • @TraderJosh “但是,我不明白你为什么要为每个图像添加一个新的 ImageTask。为什么不创建一些任务 (3-8) 并更改其中的图像?”* -因为尝试让它工作很痛苦——试图找出一项任务何时完成,然后尝试回收它,所以拥有一项只完成一项工作的任务要容易得多。当然,您可以创建一个“批处理”任务,它需要一张以上的图像,但您不会获得更多好处。看来您正在尝试过早地优化解决方案,等到实际出现问题时
猜你喜欢
  • 1970-01-01
  • 2015-05-24
  • 2019-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多