Java 中的双缓冲模式:提升动画和图形性能
大约 4 分钟
也称为
- 缓冲切换
- 乒乓缓冲
双缓冲设计模式的意图
Java 中的双缓冲模式旨在通过使用两个缓冲区来减少渲染时间并提高图形或计算应用程序的性能。此模式对于平滑的图形渲染至关重要,常用于游戏开发和其他实时应用程序。
带有实际示例的双缓冲模式详细解释
现实世界中的例子
想象一家繁忙的餐厅厨房,厨师不断准备菜肴,服务员不断取走已准备好的菜肴为顾客服务。为了避免混乱和延误,餐厅使用双缓冲系统。他们有两个柜台:一个供厨师放置新准备好的菜肴,另一个供服务员取走菜肴。当厨师在一个柜台上放置准备好的菜肴时,服务员同时从另一个柜台上取走菜肴来服务。当服务员清空了他们的柜台上的所有菜肴时,他们会切换到厨师放置新准备好的菜肴的柜台,而厨师开始填充现在空着的柜台。这种系统确保了平稳且持续的工作流程,不会让任何一方闲置等待,从而最大限度地提高效率并最小限度地减少停机时间。
简单来说
它确保了在状态被增量修改时正确渲染的状态。它被广泛用于计算机图形。
维基百科说
在计算机科学中,多缓冲是指使用多个缓冲区来存储数据块,以便“读取器”能够看到完整(虽然可能已过时)的数据版本,而不是看到“写入器”正在创建的部分更新数据版本。它非常常用于计算机显示图像。
Java 中双缓冲模式的编程示例
一个典型的例子,也是每个游戏引擎都必须解决的问题,就是渲染。当游戏绘制用户看到的场景时,它是分段进行的——远处的山脉、起伏的丘陵、树木,依次绘制。如果用户像这样观看场景的增量绘制,那么一个连贯的世界幻觉就会被打破。场景必须平滑且快速地更新,显示一系列完整的帧,每帧都立即出现。双缓冲解决了这个问题。
Buffer
接口保证了缓冲区的基本功能。
public interface Buffer {
void clear(int x, int y);
void draw(int x, int y);
void clearAll();
Pixel[] getPixels();
}
Buffer
接口的一种实现。
public class FrameBuffer implements Buffer {
public static final int WIDTH = 10;
public static final int HEIGHT = 8;
private final Pixel[] pixels = new Pixel[WIDTH * HEIGHT];
public FrameBuffer() {
clearAll();
}
@Override
public void clear(int x, int y) {
pixels[getIndex(x, y)] = Pixel.WHITE;
}
@Override
public void draw(int x, int y) {
pixels[getIndex(x, y)] = Pixel.BLACK;
}
@Override
public void clearAll() {
Arrays.fill(pixels, Pixel.WHITE);
}
@Override
public Pixel[] getPixels() {
return pixels;
}
private int getIndex(int x, int y) {
return x + WIDTH * y;
}
}
我们支持黑白像素。
public enum Pixel {
WHITE,
BLACK
}
Scene
代表游戏场景,其中当前缓冲区已完成渲染。
@Slf4j
public class Scene {
private final Buffer[] frameBuffers;
private int current;
private int next;
public Scene() {
frameBuffers = new FrameBuffer[2];
frameBuffers[0] = new FrameBuffer();
frameBuffers[1] = new FrameBuffer();
current = 0;
next = 1;
}
public void draw(List<? extends Pair<Integer, Integer>> coordinateList) {
LOGGER.info("Start drawing next frame");
LOGGER.info("Current buffer: " + current + " Next buffer: " + next);
frameBuffers[next].clearAll();
coordinateList.forEach(coordinate -> {
var x = coordinate.getKey();
var y = coordinate.getValue();
frameBuffers[next].draw(x, y);
});
LOGGER.info("Swap current and next buffer");
swap();
LOGGER.info("Finish swapping");
LOGGER.info("Current buffer: " + current + " Next buffer: " + next);
}
public Buffer getBuffer() {
LOGGER.info("Get current buffer: " + current);
return frameBuffers[current];
}
private void swap() {
current = current ^ next;
next = current ^ next;
current = current ^ next;
}
}
现在,我们可以展示驱动双缓冲示例的 App
类。
@Slf4j
public class App {
public static void main(String[] args) {
final var scene = new Scene();
var drawPixels1 = List.of(
new MutablePair<>(1, 1),
new MutablePair<>(5, 6),
new MutablePair<>(3, 2)
);
scene.draw(drawPixels1);
var buffer1 = scene.getBuffer();
printBlackPixelCoordinate(buffer1);
var drawPixels2 = List.of(
new MutablePair<>(3, 7),
new MutablePair<>(6, 1)
);
scene.draw(drawPixels2);
var buffer2 = scene.getBuffer();
printBlackPixelCoordinate(buffer2);
}
private static void printBlackPixelCoordinate(Buffer buffer) {
StringBuilder log = new StringBuilder("Black Pixels: ");
var pixels = buffer.getPixels();
for (var i = 0; i < pixels.length; ++i) {
if (pixels[i] == Pixel.BLACK) {
var y = i / FrameBuffer.WIDTH;
var x = i % FrameBuffer.WIDTH;
log.append(" (").append(x).append(", ").append(y).append(")");
}
}
LOGGER.info(log.toString());
}
}
控制台输出
12:33:02.525 [main] INFO com.iluwatar.doublebuffer.Scene -- Start drawing next frame
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 0 Next buffer: 1
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Swap current and next buffer
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Finish swapping
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 1 Next buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Get current buffer: 1
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.App -- Black Pixels: (1, 1) (3, 2) (5, 6)
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Start drawing next frame
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 1 Next buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Swap current and next buffer
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Finish swapping
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 0 Next buffer: 1
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Get current buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.App -- Black Pixels: (6, 1) (3, 7)
何时在 Java 中使用双缓冲模式
- 实时应用:非常适合视频游戏、模拟和 GUI 应用程序,这些应用程序需要频繁且平滑的显示更新。
- 高计算任务:适用于需要密集数据准备的应用程序,能够实现并行处理和显示。
- 最大限度地减少延迟:有效地减少数据或图形显示中的延迟或卡顿。
Java 中双缓冲模式的实际应用
- 图形渲染引擎:广泛用于 2D 和 3D 渲染引擎,以确保流畅的动画和过渡。
- GUI 框架:增强用户界面的响应能力和平滑度。
- 模拟和建模:确保模拟中的实时更新,而不会中断正在进行的进程。
- 视频播放软件:通过在显示当前帧时预加载下一帧来提供无缝的视频播放。
双缓冲模式的优缺点
优点
- 流畅的用户体验:预渲染帧以提供流畅的动画和过渡。
- 性能优化:允许后台渲染,优化应用程序的整体性能。
- 最小化闪烁:减少图形应用程序中的闪烁和视觉伪像。
缺点
- 内存开销:需要为辅助缓冲区分配额外的内存,可能会增加内存使用量。
- 实现复杂性:增加了体系结构的复杂性,需要仔细管理缓冲区。
- 延迟:可能会引入轻微的延迟,因为数据必须在显示之前完全渲染到后缓冲区中。
相关的 Java 设计模式
- 三缓冲:双缓冲模式的扩展,使用三个缓冲区以进一步优化渲染并减少延迟。
- 生产者-消费者:双缓冲模式可以看作是生产者-消费者模式的变体,其中一个缓冲区被“生产”,而另一个被“消费”。
- 策略:通常与策略模式一起使用,根据运行时条件动态选择缓冲策略。