Surface
Surface
對(duì)應(yīng)了一塊屏幕緩沖區(qū),是要顯示到屏幕的內(nèi)容的載體各墨。每一個(gè) Window
都對(duì)應(yīng)了一個(gè)自己的 Surface
。這里說(shuō)的 window
包括 Dialog, Activity, Status Bar 等。SurfaceFlinger
最終會(huì)把這些 Surface
在 z 軸方向上以正確的方式繪制出來(lái)(比如 Dialog 在 Activity 之上)。SurfaceView
的每個(gè) Surface
都包含兩個(gè)緩沖區(qū)踩身,而其他普通 Window
的對(duì)應(yīng)的 Surface
則不是。
Window
Window
是 Surface
的載體社露,一個(gè) Window
對(duì)應(yīng)一個(gè) Surface
挟阻。一個(gè) Window
由 WindowManager
創(chuàng)建,并提供給 application 使用峭弟。Window
除了包含繪制的內(nèi)容附鸽,還包含其他的一些窗口屬性。
View
View
是 Window
中的一個(gè) UI 元素瞒瘸。一個(gè) Window
包含一個(gè)唯一的 View
層次結(jié)構(gòu)的樹(shù)坷备,樹(shù)的根節(jié)點(diǎn)就是 RootViewImpl
。每當(dāng) Window
需要重繪時(shí)情臭,先 lock Surface
击你,然后使用 Surface
返回的 Canvas
來(lái)完成 draw
的過(guò)程。draw
的過(guò)程中谎柄,Canvas
被按照層次依次傳給每個(gè) View
,每個(gè) View
調(diào)用自己的 onDraw
方法完成在 Canvas
上的繪制惯雳。繪制完成后朝巫,unlock Surface
,并將這個(gè) Surface
的緩沖 post 到前端石景,再由 SurfaceFlinger
將緩沖刷新到屏幕上劈猿。
SurfaceView
SurfaceView
是 View
的一種特殊實(shí)現(xiàn),它有自己的 Surface
提供給 application 進(jìn)行繪制潮孽。SurfaceView
存在于 View Tree 的層次結(jié)構(gòu)中揪荣,但是它的繪制流程卻獨(dú)立于這個(gè) View Tree 的繪制。每個(gè) SurfaceView
都有一個(gè)自己的 Surface
往史,可以認(rèn)為 SurfaceView
就是用來(lái)展示 Surface
數(shù)據(jù)的地方仗颈,用來(lái)控制 Surface
中繪制內(nèi)容的位置和尺寸。
SurfaceHolder
SurfaceHolder
是一個(gè)接口椎例,提供訪問(wèn)和控制 SurfaceView
中 Surface
的相關(guān)方法挨决。它有一下幾個(gè)主要方法:
-
Canvas lockCanvas()
獲取一個(gè)Canvas
對(duì)象并上鎖。這個(gè)鎖是重入鎖订歪,因此同一個(gè)線程可以多次鎖定脖祈,但是第二次鎖定時(shí),返回的Canvas
為null
刷晋。鎖定后返回Canvas
對(duì)象盖高。 -
Canvas lockCanvas(Rect dirty)
同上慎陵,只不過(guò)指定一個(gè)區(qū)域。這個(gè)需要被更新的區(qū)域稱為“臟區(qū)”喻奥。后面會(huì)講到這個(gè)概念對(duì)于理解雙緩沖很重要席纽。 -
void unlockCanvasAndPost(Canvas canvas)
修改完Surface
中數(shù)據(jù)后,釋放同步鎖映凳,并提交修改胆筒,然后數(shù)據(jù)會(huì)被送顯。雖然數(shù)據(jù)被送顯了诈豌,但是Surface
對(duì)應(yīng)的緩沖區(qū)中的數(shù)據(jù)卻不會(huì)被清空仆救,這與系統(tǒng)的普通View
不一樣。系統(tǒng)的普通View
在onDraw(Canvas canvas)
回調(diào)時(shí)矫渔,Canvas
中的內(nèi)容已經(jīng)被清空彤蔽。
SurfaceHolder.Callback
這個(gè)回調(diào)主要定義了 Surface
的生命周期。
-
void surfaceCreated(SurfaceHolder holder)
當(dāng)Surface
對(duì)象創(chuàng)建后庙洼,該方法立即被調(diào)用顿痪。 -
void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
當(dāng)Surface
發(fā)生任何結(jié)構(gòu)性變化時(shí),該方法立即被調(diào)用油够。 -
void surfaceDestroyed(SurfaceHolder holder)
當(dāng)surface對(duì)象在將要銷(xiāo)毀前蚁袭,該方法會(huì)被立即調(diào)用。
雙緩沖
每個(gè) SurfaceView
都有兩個(gè)獨(dú)立的 GraphicBuffer
石咬,分別是 frontBuffer
和 backBuffer
status_t err = dequeueBuffer(&out, &fenceFd);
...
if (err == NO_ERROR) {
....
sp<GraphicBuffer> backBuffer(GraphicBuffer::getSelf(out));
...
const sp<GraphicBuffer>& frontBuffer(mPostedBuffer);
}
上面的代碼取自 framework 的 Surface.lock(ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds)
方法揩悄。可以看到鬼悠,系統(tǒng)先從 buffer 池中 dequeueBuffer 出來(lái)一個(gè)可用的 out
删性,然后將 out
賦給 backBuffer
。mPostedBuffer
為已經(jīng)顯示的 buffer焕窝,將 mPostedBuffer
的內(nèi)容賦給 frontBuffer
蹬挺。
下面我們將 frontBuffer
稱為前景,將 backBuffer
稱為后景它掂。
- 調(diào)用
Surface.lock
- 系統(tǒng)從正在顯示的內(nèi)容
mPostedBuffer
中拷貝內(nèi)容到前景中巴帮,將要繪制的內(nèi)容拷貝到后景中。 - 系統(tǒng)通過(guò)前景虐秋、后景的數(shù)據(jù)計(jì)算出最終后景數(shù)據(jù)晰韵。這個(gè)后景數(shù)據(jù)關(guān)聯(lián)到返回給應(yīng)用層的
Canvas
,因此應(yīng)用層操作Canvas
時(shí)熟妓,會(huì)將數(shù)據(jù)直接繪制到后景上雪猪。 - 系統(tǒng)將后景指針賦值給
mLockedBuffer
- 調(diào)用
Surface.unlockAndPost
- 將
mLockedBuffer
解鎖,并賦值給mPostedBuffer
起愈,渲染到屏幕
雙緩沖的同步過(guò)程
這個(gè)同步過(guò)程就是每次調(diào)用 Surface.lock
時(shí)只恨,將前景內(nèi)容同步到后景的算法译仗。
集合的運(yùn)算
如上圖所示,從上到下官觅,從左到右纵菌,依次是集合的交、并休涤、補(bǔ)咱圆、差。
同步算法
1. 計(jì)算新的臟區(qū)
const Rect bounds(backBuffer->width, backBuffer->height);
Region newDirtyRegion;
if (inOutDirtyBounds) {
newDirtyRegion.set(static_cast<Rect const&>(*inOutDirtyBounds));
newDirtyRegion.andSelf(bounds);
} else {
newDirtyRegion.set(bounds);
}
如果應(yīng)用層通過(guò)調(diào)用 lockCanvas(Rect dirtyRect)
傳遞了一個(gè)新的臟區(qū)進(jìn)來(lái)(if (inOutDirtyBounds)
)功氨,那么這個(gè)新的臟區(qū)就和當(dāng)前后景做與運(yùn)算序苏,得到新的臟區(qū)大小。否則捷凄,這個(gè)新的臟區(qū)大小就是后景的大小忱详。
2. 確定是否需要將前景數(shù)據(jù)拷貝到后景
// figure out if we can copy the frontbuffer back
const sp<GraphicBuffer>& frontBuffer(mPostedBuffer);
const bool canCopyBack = (frontBuffer != 0 &&
backBuffer->width == frontBuffer->width &&
backBuffer->height == frontBuffer->height &&
backBuffer->format == frontBuffer->format);
- 前景中是有內(nèi)容的
- 前景、后景的長(zhǎng)跺涤、寬匈睁、格式都沒(méi)有改變
這種情況會(huì)是我們應(yīng)用SurfaceView
雙緩沖時(shí)的大部分情況。但是第一次將內(nèi)容繪到雙緩沖上時(shí)桶错,前景是沒(méi)有內(nèi)容的航唆。
3. 怎么拷貝?
if (canCopyBack) {
// copy the area that is invalid and not repainted this round
const Region copyback(mDirtyRegion.subtract(newDirtyRegion));
if (!copyback.isEmpty()) {
copyBlt(backBuffer, frontBuffer, copyback, &fenceFd);
}
} else {
// if we can't copy-back anything, modify the user's dirty
// region to make sure they redraw the whole buffer
newDirtyRegion.set(bounds);
mDirtyRegion.clear();
Mutex::Autolock lock(mMutex);
for (size_t i=0 ; i<NUM_BUFFER_SLOTS ; i++) {
mSlots[i].dirtyRegion.clear();
}
}
-
可以拷貝時(shí)院刁,將上一個(gè)緩沖的臟區(qū)減去這次新的臟區(qū)糯钙,將這個(gè)差值拷貝到后景。
不能拷貝時(shí)黎比,這次新的臟區(qū)就是整個(gè)后景大小。一般發(fā)生在這個(gè)
SurfaceView
第一次被繪制時(shí)鸳玩。
最后返回給應(yīng)用的 Canvas
內(nèi)容就是經(jīng)由上面前阅虫、后景同步算法后的內(nèi)容。
驗(yàn)證
@Override
public void surfaceCreated(SurfaceHolder holder) {
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lenna);
drawBitmap();
// lock - unlock 一次不跟,兩塊緩沖區(qū)做一次 flip颓帝,讓兩塊緩沖區(qū)中的內(nèi)容保持一致。
// 如果去掉這次 flip 可以看到第一次點(diǎn)擊時(shí)有黑塊(因?yàn)?backedBuffer 還沒(méi)有內(nèi)容)
// holder.lockCanvas(new Rect(0, 0, 0, 0));
// holder.unlockCanvasAndPost(canvas);
mCompositeDisposable.add(RxView.touches(this)
.filter(event -> MotionEvent.ACTION_DOWN == event.getAction())
.subscribe(event -> {
int x = (int) event.getX();
int y = (int) event.getY();
canvas = holder.lockCanvas(new Rect(x - 50, y - 50,
x + 50, y + 50));
Log.i(TAG, "click canvas clipBounds " + canvas.getClipBounds());
canvas.save();
canvas.rotate(30, x, y);
mPaint.setColor(Color.RED);
canvas.drawRect(x - 40, y - 40, x, y, mPaint);
canvas.restore();
mPaint.setColor(Color.GREEN);
canvas.drawRect(x, y, x + 40, y + 40, mPaint);
holder.unlockCanvasAndPost(canvas);
// 這里需要做一次 flip窝革,將兩個(gè)緩沖區(qū)的內(nèi)容同步购城。如果不同步,則會(huì)出現(xiàn)這次點(diǎn)擊時(shí)的透明角
// 會(huì)覆蓋上一次所繪制的色塊虐译。如果不 delay 100ms瘪板,則這次的 flip 可能不會(huì)生效,這個(gè)可能涉及到
// 底層 flip 的時(shí)間間隔漆诽,在這個(gè)時(shí)間間隔內(nèi)再做 flip 可能不生效
// BitmapSurfaceView.this.postDelayed(() -> {
// canvas = holder.lockCanvas(new Rect(0, 0, 0, 0));
// holder.unlockCanvasAndPost(canvas);
// }, 100);
}));
}
上面的代碼很簡(jiǎn)單:畫(huà)一個(gè)全屏的 lena 圖侮攀,之后每次用戶點(diǎn)擊時(shí)锣枝,都在點(diǎn)擊處的 100 * 100 newRect 內(nèi)繪紅、綠兩個(gè)色塊兰英∑踩可是我們卻看到了如下現(xiàn)象:
現(xiàn)象1
第一次點(diǎn)擊時(shí),newRect 有黑色背景畦贸,但是第二次點(diǎn)擊以及之后的點(diǎn)擊都沒(méi)有黑色背景陨闹。而且,第二次點(diǎn)擊會(huì)覆蓋掉第一次的內(nèi)容薄坏。
解釋:
- 應(yīng)用先繪了一次全屏的 lena 圖到前景趋厉,當(dāng)用戶第一次點(diǎn)擊時(shí),用戶新的臟區(qū) newRect1 在后景上颤殴,此時(shí)后景沒(méi)有任何內(nèi)容觅廓,因此就是 window 的本來(lái)顏色。按照算法涵但,oldRect - newRect1 = copyBound1杈绸,即舊的臟區(qū)(即此時(shí)前景的臟區(qū),是有整個(gè) lena 圖的)減新的臟區(qū)后得到的區(qū)域?qū)⒖截惖胶缶?? 后景被同步成整屏 lena 圖中被摳了一個(gè)黑色背景的 newRect1矮瘟。
- 第二次點(diǎn)擊時(shí)瞳脓,此時(shí)前景變成了上面第一步的結(jié)果,即整屏 lena 圖帶一個(gè)黑色小區(qū)域 newRect1澈侠。前景的臟區(qū)變?yōu)榱说谝徊降暮谏【匦?newRect1劫侧。第二次點(diǎn)擊的臟區(qū) newRect2 覆蓋了 newRect1 一個(gè)右下角,按照同步算法中的減法哨啃,newRect1 中的剩余區(qū)域?qū)⒈豢截惖胶缶吧斩啊S捎谕剿惴](méi)有改變 newRect2 中的內(nèi)容,因此 newRect2 中是有 lena 圖局部的拳球。因此最后就出現(xiàn)了如圖的結(jié)果毁欣。
現(xiàn)象2
去掉代碼中第一次 drawBitmap()
后的那次 lock - unlock
調(diào)用的注釋债蓝。與現(xiàn)象1的區(qū)別就在于缅糟,第一次畫(huà)了前景后堵漱,我們制造了一個(gè) 大小為 (0, 0) 臟區(qū) newRect3,按照同步算法莱找,這會(huì)導(dǎo)致 flip 兩個(gè)緩沖時(shí)酬姆,前景和后景的內(nèi)容進(jìn)行了一次同步。這樣奥溺,當(dāng)用戶第一次點(diǎn)擊時(shí)辞色,后景的臟區(qū)就不會(huì)再是黑色。但是第二次點(diǎn)擊時(shí)浮定,點(diǎn)擊的方塊仍然會(huì)覆蓋上一次點(diǎn)擊的區(qū)域淫僻,即使方塊透明的部分會(huì)把上一次區(qū)域中有顏色的色塊給清掉诱篷。這個(gè)問(wèn)題還是因?yàn)辄c(diǎn)擊之后,前后景沒(méi)有進(jìn)行同步導(dǎo)致的雳灵。
現(xiàn)象3
不再累述棕所,我們?cè)诿看吸c(diǎn)擊后都做一次 lock - unlock
調(diào)用,進(jìn)行一次前悯辙、后景同步琳省。這樣就能解決后一次的方塊透明區(qū)域會(huì)覆蓋前一次的色塊區(qū)域的問(wèn)題。注意到代碼中使用了 postDelayed
來(lái)延時(shí)操作 lock - unlock
躲撰,這是因?yàn)槿绻o接著操作 lock - unlock
的話會(huì)不起作用针贬,這可能是因?yàn)榫o接著操作時(shí),底層對(duì) mLockedBuffer
的鎖還沒(méi)有釋放拢蛋,而導(dǎo)致操作失效桦他。
結(jié)論
由以上分析我們知道了雙緩沖的同步機(jī)制,這個(gè)很好的解釋了為什么如果只 lockCanvas
會(huì)出現(xiàn)閃屏的問(wèn)題(因?yàn)閮蓚€(gè)緩沖臟區(qū)都是整個(gè)屏幕谆棱,兩個(gè)緩沖又沒(méi)有同步快压,因此分別繪在兩個(gè)緩沖上的內(nèi)容會(huì)交替顯示)。
一個(gè)方便且根本的解決上述問(wèn)題的一個(gè)方法就是垃瞧,應(yīng)用在操作 lockCanvas
返回的 Canvas
時(shí)蔫劣,每次都給這個(gè) Canvas
設(shè)置同一張 bitmap,于是所有的繪制都在這個(gè)共享的 bitmap 上得到了同步个从。