Java Swing Graphics性能问题:图像移动卡顿如何优化?
Java图形程序移动卡顿优化方案
我正在学习Java图形编程,写了一个加载图像并绘制到屏幕上的程序,用户可以通过键盘移动图像。但图像移动时非常卡顿,不过程序的CPU和内存占用并不高:
$ ps -p $(pgrep java) -o %cpu,%mem,cmd %CPU %MEM CMD 5.5 0.3 java -jar app.jar
请问该如何优化解决卡顿问题?
环境详情
$ java --version java 21.0.2 2024-01-16 LTS Java(TM) SE Runtime Environment (build 21.0.2+13-LTS-58) Java HotSpot(TM) 64-Bit Server VM (build 21.0.2+13-LTS-58, mixed mode, sharing)
$ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 21.0.2, vendor: Oracle Corporation, runtime: /usr/lib/jvm/jdk-21-oracle-x64 Default locale: en_US, platform encoding: UTF-8 OS name: "linux", version: "6.5.0-15-generic", arch: "amd64", family: "unix"
import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; import javax.swing.*; class Character { int x, y; BufferedImage image; public Character(String sprite_sheet, int x, int y) { this.x = x; this.y = y; try { image = ImageIO.read(new File(sprite_sheet)); image = image.getSubimage(0, 0, 16, 16); Image tmp = image.getScaledInstance(64, 64, Image.SCALE_SMOOTH); image = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); image.getGraphics().drawImage(tmp, 0, 0, null); } catch (IOException e) { e.printStackTrace(); } } public void draw(Graphics g) { g.drawImage(image, x, y, null); } public void move(int dx, int dy) { x += dx; y += dy; } } class KeyboardState { boolean left, right, up, down; } class Game extends JPanel { Character character; KeyboardState keyboardState; int numPaints = 0; @Override public void paintComponent(Graphics g) { super.paintComponent(g); if (character != null) { character.draw(g); } numPaints++; } public void moveCharacter(int dx, int dy) { if (character != null) { character.move(dx, dy); } } public void update() { if (keyboardState.left) { character.move(-5, 0); } if (keyboardState.right) { character.move(5, 0); } if (keyboardState.up) { character.move(0, -5); } if (keyboardState.down) { character.move(0, 5); } repaint(); } public void start() { Timer timer = new Timer(1000 / 30, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { update(); } }); timer.start(); } } public class Main { public static void main(String[] args) { JFrame frame = new JFrame("TODO"); Game game = new Game(); game.character = new Character( "sprite-sheet.png", 100, 100); game.keyboardState = new KeyboardState(); frame.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { game.keyboardState.left = true; } else if (key == KeyEvent.VK_RIGHT) { game.keyboardState.right = true; } else if (key == KeyEvent.VK_UP) { game.keyboardState.up = true; } else if (key == KeyEvent.VK_DOWN) { game.keyboardState.down = true; } else if (key == KeyEvent.VK_ESCAPE) { System.out.println("Num paints: " + game.numPaints); System.exit(0); } } @Override public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { game.keyboardState.left = false; } else if (key == KeyEvent.VK_RIGHT) { game.keyboardState.right = false; } else if (key == KeyEvent.VK_UP) { game.keyboardState.up = false; } else if (key == KeyEvent.VK_DOWN) { game.keyboardState.down = false; } } }); frame.add(game); frame.setSize(800, 600); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); game.start(); } }
卡顿原因及优化方案
1. 修复键盘事件焦点问题
KeyListener依赖组件获取焦点,当前把监听器加在JFrame上,内部的Game面板可能没拿到焦点,导致键盘事件响应延迟。
解决方法:
把监听器移到Game面板上,并设置面板可获取焦点:
// 替换Main类中原有的frame.addKeyListener代码 game.setFocusable(true); game.requestFocusInWindow(); game.addKeyListener(new KeyAdapter() { // 原有的keyPressed、keyReleased逻辑不变 });
2. 启用显式双缓冲
虽然JPanel默认开启双缓冲,但显式确认可以避免平台差异导致的问题,同时减少背景重绘开销:
在Game类中添加构造方法:
public Game() { setDoubleBuffered(true); }
3. 优化定时器与渲染逻辑分离
javax.swing.Timer的回调在EDT线程执行,若线程有其他任务会导致定时不准。改用单独线程处理更新,渲染仍在EDT执行,保证流畅性:
修改Game的start方法:
public void start() { new Thread(() -> { long lastTime = System.nanoTime(); final double nsPerFrame = 1_000_000_000.0 / 60; // 锁定60帧 double delta = 0; while (true) { long now = System.nanoTime(); delta += (now - lastTime) / nsPerFrame; lastTime = now; // 累积足够时间再执行更新 while (delta >= 1) { update(); delta--; } // 确保渲染在EDT线程执行 SwingUtilities.invokeLater(this::repaint); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }
4. 优化图像缩放效率
原有的getScaledInstance是异步操作,改用Graphics2D直接缩放更高效:
修改Character构造方法中的缩放代码:
// 替换原有的缩放逻辑 BufferedImage scaledImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = scaledImage.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.drawImage(image, 0, 0, 64, 64, null); g2d.dispose(); image = scaledImage;
5. 减少不必要的重绘区域
每次调用repaint()会重绘整个面板,改为只重绘角色的旧位置和新位置:
修改Game的update方法:
public void update() { int oldX = character.x; int oldY = character.y; int size = 64; // 角色的宽高 // 原有的移动逻辑 if (keyboardState.left) character.move(-5, 0); if (keyboardState.right) character.move(5, 0); if (keyboardState.up) character.move(0, -5); if (keyboardState.down) character.move(0, 5); // 只重绘需要更新的区域 repaint(oldX, oldY, size, size); repaint(character.x, character.y, size, size); }
内容的提问来源于Stack Exchange,提问作者Display name
相关产品推荐
相关产品推荐

