SurfaceView的使用以及空指针异常的处理

Surfaceview简介
1.什么是Surfaceview?
SurfaceView是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的surface。我们可以控制这个surface的格式和尺寸。SurfaceView控制这个surface绘制的位置。surface类直接继承Object,对应了一块屏幕缓冲区,每个窗口window对应一个Surface,任何View都是画在Surface上的,传统的view共享一块屏幕缓冲区。
2.Surfaceview的特点
首先,我们先来查看官方API对SurfaceView的介绍,如图1-1所示:

                                     图1-1
      Surfaceview的特性:内部维护了两个线程即主线程和渲染线程,渲染线程可以在主线程之外的线程中向屏幕上绘图。这样可以避免主线程因绘图任务繁重导致程序的阻塞,从而提高了程序的反应速度。在游戏开发中多用surfaceView,游戏的背景,任务,动作尽量在画布Canvas中绘制。这种双线程的设计模式,极大的消耗了CPU内存,为此,SurfaceView可见时才被创建,SurfaceView隐藏时便被销毁,从而达到节约内存的目的
3.Surfaceview的用法
      在项目中,我们常常会编写自己的SurfaceView,很少使用原生态的SurfaceView。不过这两种使用方法的内部原理是一样的。下面我们就使用原生态SurfaceView的过程做如下阐述:
1.创建SurfaceView对象
2.调用SurfaceView的getHolder方法,得到SurfaceView的控制器
3.SurfaceHolder添加回调函数,重写该回调函数的onCreate方法和onDestroy方法。
4.SurfaceView只在surfaceCreated和surfaceDestroyed之间有效,所以要确保渲染线程访问的是合法有效的Surface。

      但是我们在实际操作SurfaceView的时候,总会遇到空指针的问题,并且不好找问题出在哪,在下面我会总结了几个常见的会报空指针的情况,给大家参考下。


     熟悉Android的工程师都知道,在主线程中不可以做耗时操作,在子线程中不能更新UI,那么我们先做一个小例子来看看SurfaceView是否能在子线程更新UI呢?
首先打开Studio
一、设计布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.zhiyuan3g.surfaceviewdemo.MainActivity">
<SurfaceView
    android:id="@+id/sv_surfaceView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开启子线程更新UI"
        android:id="@+id/btnOK"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>
二、在MainActivity的Button按钮点击事件中,我们做一个在子线程中画圆的操作来展示在子线程中UI确实被更新了,在这面我们用到的是一个画圆的操作,每一次因为圆的半径在累加,所以能直观的看出效果,我还在注释中解释了MVC模式,画布,画笔,上色等,相信只要有安卓基础的人按照注释来就没有问题,下面代码中每一步都有注释,解释的十分明白(注意必须有释放画布的操作)。
 public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnOK:
                //根据点击事件开启子线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //从小到大,画一个100的圆,为了能够看到效果,所以画一个圆就睡上一段时间
                        for (int x = 0; x < 100; x++) {
                            //MVC m:mode 模型 v:View 视图  c:Control 控制层
                            //一般一个控件比较复杂的话,会采用MVC模式,比如listView
                            //获取SurfaceView的控制器,SurfaceHolder
                            SurfaceHolder holder = sv_surfaceView.getHolder();
                            //通过SurfaceHolder拿到SurfaceView的画布,注意:用完后,必须有释放画布的操作
                            Canvas canvas = holder.lockCanvas();
                            //把画布上的图片清空掉,重新给画布设置颜色即可
                            canvas.drawColor(Color.BLACK);
                            //创建一个画笔的对象
                            Paint paint = new Paint();
                            //为画笔上颜色
                            paint.setColor(Color.BLUE);
                            //定义一个圆的半径,为了使每次画的圆都比上一次的大,所以加x
                            int r = 5 + x;
                            //使用画布对象,就可以改变SurfaceView的形状
                            //画圆的方法,其中第一个和第二个参数是规定圆心坐标,第三个参数就是画圆的半径,第四个参数就是画笔对象
                            canvas.drawCircle(250, 250, r, paint);
                            //通过SurfaceHolder对象我们拿到画布,用完必须解锁的操作,参数就是画布对象
                            holder.unlockCanvasAndPost(canvas);
                            //每一次循环后,都睡一段时间
                            SystemClock.sleep(1000);
                        }
                    }
                }).start();
                break;
        }
    }
效果图如图2-2。

图2-2                
   
           

二、我们会发现,按照上面这么做的话,会出现两个问题
1.当退出或隐藏这个程序时,会报出空指针异常( java.lang.NullPointerException )。
2.多次点击会有问题。
       原因很简单,上面就已经提及。就是 SurfaceView可见时才被创建,SurfaceView隐藏时便被销毁,当我们隐藏了这个程序,界面不可见我们就看不见了SurfaceView,所以 ,SurfaceView被销毁。但是在子线程中,for循环还在执行,还在一遍一遍的执行画圆的操作,而SurfaceView对象已经被销毁,所以就会报出空指针异常。
        而当我们多次点击按钮时,因为每一次点击,就会在开一个子线程来进行画圆的操作,连续多次点击就会产生不同的效果,多等一会再点击等到100次循环完事画完这个圆在进行点击就好了。
三、还有一种情况也会报出空指针的异常,那就是我们直接把SurfaceView写在MainActivity的onCreate中。
同样我把每一步的步骤,以及操作在注释中写的很清楚了。
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        //获取一个SurfaceView控制器
        SurfaceHolder holder = sv_surfaceView.getHolder();
        //实现SurfaceView的状态监听
        holder.addCallback(new SurfaceHolder.Callback() {
                               @Override
                               //SurfaceView对象被创建的时候执行该方法
                               public void surfaceCreated(SurfaceHolder holder) {
                                   //如果SurfaceView被创建了标记为可以执行SurfaceView的逻辑代码  
                                   new Thread(new Runnable() {
                                       @Override
                                       public void run() {
                                           //从小到大,画一个100的圆,为了能够看到效果,所以画一个圆就睡上一段时间
                                           for (int x = 0; x < 100; x++) {
                                               //MVC m:mode 模型 v:View 视图  c:Control 控制层
                                               //一般一个控件比较复杂的话,会采用MVC模式,比如listView
                                               //获取SurfaceView的控制器,SurfaceHolder
                                               SurfaceHolder holder = sv_surfaceView.getHolder();
                                               //通过SurfaceHolder拿到SurfaceView的画布,注意:用完后,必须有释放画布的操作
                                               Canvas canvas = holder.lockCanvas();
                                               //把画布上的图片清空掉,重新给画布设置颜色即可
                                               canvas.drawColor(Color.BLACK);
                                               //创建一个画笔的对象
                                               Paint paint = new Paint();
                                               //为画笔上颜色
                                               paint.setColor(Color.BLUE);
                                               //定义一个圆的半径,为了使每次画的圆都比上一次的大,所以加x
                                               int r = 5 + x;
                                               //使用画布对象,就可以改变SurfaceView的形状
                                               //画圆的方法,其中第一个和第二个参数是规定圆心坐标,第三个参数就是画圆的半径,第四个参数就是画笔对象
                                               canvas.drawCircle(250, 250, r, paint);
                                               //通过SurfaceHolder对象我们拿到画布,用完必须解锁的操作,参数就是画布对象
                                               holder.unlockCanvasAndPost(canvas);
                                               //每一次循环后,都睡一段时间
                                               SystemClock.sleep(1000);
                                           }
                                       }
                                   }).start();
                               }
      我们这样做之后并运行程序,发现程序直接就崩溃了,并报出空指针异常,但是它跟上面的例子还不一样,第一种情况是程序可以运行完成画圆,但是隐藏程序并切换回来的时候才会报出空指针。
      我们分析一下为什么会出现这种情况:熟悉Android的人都知道,在Activity生命周期中,当一个窗体在创建时,生命周期走到onCreate,界面是没有加载出来的,用户并不可以看到界面,那么,我们上面反复提到就是SurfaceView可见时才被创建,SurfaceView隐藏时便被销毁,所以才和第一种情况不一样,程序运行时会直接崩溃。

那么问题来了怎么避免空指针的问题( java.lang.NullPointerException
      那就有这样一种思路:我们可不可以向Button按钮一样,为SurfaceView加入一个监听器,来监听SurfaceView状态的改变。当状态一旦发生改变,那我们跳出for循环,不再画圆,是不是就不会报空指针了呢?那当我们隐藏了程序,代码中怎么知道SurfaceView已经改变了呢?
MainActivity具体代码如下,我们用到了SurfaceView的监听器并实现三个方法,还有boolean标签来实现,同样我在代码的注释每一步都写的很明白。
public class MainActivity extends AppCompatActivity {

    private SurfaceView sv_surfaceView;
    //定义一个标签,当SurfaceView对象存在时,则不再执行其对象的调用方法
    boolean result = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        //获取一个SurfaceView控制器
        SurfaceHolder holder = sv_surfaceView.getHolder();
        //实现SurfaceView的状态监听
        holder.addCallback(new SurfaceHolder.Callback() {
                               @Override
                               //SurfaceView对象被创建的时候执行该方法
                               public void surfaceCreated(SurfaceHolder holder) {
                                   //如果SurfaceView被创建了标记为可以执行SurfaceView的逻辑代码                
                                   new Thread(new Runnable() {
                                       @Override
                                       public void run() {
                                           result = true;
                                           //从小到大,画一个100的圆,为了能够看到效果,所以画一个圆就睡上一段时间
                                           for (int x = 0; x < 100; x++) {
                                               //当每次执行SurfaceView调用方法时,先判断SurfaceView对象是否存在,如果不存在,就跳出程序
                                               if (result == false) {
                                                   break;
                                               }
                                               //MVC m:mode 模型 v:View 视图  c:Control 控制层
                                               //一般一个控件比较复杂的话,会采用MVC模式,比如listView
                                               //获取SurfaceView的控制器,SurfaceHolder
                                               SurfaceHolder holder = sv_surfaceView.getHolder();
                                               //通过SurfaceHolder拿到SurfaceView的画布,注意:用完后,必须有释放画布的操作
                                               Canvas canvas = holder.lockCanvas();
                                               //把画布上的图片清空掉,重新给画布设置颜色即可
                                               canvas.drawColor(Color.BLACK);
                                               //创建一个画笔的对象
                                               Paint paint = new Paint();
                                               //为画笔上颜色
                                               paint.setColor(Color.BLUE);
                                               //定义一个圆的半径,为了使每次画的圆都比上一次的大,所以加x
                                               int r = 5 + x;
                                               //使用画布对象,就可以改变SurfaceView的形状
                                               //画圆的方法,其中第一个和第二个参数是规定圆心坐标,第三个参数就是画圆的半径,第四个参数就是画笔对象
                                               canvas.drawCircle(250, 250, r, paint);
                                               //通过SurfaceHolder对象我们拿到画布,用完必须解锁的操作,参数就是画布对象
                                               holder.unlockCanvasAndPost(canvas);
                                               //每一次循环后,都睡一段时间
                                               SystemClock.sleep(1000);
                                           }
                                       }
                                   }).start();
                               }

                               @Override
                               //SurfaceView被改变的时候,执行该方法
                               public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                                   result = false;
                               }

                               @Override
                               //SurfaceView被销毁的时候,执行该方法
                               public void surfaceDestroyed(SurfaceHolder holder) {
                                   result = false;
                               }
                           }

        );
    }

    private void initView() {
        sv_surfaceView = (SurfaceView) findViewById(R.id.sv_surfaceView);
    }
}



          通过上面两个例子,我们可以进行一下总结。在使用SurfaceView时,出现了空指针异常,肯定是SurfaceView对象出了问题,肯定是它被销毁了。所以,我们应该好好揣摩这句话,SurfaceView可见时才被创建,SurfaceView隐藏时便被销毁。在我们实际操作中,如果报出了空指针,只要好好考虑一下,SurfaceView对象在那个环节出了问题就可以轻松的解决了!
        这只是我个人的一些观点,希望这篇文章对您有所帮助,有错误的地方也请大家指教。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值