天天看點

Android SurfaceView 源碼分析及使用

https://tech.youzan.com/surfaceview-sourcecode/

概述

SurfaceView 是 Android 中一種比較特殊的視圖(View),它跟平時時候的 TextView、Button 最大的差別是它跟它的視圖容器并不是在同一個視圖層上,它的 UI 顯示也可以不在一個獨立的線程中完成,是以對 SurfaceView 的繪制并不會影響到主線程的運作。綜合這些特點,SurfaceView 一般用來實作動态的或者比較複雜的圖像還有動畫的顯示。

SurfaceView 的 MVC 架構

要使用 SurfaceView ,就必須了解它的另外兩個元件:Surface 和 SurfaceHolder. Surface、SurfaceHolder 和 SurfaceView 三者之間的關系實質上就是廣為人知的 MVC,即 Model-View-Controller。Model就是模型的意思,或者說是資料模型,或者更簡單地說就是資料,也就是這裡的Surface;View即視圖,代表使用者互動界面,也就是這裡的SurfaceView;SurfaceHolder很明顯可以了解為MVC中的Controller(控制器)。

應用場景

差別于普通的控件,SurfaceView 可以運作于主線程之外,不需要及時響應使用者的輸入,也不會造成響應的 ANR 問題。SurfaceView 一般用在遊戲、視訊、攝影等一些複雜 UI 且高效的圖像的顯示,這類的圖像處理都需要開單獨的線程來處理。

分析源碼

分析 Surface,SurfaceHolder,SurfaceView 三個類

Surface

android.view.SurfaceView

/**
 * Handle onto a raw buffer that is being managed by the screen compositor.
 */
public class Surface implements Parcelable {  
    // code......
}
           

首先來看 Surface 這個類,它實作了 Parcelable 接口進行序列化(這裡主要用來在程序間傳遞 surface 對象),用來處理螢幕顯示緩沖區的資料,源碼中對它的注釋為: Handle onto a raw buffer that is being managed by the screen compositor. Surface是原始圖像緩沖區(raw buffer)的一個句柄,而原始圖像緩沖區是由螢幕圖像合成器(screen compositor)管理的。

- 由螢幕顯示内容合成器(screen compositor)所管理的原生緩沖器的句柄(類似句柄) - 名詞解釋:句柄,英文:HANDLE,資料對象進入記憶體之後擷取到記憶體位址,但是所在的記憶體位址并不是固定的,需要用句柄來存儲内容所在的記憶體位址。從資料類型上來看它隻是一個32位(或64位)的無符号整數。 - Surface 充當句柄的角色,用來擷取源生緩沖區以及其中的内容 - 源生緩沖區(raw buffer)用來儲存目前視窗的像素資料 - 于是可知 Surface 就是 Android 中用來繪圖的的地方,具體來說應該是 Surface 中的 Canvas Surface 中定義了畫布相關的 Canvas 對象

private final Canvas mCanvas = new CompatibleCanvas();  
           

Java中,繪圖通常在一個 Canvas 對象上進行的,Surface 中也包含了一個 Canvas 對象,這裡的 CompatibleCanvas 是Surface.java 中的一個内部類,其中包含一個矩陣對象Matrix(變量名mOrigMatrix)。矩陣Matrix就是一塊記憶體區域,針對View的各種繪畫操作都儲存在此記憶體中。

Surface 内部有一個 CompatibleCanvas 的内部類,這個内部類的作用是為了能夠相容 Android 各個分辨率的螢幕,根據不同螢幕的分辨率處理不同的圖像資料。

private final class CompatibleCanvas extends Canvas {  
        // A temp matrix to remember what an application obtained via {@link getMatrix}
        private Matrix mOrigMatrix = null;

        @Override
        public void setMatrix(Matrix matrix) {
            if (mCompatibleMatrix == null || mOrigMatrix == null || mOrigMatrix.equals(matrix)) {
                // don't scale the matrix if it's not compatibility mode, or
                // the matrix was obtained from getMatrix.
                super.setMatrix(matrix);
            } else {
                Matrix m = new Matrix(mCompatibleMatrix);
                m.preConcat(matrix);
                super.setMatrix(m);
            }
        }

        @SuppressWarnings("deprecation")
        @Override
        public void getMatrix(Matrix m) {
            super.getMatrix(m);
            if (mOrigMatrix == null) {
                mOrigMatrix = new Matrix();
            }
            mOrigMatrix.set(m);
        }
    }
           

兩個重要的方法

需要說明的是,這裡的 lockCanvas 并不是實際使用 SurfaceView 來進行繪圖時 SurfaceHolder 對象調用的 lockCanvas 以及 unlockCanvasAndPost 方法。實際例子中使用的方法是在 SurfaceView 内部封裝過對這兩個方法封裝之後的。 - lockCanvas(...) + Gets a Canvas for drawing into this surface. 擷取進行繪畫的 Canvas 對象 + After drawing into the provided Canvas, the caller must invoke unlockCanvasAndPost to post the new contents to the surface. 繪制完一幀的資料之後需要調用 unlockCanvasAndPost 方法把畫布解鎖,然後把畫好的圖像 Post 到目前螢幕上去顯示 + 當一個 Canvas 在被繪制的時候,它是出于被鎖定的狀态,就是說必須等待正在繪制的這一幀繪制完成之後并解鎖畫布之後才能進行别的操作 + 實際鎖住 Canvas 的過程是在 jni 層完成的 - unlockCanvasAndPost(...) + Posts the new contents of the Canvas to the surface and releases the Canvas.将新繪制的圖像内容傳給 surface 之後這個 Canvas 對象會被釋放掉(實際釋放的過程是在 jni 層完成的)

Surface 的 lockCanvas 和 unlockCanvasAndPost 兩個方法最終都是調用 jni 層的方法來處理,有興趣可以看下相關的源碼:

/frameworks/native/libs/gui/Surface.cpp /frameworks/base/core/jni/android_view_Surface.cpp

SurfaceHolder

android.view.SurfaceHolder SurfaceHolder 實際上是一個接口,它充當的是 Controller 的角色。

public interface SurfaceHolder {  
}
           

來看下注釋是怎麼說的

  • Abstract interface to someone holding a display surface. 一個針對 Surface 的抽象接口
  • Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface. 赤裸裸的 Controller 角色,可以控制 Surface 的大小和格式,監控 Surface 的變化(在回調函數中對 Surface 的變化做相應的處理)
  • When using this interface from a thread other than the one running its SurfaceView, you will want to carefully read the methods Callback.surfaceCreated() 如果用子線程來處理 SurfaceView 的繪制,需要用到接下來要介紹的關鍵接口 Callback 中的 surfaceCreated 方法。可以看到之前給的例子中就是在 surfaceCreated 方法中開啟的繪制動畫的線程

關鍵接口 Callback

Callback 是 SurfaceHolder 内部的一個接口,例子中就實作了這個接口來控制繪制動畫的線程。

接口中有以下三個方法 - public void surfaceCreated(SurfaceHolder holder); + Surface 第一次被建立時被調用,例如 SurfaceView 從不可見狀态到可見狀态時 + 在這個方法被調用到 surfaceDestroyed 方法被調用之前的這段時間,Surface 對象是可以被操作的,拿 SurfaceView 來說就是如果 SurfaceView 隻要是在界面上可見的情況下,就可以對它進行繪圖和繪制動畫 + 這裡還有一點需要注意,Surface 在一個線程中處理需要渲染的圖像資料,如果你已經在另一個線程裡面處理了資料渲染,就不需要在這裡開啟線程對 Surface 進行繪制了 - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height); + Surface 大小和格式改變時會被調用,例如橫豎屏切換時如果需要對 Sufface 的圖像和動畫進行處理,就需要在這裡實作 + 這個方法在 surfaceCreated 之後至少會被調用一次 - public void surfaceDestroyed(SurfaceHolder holder); + Surface 被銷毀時被調用,例如 SurfaceView 從可見到不可見狀态時 + 在這個方法被調用過之後,就不能夠再對 Surface 對象進行任何操作,是以需要保證繪圖的線程在這個方法調用之後不再對 Surface 進行操作,否則會報錯

SurfaceView

android.view.SurfaceView

SurfaceView,就是用來顯示 Surface 資料的 View,通過 SurfaceView 來看到 Surface 的資料。

public class SurfaceView extends View {  
    // code.....
}
           

分析一下源碼中對 SurfaceView 的注釋

  • Provides a dedicated drawing surface embedded inside of a view hierarchy. 在螢幕顯示的視圖層中嵌入了一塊用做圖像繪制的 Surface 視圖
  • the SurfaceView punches a hole in its window to allow its surface to be displayed. SurfaceView 在螢幕上挖了個洞來來世它所繪制的圖像
  • 挖洞是什麼鬼?
    • 這裡引入一個Z軸的概念,SurfaceView 視圖所在層級的Z軸位置是小于用來其宿主 Activity 視窗的 Layer 的 Z 軸的,就是說其實 SurfaceView 實際是顯示在 Activity 所在的視圖層下方的
    • 那麼問題就來了,為什麼還是能看到 SurfaceView?形象一點的說法就是你在牆上鑿了一個方形的洞,然後在洞上裝了塊玻璃,你就能看到牆後面的東西了。SurfaceView 就做了這樣的事情,它把 Activity 所在的層當作了牆
  • The Surface will be created for you while the SurfaceView's window is visible. 這裡說明了動畫是什麼時候開始的,當 SurfaceView 可見時,就可以開始在 Canvas 上繪制圖像,并把圖像資料傳遞給 Surface 用來顯示在 SurfaceView 上
  • you should implement SurfaceHolder.Callback#surfaceCreated and SurfaceHolder.Callback#surfaceDestroyed to discover when the Surface is created and destroyed as the window is shown and hidden. 在使用 SurfaceView 的地方需要實作 SurfaceHolder.CallBack 回調,來對 Surface 的建立和銷毀進行監聽以及做響應的處理,這裡的處理指的是開始對 Canvas 進行繪制并把資料傳遞給 Surface 來做顯示

使用 SurfaceView

例子中展示了如何使用 SurfaceView

  1. 需要實作 SurfaceHolder.Callback 接口
  2. 需要在 SurfaceHolder.Callback 的 surfaceCreated 方法中開啟一個線程進行動畫的逐幀的繪制
  3. 需要在 SufaceHolder.Callback 的 surfaceDestroyed 方法中結束繪畫的線程并調用 SurfaceHolder 的 removeCallbck 方法
  4. 繪畫線程每一幀開始之前需要調用 lockCanvas 方法鎖住畫布進行繪圖
  5. 繪制完一幀的資料之後需要調用 unlockCanvasAndPost 方法送出資料來顯示圖像

例子

import android.content.Context;  
import android.graphics.Bitmap;  
import android.graphics.Canvas;  
import android.graphics.Color;  
import android.graphics.Paint;  
import android.graphics.Paint.Style;  
import android.graphics.drawable.BitmapDrawable;  
import android.view.SurfaceHolder;  
import android.view.SurfaceView;  
import android.view.KeyEvent;  
import android.view.MotionEvent;  
import android.view.SurfaceHolder.Callback;

public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {  
    private SurfaceHolder mHolder; // 用于控制SurfaceView
    private Thread t; // 聲明一條線程
    private volatile boolean flag; // 線程運作的辨別,用于控制線程
    private Canvas mCanvas; // 聲明一張畫布
    private Paint p; // 聲明一支畫筆
    float m_circle_r = 10;

    public MySurfaceView(Context context) {
        super(context);

        mHolder = getHolder(); // 獲得SurfaceHolder對象
        mHolder.addCallback(this); // 為SurfaceView添加狀态監聽
        p = new Paint(); // 建立一個畫筆對象
        p.setColor(Color.WHITE); // 設定畫筆的顔色為白色
        setFocusable(true); // 設定焦點
    }

    /**
     * 當SurfaceView建立的時候,調用此函數
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        t = new Thread(this); // 建立一個線程對象
        flag = true; // 把線程運作的辨別設定成true
        t.start(); // 啟動線程
    }

    /**
     * 當SurfaceView的視圖發生改變的時候,調用此函數
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
    }

    /**
     * 當SurfaceView銷毀的時候,調用此函數
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        flag = false; // 把線程運作的辨別設定成false
        mHolder.removeCallback(this);
    }

    /**
     * 當螢幕被觸摸時調用
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        return true;
    }

    /**
     * 當使用者按鍵時調用
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        surfaceDestroyed(mHolder);
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public void run() {
        while (flag) {
            try {
                synchronized (mHolder) {
                    Thread.sleep(100); // 讓線程休息100毫秒
                    Draw(); // 調用自定義畫畫方法
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (mCanvas != null) {
                    // mHolder.unlockCanvasAndPost(mCanvas);//結束鎖定畫圖,并送出改變。

                }
            }
        }
    }

    /**
     * 自定義一個方法,在畫布上畫一個圓
     */
    protected void Draw() {
        mCanvas = mHolder.lockCanvas(); // 獲得畫布對象,開始對畫布畫畫
        if (mCanvas != null) {
            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            paint.setColor(Color.BLUE);
            paint.setStrokeWidth(10);
            paint.setStyle(Style.FILL);
            if (m_circle_r >= (getWidth() / 10)) {
                m_circle_r = 0;
            } else {
                m_circle_r++;
            }
            Bitmap pic = ((BitmapDrawable) getResources().getDrawable(
                    R.drawable.qq)).getBitmap();
            mCanvas.drawBitmap(pic, 0, 0, paint);
            for (int i = 0; i < 5; i++)
                for (int j = 0; j < 8; j++)
                    mCanvas.drawCircle(
                            (getWidth() / 5) * i + (getWidth() / 10),
                            (getHeight() / 8) * j + (getHeight() / 16),
                            m_circle_r, paint);
            mHolder.unlockCanvasAndPost(mCanvas); // 完成畫畫,把畫布顯示在螢幕上
        }
    }
}
           

可能碰到的一些問題

為什麼把一個 SurfaceView 放在布局檔案中不做任務圖像的繪制,它會顯示一個黑色的區域?

造成這個現象的原因可以在 SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType 變量被初始化為WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,是以在建立繪制這個 View 的過程中整個 Canvas 會被塗成黑色

// windowType 的預設值
int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;

@Override
    public void draw(Canvas canvas) {
        if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
            // draw() is not called when SKIP_DRAW is set
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
                // punch a whole in the view-hierarchy below us
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
        }
        super.draw(canvas);
    }