Android淺析SurfaceView

Anderson.png
  • 為什么要用SurfaceView

在自定義View中,我們知道使用onDraw可以做一些簡單的動畫效果。通過不斷循環的執行View.onDraw方法,每次執行都對內部顯示的圖形做一些調整,我們假設onDraw方法每秒執行20次,這樣就會形成一個20幀的補間動畫效果。但是現實情況是你無法簡單的控制View.onDraw的執行幀數,這邊說的幀數是指每秒View.onDraw方法被執行多少次,這是為什么呢?首先我們知道,onDraw方法是由系統幫我們調用的,我們是通過調用View的invalidate方法通知系統需要重新繪制View,然后它就會調用View.onDraw方法。這些都是由系統幫我們實現的,所以我們很難精確去定 義View.onDraw的執行幀數,這個就是為什么我們這邊要了解SurfaceView了,它能彌補View的一些不足。

  • SurfaceView與View有什么不同?

對于一個View的onDraw方法,不能從后臺線程修改一個GUI元素。
當需要快速地更新View的UI,或者當渲染代碼阻塞GUI線程的時間過長的時候,SurfaceView就是解決上述問題的最佳選擇。 SurfaceView封裝了一個Surface對象,而不是Canvas。這一點很重要,因為Surface可以使用后臺線程繪制。對于那些資源敏感的操作,或者那些要求快速更新或者高速幀率的地方,例如,使用3D圖形,創建游戲,或者實時預覽攝像頭

  • 讓我們來看一看Google文檔

Provides a dedicated drawing surface embedded inside of a view hierarchy. You can control the format of this surface and, if you like, its size; the SurfaceView takes care of placing the surface at the correct location on the screen

The surface is Z ordered so that it is behind the window holding its SurfaceView; the SurfaceView punches a hole in its window to allow its surface to be displayed. The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. This can be used to place overlays such as buttons on top of the Surface, though note however that it can have an impact on performance since a full alpha-blended composite will be performed each time the Surface changes.

The transparent region that makes the surface visible is based on the layout positions in the view hierarchy. If the post-layout transform properties are used to draw a sibling view on top of the SurfaceView, the view may not be properly composited with the surface.

Access to the underlying surface is provided via the SurfaceHolder interface, which can be retrieved by calling getHolder().

The Surface will be created for you while the SurfaceView's window is visible; you should implement surfaceCreated(SurfaceHolder) and surfaceDestroyed(SurfaceHolder) to discover when the Surface is created and destroyed as the window is shown and hidden.

One of the purposes of this class is to provide a surface in which a secondary thread can render into the screen. If you are going to use it this way, you need to be aware of some threading semantics:

All SurfaceView and SurfaceHolder.Callback methods will be called from the thread running the SurfaceView's window (typically the main thread of the application). They thus need to correctly synchronize with any state that is also touched by the drawing thread. You must ensure that the drawing thread only touches the underlying Surface while it is valid -- between SurfaceHolder.Callback.surfaceCreated() and SurfaceHolder.Callback.surfaceDestroyed().

這段話的意思是:

SurfaceView是視圖(View)的繼承類,這個視圖里內嵌了一個專門用于繪制的Surface。你可以控制這個Surface的格式和尺寸。Surfaceview控制這個Surface的繪制位置。
surface是縱深排序(Z-ordered)的,這表明它總在自己所在窗口的后面。surfaceview提供了一個可見區域,只有在這個可見區域內的surface部分內容才可見,可見區域外的部分不可見。surface的排版顯示受到視圖層級關系的影響,它的兄弟視圖結點會在頂端顯示。這意味者surface的內容會被它的兄弟視圖遮擋,這一特性可以用來放置遮蓋物(overlays)(例如,文本和按鈕等控件)。注意,如果surface上面 有透明控件,那么它的每次變化都會引起框架重新計算它和頂層控件的透明效果,這會影響性能。

你可以通過SurfaceHolder接口訪問這個surface,getHolder()方法可以得到這個接口。
surfaceview變得可見時,surface被創建;surfaceview隱藏前,surface被銷毀。這樣能節省資源。如果你要查看 surface被創建和銷毀的時機,可以重載surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder)。
surfaceview的核心在于提供了兩個線程:UI線程和渲染線程。這里應注意:
1> 所有SurfaceView和SurfaceHolder.Callback的方法都應該在UI線程里調用,一般來說就是應用程序主線程。渲染線程所要訪問的各種變量應該作同步處理。
2> 由于surface可能被銷毀,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之間有效,所以要確保渲染線程訪問的是合法有效的surface。

通過谷歌官方的介紹,我們初步了解了SurfaceView的使用方法,但是,具體怎么使用,不如我們敲代碼來的深刻。

  • Demo

我們可以寫一個自定義SurfaceView,不在UI線程中更新UI顯示的例子,通過畫圓圈,使得圓圈不斷變大變小的動畫來做案例。

首先,我們寫一個MySurfaceView 繼承于SurfaceView 且完成Callback接口


public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {

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

    public MySurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}

然后我們添加一個線程,在線程中,拿到SurfaceHolder 進行繪制,同時我們打印UI線程號和draw時候的線程號。
總體代碼如下,

/**
 * Created by Xiamin on 2016/11/26.
 */

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private DrawThread mThread;

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

    public MySurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i("iii", "surfaceCreated");
        mThread = new DrawThread(getContext(), holder);
        mThread.start();
        Log.i("iii", "UI thread id: " + Thread.currentThread().getId());
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mThread.stopThread();
    }


    class DrawThread extends Thread {
        SurfaceHolder surfaceHolder;
        Context context;
        Paint paint;
        private boolean isRunning = true;
        float r = 10;
        float diff = 0;

        public DrawThread(Context context, SurfaceHolder holder) {
            this.context = context;
            this.surfaceHolder = holder;
            paint = new Paint();
            paint.setColor(Color.BLUE);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(10);
        }

        @Override
        public void run() {
            while (isRunning) {
                synchronized (surfaceHolder) {
                    if (surfaceHolder != null) {
                        Canvas canvas = surfaceHolder.lockCanvas();
                        draw(canvas);
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            surfaceHolder.unlockCanvasAndPost(canvas);
                        }
                    }
                }
            }
        }

        public void draw(Canvas canvas) {
            if (r < 30) {
                diff = 10;
            } else if (r > 150) {
                diff = -10;
            }
            r += diff;
            canvas.drawColor(Color.WHITE);
            canvas.translate(200, 200);
            canvas.drawCircle(0, 0, r, paint);
            Log.i("iii", "draw thread id: " + Thread.currentThread().getId());
        }

        private void stopThread() {
            Log.i("iii", "stopThread()");
            isRunning = false;
        }
    }
}

執行效果如圖

圓圈demo.png

Log日志為:
11-27 05:06:10.979 2752-2752/com.example.surfaceviewdemo I/iii: surfaceCreated
11-27 05:06:10.979 2752-2752/com.example.surfaceviewdemo I/iii: UI thread id: 1
11-27 05:06:11.027 2752-2767/com.example.surfaceviewdemo I/iii: draw thread id: 107
11-27 05:06:11.231 2752-2767/com.example.surfaceviewdemo I/iii: draw thread id: 107
11-27 05:06:11.343 2752-2767/com.example.surfaceviewdemo I/iii: draw thread id: 107
11-27 05:06:11.439 2752-2767/com.example.surfaceviewdemo I/iii: draw thread id: 107
可以看出,我們的繪圖線程與UI線程不是同一個線程。這帶來的好處是巨大的。


謝謝大家閱讀,如有幫助,來個喜歡或者關注吧!


本文作者:Anderson/Jerey_Jobs

簡書地址:Anderson大碼渣

github地址:Jerey_Jobs

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容