SurfaceView 和 GLSurfaceView
Android 應(yīng)用框架 UI 是基于一個從 View 開始的對象層次體系的。所有的 UI 元素經(jīng)歷一個復(fù)雜的測量和布局過程來將它們適配入一個矩形區(qū)域,所有可見的 View 對象被渲染進(jìn)一個 SurfaceFlinger 創(chuàng)建的 Surface,而后者由 WindowManager 在應(yīng)用程序回到前臺時建立。應(yīng)用程序的 UI 線程執(zhí)行布局并渲染進(jìn)一個單獨(dú)的緩沖區(qū)(無論 Layouts 和 Views 的個數(shù),也不管 View 是否是硬件加速的)。
SurfaceView 如同其它 views 一樣接收相同的參數(shù),因此你可以給它一個位置和大小,然后圍繞它適配其它元素。當(dāng)它要渲染時,然而,其內(nèi)容完全是透明的;SurfaceView 的View 部分只是一個透視占位符。
當(dāng) SurfaceView 的 View 組件即將可見時,框架請求 WindowManager 請求 SurfaceFlinger 創(chuàng)建一個新的 Surface。(這不是同步發(fā)生的,這也是為什么你應(yīng)該提供一個回調(diào),在 Surface 創(chuàng)建完成時通知你。)默認(rèn)情況下,新的 Surface 位于應(yīng)用程序 UI Surface 的后面,但默認(rèn)的 Z-ordering 可以被覆蓋來將 Surface 放到頂部。
無論你向 Surface 渲染了什么,它們都將由 SurfaceFlinger 合成,而不是由應(yīng)用程序。這正是 SurfaceView 真正強(qiáng)大的地方:你獲得的 Surface 可以由單獨(dú)的線程或單獨(dú)的進(jìn)程渲染,完全與由應(yīng)用程序 UI 執(zhí)行的任何渲染隔離,而緩沖區(qū)直接進(jìn)入 SurfaceFlinger。你不能完全忽略 UI 線程 - 你依然不得不與 Activity 的生命周期合作,且如果 View 的大小或位置改變了你可能不得不調(diào)整一些東西 - 但你自己完全擁有整個 Surface。與應(yīng)用程序 UI 和其它 layers 的混合由 Hardware Composer 處理。
新的 Surface 是 BufferQueue 的生產(chǎn)者端,其消費(fèi)者是 SurfaceFlinger 的 layer。你可以以任何能夠喂養(yǎng) BufferQueue 的機(jī)制更新 Surface,如 Canvas 函數(shù)提供的 surface,附上一個 EGLSurface 并用 GLES 繪制它,或配置一個 MediaCodec 視頻解碼器寫入它。
合成和硬件 Scaler
讓我們更近地看一下 dumpsys SurfaceFlinger。下面的輸出是在豎屏的 Nexus 設(shè)備上,在 Grafika 的 "Play video (SurfaceView)" activity 播放一個電影時獲取的;視頻是 QVGA (320x240) 的:
type | source crop | frame name ------------+-----------------------------------+--------------------------------HWC | [ 0.0, 0.0, 320.0, 240.0] | [ 48, 411, 1032, 1149] SurfaceViewHWC | [ 0.0, 75.0, 1080.0, 1776.0] | [ 0, 75, 1080, 1776] com.android.grafika/com.android.grafika.PlayMovieSurfaceActivityHWC | [ 0.0, 0.0, 1080.0, 75.0] | [ 0, 0, 1080, 75] StatusBarHWC | [ 0.0, 0.0, 1080.0, 144.0] | [ 0, 1776, 1080, 1920] NavigationBarFB TARGET | [ 0.0, 0.0, 1080.0, 1920.0] | [ 0, 0, 1080, 1920] HWC_FRAMEBUFFER_TARGET-
排列順序 是自后向前的:SurfaceView 的 Surface 在后邊,應(yīng)用程序 UI layer 位于它的頂部,然后是狀態(tài)欄和導(dǎo)航欄在所有其它東西上面。
-
source crop 值表示 SurfaceFlinger 將顯示的 Surface 的緩沖區(qū)的部分。應(yīng)用程序UI 被賦予了一個等于顯示器全尺寸(1080x1920)的 Surface,但是由于被狀態(tài)欄和導(dǎo)航欄遮擋的像素不會被渲染和合成,所以源被裁剪為一個矩形,從距頂部 75 像素的開始并在距底部144個像素的地方結(jié)束。狀態(tài)欄和導(dǎo)航欄具有更小的 Surface,且 source crop 描述了一個矩形,從左上角的 (0,0) 開始并跨越它們的內(nèi)容。
-
frame 值描述了像素在顯示器上顯示的矩形。對于應(yīng)用程序 UI layer,frame 與 source crop 匹配,因?yàn)槲覀冋趯@示大小的 layer 的一部分復(fù)制(或疊加)到另一個顯示大小的 layer 中的相同位置。對于狀態(tài)欄和導(dǎo)航欄,幀矩形的大小是相同的,但位置要調(diào)整以使導(dǎo)航欄出現(xiàn)在屏幕的底部。
-
SurfaceView layer 持有我們的視頻內(nèi)容。source crop 與視頻大小匹配,SurfaceFlinger 知道該大小,因?yàn)?MediaCodec 解碼器(緩沖區(qū)的生產(chǎn)者)在獲取該大小的緩沖區(qū)。幀矩形具有完全不同的大小 —— 984x738。
SurfaceFlinger 通過放縮緩沖區(qū)的內(nèi)容來處理大小的差異以適配幀矩形,放大還是縮小則根據(jù)需要。之所以選擇這個特定的大小是由于它與視頻有著相同的長寬比 (4:3),且在滿足 View 布局約束的條件下盡可能的寬(這包括為了美學(xué)原因而在屏幕邊緣包含的一些填充)。
如果你在相同的 Surface 上起動播放了不同的視頻,則底層的 BufferQueue 將自動地重新分配緩沖區(qū)為新的大小,SurfaceFlinger 將調(diào)整 source crop。如果新視頻的長寬比不同,應(yīng)用將需要強(qiáng)制 re-layout 該 View 來與之匹配,這將導(dǎo)致 WindowManager 告知 SurfaceFlinger 去更新幀矩形。
如果你正在通過一些其它的方式(比如 GELS)在 Surface 上渲染,你可以使用 SurfaceHolder#setFixedSize() 調(diào)用設(shè)置 Surface 的大小。比如,你可以配置一個游戲總是以 1280x720 渲染,這將顯著地減少填充 2560x1440 的平板或 4K 電視的屏幕必須處理的像素數(shù)。顯示處理器處理放縮。如果你不想使你的游戲在固定大小的渲染,你可以通過設(shè)置大小來調(diào)整游戲的長寬比比,使窄尺寸為 720 像素,但長尺寸設(shè)置為保持物理顯示器的長寬比 (比如,1152x720 匹配一個 2560x1600 的顯示器)。使用這種方法的一個例子,請參考 Grafika 的 "Hardware scaler exerciser" activity。
GLSurfaceView
GLSurfaceView 類提供了一些輔助類來管理 EGL contexts,線程間通信,及與 Activity 生命周期的交互。僅此而已。你不需要通過使用 GLSurfaceView 來使用 GLES。
比如,GLSurfaceView 創(chuàng)建一個線程來渲染并在那兒配置一個 EGL context。當(dāng) Activity pause 時,狀態(tài)被自動地清除。通過 GLSurfaceView,大多數(shù)應(yīng)用在使用 GLES 時將無需知道任何與 EGL 有關(guān)的東西。
大多數(shù)情況下,GLSurfaceView 是非常有幫助的,且可以簡化對 GLES 的使用。在某些情況下,它會阻礙你的發(fā)展。如果它有幫助就使用它,否則就不用。
SurfaceView 和 Activity 生命周期
當(dāng)使用 SurfaceView 時,在一個主 UI 線程之外的線程中渲染 Surface 被認(rèn)為是一個良好的實(shí)踐。這產(chǎn)生了一些關(guān)于那個線程和 Activity 生命周期交互的問題。
對于一個使用了 SurfaceView 的 Activity,有兩個分開但相互依賴的狀態(tài)機(jī):
應(yīng)用程序的 onCreate/onResume/onPause
Surface 的 created/changed/destroyed
當(dāng) Activity 啟動時,你以這樣的順序得到回調(diào):
-
onCreate
-
onResume
-
surfaceCreated
-
surfaceChanged
如果你按了返回鍵你得到:
-
onPause
-
surfaceDestroyed (在 Surface 消失之前被調(diào)用)
如果你旋轉(zhuǎn)屏幕,Activity 被銷毀并創(chuàng)建,并將經(jīng)歷一個完整的周期。你可以通過檢查 isFinishing() 來快速重新啟動。有可能 start/stop Activity 太快,以至于 onPause() 之后 surfaceCreated() 實(shí)際可能還沒有發(fā)生。
如果你按下電源鍵來鎖住屏幕,只有 onPause() 被調(diào)用 - 沒有 surfaceDestroyed()。Surface 保持活躍,渲染可以繼續(xù)進(jìn)行。如果你請求它們的話你甚至可以繼續(xù)獲得 Choreographer 事件。如果你的鎖屏強(qiáng)制強(qiáng)制不同的方向,則當(dāng)設(shè)備解鎖時,你的
Activity 可能會重新啟動;如果不是,你可以使用與之前的
Surface 相同的屏幕區(qū)域。
當(dāng)使用一個單獨(dú)的渲染線程來使用 SurfaceView 時,這引發(fā)了一個基本的問題:那個線程的生命周期應(yīng)該與 Surface 還是 Activity 掛鉤?答案依賴于當(dāng)屏幕空白時你想要讓什么發(fā)生:(1). 當(dāng) Activity start/stop 時 start/stop 該線程,或者當(dāng) Surface create/destroy 時 start/stop 該線程。
選項(xiàng) 1 與應(yīng)用的生命周期交互良好。我們在 onResume() 中啟動渲染線程并在 onPause() 停止它。在創(chuàng)建和配置線程時,它有點(diǎn)尷尬,因?yàn)橛袝r Surface 將已經(jīng)存在,有時還沒有(比如在通過電源鍵關(guān)閉屏幕時它依然存活)。我們在該線程中執(zhí)行一些初始化之前不得不等待 surface 創(chuàng)建,但我們不能簡單地在 surfaceCreated() 回調(diào)中完成它,因?yàn)槿绻?Surface 沒有被重建的話那將不會再次被調(diào)用。因此我們需要查詢或者緩存 Surface 狀態(tài),并把它轉(zhuǎn)發(fā)給渲染線程。
注意: 在線程之間傳遞對象時要小心。最好通過一個 Handler 消息來傳遞 Surface 或 SurfaceHolder(而不是僅僅把它填入線程)以避免多核系統(tǒng)的問題。更多細(xì)節(jié),請參考 Android SMP Primer。
選項(xiàng) 2 之所以吸引人,是因?yàn)?Surface 和渲染器在邏輯上是交織在一起的。我們在 Surface 創(chuàng)建之后啟動線程,這將避免一些線程間通信問題,Surface created/changed 消息僅僅被簡單的轉(zhuǎn)發(fā)。我們需要確保在屏幕空白時渲染停止并在屏幕恢復(fù)時恢復(fù);告訴 Choreographer 停止調(diào)用幀繪制調(diào)用可能是一個額簡單的問題。當(dāng)且僅當(dāng)渲染器線程運(yùn)行時,我們的 onResume() 將需要恢復(fù)該調(diào)用。事情可能沒有那么簡單 - 如果我們根據(jù)幀之間的時間來做動畫,當(dāng)下一個事件到達(dá)時我們可能有一個巨大的間隙;顯式的暫停/恢復(fù)消息可能是可取的。
注意: 選項(xiàng) 2 的一個例子可以參考 Grafika 的 "Hardware scaler exerciser"。
兩種選項(xiàng)主要都被渲染器線程如何配置,以及是否執(zhí)行所困繞。一個相關(guān)的問題是在 Activity 被殺掉時 (在onPause() 或 onSaveInstanceState() 中) 從渲染線程提取狀態(tài);在這種情況下,選項(xiàng) 1 最好,因?yàn)樵阡秩酒骶€程已經(jīng) joined 之后它的狀態(tài)無需同步原語就可以訪問。
原文
總結(jié)
以上是生活随笔為你收集整理的SurfaceView 和 GLSurfaceView的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vulkan
- 下一篇: SurfaceTexture