You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 03:44:57