【问题标题】:Java Swing Timer slower than expectedJava Swing Timer 比预期慢
【发布时间】:2014-06-17 07:57:31
【问题描述】:

我正在编写一个必须每隔一段时间重新绘制的摇摆程序。出于某种原因,javax.swing.Timer 似乎每 800-900 毫秒才重新绘制一次,即使我指定了较低的延迟。 (例如 100 毫秒)我认为延迟可能是由于 repaint() 方法需要 800 毫秒才能运行,但我对其进行了计时,它只需要 0.3 毫秒。我尝试使用 Thread.sleep() 并以所需的时间间隔重新绘制。谁能帮忙解释一下为什么会这样?

我正在尝试做的似乎是javax.swing.Timer 的预期目的,所以我很困惑为什么它会减慢代码的速度。 主驱动类:

import java.awt.Dimension;

import javax.swing.JFrame;


public class GUIDriver{

public AnimationPanel animationPanel;

public static void main(String[] args){
    GUIDriver gui = new GUIDriver();
    gui.createAndShowGUI();

}

public void createAndShowGUI(){
    animationPanel = AnimationPanel(50);
    JFrame frame = new JFrame("Animations");

    frame.setContentPane(animationPanel);
    frame.setPreferredSize(new Dimension(1015, 840));

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    frame.pack();
    frame.validate();
    frame.doLayout();
    frame.setVisible(true);
    animationPanel.draw(); //only here when using Thread.sleep() method
}


}

自定义JPanel 扩展:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
import java.text.SimpleDateFormat;

import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.Timer;

/**
 * This extension of the JPanel overrides the paintComponent method to provide a     framework for
 * creating 2-D animations.
 */
@SuppressWarnings("serial")
public class AnimationPanel extends JPanel implements ActionListener{

public ArrayList<LeakInstance> data;
public Timer timer;
public SimpleDateFormat dateOutput = new SimpleDateFormat("MM/dd/yyyy");
BufferedImage background;


public Color lineColor = Color.black;
public Color under = Color.blue;
public Color over = Color.red;
public Font defaultFont = new Font("default", Font.BOLD, 14);

public float cutoff = 50;

public int timeStep = 0;
public long startTime = 0;

public Graphics2D screen;

public AnimationPanel(float cutoff) {
    super();
    setLayout(null);
    read("Data.txt");
    this.cutoff = cutoff;
    timer = new Timer(100, this); //commented out when using Thread.sleep()
    try {
        background = ImageIO.read(new File("img.png"));
    } catch (IOException e) {
        e.printStackTrace();
    }
    repaint();
    timer.start(); //commented out when using Thread.sleep()
}


/**
 * This method overrides JPanel's paintComponent method in order to customize the rendering of 2-D graphics
 * and thus make animation possible. It is not called directly; it is called by repaint() which fully refreshes
 * the screen.
 */
@Override
public void paintComponent(Graphics g) {
    ArrayList<String> drawn = new ArrayList<String>();
    screen = (Graphics2D)g;
    RenderingHints renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
    renderHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    screen.setRenderingHints(renderHints);


    screen.drawImage(background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH), 0, 0, this);

    g.setFont(defaultFont);

    screen.setColor(Color.orange);
    screen.fillRect(485, 735, 100, 20); //cover old date
    screen.setColor(lineColor);
    screen.drawString(dateOutput.format(data.get(timeStep).getDate()), 500, 750);
    screen.drawString(Integer.toString(timeStep), 300, 750);
    System.out.println((System.nanoTime() - startTime)/1e9f);
    startTime = System.nanoTime();

    float x, y;
    float z;
    int xoffset, yoffset;
    for(int i = 0; drawn.size() < 24 && (timeStep-i) > -1; i++){
        if(!drawn.contains(data.get(timeStep-i).getName())){
            xoffset = 0;
            yoffset = 15;
            String name = data.get(timeStep-i).getName();
            drawn.add(name);
            x = data.get(timeStep-i).getLocation().x;
            y = data.get(timeStep-i).getLocation().y;
            z = data.get(timeStep-i).getZ();

            if(z > cutoff)
                screen.setColor(over);
            else
                screen.setColor(under);
            switch(name){
                            //various cases to change x or y offset
            }
            screen.drawString(Float.toString(z), x+xoffset, y+yoffset);
            screen.setColor(lineColor);
            screen.drawLine((int)x-2, (int)y, (int)x+2,(int) y);
            screen.drawLine((int)x, (int)y-2, (int)x, (int)y+2);
        }
    }
}

public void draw(){
    try{
        for(; timeStep < data.size()-1; timeStep++){
            Thread.sleep(100);
            repaint();
        }
    }catch(Exception e){
        e.printStackTrace();
    }
}

public void read(String filename){
    File file = new File(filename);
    data = new ArrayList<MyType>(100);
    try(Scanner scan = new Scanner(file)){
        while(scan.hasNextLine())
            data.add(new MyType(scan.next(), scan.next(), scan.nextFloat(),
                    new Point2D.Float(scan.nextFloat(), scan.nextFloat())));
    }catch (Exception e){
        e.printStackTrace();
    }
    Collections.sort(data);
}


@Override
public void actionPerformed(ActionEvent e) {
    if(e.getSource().equals(timer)){
        if(timeStep < data.size()-1){
            timeStep++;
            repaint();
        }
        else
            timer.stop();
    }
}
 }

【问题讨论】:

  • 能否发布完整的可运行代码?
  • javax.swing.timer 函数旨在在计时器从启动时达到特定时间时执行特定操作,而 sleep 函数只是将程序暂停一定时间,因此它们不会t 完全设计为能够相互替换。
  • 您是否尝试过使用ScheduledExecutorService.scheduleAtFixedRatedocs.oracle.com/javase/7/docs/api/java/util/concurrent/…
  • 尽量避免每次都使用“新字体”...也许它会在图形资源中进行一些扫描。
  • 根据您断开连接的代码 sn-ps,您的 Thread.sleep 示例根本不应该工作,因为它会阻塞 EDT。我无法制作绘图循环的头部或尾部。考虑提供一个可运行的示例来演示您的问题

标签: java swing timer


【解决方案1】:

我遇到的主要问题是

screen.drawImage(background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH), 0, 0, this);

这导致repaint 事件持续不断,并将更新重绘时间减少到大约 0.25 毫秒

例如,当我预缩放图像时(我必须将其类型更改为Image)...

try {
    background = ImageIO.read(new File("C:\\Users\\Shane Whitehead\\Dropbox\\Wallpapers\\5781217115_4182ee16da_o.jpg"));
    background = background.getScaledInstance(1000, 800, Image.SCALE_SMOOTH);
} catch (IOException e) {
    e.printStackTrace();
}

我能够获得 0.1 毫秒的重绘时间。

我尝试在 data 列表中使用 100, 1000 和 10, 000 元素没有太大问题(只是为了好玩,我尝试了 100, 000 并且仍然得到了 0.1 毫秒的重绘)

您需要特别注意确保绘制过程得到很好的优化

根据评论更新

但是它仍然没有解释为什么使用绘制方法 ~.3ms 导致计时器花费 ~800ms。或者为什么使用 javax.swing.Timer 似乎比使用慢一个数量级 线程.sleep()

其实可以,但是你需要了解事件调度线程、事件队列和RepaintManager是如何工作的。

基本上...

  • paintComponent被调用,你调用getScaledInstance,通过ImageObserver触发repaint请求。 getScaledInstance 很慢,慢了 100 毫秒。 RepaintManager 优化了重绘请求并尝试减少在 EDT 上发布的绘制事件的数量,这有时是好的,有时是坏的。这些绘制事件被放置在事件队列中
  • javax.swing.Timer 触发一个事件,这个事件被放到事件队列中,由 EDT 处理。现在,这就是它变得“有点奇怪”的地方。 javax.swing.Timer 进行了优化,使得 “如果 coalesce 为真(这是默认设置),则只允许一个 Runnable 在 EventQueue 上排队并处于挂起状态” - 所以如果有一个预先存在的“计时器”事件已在事件队列中,没有发布新事件。
  • actionPerformed 方法(最终)被调用,也是在 EDT 的上下文中,从处理其他事件中花费(即使是非常少的时间)...这一切加起来...
  • 和/或paintComponent 再次被调用,重复...

所以,可能发生了什么,getScaledInstance 引起的重复更新相距足够远,以防止 RepaintManager 优化这些调用,这给 EDT 带来了压力,Timer 正在滴答作响,但由于事件处理速度可能不够快,因此有些事件会被丢弃,从长远来看,这会导致“绘画”之间的距离更远。

Thread 方法之所以没有遇到这些问题,是因为它只会向 EDT 发送新的绘制请求,而不考虑事件队列的状态...

另外,我可以打破Thread 更新,这意味着在处理data 列表中的所有项目之前,不会绘制任何内容,请查看Initial Threads 了解更多详细信息

有关如何在 Swing 中进行绘画的更多详细信息,请查看Painting in AWT and Swing

您可能还想查看The Perils of Image.getScaledInstance()Java: maintaining aspect ratio of JPanel background image 了解有关缩放的更多详细信息...

更新了一些额外的测试

所以我加了...

long scaleStart = System.currentTimeMillis();
screen.drawImage(background.getScaledInstance(getWidth(), getHeight(), Image.SCALE_SMOOTH), 0, 0, this);
System.out.println("Scaled in: " + ((System.currentTimeMillis()- scaleStart) / 1000f));

为了测试缩放过程,这通常需要大约 200-240 毫秒。其余的绘制过程只增加了大约 10 毫秒。

我是在使用 timer.setCoalesce(false); 时执行此操作的,因此关闭合并没有额外的好处。

通过预缩放图像,我得到了 0.1 毫秒的恒定更新(有和没有timer.setCoalesce(false);

【讨论】:

  • 这确实加快了绘制方法的速度,谢谢。使用javax.swing.Timer,我现在可以足够快地迭代帧以供我使用。然而,它仍然没有解释为什么使用 ~.3ms 的绘制方法导致计时器需要 ~800ms。或者为什么使用javax.swing.Timer 似乎比使用Thread.sleep() 慢一个数量级。
  • 谢谢。我不确定我是否完全理解你在说什么(“getScaledInstance 很慢,慢了 100 毫秒”似乎是不真实的,因为使用不安全的Thread 方法比这更快)但是Timer 的想法是较慢,因为似乎有太多的事情卡在事件队列中。这解决了我的问题并让我对它有了一些了解,所以我会接受它作为答案,直到找到更好的答案,如果有的话(我怀疑没有)。
  • 即使使用Thread 而不是TimergetScaledInstance 也需要大约 200 毫秒
  • 如果您想在较短的时间内获得准确的读数,您真的应该使用System.nanoTime()Timing。但是,我同意 getScaledInstance 解决了我的问题。我重新计时,它确实占用了几乎所有时间步之间的时间。我想我的paintComponent 最初是通过愚蠢地只计时repaint 方法得到0.3ms,该方法被更频繁地调用。所以你原来的答案是对的,getScaledInstance 减慢了这两种方法的速度,我只是时间不正确。
  • 就个人而言,我认为我不需要 100 毫秒的纳秒精度
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-29
  • 2012-03-22
相关资源
最近更新 更多