java的双缓冲技术

Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java
Appilication简单动画实例展开,对屏幕闪烁的原因进行了分析,找出了闪烁成因的关键:update(Graphics g)函数对于前端屏幕的清屏。由此引出消除闪烁的方法——双缓冲。双缓冲是计算机动画处理中的传统技术,在用其他语言编程时也可以实现。本文从实例出发,着重介绍了用双缓冲消除闪烁的原理以及双缓冲在Java中的两种常用实现方法(即在update(Graphics
g)中实现和在paint(Graphics g)中实现),以期读者能对双缓冲在Java编程中的应用能有个较全面的认识。

一、问题的引入

在编写Java多媒体动画程序或用Java编写游戏程序的时候,我们得到的动画往往存在严重的闪烁(或图片断裂)。这种闪烁虽然不会给程序的效果造成太大的影响,但着实有违我们的设计初衷,也给程序的使用者造成了些许不便。闪烁到底是什么样的呢?下面的JavaApplication再现了这种屏幕闪烁的情况:

代码段一,闪烁再现

[java] view
plain
 copy

  1. import java.awt.*;  
  2. import java.awt.event.*;  
  3. public class DoubleBuffer extends Frame//主类继承Frame类  
  4. {  
  5.     public paintThread pT;//绘图线程  
  6.     public int ypos=-80; //小圆左上角的纵坐标  
  7.     public DoubleBuffer()//构造函数  
  8.     {  
  9.        pT=new paintThread(this);  
  10.        this.setResizable(false);  
  11.        this.setSize(300,300); //设置窗口的首选大小  
  12.        this.setVisible(true); //显示窗口  
  13.        pT.start();//绘图线程启动  
  14.     }  
  15.    public void paint(Graphics scr) //重载绘图函数  
  16.    {  
  17.        scr.setColor(Color.RED);//设置小圆颜色  
  18.        scr.fillOval(90,ypos,80,80); //绘制小圆  
  19.     }  
  20.     public static void main(String[] args)  
  21.     {  
  22.        DoubleBuffer DB=new DoubleBuffer();//创建主类的对象  
  23.        DB.addWindowListener(new WindowAdapter()//添加窗口关闭处理函数  
  24.        {  
  25.            public void windowClosing(WindowEvent e)  
  26.            {  
  27.               System.exit(0);  
  28.            }});  
  29.     }  
  30. }  
  31. class paintThread extends Thread//绘图线程类  
  32. {  
  33.     DoubleBuffer DB;  
  34.        public paintThread(DoubleBuffer DB) //构造函数  
  35.        {  
  36.            this.DB=DB;  
  37.        }  
  38.        public void run()//重载run()函数  
  39.        {  
  40.            while(true)//线程中的无限循环  
  41.            {  
  42.               try{  
  43.                   sleep(30); //线程休眠30ms  
  44.                   }catch(InterruptedException e){}  
  45.               DB.ypos+=5; //修改小圆左上角的纵坐标  
  46.               if(DB.ypos>300) //小圆离开窗口后重设左上角的纵坐标  
  47.                   DB.ypos=-80;  
  48.               DB.repaint();//窗口重绘  
  49.            }  
  50.        }  
  51. }  

编译、运行上述例子程序后,我们会看到窗体中有一个从上至下匀速运动的小圆,但仔细观察,你会发现小圆会不时地被白色的不规则横纹隔开,即所谓的屏幕闪烁,这不是我们预期的结果。

这种闪烁是如何出现的呢?

首先我们分析一下这段代码。DoubleBuffer的对象建立后,显示窗口,程序首先自动调用重载后的paint(Graphics g)函数,在窗口上绘制了一个小圆,绘图线程启动后,该线程每隔30ms修改一下小圆的位置,然后调用repaint()函数。

注意,这个repaint()函数并不是我们重载的,而是从Frame类继承而来的。它先调用update(Graphics
g)函数,update(Graphics g)再调用paint(Graphics g)函数
。问题就出在update(Graphics
g)函数,我们来看看这个函数的源代码:

[java] view
plain
 copy

  1. public void update(Graphics g)  
  2. {  
  3. if (isShowing())  
  4. {  
  5.         if (! (peer instanceof LightweightPeer))  
  6. {  
  7.              g.clearRect(0, 0, width, height);  
  8.          }  
  9.         paint(g);  
  10.     }  
  11. }  

以上代码的意思是:(如果该组件是轻量组件的话)先用背景色覆盖整个组件,然后再调用paint(Graphics g)函数,重新绘制小圆。这样,我们每次看到的都是一个在新的位置绘制的小圆,前面的小圆都被背景色覆盖掉了。这就像一帧一帧的画面匀速地切换,以此来实现动画的效果。

    但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。在两次看到不同位置小圆的中间时刻,总是存在一个在短时间内被绘制出来的空白画面(颜色取背景色)。但即使时间很短,如果重绘的面积较大的话花去的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。

    另外,用paint(Graphics g)函数在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图象,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。

    就像以前课堂上老师用的旧式的幻灯机,放完一张胶片,老师会将它拿下去,这个时候屏幕上一片空白,直到放上第二张,中间时间间隔较长。当然,这不是在放动画,但上述闪烁的产生原因和这很类似。

二、问题的解决

知道了闪烁产生的原因,我们就有了更具针对性的解决闪烁的方案。已经知道update(Graphics g)是造成闪烁的主要原因,那么就从这里入手。

(1)尝试这样重载update(Graphics g)函数(基于代码段一修改):

[java] view
plain
 copy

  1. public void update(Graphics scr)  
  2. {  
  3.     paint(scr);  
  4. }  

以上代码在重绘小圆之前没有用背景色重绘整个画面,而是直接调用paint(Graphics g)函数,这就从根本上避免了上述的那幅空白画面。

    看看运行结果,闪烁果然消除了!但是更大的问题出现了,不同时刻绘制的小圆重叠在一起形成了一条线!这样的结果我们更不能接受了。为什么会这样呢?仔细分析一下,重载后的update(Graphics g)函数中没有了任何清屏的操作,每次重绘都是在先前已经绘制好的图象的基础上,当然会出现重叠的现象了。

2)使用双缓冲:

这是本文讨论的重点。所谓双缓冲,就是在内存中开辟一片区域,作为后台图象,程序对它进行更新、修改,绘制完成后再显示到屏幕上。  

1、重载paint(Graphics g)实现双缓冲:

    这种方法要求我们将双缓冲的处理放在paint(Graphics g)函数中,那么具体该怎么实现呢?先看下面的代码(基于代码段一修改):

在DoubleBuffer类中添加如下两个私有成员:

[java] view
plain
 copy

  1. private Image iBuffer;  
  2. private Graphics gBuffer;  
  3. //重载paint(Graphics scr)函数:  
  4. public void paint(Graphics scr)  
  5. {  
  6.     if(iBuffer==null)  
  7.     {  
  8.        iBuffer=createImage(this.getSize().width,this.getSize().height);  
  9.        gBuffer=iBuffer.getGraphics();  
  10.     }  
  11.     gBuffer.setColor(getBackground());  
  12.     gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);  
  13.    gBuffer.setColor(Color.RED);  
  14.    gBuffer.fillOval(90,ypos,80,80);  
  15.     scr.drawImage(iBuffer,0,0,this);  
  16. }  

分析上述代码:我们首先添加了两个成员变量iBuffer和gBuffer作为缓冲(这就是所谓的双缓冲名字的来历)。在paint(Graphics
scr)函数中,首先检测如果iBuffer为null,则创建一个和屏幕上的绘图区域大小一样的缓冲图象,再取得iBuffer的Graphics类型的对象的引用,并将其赋值给gBuffer,然后对gBuffer这个内存中的后台图象先用fillRect(int,int,int,int)清屏,再进行绘制操作,完成后将iBuffer直接绘制到屏幕上。

    这段代码看似可以完美地完成双缓冲,但是,运行之后我们看到的还是严重的闪烁!为什么呢?回想上文所讨论的,问题还是出现在update(Graphics g)函数!这段修改后的程序中的update(Graphics
g)函数还是我们从父类继承的。在update(Graphics g)中,clearRect(int,int,int,int)对前端屏幕进行了清屏操作,而在paint(Graphics
g)中,对后台图象又进行了清屏操作。那么如果保留后台清屏,去掉多余的前台清屏应该就会消除闪烁。所以,我们只要按照(1)中的方法重载update(Graphics g)即可:

[java] view
plain
 copy

  1. public void update(Graphics scr)  
  2. {  
  3.     paint(scr);  
  4. }  

这样就避开了对前端图象的清屏操作,避免了屏幕的闪烁。虽然和(1)中用一样的方法重载update(Graphics g),但(1)中没有了清屏操作,消除闪烁的同时严重破坏了动画效果,这里我们把清屏操作放在了后台图象上,消除了闪烁的同时也获得了预期的动画效果。

2、重载update(Graphics g)实现双缓冲:

    这是比较传统的做法。也是实际开发中比较常用的做法。我们看看实现这种方法的代码(基于代码段一修改):

在DoubleBuffer类中添加如下两个私有成员:

[java] view
plain
 copy

  1. private Image iBuffer;  
  2. private Graphics gBuffer;  
  3. //重载paint(Graphics scr)函数:  
  4. public void paint(Graphics scr)  
  5. {  
  6.     scr.setColor(Color.RED);  
  7.     scr.fillOval(90,ypos,80,80);  
  8. }  

重载update(Graphics scr)函数:

[java] view
plain
 copy

  1. public void update(Graphics scr)  
  2. {  
  3.     if(iBuffer==null)  
  4.     {  
  5.        iBuffer=createImage(this.getSize().width,this.getSize().height);  
  6.        gBuffer=iBuffer.getGraphics();  
  7.     }  
  8.        gBuffer.setColor(getBackground());  
  9.        gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);  
  10.        paint(gBuffer);  
  11.        scr.drawImage(iBuffer,0,0,this);  
  12. }  

分析上述代码:我们把对后台图象的创建、清屏以及重绘等一系列动作都放在了update(Graphicsscr)函数中,而paint(Graphics g)函数只是负责绘制什么样的图象,以及怎样绘图,函数的最后实现了后台图象向前台绘制的过程。

运行上述修改后的程序,我们会看到完美的消除闪烁后的动画效果。就像在电影院看电影,每张胶片都是在后台准备好的,播放完一张胶片之后,下一张很快就被播放到前台,自然不会出现闪烁的情形。

    为了让读者能对双缓冲有个全面的认识现将上述双缓冲的实现概括如下:

(1)定义一个Graphics对象gBuffer和一个Image对象iBuffer。按屏幕大小建立一个缓冲对象给iBuffer。然后取得iBuffer的Graphics赋给gBuffer。此处可以把gBuffer理解为逻辑上的缓冲屏幕,而把iBuffer理解为缓冲屏幕上的图象。

(2)在gBuffer(逻辑上的屏幕)上用paint(Graphics
g)函数绘制图象。

(3)将后台图象iBuffer绘制到前台。

以上就是一次双缓冲的过程。注意,将这个过程联系起来的是repaint()函数。paint(Graphics g)是一个系统调用语句,不能由程序员手工调用。只能通过repaint()函数调用。

三、问题的扩展

1、关于闪烁的补充:

其实引起闪烁的不仅仅是上文提到的那样,多种物理因素也可以引起闪烁,无论是CRT显示器还是LCD显示器都存在闪烁的现象。本文只讨论软件编程引起的闪烁。但是即使双缓冲做得再好,有时也是会有闪烁,这就是硬件方面的原因了,我们只能修改程序中的相关参数来降低闪烁(比如让画面动得慢一点),而不是编程方法的问题。

2、关于消除闪烁的方法的补充:

    上文提到的双缓冲的实现方法只是消除闪烁的方法中的一种。如果在swing中,组件本身就提供了双缓冲的功能,我们只需要进行简单的函数调用就可以实现组件的双缓冲,在awt中却没有提供此功能。另外,一些硬件设备也可以实现双缓冲,每次都是先把图象画在缓冲中,然后再绘制在屏幕上,而不是直接绘制在屏幕上,基本原理还是和文中的类似的。还有其他用软件实现消除闪烁的方法,但双缓冲是个简单的、值得推荐的方法。

2、关于双缓冲的补充:

双缓冲技术是编写J2ME游戏的关键技术之一。双缓冲付出的代价是较大的额外内存消耗。但现在节省内存已经不再是程序员们考虑的最首要的问题了,游戏的画面在游戏制作中是至关重要的,所以以额外的内存消耗换取程序质量的提高还是值得肯定的。

3、双缓冲的改进:

有时动画中相邻的两幅画面只是有很少部分的不同,这就没必要每次都对整个绘图区进行清屏。我们可以对文中的程序进行修改,使之每次只对部分屏幕清屏,这样既能节省内存,又能减少绘制图象的时间,使动画更加连贯!

时间: 2024-10-24 23:33:44

java的双缓冲技术的相关文章

java-关于JAVA绘图双缓冲技术

问题描述 关于JAVA绘图双缓冲技术 package two; import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.Rectangle; import javax.swing.JFrame; public class G1P2 extends JFrame{ final int WIDTH = 900,HEIGHT = 650; double p1Speed = 0.5,p

java中的双缓冲技术

毕业设计有个远程协助功能,得到对方的屏幕后,老是会闪,很是不爽,今天用java的双缓冲技术解决了.代码如下,本类重写了Swing中的JLabel,当Label重绘时,会默认的调用它的update方法,主要用于清除界面,然后update方法会调用paint方法,再把界面画上去,所以我现在update方法中创建了一个Image和Graphics对象Image off_screen_buf和off_screen_gc同时设置其大小和MyLabel对象的大小一样,用于把要画的东东先绘制到后台内存中,然后

【求助】关于Java中的双缓冲技术

问题描述 我尝试写一个人物行走的动画.我使用了双缓冲技术,但是屏幕仍然会闪烁,希望大家能够帮我找到其中的错误.代码如下:(1)精灵类packagesprite;importjava.awt.*;importjava.awt.image.*;importjavax.swing.*;publicclassSprite{publicintm_posX=0,m_posY=0;privateImagepic[]=null;privateintmPlayID=0;booleanmFacus=true;pub

双缓冲技术实现Android 画板应用_Android

什么是双缓冲技术?双缓冲技术就是当用户操作界面完成后,会有一个缓冲区保存用户操作的结果. 为什么要使用双缓冲技术?拿Android 游戏开发来说,界面贞每次都是全部重画的,也就说画了新的,旧的就没了,所以需要使用双缓冲技术保存之前的内容. 如何实现双缓冲?使用一个Bitmap对象保留之前的画布即可. package com.example.phonegaptest; import android.content.Context; import android.graphics.Bitmap; i

Android采用双缓冲技术实现画板_Android

本文实例为大家分享了Android实现画板的具体代码,采用的技术是双缓冲技术,供大家参考,具体内容如下 1.双缓冲技术的概念 所谓的双缓冲技术其实很简单,当程序需要在指定的View上进行绘制时,程序并不需要直接绘制到该View组件,而是先绘制到一个内存中的Bitmap图片上(就是缓冲),等内存中的Bitmap绘制好之后,再一次性将Bitmap绘制到View组件上. 2.Android采用双缓冲实现画板  实现的思路: 1).定义一个内存中图片,将他作为缓冲区Bitmap cacheBitmap

使用双缓冲技术绘制曲线图

当数据量很大时,绘图可能需要几秒钟甚至更长的时间,而且有时还会出现闪烁现象,为了解决这些问题,可采用双缓冲技术来绘制曲线图. 双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度.双缓冲实现过程如下: 1.在内存中创建与画布一致的缓冲区 BufferBmp=new Graphics::TBitmap(); BufferBmp->Canvas->Handle=CreateCompatibleDC(Ca

使用双缓冲技术实现Android画板应用

双缓冲技术就是当用户操作界面完成后,会有一个缓冲区保存用户操作的结果. 为什么要使用双缓冲技术?拿Android 游戏开发来说,界面贞每次都是全部重画的,也就说画了新的,旧的就没了,所以需要使用双缓冲技术保存之前的内容. 如何实现双缓冲?使用一个Bitmap对象保留之前的画布即可. package com.example.phonegaptest; import android.content.Context; import android.graphics.Bitmap; import and

MFC小游戏,使用双缓冲技术但是有些位图无法释放,导致GDI泄露

问题描述 MFC小游戏,使用双缓冲技术但是有些位图无法释放,导致GDI泄露 用MFC写了一个类似于魔塔的小游戏,但是发现会产生GDI泄露,运行一段时间就会崩掉,但是有些位图文件不能通过deleteObject释放,自己搞不太明白,请教一下各位. 解决方案 hero等对象应该是成员变量吧.从代码来看,hero中的picture载入的图片是固定的,那么完全没有必要在OnPaint函数中进行重复调用,完全是浪费,应该将这些代码放到hero对象初始化的地方,一次性完成就可以了. 另外,cDC指针最好也R

android双缓冲技术实例详解

Android中的SurfaceView类就是双缓冲机制.因此,在进行Android游戏开发时应尽量使用SurfaceView而不要使用View,这样的话效率较高,并且SurfaceView的功能也更加完善.为了更容易的了解双缓冲技术,下面介绍用View实现双缓冲的方法. 在此需要说明一下,双缓冲的核心技术就是先通过setBitmap方法将要绘制的所有的图形绘制到一个Bitmap上,然后再来调用drawBitmap方法绘制出这个Bitmap,显示在屏幕上.其具体的实现代码如下: 先贴出View类