1. 程式人生 > >SurfaceView 練習——多執行緒同時畫圖

SurfaceView 練習——多執行緒同時畫圖

為什麼需要 SurfaceView

學習一個東西,首先我們要明白,為什麼要學?在 Android 開發中,我們已經有了 TextView, ImageView, ...等形形色色的 view,如果對Android的實現,我們還的可以繼承他們,新增自定義的行為。那麼,SurfaceView 又有什麼優勢呢?

我們知道,Android 預設所有的操作預設都在主執行緒執行,包括所有的普通 UI 操作。如此一來,當我們需要繪製複雜的UI時,就容易出現卡頓、掉幀等情況。這樣,就有了我們的SurfaceView

教程

這個 是Google出品的關於SurfaceView 的很好的入門級教程,我就不班門弄斧了。

值得一提的是,Google 的例子中,只是一個執行緒寫 SurfaceView,作為練習,下面的例子中,使用 6 個執行緒來畫圖。

一個小練習

在下面的練習中,我們使用 6 個執行緒同時在 SurfaceView 上繪圖,他們分別在螢幕中間寫上 0 ~ 5。完整的程式碼可以在 Github 下載。

首先,是我們的主要的業務類 GamblingView

public class GamblingView extends SurfaceView {}

在初始化的過程中,我們直接拿到 通過 SurfaceView.getHolder() 拿到 SurfaceHolder

值得注意的是,我們也把 context

存了下來。在低版本的 Android 中,View.mContext 是一個 protected 的成員變數,但高版本中被隱藏了起來,不再出現在公有的 api 中。為了相容,即便是使用低版本的 Android 編譯,也應該自己宣告一個 mContext

private void init(Context context) {
    // In newer version of Android, View.mContext is not protected as before
    mContext = context;
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.BLACK);
    mSurfaceHolder = getHolder();
}

這個類最關鍵的便是 draw() 方法。後臺執行緒通過呼叫 draw(n),可以在螢幕中央畫一個數字。

private void draw(int n) {
    synchronized (mLock) {
        if (mSurfaceHolder.getSurface().isValid()) {
            // Note: Race condition can happen when surfaceView is destroyed after checking
            // if it is valid.
            Canvas canvas = mSurfaceHolder.lockCanvas();

            // String.valueOf(n) is better than Integer.toString(n)
            String text = String.valueOf(n);
            float textLength = mPaint.measureText(text);
            // place in the center
            float xPos = (canvas.getWidth() - textLength) / 2;
            float yPos = (canvas.getHeight() / 2) - ((mPaint.descent() + mPaint.ascent()) / 2);
            canvas.save();
            canvas.drawColor(Color.WHITE);
            canvas.drawText(text, xPos, yPos, mPaint);
            canvas.restore();
            mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
    }
}

這段程式碼有幾個值得注意的地方:
1. 存在多個執行緒同時訪問 SurfaceView 的情況下,我們不能再像 Google 例子中那樣,以不加鎖的方式呼叫 isValid() 然後獲取 canvas。因為,在呼叫完 isValid()lockCanvas() 之前,其他執行緒可能會被排程並且同樣檢查 isValid() 得到 true。這樣一來,lockCanvas() 將丟擲異常。
2. 那你可能要說了,我們不加鎖,捕獲 lockCanvas() 的異常就可以了。沒錯,這種方法可以得到正確的結果,但是不雅。為什麼呢?如果這樣做,我們就依賴了 lockCanvas() 的內部實現;同時,異常應當在真正發生異常的時候使用,不應該用來作為程式的控制邏輯。
3. 遺憾的是,即便我們這樣加鎖,還是有存在競爭條件。雖然我們的後臺執行緒都使用同一個鎖來提供對 surfaceView 的訪問控制,但是 Android framework 並沒有使用這個鎖。也就是說,我們執行完 isValid() 後,如果剛好系統把這個 surfaceView destroy 掉,後面的 lockCanvas() 還是會出錯。作為一個小玩具,就沒有處理這種情況了。
4. 為什麼說 String.valueOf(n) 優於 Integer.toString() 呢?試著想象這麼一種情況,我們把 n 改為了 double。對於前者,所有的程式碼都不需要改變;而後者,就需要修改程式碼為 Double.toString()了。

2018.3.6

更好的在 canvas 中間繪製文字的方法(程式碼已同步至 git 倉庫)

mPaint.getTextBounds(text, 0, text.length(), mBound);
float xPos = canvas.getWidth() / 2 - mBound.centerX();
float yPos = canvas.getHeight() / 2 - mBound.centerY();
// yPos is the baseline and centerY() will give a negative value
canvas.drawText(text, xPos, yPos, mPaint);

看到這段程式碼,你可能會產生一個衝動,然後寫下下面這樣的程式碼:

float xPos = (canvas.getWidth() - mBound.width()) / 2;
float yPos = (canvas.getHeight() - mBound.height()) / 2;

遺憾的是,下面這段程式碼不會得到我們想要的結果。這時候,如果把 centerX(), centerY() 的值打印出來,就會發現,centerX() 是正值,centerY() 是負值。結果就是,X 軸可以正常的位於中央,而 Y 軸卻偏上。原因是,canvas.drawText() 的引數 y 是 baseline,而不是文字的頂部;而 centerY() 返回一個負值,剛好就使得它能夠被繪製在中間