SurfaceView 的雙緩沖

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

WindowSurface 的載體社露,一個(gè) Window 對(duì)應(yīng)一個(gè) Surface挟阻。一個(gè) WindowWindowManager 創(chuàng)建,并提供給 application 使用峭弟。Window 除了包含繪制的內(nèi)容附鸽,還包含其他的一些窗口屬性。

View

ViewWindow 中的一個(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

SurfaceViewView 的一種特殊實(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)和控制 SurfaceViewSurface 的相關(guān)方法挨决。它有一下幾個(gè)主要方法:

  • Canvas lockCanvas() 獲取一個(gè) Canvas 對(duì)象并上鎖。這個(gè)鎖是重入鎖订歪,因此同一個(gè)線程可以多次鎖定脖祈,但是第二次鎖定時(shí),返回的 Canvasnull刷晋。鎖定后返回 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)的普通 ViewonDraw(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石咬,分別是 frontBufferbackBuffer

status_t err = dequeueBuffer(&out, &fenceFd);
...
if (err == NO_ERROR) {
    ....
    sp<GraphicBuffer> backBuffer(GraphicBuffer::getSelf(out));
    ...
    const sp<GraphicBuffer>& frontBuffer(mPostedBuffer);
}

上面的代碼取自 frameworkSurface.lock(ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds) 方法揩悄。可以看到鬼悠,系統(tǒng)先從 buffer 池中 dequeueBuffer 出來(lái)一個(gè)可用的 out删性,然后將 out 賦給 backBuffermPostedBuffer 為已經(jīng)顯示的 buffer焕窝,將 mPostedBuffer 的內(nèi)容賦給 frontBuffer蹬挺。

下面我們將 frontBuffer 稱為前景,將 backBuffer 稱為后景它掂。

  1. 調(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
  1. 調(diào)用 Surface.unlockAndPost
  • mLockedBuffer 解鎖,并賦值給 mPostedBuffer起愈,渲染到屏幕

雙緩沖的同步過(guò)程

這個(gè)同步過(guò)程就是每次調(diào)用 Surface.lock 時(shí)只恨,將前景內(nèi)容同步到后景的算法译仗。

集合的運(yùn)算
集合的運(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ū)大小就是后景的大小忱详。

inOutDirtyBounds != null

inOutDirtyBounds == null

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è)差值拷貝到后景。


    臟區(qū)的拷貝
  • 不能拷貝時(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
現(xiàn)象1

第一次點(diǎn)擊時(shí),newRect 有黑色背景畦贸,但是第二次點(diǎn)擊以及之后的點(diǎn)擊都沒(méi)有黑色背景陨闹。而且,第二次點(diǎn)擊會(huì)覆蓋掉第一次的內(nèi)容薄坏。

解釋:

  1. 應(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矮瘟。
  2. 第二次點(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
現(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 上得到了同步个从。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末脉幢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嗦锐,更是在濱河造成了極大的恐慌嫌松,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奕污,死亡現(xiàn)場(chǎng)離奇詭異萎羔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)菊值,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)外驱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)育灸,“玉大人腻窒,你說(shuō)我怎么就攤上這事“跽福” “怎么了儿子?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)砸喻。 經(jīng)常有香客問(wèn)我柔逼,道長(zhǎng)蒋譬,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任愉适,我火速辦了婚禮犯助,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘维咸。我一直安慰自己剂买,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布癌蓖。 她就那樣靜靜地躺著瞬哼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪租副。 梳的紋絲不亂的頭發(fā)上坐慰,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音用僧,去河邊找鬼结胀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛永毅,可吹牛的內(nèi)容都是我干的把跨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼沼死,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼着逐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起意蛀,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤耸别,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后县钥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秀姐,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年若贮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了省有。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谴麦,死狀恐怖蠢沿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匾效,我是刑警寧澤舷蟀,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響野宜,放射性物質(zhì)發(fā)生泄漏扫步。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一匈子、第九天 我趴在偏房一處隱蔽的房頂上張望河胎。 院中可真熱鬧,春花似錦虎敦、人聲如沸仿粹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吭历。三九已至,卻和暖如春擂橘,著一層夾襖步出監(jiān)牢的瞬間晌区,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工通贞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朗若,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓昌罩,卻偏偏與公主長(zhǎng)得像哭懈,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茎用,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容