問題:
舉例一個Activity的布局文件和邏輯如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="MissingDefaultResource"
android:background="@android:color/holo_red_dark">
<FrameLayout
android:id="@+id/surfaceView_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="10dp"
android:background="@android:color/holo_blue_bright">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_gravity="center"
android:layout_width="200dp"
android:layout_height="200dp"></SurfaceView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal">
<Button
android:id="@+id/gone_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="set container gone"></Button>
<Button
android:id="@+id/remove_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="remove surfaceView"></Button>
</LinearLayout>
</FrameLayout>
</FrameLayout>
container.findViewById(R.id.remove_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
surfaceViewContainer.removeView(surface); //這個會回調(diào)到surfaceDestroyed, surfaceView立即就會消失,會出現(xiàn)黑塊
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
當我們點擊remove_btn時,會出現(xiàn)SurfaceView所在的區(qū)域會出現(xiàn)10s黑塊的現(xiàn)象,這個現(xiàn)象在我們平時開發(fā)中用到SurafceView時常常遇到,往往在主線程同時存在耗時操作和SurfaceView detach操作的時候出現(xiàn),那么為什么Surfaceview從parent view上面detach的時候容易出現(xiàn)黑塊現(xiàn)象呢?開發(fā)中遇到SUrfaceView黑塊問題又該如何解決呢?下面對這兩個問題進行講解。
回答問題之前,我們先了解下Android 普通View的刷新流程和SurfaceView的刷新有什么區(qū)別。
VSync信號的產(chǎn)生:
關(guān)于頁面渲染,我們經(jīng)常關(guān)注的性能指標就是幀率,一般認為達到60 幀/秒 就可以騙過人眼,給人比較順滑的視覺體驗,在Android中有一個很重要的概念就是VSync信號,一般認為是16ms發(fā)送一次,Vsync機制的引入,主要有以下兩個作用:
提升UI刷新的優(yōu)先級,使得UI刷新操作能夠及時執(zhí)行;
在CPU、GPU和Display之間保持同步,減少Jank幀和屏幕渲染延遲。

VSync信號由硬件產(chǎn)生,決定于顯示器的掃描頻率,硬件產(chǎn)生原始的VSync信號后,會被轉(zhuǎn)化為兩個VSync信號,一個用于通知APP層去刷新UI,一個用于通知SurfaceFlingger取graphic buffer組合處理后給顯示屏顯示。VSync信號分發(fā)流程如下:

SurfaceFlinger:
SurfaceFlinger是系統(tǒng)進程,用于整合不同APP不同Window的圖像,合成之后給硬件顯示。
每一個Layer對應java層的Surface,即一個窗口,一個Activity對應一個Surface,一個WindowManager創(chuàng)建出來的小窗對應一個獨立的Surface,SurfaceView比較特殊,盡管可以嵌入在Activity的布局中,但實際上它獨占一個Surface;這個特性與本文最開始提出的問題息息相關(guān),后文會繼續(xù)分析。


基本流程如下:

步驟1,2:CPU和GPU處理完之后將buffer放到BufferQueue,并調(diào)用onFrameAvailable通知SurfaceFlinger有可用buffer了。
步驟3:SurfaceFlinger再通過內(nèi)部MessageQueue調(diào)用requestNextVsync請求接收下一個VSYNC用于合成。
步驟4,5:下一個VSYNC到了之后回調(diào)MessageQueue的handleMessage函數(shù),實際調(diào)到SurfaceFlinger的onMessageReceived函數(shù)處理如下兩種類型消息:

步驟6,7:在處理REFRESH消息時最終會調(diào)用acquireBuffer函數(shù)從BufferQueue中將之前APP繪制完成的buffer取出來合成。
從上文可以看出,SurfaceFlinger的組合圖層給硬件顯示之前,需要先去取graphic buffer,那么graphic buffer又是誰去更新的呢?對于普通View和SurfaceView來說,這個機制會有所差別。
普通View刷新機制:
舉例Activity中的一個TextView的更新如下:
如果應用層通過調(diào)用TextView的setText方法修改顯示的文案,總體的執(zhí)行流程如下:

步驟描述:
TextView調(diào)用setText方法,會執(zhí)行到TextView的invalidate方法,這就會遞歸調(diào)用parent的invalidate,一直到ViewRootImpl類的invalidate方法,這個方法會調(diào)用到scheduleTraversals
ViewRootImpl通過scheduleTraversals方法會調(diào)用到Choreographer的postCallback方法,postCallback會記錄ViewRootImpl中的mTraversalRunnable,并向底層注冊監(jiān)聽下一個vSync信號
底層的vSync信號過來之后,才會通過給主線程發(fā)送Runnable任務,執(zhí)行Choreographer的doFrame方法,這里面真正調(diào)用執(zhí)行ViewRootImpl中的doTraversal(包括performMeasure、performLayout、performDraw)流程
draw的具體實現(xiàn)通過ThreadedRenderer類,調(diào)用到c++層的RenderThread,實現(xiàn)在render thread執(zhí)行GPU計算,更新SurfaceFlinger中buffer 隊列
下一次SurfaceFlinger收到Vsync信號的時候,就可以真正將這次setText的內(nèi)容交給硬件,顯示給用戶了
因此,Android系統(tǒng)中普通View的渲染,并不是代碼執(zhí)行完立即顯示到屏幕上的,而是需要在設(shè)置變化之后,等待消費下一次給APP的vSync信號,才能把新的圖像更新給SurfaceFlinger,而后才能真正顯示出來。
UI刷新通用流程總結(jié)如下:

步驟1:View調(diào)用invalidate方法進行重繪時最終會遞歸調(diào)用到ViewRootImpl中。
步驟2: ViewRootImpl并不會立即會View進行繪制,而是調(diào)用scheduleTraversals將繪制請求給到Choreographer,并開始同步屏障,保證UI處理的高優(yōu)先級。
步驟3,4: 通過postCallback將繪制請求給到Choreographer之后,Choreographer最終會將監(jiān)聽下一個VSYNC的請求發(fā)送到SurfaceFlinger進程的DispSync這個類,這是VSYNC分發(fā)的核心。
步驟5,6:當下一個VSYNC到來之后會回調(diào)Choreographer的onVsync方法,onVsync中調(diào)用doFrame,doCallbacks處理View的繪制請求。
步驟7:View繪制請求的入口即ViewRootImpl的performTraversals,這個方法會依次執(zhí)行View的onMeasure,onLayout,onDraw開始View的繪制流程。
步驟8:硬件加速引入之后UI的具體繪制會在一個單獨的渲染線程RenderThread,CPU為View構(gòu)建DisplayList(包含繪制指令和數(shù)據(jù))之后將數(shù)據(jù)共享給GPU,剩下的繪制操作由GPU在RenderThread線程完成。
步驟9,10,11:向BufferQueue中dequeue一塊可用GraphicBuffer之后由GPU對這個塊buffer進行操作,完成之后交換buffer(dequeue的是back buffer,front buffer用于顯示,back buffer繪制完成之后和front buffer交換)。
步驟12:此時CPU和GPU對buffer的繪制已經(jīng)完成(概念上已經(jīng)完成,實際上GPU可能還在操作,依賴Fence進行同步),接著通過queueBuffer函數(shù)將buffer轉(zhuǎn)移到BufferQueue,然后通知SurfaceFlinger有可用buffer了。
CPU、GPU、SurfaceFlinger如何協(xié)作:

SurfaceView的刷新與銷毀:
挖洞與繪制:
前面提到過,SurfaceView與普通的View有很大的區(qū)別,它可以嵌入到Activity的布局中,但是它是一個獨立的Surface(Layer),內(nèi)容的刷新流程也跟普通的View完全不一樣。SurfaceView在Activity中的布局,只決定它的顯示位置。如果沒有設(shè)置setZOrderOnTop為true,SurfaceView的窗口在Activity窗口的下面,SurfaceView這個Layer的顯示,依賴于ViewRootImpl中挖洞的邏輯(gatherTransparentRegion),在ViewRootImpl類中performLayout邏輯執(zhí)行完之后,會收集SurfaceView需要透出的區(qū)域,并把這個信息傳遞給底層,將這個區(qū)域設(shè)置為透明,這樣Actvity這一層的Layer就不會遮擋下面SurfaceView的Layer。

挖洞流程如下:

SurfaceView支持在后臺線程直接繪制內(nèi)容,基本繪制流程如下,調(diào)用了unlockCanvasAndPost之后,便會將在Canvas上繪制的內(nèi)容通過獨立的RenderProxy處理后提交給SurfaceFlinger合成,后面就可顯示出來了。也可以通過holder.getSurface()獲取到Surface之后,直接通過OpenGl渲染。

銷毀:
這里講SurfaceView的銷毀主要指的是將SurfaceView對應的Layer從SurfaceFlinger中移除。一般可以通過直接設(shè)置這個SurfaceView本身不可見(注意設(shè)置這個SurfaceView的父View不可見不會觸發(fā)Layer的移除)或者將這個SurfaceView從ViewTree上remove掉實現(xiàn)。如VC中使用的是從父View remove這個SurfaceView的方法實現(xiàn)SurfaceView資源的釋放和視圖的刷新。
當調(diào)用parent.removeView將SurfaceView移除時,流程如下:

可以看出,當SurfaceView被從父View上remove掉時,是直接調(diào)用代碼,將自己對應的Layer從的SurfaceFlinger中移除掉了。并不像普通的View更新一樣,需要等待下一個vSync信號,在主線程插入Runnable任務觸發(fā)doTraversal的流程,然后再將這個變化反應給SurfaceFlinger。
回到文初的問題:
結(jié)合前面的調(diào)用流程,可以知道,在refreshAllUnit的過程中,由于這個方法總體耗時較長,并且在主線程執(zhí)行,這期間Choreographer沒辦法插入任務去執(zhí)行doTraversal的流程,因此Activity對應的代碼執(zhí)行了,但實際上并沒有更新顯示。而SurfaceView被remove掉之后,會直接更新顯示,這中間就有一個時間差,導致SurfaceView原來顯示的區(qū)域出現(xiàn)了黑塊(挖出來的洞)。

那么如何解決SurfaceView黑塊的問題呢?我們可以在調(diào)用SurfaceView的detach方法之前,插入16ms的延時,先讓SurfaceView的parent視圖區(qū)域變得不可見,切換為新的視圖成功之后,再調(diào)用SurfaceView的detach方法。
參考:
https://www.mtyun.com/library/hardware-accelerate
https://developer.android.com/guide/topics/graphics/hardware-accel?hl=zh-cn
https://sharrychoo.github.io/blog/android-source/surfaceflinger-vsync-dispatch
https://source.android.google.cn/devices/graphics/implement-vsync
https://www.youtube.com/watch?v=zdQRIYOST64
https://blog.csdn.net/qq_31339141/article/details/108503315
https://mp.weixin.qq.com/s/IIh2g1i6Y4rZeCTY-t6_8w
https://www.codenong.com/cs107053967/
https://source.android.google.cn/devices/graphics/implement-vsync
https://blog.csdn.net/qq_34211365/article/details/107996767
https://juejin.cn/post/7004420015038414885