Java 调整帧大小时渲染图像的时间长得可笑

Java 调整帧大小时渲染图像的时间长得可笑,java,image,performance,swing,Java,Image,Performance,Swing,我目前正在开发一款纸牌游戏《芒奇金》,供我和我的朋友在流感大流行期间玩,并在发射器上显示芒奇金标志。该代码实际上可以很好地将徽标设置在它应该位于的位置 但是,我在调整JFrame事件的大小时遇到了一个问题:最初,当将帧大小调整到其最小大小时,图像加载大约需要5秒钟,并且仅在更新帧中包含的内容之后。在重新加载图像之前,内容在屏幕内仅部分可见,例如按钮仍在画面外 反复调整大小后,UI的加载时间似乎呈指数增长,这可能是因为图像的加载和缩放是针对缩放前后大小之间的多个帧大小执行的。此外,当新生成帧时,加

我目前正在开发一款纸牌游戏《芒奇金》,供我和我的朋友在流感大流行期间玩,并在发射器上显示芒奇金标志。该代码实际上可以很好地将徽标设置在它应该位于的位置

但是,我在调整JFrame事件的大小时遇到了一个问题:最初,当将帧大小调整到其最小大小时,图像加载大约需要5秒钟,并且仅在更新帧中包含的内容之后。在重新加载图像之前,内容在屏幕内仅部分可见,例如按钮仍在画面外

反复调整大小后,UI的加载时间似乎呈指数增长,这可能是因为图像的加载和缩放是针对缩放前后大小之间的多个帧大小执行的。此外,当新生成帧时,加载图像需要可感知的时间量,如0.5秒

为了更清楚地说明我的意思,以下是它最初和调整大小后的外观:

之前:

之后:

为了让您了解一些情况,下面是执行所有UI操作的代码:

package de.munchkin.frontend;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class MunchkinLauncher extends JFrame{

    private static final long serialVersionUID = 1598309638008663371L;
    
    private Toolkit toolkit = Toolkit.getDefaultToolkit();
    private Dimension screenDim = toolkit.getScreenSize();
    private JPanel contentPane;
    private JButton btnHostGame, btnJoinGame;
    private JLabel imageLabel;
    
    
    public static void main(String[] args) {
        new MunchkinLauncher();
    }
    
    public MunchkinLauncher() {
        setTitle("Munchkin Launcher");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocation(screenDim.width / 4, screenDim.height / 4);
        setSize(screenDim.width / 2, screenDim.height / 2);
        setMinimumSize(new Dimension(700, 387));
        
        contentPane = new JPanel(null);
        contentPane.setLayout(null);
        setLayout(null);
        setContentPane(contentPane);
        contentPane.setBackground(new Color(253, 205, 136));
        
        setVisible(true);
        
        loadComponents();
        loadBounds();
        addActionListeners();
        
    }
    
    private void loadComponents() {
        
        // Load header image
        imageLabel = new JLabel("");
        
        btnHostGame = new JButton("Host Game");
        
        btnJoinGame = new JButton("Join Game");
        
    }
    
    private void loadBounds() {
        
        int width = contentPane.getSize().width;
        int height = contentPane.getSize().height;
        
        btnHostGame.setBounds(width/2 - 300, height * 2 / 3, 250, 100);
        contentPane.add(btnHostGame);
        
        btnJoinGame.setBounds(width/2 + 50, height * 2 / 3, 250, 100);
        contentPane.add(btnJoinGame);
        
        imageLabel.setIcon(new ImageIcon(new ImageIcon(this.getClass().getResource("/Munchkin_logo.jpg"))
                .getImage().getScaledInstance(width - 40, height * 2 / 3 - 40, Image.SCALE_DEFAULT)));
        imageLabel.setBounds(20, 0, width - 40, height * 2 / 3);
        contentPane.add(imageLabel);
        
        revalidate();
        repaint();
        
    }
    
    private void addActionListeners() {
        
        addComponentListener(new ComponentAdapter() {
            
            @Override
            public void componentResized(ComponentEvent e) {
                super.componentResized(e);
                loadBounds();
            }
            
        });
        
        btnHostGame.addActionListener(e -> {
            new MatchCreation();
            dispose();
        });
        
        btnJoinGame.addActionListener(e -> {
            new MatchJoin();
            dispose();
        });
        
    }
    
    
}
我还尝试在loadComponents中单独加载内部ImageIcon资源本身,以减少加载时间,但在我看来,这样做似乎需要更长的时间来更新。我认为这可能与图像的原始大小有关,因为它的分辨率为4961 x 1658 px。我还尝试了不同的缩放算法,还使用了Image.SCALE\u FAST,这并没有明显加快这个过程

最让我困惑的是,这个过程是在高端硬件3900X+2080Ti上执行的,这意味着中端或低端PC可能需要更长的时间来加载和更新它,这让我觉得更荒谬。如果没有其他的解决方案,我想即使是基本的2D游戏在当今硬件上也会像地狱一样落后,而他们显然没有


加载用于调整大小的图像时,是否有我缺少的最佳做法,或者通常如何解决此问题?

因此,我重新编写了部分代码

如果您运行旧代码并添加了一些输出,那么在输出中您将看到实际问题所在:实际完成大小调整的次数。 第二个问题是:如果将GetScaleInstance与new ImageIcon一起使用,ImageIcon初始化将等待图片完全创建/绘制完成。这将阻止所有其他UI操作,直到完成

另一个小问题是,您将控件重新添加到它们的父控件,这也可能导致大量不必要的重新布局和重新绘制

更简单的方法是使用/编写ImagePanel。 Graphics.drawImage方法有一个ImageObserver。 无论何时调用drawImage,都可以非阻塞地异步绘制大型图像,因此不会有如此可怕的加载时间。 另外,它将自己执行验证等操作,因此您不需要对此进行任何调用,这可能会导致不必要的布局和重新绘制

在下面的示例中,我还包括了一些额外的提示,如使用“final”、“early initialization”和“fail fast fail early”

因此,请注意解决方案是多么简单:

例如:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class MunchkinLauncher extends JFrame {

    private static final long serialVersionUID = 1598309638008663371L;

    private final Toolkit       toolkit     = Toolkit.getDefaultToolkit();
    private final Dimension     screenDim   = toolkit.getScreenSize();
    private final JPanel        contentPane = new JPanel(null);
    private final JButton       btnHostGame = new JButton("Host Game");     // make as many 'final' as possible. earlier initialization saves you from a lot of problems when it comes to inheritance and events
    private final JButton       btnJoinGame = new JButton("Join Game");
    private final ImagePanel    imageLabel  = new ImagePanel();

    private final Image backgroundImageicon; // pardon my camelCase, imo composite logic is more important


    public static void main(final String[] args) {
        new MunchkinLauncher().setVisible(true);
    }

    public MunchkinLauncher() {
        setDefaultCloseOperation(EXIT_ON_CLOSE); // should always be first, you never know what exception happens next and this might keep a lot of JVMs running if due to exception a visual jframe is not disposed properly
        setTitle("Munchkin Launcher");
        setLocation(screenDim.width / 4, screenDim.height / 4);
        setSize(screenDim.width / 2, screenDim.height / 2);
        setMinimumSize(new Dimension(700, 387));
        setLayout(null);

        contentPane.setLayout(null);
        contentPane.setBackground(new Color(253, 205, 136));
        contentPane.add(imageLabel);
        contentPane.add(btnHostGame);
        contentPane.add(btnJoinGame);
        setContentPane(contentPane);

        loadComponents();

        backgroundImageicon = loadBgImage();
        imageLabel.setImage(backgroundImageicon);

        loadBounds();

        addActionListeners();

        //      setVisible(true); // should be last, unless you need something specific to happen durint init
        // and ACTUALLY it should be used outside the CTOR so caller has more control
    }

    private Image loadBgImage() {
        final long nowMs = System.currentTimeMillis();
        final URL res = this.getClass().getResource("/Munchkin_logo.jpg");

        // load Image
        //      final ImageIcon ret1 = new ImageIcon(res); // is not as good, we eventually only need an Image, we do not need the wrapper that ImageIcon provides
        //      final BufferedImage ret2 = ImageIO.read(res); // is better, but gives us BufferedImage, which is also more than we need. plus throws IOException
        final Image ret3 = Toolkit.getDefaultToolkit().getImage(res); // best way. If behaviour plays a role: Loads the image the same way that ImageIcon CTOR would.

        final long durMs = System.currentTimeMillis() - nowMs;
        System.out.println("Loading BG Image took " + durMs + "ms.");

        return ret3;
    }

    private void loadComponents() {}

    void loadBounds() {
        final int width = contentPane.getSize().width;
        final int height = contentPane.getSize().height;
        btnHostGame.setBounds(width / 2 - 300, height * 2 / 3, 250, 100);
        btnJoinGame.setBounds(width / 2 + 50, height * 2 / 3, 250, 100);
        imageLabel.setBounds(20, 0, width - 40, height * 2 / 3);
    }

    private void addActionListeners() {
        addComponentListener(new ComponentAdapter() {
            @Override public void componentResized(final ComponentEvent e) {
                super.componentResized(e);
                loadBounds(); // this access needs protected loadBounds() or synthetic accessor method
            }
        });
        btnHostGame.addActionListener(e -> {
            System.out.println("MunchkinLauncher.addActionListeners(1)");
            //          new MatchCreation();
            dispose();
        });
        btnJoinGame.addActionListener(e -> {
            System.out.println("MunchkinLauncher.addActionListeners(2)");
            //          new MatchJoin();
            dispose();
        });
    }
}
以及ImagePanel:

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;

import javax.swing.JPanel;

public class ImagePanel extends JPanel {


    private static final long serialVersionUID = 6196514831308649310L;



    private Image mImage;

    public ImagePanel(final Image pImage) {
        mImage = pImage;
    }
    public ImagePanel() {}


    public Image getImage() {
        return mImage;
    }
    public void setImage(final Image pImage) {
        mImage = pImage;
        repaint();
    }



    @Override protected void paintComponent(final Graphics pG) {
        if (mImage == null) return;

        final Graphics2D g = (Graphics2D) pG;
        g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);// can add more
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.drawImage(mImage, 0, 0, getWidth(), getHeight(), this);
    }
}
编辑: 另一方面,您应该将嵌入图像的大小限制在合理的大小,例如,如果在全屏显示中使用4K。作为窗口控件显示,720p通常就足够了。
其他一切只会让你的软件膨胀,让它变慢。在很多情况下,建议使用矢量图形。

因此,我重新编写了部分代码

如果您运行旧代码并添加了一些输出,那么在输出中您将看到实际问题所在:实际完成大小调整的次数。 第二个问题是:如果将GetScaleInstance与new ImageIcon一起使用,ImageIcon初始化将等待图片完全创建/绘制完成。这将阻止所有其他UI操作,直到完成

另一个小问题是,您将控件重新添加到它们的父控件,这也可能导致大量不必要的重新布局和重新绘制

更简单的方法是使用/编写ImagePanel。 Graphics.drawImage方法有一个ImageObserver。 无论何时调用drawImage,都可以非阻塞地异步绘制大型图像,因此不会有如此可怕的加载时间。 另外,它将自己执行验证等操作,因此您不需要对此进行任何调用,这可能会导致不必要的布局和重新绘制

在下面的示例中,我还包括了一些额外的提示,如使用“final”、“early initialization”和“fail fast fail early”

因此,请注意解决方案是多么简单:

例如:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class MunchkinLauncher extends JFrame {

    private static final long serialVersionUID = 1598309638008663371L;

    private final Toolkit       toolkit     = Toolkit.getDefaultToolkit();
    private final Dimension     screenDim   = toolkit.getScreenSize();
    private final JPanel        contentPane = new JPanel(null);
    private final JButton       btnHostGame = new JButton("Host Game");     // make as many 'final' as possible. earlier initialization saves you from a lot of problems when it comes to inheritance and events
    private final JButton       btnJoinGame = new JButton("Join Game");
    private final ImagePanel    imageLabel  = new ImagePanel();

    private final Image backgroundImageicon; // pardon my camelCase, imo composite logic is more important


    public static void main(final String[] args) {
        new MunchkinLauncher().setVisible(true);
    }

    public MunchkinLauncher() {
        setDefaultCloseOperation(EXIT_ON_CLOSE); // should always be first, you never know what exception happens next and this might keep a lot of JVMs running if due to exception a visual jframe is not disposed properly
        setTitle("Munchkin Launcher");
        setLocation(screenDim.width / 4, screenDim.height / 4);
        setSize(screenDim.width / 2, screenDim.height / 2);
        setMinimumSize(new Dimension(700, 387));
        setLayout(null);

        contentPane.setLayout(null);
        contentPane.setBackground(new Color(253, 205, 136));
        contentPane.add(imageLabel);
        contentPane.add(btnHostGame);
        contentPane.add(btnJoinGame);
        setContentPane(contentPane);

        loadComponents();

        backgroundImageicon = loadBgImage();
        imageLabel.setImage(backgroundImageicon);

        loadBounds();

        addActionListeners();

        //      setVisible(true); // should be last, unless you need something specific to happen durint init
        // and ACTUALLY it should be used outside the CTOR so caller has more control
    }

    private Image loadBgImage() {
        final long nowMs = System.currentTimeMillis();
        final URL res = this.getClass().getResource("/Munchkin_logo.jpg");

        // load Image
        //      final ImageIcon ret1 = new ImageIcon(res); // is not as good, we eventually only need an Image, we do not need the wrapper that ImageIcon provides
        //      final BufferedImage ret2 = ImageIO.read(res); // is better, but gives us BufferedImage, which is also more than we need. plus throws IOException
        final Image ret3 = Toolkit.getDefaultToolkit().getImage(res); // best way. If behaviour plays a role: Loads the image the same way that ImageIcon CTOR would.

        final long durMs = System.currentTimeMillis() - nowMs;
        System.out.println("Loading BG Image took " + durMs + "ms.");

        return ret3;
    }

    private void loadComponents() {}

    void loadBounds() {
        final int width = contentPane.getSize().width;
        final int height = contentPane.getSize().height;
        btnHostGame.setBounds(width / 2 - 300, height * 2 / 3, 250, 100);
        btnJoinGame.setBounds(width / 2 + 50, height * 2 / 3, 250, 100);
        imageLabel.setBounds(20, 0, width - 40, height * 2 / 3);
    }

    private void addActionListeners() {
        addComponentListener(new ComponentAdapter() {
            @Override public void componentResized(final ComponentEvent e) {
                super.componentResized(e);
                loadBounds(); // this access needs protected loadBounds() or synthetic accessor method
            }
        });
        btnHostGame.addActionListener(e -> {
            System.out.println("MunchkinLauncher.addActionListeners(1)");
            //          new MatchCreation();
            dispose();
        });
        btnJoinGame.addActionListener(e -> {
            System.out.println("MunchkinLauncher.addActionListeners(2)");
            //          new MatchJoin();
            dispose();
        });
    }
}
以及ImagePanel:

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;

import javax.swing.JPanel;

public class ImagePanel extends JPanel {


    private static final long serialVersionUID = 6196514831308649310L;



    private Image mImage;

    public ImagePanel(final Image pImage) {
        mImage = pImage;
    }
    public ImagePanel() {}


    public Image getImage() {
        return mImage;
    }
    public void setImage(final Image pImage) {
        mImage = pImage;
        repaint();
    }



    @Override protected void paintComponent(final Graphics pG) {
        if (mImage == null) return;

        final Graphics2D g = (Graphics2D) pG;
        g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);// can add more
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.drawImage(mImage, 0, 0, getWidth(), getHeight(), this);
    }
}
编辑: 另一方面,你应该限制 如果在全屏显示中使用,则可以将嵌入的图像大小设置为合理的大小,例如4K。作为窗口控件显示,720p通常就足够了。
其他一切只会让你的软件膨胀,让它变慢。在很多情况下,建议使用矢量图形。

不确定,因为在这方面没有经验,但请看这里:@JanosVinceller谢谢,这是一个开始的想法。但除此之外,我认为Java应该能够比这种开箱即用的方式更快地执行这样的操作。我无法想象香草Java在高端硬件上需要几百毫秒或几秒钟的时间来完成一个需要其他库(未知库为80ms)的操作,如果是openCV,甚至只需要13ms,正如您链接的帖子的示例答案所示。实际上,考虑到原始尺寸更大,缩放尺寸比我的情况小得多。这感觉不太对劲。@JanosVinceller我也不确定我是否能正确地集成它,因为JLabel使用了一个ImageIcon,而链接文章中的方法使用的是BuffereImage。这里至少有一部分问题是,在每次调整组件大小时,您不仅要重新渲染,而且还要重新加载和缩放整个图像。考虑到图像的大小,这可能会浪费相当多的内存,从而导致过多的垃圾收集和缓慢的UI响应。相反,在构造器中读取一次图像,可能最好不要使用GetScaleInstance和ImageIcon,而是使用带有图像背景的自定义JPanel。请参阅@JanosVinceller的链接。我肯定会在关键字中添加swing。将另外删除jlabel和imageicon。不确定,因为在这方面没有经验,但请看这里:@JanosVinceller谢谢,这是一个开始的想法。但除此之外,我认为Java应该能够比这种开箱即用的方式更快地执行这样的操作。我无法想象香草Java在高端硬件上需要几百毫秒或几秒钟的时间来完成一个需要其他库(未知库为80ms)的操作,如果是openCV,甚至只需要13ms,正如您链接的帖子的示例答案所示。实际上,考虑到原始尺寸更大,缩放尺寸比我的情况小得多。这感觉不太对劲。@JanosVinceller我也不确定我是否能正确地集成它,因为JLabel使用了一个ImageIcon,而链接文章中的方法使用的是BuffereImage。这里至少有一部分问题是,在每次调整组件大小时,您不仅要重新渲染,而且还要重新加载和缩放整个图像。考虑到图像的大小,这可能会浪费相当多的内存,从而导致过多的垃圾收集和缓慢的UI响应。相反,在构造器中读取一次图像,可能最好不要使用GetScaleInstance和ImageIcon,而是使用带有图像背景的自定义JPanel。请参阅@JanosVinceller的链接。我肯定会在关键字中添加swing。将另外删除jlabel和imageicon。非常感谢,这非常有效。关于您对ComponentListener中loadBounds调用的评论,我有一个问题:为什么需要保护它?将其设置为private似乎不会改变任何性能方面的变化,那么这背后的原因是什么呢?我认为如果继承起作用,则仅使用protected(受保护)是很重要的,但该窗口不是这样,因为它不是从任何其他类继承的?第二个问题是:如果使用GetScaleInstance,该方法将等待图片完全创建/绘制。这将阻止所有其他UI操作,直到完成。-事实并非如此。API文档声明[…]即使原始源映像已完全加载,也可以异步加载新映像对象。实际上,整个java.awt.Image API是异步的。@Samaranth当您在匿名类“ComponentAdapter”中时,从技术上讲,您无法访问类MunchkinLauncher的私有成员,即使从技术上讲,您仍然“在”它里面。一些JVM不允许这种偏离标准的行为。因此,为了保持应用程序的兼容性,编译器意识到了这种情况,并且在编译时,它将创建一个额外的方法,即所谓的“合成访问器方法”,该方法允许ComponentAdapter.ComponentResistized中的代码访问私有方法MunchkinLauncher.loadBounds。寻找more@haraldK这是真的。我说得很糟糕。更好:ImageIcon使用自己的MediaTracker。一旦使用图像作为参数调用ImageIcon构造函数,ImageIcon将调用其内部的“loadImage”方法,该方法将运行到一个同步块中,并在其中等待MediaTracker完成图片加载。因此,在使用getScaledInstance之后再使用新的ImageIconImage将始终被阻止。这一点——以及多个重新调整尺寸——非常重要
是什么让渲染速度如此之慢。简单化的方法解决了这个问题。非常感谢,这非常有效。关于您对ComponentListener中loadBounds调用的评论,我有一个问题:为什么需要保护它?将其设置为private似乎不会改变任何性能方面的变化,那么这背后的原因是什么呢?我认为如果继承起作用,则仅使用protected(受保护)是很重要的,但该窗口不是这样,因为它不是从任何其他类继承的?第二个问题是:如果使用GetScaleInstance,该方法将等待图片完全创建/绘制。这将阻止所有其他UI操作,直到完成。-事实并非如此。API文档声明[…]即使原始源映像已完全加载,也可以异步加载新映像对象。实际上,整个java.awt.Image API是异步的。@Samaranth当您在匿名类“ComponentAdapter”中时,从技术上讲,您无法访问类MunchkinLauncher的私有成员,即使从技术上讲,您仍然“在”它里面。一些JVM不允许这种偏离标准的行为。因此,为了保持应用程序的兼容性,编译器意识到了这种情况,并且在编译时,它将创建一个额外的方法,即所谓的“合成访问器方法”,该方法允许ComponentAdapter.ComponentResistized中的代码访问私有方法MunchkinLauncher.loadBounds。寻找more@haraldK这是真的。我说得很糟糕。更好:ImageIcon使用自己的MediaTracker。一旦使用图像作为参数调用ImageIcon构造函数,ImageIcon将调用其内部的“loadImage”方法,该方法将运行到一个同步块中,并在其中等待MediaTracker完成图片加载。因此,在使用getScaledInstance之后再使用新的ImageIconImage将始终被阻止。这——以及多次重新调整大小——是导致渲染速度如此缓慢的原因。简单化的方法解决了这个问题。