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
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()
返回一個負值,剛好就使得它能夠被繪製在中間