Surface、SurfaceHolder、EGLSurface、SurfaceView、GLSurfaceView、SurfaceTexture、TextureView、SurfaceFlinger 和 Vulkan。java
BufferQueue 和 gralloc。BufferQueue 將可生成圖形數據緩衝區的組件(生產者)鏈接到接受數據以便進行顯示或進一步處理的組件(消費者)。經過供應商專用 HAL 接口實現的 gralloc 內存分配器將用於執行緩衝區分配任務。android
SurfaceFlinger、Hardware Composer 和虛擬顯示屏。SurfaceFlinger 接受來自多個源的數據緩衝區,而後將它們進行合成併發送到顯示屏。Hardware Composer HAL (HWC) 肯定使用可用硬件合成緩衝區的最有效的方法,虛擬顯示屏使合成輸出可在系統內使用(錄製屏幕或經過網絡發送屏幕)。編程
Surface、Canvas 和 SurfaceHolder。Surface 可生成一個一般由 SurfaceFlinger 使用的緩衝區隊列。當渲染到 Surface 上時,結果最終將出如今傳送給消費者的緩衝區中。Canvas API 提供一種軟件實現方法(支持硬件加速),用於直接在 Surface 上繪圖(OpenGL ES 的低級別替代方案)。與視圖有關的任何內容均涉及到 SurfaceHolder,其 API 可用於獲取和設置 Surface 參數(如大小和格式)。canvas
EGLSurface 和 OpenGL ES。OpenGL ES (GLES) 定義了用於與 EGL 結合使用的圖形渲染 API。EGI 是一個規定如何經過操做系統建立和訪問窗口的庫(要繪製紋理多邊形,請使用 GLES 調用;要將渲染放到屏幕上,請使用 EGL 調用)。此頁還介紹了 ANativeWindow,它是 Java Surface 類的 C/C++ 等價類,用於經過原生代碼建立 EGL 窗口表面。數組
Vulkan。Vulkan 是一種用於高性能 3D 圖形的低開銷、跨平臺 API。與 OpenGL ES 同樣,Vulkan 提供用於在應用中建立高質量實時圖形的工具。Vulkan 的優點包括下降 CPU 開銷以及支持 SPIR-V 二進制中間語言。緩存
SurfaceView 和 GLSurfaceView。SurfaceView 結合了 Surface 和 View。SurfaceView 的 View 組件由 SurfaceFlinger(而不是應用)合成,從而能夠經過單獨的線程/進程渲染,並與應用界面渲染隔離。GLSurfaceView 提供幫助程序類來管理 EGL 上下文、線程間通訊以及與「Activity 生命週期」的交互(但使用 GLES 時並不須要 GLSurfaceView)。安全
SurfaceTexture。SurfaceTexture 將 Surface 和 GLES 紋理相結合來建立 BufferQueue,而您的應用是 BufferQueue 的消費者。當生產者將新的緩衝區排入隊列時,它會通知您的應用。您的應用會依次釋放先前佔有的緩衝區,從隊列中獲取新緩衝區並執行 EGL 調用,從而使 GLES 可將此緩衝區做爲外部紋理使用。Android 7.0 增長了對安全紋理視頻播放的支持,以便用戶可以對受保護的視頻內容進行 GPU 後處理。bash
TextureView。TextureView 結合了 View 和 SurfaceTexture。TextureView 對 SurfaceTexture 進行包裝,並負責響應回調以及獲取新的緩衝區。在繪圖時,TextureView 使用最近收到的緩衝區的內容做爲其數據源,根據 View 狀態指示,在它應該渲染的任何位置和以它應該採用的任何渲染方式進行渲染。View 合成始終經過 GLES 來執行,這意味着內容更新可能會致使其餘 View 元素重繪。網絡
BufferQueue 類是 Android 中全部圖形處理操做的核心。它的做用很簡單:將生成圖形數據緩衝區的一方(生產方)鏈接到接受數據以進行顯示或進一步處理的一方(消耗方)。幾乎全部在系統中移動圖形數據緩衝區的內容都依賴於 BufferQueue。數據結構
gralloc 內存分配器會進行緩衝區分配,並經過供應商特定的 HAL 接口來實現。alloc() 函數得到預期的參數(寬度、高度、像素格式)以及一組用法標記。
基本用法很簡單:生產方請求一個可用的緩衝區 (dequeueBuffer()),並指定一組特性,包括寬度、高度、像素格式和用法標記。生產方填充緩衝區並將其返回到隊列 (queueBuffer())。隨後,消耗方獲取該緩衝區 (acquireBuffer()) 並使用該緩衝區的內容。當消耗方操做完畢後,將該緩衝區返回到隊列 (releaseBuffer())。
最新的 Android 設備支持「同步框架」,這使得系統可以在與能夠異步處理圖形數據的硬件組件結合使用時提升工做效率。例如,生產方能夠提交一系列 OpenGL ES 繪製命令,而後在渲染完成以前將輸出緩衝區加入隊列。該緩衝區伴有一個柵欄,當內容準備就緒時,柵欄會發出信號。當該緩衝區返回到空閒列表時,會伴有第二個柵欄,所以消耗方能夠在內容仍在使用期間釋放該緩衝區。該方法縮短了緩衝區經過系統時的延遲時間,並提升了吞吐量。
隊列的一些特性(例如能夠容納的最大緩衝區數)由生產方和消耗方聯合決定。可是,BufferQueue 負責根據須要分配緩衝區。除非特性發生變化,不然將會保留緩衝區;例如,若是生產方請求具備不一樣大小的緩衝區,則系統會釋放舊的緩衝區,並根據須要分配新的緩衝區。
生產方和消耗方能夠存在於不一樣的進程中。目前,消耗方始終建立和擁有數據結構。在舊版本的 Android 中,只有生產方纔進行 Binder 處理(即生產方可能在遠程進程中,但消耗方必須存在於建立隊列的進程中)。Android 4.4 和更高版本已發展爲更常規的實現。
BufferQueue 永遠不會複製緩衝區內容(移動如此多的數據是很是低效的操做)。相反,緩衝區始終經過句柄進行傳遞。
擁有圖形數據緩衝區的確不錯,若是還能在設備屏幕上查看它們就更是錦上添花了。這正是 SurfaceFlinger 和 Hardware Composer HAL 的用武之地。
SurfaceFlinger 的做用是接受來自多個來源的數據緩衝區,對它們進行合成,而後發送到顯示設備。當應用進入前臺時,WindowManager 服務會向 SurfaceFlinger 請求一個繪圖 Surface。SurfaceFlinger 會建立一個其主要組件爲 BufferQueue 的層,而 SurfaceFlinger 是其消耗方。生產方端的 Binder 對象經過 WindowManager 傳遞到應用,而後應用能夠開始直接將幀發送到 SurfaceFlinger。
大多數應用一般在屏幕上有三個層:屏幕頂部的狀態欄、底部或側面的導航欄以及應用的界面。有些應用會擁有更多或更少的層(例如,默認主屏幕應用有一個單獨的壁紙層,而全屏遊戲可能會隱藏狀態欄)。每一個層均可以單獨更新。狀態欄和導航欄由系統進程渲染,而應用層由應用渲染,二者之間不進行協調。
設備顯示會按必定速率刷新,在手機和平板電腦上一般爲每秒 60 幀。若是顯示內容在刷新期間更新,則會出現撕裂現象;所以,請務必只在週期之間更新內容。在能夠安全更新內容時,系統便會收到來自顯示設備的信號。因爲歷史緣由,咱們將該信號稱爲 VSYNC 信號。
刷新率可能會隨時間而變化,例如,一些移動設備的刷新率範圍在 58 fps 到 62 fps 之間,具體要視當前條件而定。對於鏈接了 HDMI 的電視,刷新率在理論上能夠降低到 24 Hz 或 48 Hz,以便與視頻相匹配。因爲每一個刷新週期只能更新屏幕一次,所以以 200 fps 的刷新率爲顯示設備提交緩衝區只是在作無用功,由於大多數幀永遠不會被看到。SurfaceFlinger 不會在應用提交緩衝區時執行操做,而是在顯示設備準備好接收新的緩衝區時纔會喚醒。
當 VSYNC 信號到達時,SurfaceFlinger 會遍歷它的層列表,以尋找新的緩衝區。若是找到新的緩衝區,它會獲取該緩衝區;不然,它會繼續使用之前獲取的緩衝區。SurfaceFlinger 老是須要可顯示的內容,所以它會保留一個緩衝區。若是在某個層上沒有提交緩衝區,則該層會被忽略。
SurfaceFlinger 在收集可見層的全部緩衝區以後,便會詢問 Hardware Composer 應如何進行合成。
Hardware Composer HAL (HWC) 是在 Android 3.0 中推出的,而且多年來一直都在不斷演進。它主要是用來肯定經過可用硬件來合成緩衝區的最有效方法。做爲 HAL,其實現是特定於設備的,並且一般由顯示設備硬件原始設備製造商 (OEM) 完成。
Surface 表示一般(但並不是老是!)由 SurfaceFlinger 消耗的緩衝區隊列的生產方。當您渲染到 Surface 上時,產生的結果將進入相關緩衝區,該緩衝區被傳遞給消耗方。Surface 不只僅是您能夠隨意擦寫的原始內存數據塊。
用於顯示 Surface 的 BufferQueue 一般配置爲三重緩衝;但按需分配緩衝區。所以,若是生產方足夠緩慢地生成緩衝區 - 也許是以 30fps 的速度在 60fps 的顯示屏上播放動畫 - 隊列中可能只有兩個分配的緩衝區。這有助於最小化內存消耗。
曾經有一段時間全部渲染都是用軟件完成的,您今天仍然能夠這樣作。低級實現由 Skia 圖形庫提供。若是要繪製一個矩形,您能夠調用庫,而後它會在緩衝區中適當地設置字節。爲了確保兩個客戶端不會同時更新某個緩衝區,或者在該緩衝區正在被顯示時寫入該緩衝區,您必須鎖定該緩衝區才能進行訪問。lockCanvas() 可鎖定該緩衝區並返回用於繪製的 Canvas,unlockCanvasAndPost() 則解鎖該緩衝區並將其發送到合成器。
隨着時間的推移,出現了具備通用 3D 引擎的設備,因而 Android 圍繞 OpenGL ES 進行了從新定位。然而,必須確保舊 API 依然適用於應用和應用框架代碼,因此咱們努力對 Canvas API 進行了硬件加速。從硬件加速頁面的圖表能夠看出,整個過程並不是一路順風。特別要注意的一點是,雖然提供給 View 的 onDraw() 方法的 Canvas 可能已硬件加速,可是當應用經過 lockCanvas() 直接鎖定 Surface 時所得到的 Canvas 從未得到硬件加速。
當您鎖定一個 Surface 以便訪問 Canvas 時,「CPU 渲染器」將鏈接到 BufferQueue 的生產方,直到 Surface 被銷燬時纔會斷開鏈接。大多數其餘生產方(如 GLES)能夠斷開鏈接並從新鏈接到 Surface,可是基於 Canvas 的「CPU 渲染器」則不能。這意味着若是您已經爲某個 Canvas 將相關 Surface 鎖定,則沒法使用 GLES 在該 Surface 上進行繪製或從視頻解碼器向其發送幀。
生產方首次從 BufferQueue 請求緩衝區時,緩衝區將被分配並初始化爲零。有必要進行初始化,以免意外地在進程之間共享數據。然而,當您從新使用緩衝區時,之前的內容仍然存在。若是您反覆調用 lockCanvas() 和 unlockCanvasAndPost() 而不繪製任何內容,則會在先前渲染的幀之間循環。
Surface 鎖定/解鎖代碼會保留對先前渲染的緩衝區的引用。若是在鎖定 Surface 時指定了髒區域,那麼它將從之前的緩衝區複製非髒像素。緩衝區頗有可能由 SurfaceFlinger 或 HWC 處理;可是因爲咱們只需從中讀取內容,因此無需等待獨佔訪問。
應用直接在 Surface 上進行繪製的主要非 Canvas 方法是經過 OpenGL ES。
與 Surface 配合使用的一些功能須要 SurfaceHolder,特別是 SurfaceView。最初的想法是,Surface 表明合成器管理的原始緩衝區,而 SurfaceHolder 由應用管理,並跟蹤更高層次的信息(如維度和格式)。Java 語言定義對應的是底層本機實現。能夠說,這種劃分方式已再也不有用,但它長期以來一直是公共 API 的一部分。
通常來講,與 View 相關的任何內容都涉及到 SurfaceHolder。一些其餘 API(如 MediaCodec)將在 Surface 自己上運行。您能夠輕鬆地從 SurfaceHolder 獲取 Surface,所以當您擁有 SurfaceHolder 時,使用它便可。
用於獲取和設置 Surface 參數(例如大小和格式)的 API 是經過 SurfaceHolder 實現的。
OpenGL ES 定義了一個渲染圖形的 API,但沒有定義窗口系統。爲了讓 GLES 可以適合各類平臺,GLES 將與知道如何經過操做系統建立和訪問窗口的庫結合使用。用於 Android 的庫稱爲 EGL。若是要繪製紋理多邊形,應使用 GLES 調用;若是要在屏幕上進行渲染,應使用 EGL 調用。
在使用 GLES 進行任何操做以前,須要建立一個 GL 上下文。在 EGL 中,這意味着要建立一個 EGLContext 和一個 EGLSurface。GLES 操做適用於當前上下文,該上下文經過線程局部存儲訪問,而不是做爲參數進行傳遞。這意味着您必須注意渲染代碼在哪一個線程上執行,以及該線程上的當前上下文。
EGLSurface 能夠是由 EGL 分配的離屏緩衝區(稱爲「pbuffer」),或由操做系統分配的窗口。EGL 窗口 Surface 經過 eglCreateWindowSurface() 調用被建立。該調用將「窗口對象」做爲參數,在 Android 上,該對象能夠是 SurfaceView、SurfaceTexture、SurfaceHolder 或 Surface,全部這些對象下面都有一個 BufferQueue。當您進行此調用時,EGL 將建立一個新的 EGLSurface 對象,並將其鏈接到窗口對象的 BufferQueue 的生產方接口。此後,渲染到該 EGLSurface 會致使一個緩衝區離開隊列、進行渲染,而後排隊等待消耗方使用。(術語「窗口」表示預期用途,但請注意,輸出內容不必定會顯示在顯示屏上。)
EGL 不提供鎖定/解鎖調用,而是由您發出繪製命令,而後調用 eglSwapBuffers() 來提交當前幀。方法名稱來自傳統的先後緩衝區交換,但實際實現可能會有很大的不一樣。
一個 Surface 一次只能與一個 EGLSurface 關聯(您只能將一個生產方鏈接到一個 BufferQueue),可是若是您銷燬該 EGLSurface,它將與該 BufferQueue 斷開鏈接,並容許其餘內容鏈接到該 BufferQueue。
經過更改「當前」EGLSurface,指定線程可在多個 EGLSurface 之間進行切換。一個 EGLSurface 一次只能在一個線程上處於當前狀態。
關於 EGLSurface 最多見的一個錯誤理解就是假設它只是 Surface 的另外一方面(如 SurfaceHolder)。它是一個相關但獨立的概念。您能夠在沒有 Surface 做爲支持的 EGLSurface 上繪製,也能夠在沒有 EGL 的狀況下使用 Surface。EGLSurface 僅爲 GLES 提供一個繪製的地方。
公開的 Surface 類以 Java 編程語言實現。C/C++ 中的同等項是 ANATIONWindow 類,由 Android NDK 半公開。您可使用 ANativeWindow_fromSurface() 調用從 Surface 獲取 ANativeWindow。就像它的 Java 語言同等項同樣,您能夠對 ANativeWindow 進行鎖定、在軟件中進行渲染,以及解鎖併發布。
要從原生代碼建立 EGL 窗口 Surface,可將 EGLNativeWindowType 的實例傳遞到 eglCreateWindowSurface()。EGLNativeWindowType 是 ANativeWindow 的同義詞,您能夠自由地在它們之間轉換。
基本的「原生窗口」類型只是封裝 BufferQueue 的生產方,這一點並不足爲奇。
Android 7.0 添加了對 Vulkan 的支持。Vulkan 是用於高性能 3D 圖形的低開銷、跨平臺 API。與 OpenGL ES 同樣,Vulkan 提供多種用於在應用中建立高質量的實時圖形的工具。Vulkan 的優點包括下降 CPU 開銷以及支持 SPIR-V 二進制中間語言。
應用開發者能夠利用 Vulkan 來建立在 GPU 上執行命令的應用,大幅下降開銷。此外,Vulkan 還能夠更直接地映射到當前圖形硬件中的功能,最大限度地下降驅動程序的出錯機率,並減小開發者的測試時間(例如,排查 Vulkan 錯誤所需的時間更短)。
Vulkan 支持包含如下組件:
Vulkan 驗證層(在 Android NDK 中提供)。這是開發者在開發 Vulkan 應用期間使用的一組庫。圖形供應商提供的 Vulkan 運行時庫和 Vulkan 驅動程序不包含使 Vulkan 運行時保持高效的運行時錯誤檢查功能,而是使用驗證庫(僅在開發過程當中)來查找應用在使用 Vulkan API 時出現的錯誤。Vulkan 驗證庫在開發過程當中關聯到應用並執行此錯誤檢查。在找出全部 API 使用問題以後,該應用將再也不須要包含這些庫。
Vulkan 運行時(由 Android 提供)。這是一個原生庫 ((libvulkan.so),提供稱爲 Vulkan 的新公共原生 API。大多數功能由 GPU 供應商提供的驅動程序實現;運行時會封裝驅動程序、提供 API 攔截功能(針對調試和其餘開發者工具)以及管理驅動程序與平臺依賴項(如 BufferQueue)之間的交互。
Vulkan 驅動程序(由 SoC 提供)。將 Vulkan API 映射到特定於硬件的 GPU 命令以及與內核圖形驅動程序的交互。
Android 應用框架界面是以使用 View 開頭的對象層次結構爲基礎。全部界面元素都會通過一個複雜的測量和佈局過程,該過程會將這些元素融入到矩形區域中,而且全部可見 View 對象都會渲染到一個由 SurfaceFlinger 建立的 Surface(在應用置於前臺時,由 WindowManager 進行設置)。應用的界面線程會執行佈局並渲染到單個緩衝區(不考慮 Layout 和 View 的數量以及 View 是否已通過硬件加速)。
SurfaceView從Android 1.0(API level 1)時就有 。它繼承自類View,所以它本質上是一個View。但與普通View不一樣的是,它有本身的Surface。咱們知道,通常的Activity包含的多個View會組成View hierachy的樹形結構,只有最頂層的DecorView,也就是根結點視圖,纔是對WMS可見的。這個DecorView在WMS中有一個對應的WindowState。相應地,在SurfaceFlinger(SF)中對應的Layer。而SurfaceView自帶一個Surface,這個Surface在WMS中有本身對應的WindowState,在SurfaceFlinger中也會有本身的Layer。以下圖所示:
也就是說,雖然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它與宿主窗口是分離的。這樣的好處是對這個Surface的渲染能夠放到單獨線程去作,渲染時能夠有本身的GL context。這對於一些遊戲、視頻等性能相關的應用很是有益,由於它不會影響主線程對事件的響應。但它也有缺點,由於這個Surface不在View hierachy中,它的顯示也不受View的屬性控制,因此不能進行平移,縮放等變換,也不能放在其它ViewGroup中,一些View中的特性也沒法使用。
SurfaceView 採用與其餘視圖相同的參數,所以您能夠爲 SurfaceView 設置位置和大小,並在其周圍填充其餘元素。可是,當須要渲染時,內容會變得徹底透明;SurfaceView 的 View 部分只是一個透明的佔位符。
當 SurfaceView 的 View 組件即將變得可見時,框架會要求 WindowManager 命令 SurfaceFlinger 建立一個新的 Surface。(這個過程並不是同步發生,所以您應該提供回調,以便在 Surface 建立完畢後收到通知。)默認狀況下,新的 Surface 將放置在應用界面 Surface 的後面,但能夠替換默認的 Z 排序,將 Surface 放在頂層。
渲染到該 Surface 上的內容將會由 SurfaceFlinger(而非應用)進行合成。這是 SurfaceView 的真正強大之處:您得到的 Surface 能夠由單獨的線程或單獨的進程進行渲染,並與應用界面執行的任何渲染隔離開,而緩衝區可直接轉至 SurfaceFlinger。
您不能徹底忽略界面線程,由於您仍然須要與 Activity 生命週期相協調,而且若是 View 的大小或位置發生變化,您可能須要調整某些內容,可是您能夠擁有整個 Surface。與應用界面和其餘圖層的混合由 Hardware Composer 處理。
新的 Surface 是 BufferQueue 的生產者端,其消費者是 SurfaceFlinger 層。您可使用任意提供 BufferQueue 的機制(例如,提供 Surface 的 Canvas 函數)來更新 Surface,附加 EGLSurface 並使用 GLES 進行繪製,或者配置 MediaCodec 視頻解碼器以便於寫入。
SurfaceView能夠自定義子類, 繼承於SurfaceView就能夠了. 使用的時候能夠直接new 一個不帶參數的構造函數的對象, 或者隨意定義. 好比這樣:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
複製代碼
在Activity中直接使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int type = getIntent().getIntExtra("type", 0);
if (type == 0) {
setContentView(new MySurfaceView(this));
複製代碼
也能夠在xml中定義, 那麼自定義類裏面就必須實現三個參數的那個構造方法.
自定義SurfaceView中若是要動態的更新顯示, 就得啓動一個線程來作. 線程啓動的最好的時機就是在surfaceCreated()回調方法中. 在這個線程中畫圖時, 經過SurfaceHolder來獲取Canvas對象, 並且用完以後必須unlock. canvas = surfaceHolder.lockCanvas(); //獲取Canvas對象 surfaceHolder.unlockCanvasAndPost(canvas); // 釋放Canvas對象. SurfaceView也是直接繼承自View的, 也有onTouchEvent等方法, 因此除了觸摸事件之類的不是問題 onMeasure(), onLayout(), onSizeChanged(), onWindowVisibilityChanged(), onAttachedToWindow() 這些方法都會被回調, 並且都是在UI線程. 可是 onDraw(),onFinishInflate()方法都 不會被回調, 這個須要注意!
咱們來仔細研究一下 dumpsys SurfaceFlinger。當在 Nexus 5 上,以縱向方向在 Grafika 的「播放視頻 (SurfaceView)」活動中播放電影時,採用如下輸出;視頻是 QVGA (320x240):
type | source crop | frame name
------------+-----------------------------------+--------------------------------
HWC | [ 0.0, 0.0, 320.0, 240.0] | [ 48, 411, 1032, 1149] SurfaceView
HWC | [ 0.0, 75.0, 1080.0, 1776.0] | [ 0, 75, 1080, 1776] com.android.grafika/com.android.grafika.PlayMovieSurfaceActivity
HWC | [ 0.0, 0.0, 1080.0, 75.0] | [ 0, 0, 1080, 75] StatusBar
HWC | [ 0.0, 0.0, 1080.0, 144.0] | [ 0, 1776, 1080, 1920] NavigationBar
FB TARGET | [ 0.0, 0.0, 1080.0, 1920.0] | [ 0, 0, 1080, 1920] HWC_FRAMEBUFFER_TARGET
複製代碼
SurfaceFlinger 經過縮放(根據須要放大或縮小)緩衝區內容來填充框架矩形,以處理大小差別。之因此選擇這種特定尺寸,是由於它具備與視頻相同的寬高比 (4:3),而且因爲 View 佈局的限制(爲了美觀,在屏幕邊緣處留有必定的內邊距),所以應儘量地寬。
若是您在同一 Surface 上開始播放不一樣的視頻,底層 BufferQueue 會將緩衝區自動從新分配爲新的大小,而 SurfaceFlinger 將調整源剪裁。若是新視頻的寬高比不一樣,則應用須要強制從新佈局 View 才能與之匹配,這將致使 WindowManager 通知 SurfaceFlinger 更新框架矩形。
若是您經過其餘方式(如 GLES)在 Surface 上進行渲染,則可使用 SurfaceHolder#setFixedSize() 調用設置 Surface 尺寸。例如,您能夠將遊戲配置爲始終採用 1280x720 的分辨率進行渲染,這將大大減小填充 2560x1440 平板電腦或 4K 電視機屏幕所需處理的像素數。顯示處理器會處理縮放。若是您不但願給遊戲加上水平或垂直黑邊,您能夠經過設置尺寸來調整遊戲的寬高比,使窄尺寸爲 720 像素,但長尺寸設置爲維持物理顯示屏的寬高比(例如,設置爲 1152x720 來匹配 2560x1600 的顯示屏)。有關此方法的示例,請參閱 Grafika 的「硬件縮放練習程序」活動。
GLSurfaceView 類提供幫助程序類,用於管理 EGL 上下文、線程間通訊以及與 Activity 生命週期的交互。這就是其功能。您無需使用 GLSurfaceView 來應用 GLES。
例如,GLSurfaceView 建立一個渲染線程,並配置 EGL 上下文。當活動暫停時,狀態將自動清除。大多數應用都不須要知道 EGL,即可經過 GESurfaceView 使用 GLES。
GLSurfaceView從Android 1.5(API level 3)開始加入,做爲SurfaceView的補充。它能夠看做是SurfaceView的一種典型使用模式。在SurfaceView的基礎上,它加入了EGL的管理,並自帶了渲染線程。另外它定義了用戶須要實現的Render接口,提供了用Strategy pattern更改具體Render行爲的靈活性。做爲GLSurfaceView的Client,只須要將實現了渲染函數的Renderer的實現類設置給GLSurfaceView便可。如:
public class TriangleActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
mGLView = new GLSurfaceView(this);
mGLView.setRenderer(new RendererImpl(this));
複製代碼
其中SurfaceView中的SurfaceHolder主要是提供了一些操做Surface的接口。GLSurfaceView中的EglHelper和GLThread分別實現了上面提到的管理EGL環境和渲染線程的工做。GLSurfaceView的使用者須要實現Renderer接口。
當使用 SurfaceView 時,使用主界面線程以外的線程渲染 Surface 是很好的作法。不過,這樣就會產生一些與線程和 Activity 生命週期之間的交互相關的問題。
對於具備 SurfaceView 的 Activity,存在兩個單獨但相互依賴的狀態機:
當 Activity 開始時,將按如下順序得到回調:
若是返回,您將獲得:
若是旋轉屏幕,Activity 將被消解並從新建立,而您將得到整個週期。您能夠經過檢查 isFinishing() 告知屏幕快速從新啓動。啓動/中止 Activity 可能很是快速,從而可能致使 surfaceCreated() 其實是在 onPause() 以後發生。
若是您點按電源按鈕鎖定屏幕,則只會獲得 onPause()(沒有 surfaceDestroyed())。Surface 仍處於活躍狀態,而且渲染能夠繼續。若是您繼續請求,甚至能夠持續得到 Choreographer 事件。若是您使用強制變向的鎖屏,則當設備未鎖定時,您的 Activity 可能會從新啓動;但若是沒有,您可使用與以前相同的 Surface 脫離屏幕鎖定。
當使用具備 SurfaceView 的單獨渲染器線程時,會引起一個基本問題:線程壽命是否依賴 Surface 或 Activity 的壽命?答案取決於鎖屏時您想要看到的狀況:(1) 在 Activity 啓動/中止時啓動/中止線程,或 (2) 在 Surface 建立/銷燬時啓動/中止線程。
選項 1 與應用生命週期交互良好。咱們在 onResume() 中啓動渲染器線程,並在 onPause() 中將其中止。當建立和配置線程時,會顯得有點奇怪,由於有時 Surface 已經存在,有時不存在(例如,在使用電源按鈕切換屏幕後,它仍然存在)。咱們必須先等待 Surface 完成建立,而後再在線程中進行一些初始化操做,可是咱們不能簡單地在 surfaceCreated() 回調中進行操做,由於若是未從新建立 Surface,將不會再次觸發。所以,咱們須要查詢或緩存 Surface 狀態,並將其轉發到渲染器線程。
選項 2 很是具備吸引力,由於 Surface 和渲染器在邏輯上互相交織。咱們在建立 Surface 後啓動線程,避免了一些線程間通訊問題,也可輕鬆轉發 Surface 已建立/更改的消息。當屏幕鎖定時,咱們須要確保渲染中止,並在未鎖定時恢復渲染;要實現這一點,可能只需告知 Choreographer 中止調用框架繪圖回調。當且僅當渲染器線程正在運行時,咱們的 onResume() 才須要恢復回調。儘管如此,若是咱們根據框架之間的已播放時長進行動畫繪製,咱們可能發現,在下一個事件到來前存在很大的差距;應使用一個明確的暫停/恢復消息。
這兩個選項主要關注如何配置渲染器線程以及線程是否正在執行。一個相關問題是,終止 Activity 時(在 onPause() 或 onSaveInstanceState() 中)從線程中提取狀態;在此狀況下,選項 1 最有效,由於在渲染器線程加入後,不須要使用同步基元就能夠訪問其狀態。
SurfaceTexture 類是在 Android 3.0 中推出的。就像 SurfaceView 是 Surface 和 View 的組合同樣,SurfaceTexture 是 Surface 和 GLES 紋理的粗略組合(包含幾個注意事項)。
和SurfaceView不一樣的是,它對圖像流的處理並不直接顯示,而是轉爲GL外部紋理,所以可用於圖像流數據的二次處理(如Camera濾鏡,桌面特效等)。好比Camera的預覽數據,變成紋理後能夠交給GLSurfaceView直接顯示,也能夠經過SurfaceTexture交給TextureView做爲View heirachy中的一個硬件加速層來顯示。
當您建立 SurfaceTexture 時,會建立一個應用是其消耗方的 BufferQueue。若是生產方將新的緩衝區加入隊列,您的應用便會經過回調 (onFrameAvailable()) 得到通知。應用調用 updateTexImage()(這會釋放先前保留的緩衝區),從隊列中獲取新的緩衝區,而後發出一些 EGL 調用,讓緩衝區可做爲外部紋理供 GLES 使用。
首先,SurfaceTexture從圖像流(來自Camera預覽,視頻解碼,GL繪製場景等)中得到幀數據,當調用updateTexImage()時,根據內容流中最近的圖像更新SurfaceTexture對應的GL紋理對象,接下來,就能夠像操做普通GL紋理同樣操做它了。從下面的類圖中能夠看出,它核心管理着一個BufferQueue的Consumer和Producer兩端。Producer端用於內容流的源輸出數據,Consumer端用於獲取GraphicBuffer並生成紋理。SurfaceTexture.OnFrameAvailableListener用於讓SurfaceTexture的使用者知道有新數據到來。JNISurfaceTextureContext是OnFrameAvailableListener從Native到Java的JNI跳板。其中SurfaceTexture中的attachToGLContext()和detachToGLContext()可讓多個GL context共享同一個內容源。
外部紋理 (GL_TEXTURE_EXTERNAL_OES) 與 GLES (GL_TEXTURE_2D) 建立的紋理並不徹底相同:您對渲染器的配置必須有所不一樣,並且有一些操做是不能對外部紋理執行的。關鍵是,您能夠直接從 BufferQueue 接收到的數據中渲染紋理多邊形。gralloc 支持各類格式,所以咱們須要保證緩衝區中數據的格式是 GLES 能夠識別的格式。爲此,當 SurfaceTexture 建立 BufferQueue 時,它將消耗方用法標記設置爲 GRALLOC_USAGE_HW_TEXTURE,確保由 gralloc 建立的緩衝區都可供 GLES 使用。
因爲 SurfaceTexture 會與 EGL 上下文交互,所以您必須當心地從正確的會話中調用其方法。
事實證實,BufferQueue 不僅是向消耗方傳遞緩衝區句柄。每一個緩衝區都附有時間戳和轉換參數。
提供轉換是爲了提升效率。在某些狀況下,源數據可能以錯誤的方向傳遞給消耗方;可是,咱們能夠按照數據的當前方向發送數據並使用轉換對其進行更正,而不是在發送數據以前對其進行旋轉。在使用數據時,轉換矩陣能夠與其餘轉換合併,從而最大限度下降開銷。
時間戳對於某些緩衝區來源很是有用。例如,假設您將生產方接口鏈接到相機的輸出端(使用 setPreviewTexture())。要建立視頻,您須要爲每一個幀設置演示時間戳;不過您須要根據截取幀的時間(而不是應用收到緩衝區的時間)來設置該時間戳。隨緩衝區提供的時間戳由相機代碼設置,從而得到一系列更一致的時間戳。
Android 5.0中將BufferQueue的核心部分分離出來,放在BufferQueueCore這個類中。BufferQueueProducer和BufferQueueConsumer分別是它的生產者和消費者實現基類(分別實現了IGraphicBufferProducer和IGraphicBufferConsumer接口)。它們都是由BufferQueue的靜態函數createBufferQueue()來建立的。Surface是生產者端的實現類,提供dequeueBuffer/queueBuffer等硬件渲染接口,和lockCanvas/unlockCanvasAndPost等軟件渲染接口,使內容流的源能夠往BufferQueue中填graphic buffer。GLConsumer繼承自ConsumerBase,是消費者端的實現類。它在基類的基礎上添加了GL相關的操做,如將graphic buffer中的內容轉爲GL紋理等操做。到此,以SurfaceTexture爲中心的一個pipeline大致是這樣的:
若是仔細觀察 API,您會發現應用只能經過一種方式來建立簡單 Surface,即經過將 SurfaceTexture 做爲惟一參數的構造函數來建立。(在 API 11 以前,根本沒有用於 Surface 的公開構造函數。)若是將 SurfaceTexture 視爲 Surface 和紋理的組合,這看起來可能有點落後。
深刻來看,SurfaceTexture 稱爲 GLConsumer,它更準確地反映了其做爲 BufferQueue 的全部方和消耗方的角色。從 SurfaceTexture 建立 Surface 時,您所作的是建立一個表示 SurfaceTexture 的 BufferQueue 生產方端的對象。
相機能夠提供一個適合做爲電影進行錄製的幀流。要在屏幕上顯示它,您能夠建立一個 SurfaceView,將 Surface 傳遞給 setPreviewDisplay(),而後讓生產方(相機)和消耗方 (SurfaceFlinger) 完成全部工做。要錄製視頻,您可使用 MediaCodec 的 createInputSurface() 建立一個 Surface,將其傳遞給相機,而後即可高枕無憂了。若要邊顯示邊錄製,您必須使用更多內容。
在錄製視頻時,連續拍攝 Activity 會顯示相機錄製的視頻。在這種狀況下,已編碼的視頻將寫入內存中的環形緩衝區,該緩衝區可隨時保存到磁盤。實現起來很是簡單,只要您跟蹤全部內容所在的位置便可。
該流程涉及三個 BufferQueue,分別是由應用、SurfaceFlinger 和 mediaserver 所建立:
圖 1.Grafika 的連續拍攝 Activity。箭頭指示相機的數據傳輸路徑,BufferQueue 則用顏色標示(生產方爲青色,消耗方爲綠色)。
已編碼的 H.264 視頻在應用進程中進入 RAM 中的環形緩衝區,並在點擊拍攝按鈕時使用 MediaMuxer 類寫入到磁盤上的 MP4 文件中。
三個 BufferQueue 都在應用中經過單個 EGL 上下文處理,且 GLES 操做在 UI 線程上執行。一般,不鼓勵在 UI 線程上執行 SurfaceView 渲染,可是因爲咱們執行的是由 GLES 驅動程序異步處理的簡單操做,所以沒有問題。(若是視頻編碼器鎖定,而且咱們阻止嘗試將緩衝區移出隊列,該應用便會中止響應。而此時,咱們不管怎樣操做均可能會失敗。)對已編碼數據的處理(管理環形緩衝區並將其寫入磁盤)是在單獨的線程中執行的。
大部分配置是在 SurfaceView 的 surfaceCreated() 回調中進行的。系統會建立 EGLContext,併爲顯示設備和視頻編碼器建立 EGLSurface。當新的幀到達時,咱們會告知 SurfaceTexture 去獲取它,並將其做爲 GLES 紋理進行提供,而後使用 GLES 命令在每一個 EGLSurface 上渲染它(從 SurfaceTexture 轉發轉換和時間戳)。編碼器線程從 MediaCodec 拉取編碼的輸出內容,並將其存儲在內存中。
Android 7.0 支持對受保護的視頻內容進行 GPU 後處理。這容許將 GPU 用於複雜的非線性視頻效果(例如扭曲),將受保護的視頻內容映射到紋理,以用於常規圖形場景(例如,使用 OpenGL ES)和虛擬現實 (VR)。
圖 2.安全紋理視頻播放
使用如下兩個擴展程序實現支持:
Android 7.0 還會更新 SurfaceTexture 和 ACodec (libstagefright.so),這樣一來,即便窗口 Surface 沒有加入窗口編寫器(例如 SurfaceFlinger)的隊列,也容許發送受保護的內容,而且提供受保護的視頻 Surface 以在受保護的上下文中使用。該操做經過在受保護的上下文(由 ACodec 驗證)中建立的 Surface 上設置正確的受保護消耗方位 (GRALLOC_USAGE_PROTECTED) 來完成。
應用程序的視頻或者opengl內容每每是顯示在一個特別的UI控件中:SurfaceView。SurfaceView的工做方式是建立一個置於應用窗口以後的新窗口。這種方式的效率很是高,由於SurfaceView窗口刷新的時候不須要重繪應用程序的窗口(android普通窗口的視圖繪製機制是一層一層的,任何一個子元素或者是局部的刷新都會致使整個視圖結構所有重繪一次,所以效率很是低下,不過知足普通應用界面的需求仍是綽綽有餘),可是SurfaceView也有一些很是不便的限制。
由於SurfaceView的內容不在應用窗口上,因此不能使用變換(平移、縮放、旋轉等)。也難以放在ListView或者ScrollView中,不能使用UI控件的一些特性好比View.setAlpha()。
TextureView在4.0(API level 14)中引入。若是你想顯示一段在線視頻或者任意的數據流好比視頻或者OpenGL 場景,你能夠用android中的TextureView作到。它能夠將內容流直接投影到View中,能夠用於實現Live preview等功能。和SurfaceView不一樣,它不會在WMS中單首創建窗口,而是做爲View hierachy中的一個普通View,所以能夠和其它普通View同樣進行移動,旋轉,縮放,動畫等變化。值得注意的是TextureView必須在硬件加速的窗口中。它顯示的內容流數據能夠來自App進程或是遠端進程。從類圖中能夠看到,TextureView繼承自View,它與其它的View同樣在View hierachy中管理與繪製。TextureView重載了draw()方法,其中主要把SurfaceTexture中收到的圖像數據做爲紋理更新到對應的HardwareLayer中。SurfaceTexture.OnFrameAvailableListener用於通知TextureView內容流有新圖像到來。SurfaceTextureListener接口用於讓TextureView的使用者好比MediaPlayer?知道SurfaceTexture已準備好,這樣就能夠把SurfaceTexture交給相應的內容源。Surface爲BufferQueue的Producer接口實現類,使生產者能夠經過它的軟件或硬件渲染接口爲SurfaceTexture內部的BufferQueue提供graphic buffer。
咱們已經知道,SurfaceTexture 是一個「GL 消費者」,它會佔用圖形數據的緩衝區,並將它們做爲紋理進行提供。TextureView 會對 SurfaceTexture 進行封裝,並接管對回調作出響應以及獲取新緩衝區的責任。新緩衝區的就位會致使 TextureView 發出 View 失效請求。當被要求進行繪圖時,TextureView 會使用最近收到的緩衝區的內容做爲數據源,並根據 View 狀態的指示,以相應的方式在相應的位置進行呈現。
您可使用 GLES 在 TextureView 上呈現內容,就像在 SurfaceView 上同樣。只需將 SurfaceTexture 傳遞到 EGL 窗口建立調用便可。不過,這樣作會致使潛在問題。
在咱們看到的大部份內容中,BufferQueue 是在不一樣進程之間傳遞緩衝區。當使用 GLES 呈現到 TextureView 時,生產者和消費者處於同一進程中,它們甚至可能會在單個線程上獲得處理。假設咱們以快速連續的方式從界面線程提交多個緩衝區。EGL 緩衝區交換調用須要使一個緩衝區從 BufferQueue 出列,而在有可用的緩衝區以前,它將處於暫停狀態。只有當消費者獲取一個緩衝區用於呈現時纔會有可用的緩衝區,可是這一過程也會發生在界面線程上…所以咱們陷入了困境。
解決方案是讓 BufferQueue 確保始終有一個可用的緩衝區可以出列,以使緩衝區交換始終不會暫停。要保證可以實現這一點,一種方法是讓 BufferQueue 在新緩衝區加入隊列時捨棄以前加入隊列的緩衝區的內容,並對最小緩衝區計數和最大獲取緩衝區計數施加限制(若是您的隊列有三個緩衝區,而全部這三個緩衝區均被消費者獲取,那麼就沒有能夠出列的緩衝區,緩衝區交換調用必然會暫停或失敗。所以咱們須要防止消費者一次獲取兩個以上的緩衝區)。丟棄緩衝區一般是不可取的,所以僅容許在特定狀況下發生,例如生產者和消費者處於同一進程中時。
惟一要作的就是獲取用於渲染內容的SurfaceTexture。具體作法是先建立TextureView對象,而後實現SurfaceTextureListener接口,代碼以下:
private TextureView myTexture;
public class MainActivity extends Activity implements SurfaceTextureListener{
protected void onCreate(Bundle savedInstanceState) {
myTexture = new TextureView(this);
myTexture.setSurfaceTextureListener(this);
setContentView(myTexture);
}
}
複製代碼
Activity implements了SurfaceTextureListener接口所以activity中須要重寫以下方法:
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//當TextureView初始化時調用,事實上當你的程序退到後臺它會被銷燬,你再次打開程序的時候它會被從新初始化
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//當TextureView的大小改變時調用
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
//當TextureView被銷燬時調用
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
//當TextureView更新時調用,也就是當咱們調用unlockCanvasAndPost方法時
}
複製代碼
TextureView可使用setAlpha和setRotation方法達到改變透明度和旋轉的效果。
myTexture.setAlpha(1.0f);
myTexture.setRotation(90.0f);
複製代碼
第一步:讀取圖片到內存中
//從本地讀取圖片,這裏的path必須是絕對地址
private Bitmap readBitmap(String path) throws IOException {
return BitmapFactory.decodeFile(path);
}
//從Assets讀取圖片
private Bitmap readBitmap(String path) throws IOException {
return BitmapFactory.decodeStream(getResources().getAssets().open(path));
}
複製代碼
第二步:將內存中的圖片畫到畫布上,這裏在畫完以後須要釋放Bitmap
//將圖片畫到畫布上,圖片將被以寬度爲比例畫上去
private void drawBitmap(Bitmap bitmap) {
Canvas canvas = lockCanvas(new Rect(0, 0, getWidth(), getHeight()));//鎖定畫布
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Rect dst = new Rect(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
canvas.drawBitmap(bitmap, src, dst, mPaint);//將bitmap畫到畫布上
unlockCanvasAndPost(canvas);//解鎖畫布同時提交
}
複製代碼
很明顯TextureView比起正常的View的優點就是能夠在異步將圖片畫到畫布上,咱們能夠建立一個異步線程,而後經過SystemClock.sleep()這個函數在每畫完一幀都暫停必定時間,這樣就實現了一個完整的過程。
private class PlayThread extends Thread {
@Override
public void run() {
try {
while (mPlayFrame < mFrameCount) {//若是尚未播放完全部幀
Bitmap bitmap = readBitmap(mPaths[mPlayFrame]);
drawBitmap(bitmap);
recycleBitmap(bitmap);
mPlayFrame++;
SystemClock.sleep(mDelayTime);//暫停間隔時間
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
可是,sleep函數並不能按照指定的秒數進行暫停。每每致使比須要的秒數更多。因此能夠採用handler來進行播放速度的控制。
若是消息隊列裏目前沒有合適的消息能夠摘取,那麼不能讓它所屬的線程「傻轉」,而應該使之阻塞。
隊列裏的消息應該按其「到時」的順序進行排列,最早到時的消息會放在隊頭,也就是mMessages域所指向的消息,其後的消息依次排開。
阻塞的時間最好能精確一點兒,因此若是暫時沒有合適的消息節點可摘時,要考慮鏈表首個消息節點將在何時到時,因此這個消息節點距離當前時刻的時間差,就是咱們要阻塞的時長。
有時候外界但願隊列能在即將進入阻塞狀態以前作一些動做,這些動做能夠稱爲idle動做,咱們須要兼顧處理這些idle動做。一個典型的例子是外界但願隊列在進入阻塞以前作一次垃圾收集。
複製代碼
建立一個Thread。
初始化Looper,這裏能夠直接繼承HandlerThread。
建立一個Handler。
經過SystemClock.uptimeMillis()取得時間,而後向Handler發送消息。
接收到消息後判斷是否結束,若是未結束則將當前的時間加上間隔時間(好比40ms)後繼續發送消息,不斷進行循環過程。
複製代碼
//開啓線程
mScheduler = new Scheduler(duration, paths.length, new FrameUpdateListener());
mScheduler.start();
private class FrameUpdateListener implements OnFrameUpdateListener {
@Override
public void onFrameUpdate(long frameIndex) {
try {
Bitmap bitmap = readBitmap(mPaths[(int) frameIndex]);
drawBitmap(bitmap);
recycleBitmap(bitmap);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
咱們須要新建一個線程將readBitmap()移到新線程中執行,而後經過一個緩存數組(多線程之間須要加鎖)進行交互。
private List<Bitmap> mCacheBitmaps;//緩存幀集合
private int mReadFrame;//當前讀取到那一幀,總幀數相關
//開啓線程
mReadThread = new ReadThread();
mReadThread.start();
mScheduler = new Scheduler(duration, mFrameCount, new FrameUpdateListener());
private class ReadThread extends Thread {
@Override
public void run() {
try {
while (mReadFrame < mFrameCount) {//而且沒有讀完則繼續讀取
if (mCacheBitmaps.size() >= MAX_CACHE_NUMBER) {//若是讀取的超過最大緩存則暫停讀取
SystemClock.sleep(1);
continue;
}
Bitmap bmp = readBitmap(mPaths[mReadFrame]);
mCacheBitmaps.add(bmp);
mReadFrame++;
if (mReadFrame == 1) {//讀取到第一幀後在開始調度器
mScheduler.start();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private class FrameUpdateListener implements OnFrameUpdateListener {
@Override
public void onFrameUpdate(long frameIndex) {
if (mCacheBitmaps.isEmpty()) {//若是當前沒有幀,則直接跳過
return;
}
Bitmap bitmap = mCacheBitmaps.remove(0);//獲取第一幀同時從緩存裏刪除
drawBitmap(bitmap);
recycleBitmap(bitmap);
}
}
複製代碼
事實上如今還有一個比較嚴重的問題,這個問題就是內存抖動。內存抖動是指在短期內有大量的對象被建立或者被回收的現象。意思就是你在循環中或者onDraw()被頻繁運行的方法中去建立對象,結果致使頻繁的gc,而gc會致使線程卡頓,若是你在onDraw()或者onLayout()方法中去建立對象,AS應該會提示你(Avoid object allocations during draw/layout operations (preallocate and reuse instead))。要解決這個問題必須儘量的減小建立對象,去複用以前已經建立的對象,這一點咱們能夠經過建立對象池解決,但是咱們要如何才能複用Bitmap?
在BitmapFactory.Options對象中有個inBitmap屬性,若是你設置inBitmap等於某個Bitmap(固然這裏有限制,上面的文章已經講的很清楚了),你在用這個BitmapFactory.Options去加載Bitmap,它就會複用這塊內存,若是這個Bitmap在繪製中,你有可能會看見撕裂現象。 咱們要作的就是建立一個Bitmap對象池,將已經畫完的Bitmap放回對象池,當咱們要讀取的時候,從對象池中獲取合適的對象賦予inBitmap。
先看下BitmapFactory.Options裏咱們使用的主要屬性
inBitmap:若是該值不等於空,則在解碼時從新使用這個Bitmap。
inMutable:Bitmap是否可變的,若是設置了inBitmap,該值必須爲true。
inPreferredConfig:指定解碼顏色格式。
inJustDecodeBounds:若是設置爲true,將不會將圖片加載到內存中,可是能夠得到寬高。
inSampleSize:圖片縮放的倍數,若是設置爲2表明加載到內存中的圖片大小爲原來的2分之一,這個值老是和inJustDecodeBounds配合來加載大圖片,在這裏我直接設置爲1,這樣作其實是有問題的,若是圖片過大很容易發生OOM。
複製代碼
readBitmap方法修改以下
private Bitmap readBitmap(String path) throws IOException {
InputStream is = getResources().getAssets().open(path);//這裏須要以流的形式讀取
BitmapFactory.Options options = getReusableOptions(is);//獲取參數設置
Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
is.close();
return bmp;
}
//實現複用,
private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.inSampleSize = 1;
options.inJustDecodeBounds = true;//這裏設置爲不將圖片讀取到內存中
is.mark(is.available());
BitmapFactory.decodeStream(is, null, options);//得到大小
options.inJustDecodeBounds = false;//設置回來
is.reset();
Bitmap inBitmap = getBitmapFromReusableSet(options);
options.inMutable = true;
if (inBitmap != null) {//若是有符合條件的設置屬性
options.inBitmap = inBitmap;
}
return options;
}
//從複用池中尋找合適的bitmap
private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
if (mReusableBitmaps.isEmpty()) {
return null;
}
int count = mReusableBitmaps.size();
for (int i = 0; i < count; i++) {
Bitmap item = mReusableBitmaps.get(i);
if (ImageUtil.canUseForInBitmap(item, options)) {//尋找符合條件的bitmap
return mReusableBitmaps.remove(i);
}
}
return null;
}
/*
* 判斷該Bitmap是否能夠設置到BitmapFactory.Options.inBitmap上
*/
public static boolean canUseForInBitmap(Bitmap bitmap, BitmapFactory.Options options) {
// 在Android4.4之後,若是要使用inBitmap的話,只須要解碼的Bitmap比inBitmap設置的小就好了,對inSampleSize沒有限制
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int width = options.outWidth;
int height = options.outHeight;
if (options.inSampleSize > 0) {
width /= options.inSampleSize;
height /= options.inSampleSize;
}
int byteCount = width * height * getBytesPerPixel(bitmap.getConfig());
return byteCount <= bitmap.getAllocationByteCount();
}
// 在Android4.4以前,若是想使用inBitmap的話,解碼的Bitmap必須和inBitmap設置的寬高相等,且inSampleSize爲1
return bitmap.getWidth() == options.outWidth
&& bitmap.getHeight() == options.outHeight
&& options.inSampleSize == 1;
}
複製代碼
由於 TextureView 是 View 層次結構的固有成員,因此其行爲與其餘全部 View 同樣,能夠與其餘元素相互疊加。您能夠執行任意轉換,並經過簡單的 API 調用將內容檢索爲位圖。
影響 TextureView 的主要因素是合成步驟的表現。使用 SurfaceView 時,內容能夠寫到 SurfaceFlinger(理想狀況下使用疊加層)合成的獨立分層中。使用 TextureView 時,View 合成每每使用 GLES 執行,而且對其內容進行的更新也可能會致使其餘 View 元素重繪(例如,若是它們位於 TextureView 上方)。View 呈現完成後,應用界面層必須由 SurfaceFlinger 與其餘分層合成,以便您能夠高效地將每一個可見像素合成兩次。對於全屏視頻播放器,或任何其餘至關於位於視頻上方的界面元素的應用,SurfaceView 能夠帶來更好的效果。
如以前所述,受 DRM 保護的視頻只能在疊加平面上呈現。支持受保護內容的視頻播放器必須使用 SurfaceView 進行實現。
Grafika 包括一對視頻播放器,一個用 TextureView 實現,另外一個用 SurfaceView 實現。對於這兩個視頻播放器來講,僅將幀從 MediaCodec 發送到 Surface 的視頻解碼部分是同樣的。這兩種實現之間最有趣的區別是呈現正確寬高比所需的步驟。
SurfaceView 須要 FrameLayout 的自定義實現,而要從新調整 SurfaceTexture 的大小,只需使用 TextureView#setTransform() 配置轉換矩陣便可。對於前者,您會經過 WindowManager 向 SurfaceFlinger 發送新的窗口位置和大小值;對於後者,您僅僅是在以不一樣的方式呈現它。
不然,兩種實現均遵循相同的模式。建立 Surface 後,系統會啓用播放。點擊「播放」時,系統會啓動視頻解碼線程,並將 Surface 做爲輸出目標。以後,應用代碼不須要執行任何操做,SurfaceFlinger(適用於 SurfaceView)或 TextureView 會處理合成和顯示。
此操做組件演示了在 TextureView 中對 SurfaceTexture 的操控。
此操做組件的基本結構是一對顯示兩個並排播放的不一樣視頻的 TextureView。爲了模擬視頻會議應用的需求,咱們但願在操做組件因屏幕方向發生變化而暫停和恢復時,MediaCodec 解碼器能保持活動狀態。緣由在於,若是不對 MediaCodec 解碼器使用的 Surface 進行徹底從新配置,就沒法更改它,而這是成本至關高的操做;所以咱們但願 Surface 保持活動狀態。Surface 只是 SurfaceTexture 的 BufferQueue 中生產者界面的句柄,而 SurfaceTexture 由 TextureView 管理;所以咱們還須要 SurfaceTexture 保持活動狀態。那麼咱們如何處理 TextureView 被關閉的狀況呢?
TextureView 提供的 setSurfaceTexture() 調用正好可以知足咱們的需求。咱們從 TextureView 獲取對 SurfaceTexture 的引用,並將它們保存在靜態字段中。當操做組件被關閉時,咱們從 onSurfaceTextureDestroyed() 回調返回「false」,以防止 SurfaceTexture 被銷燬。當操做組件從新啓動時,咱們將原來的 SurfaceTexture 填充到新的 TextureView 中。TextureView 類負責建立和破壞 EGL 上下文。
每一個視頻解碼器都是從單獨的線程驅動的。乍一看,咱們彷佛須要每一個線程的本地 EGL 上下文;但請注意,具備解碼輸出的緩衝區其實是從 mediaserver 發送給咱們的 BufferQueue 消費者 (SurfaceTexture)。TextureView 會爲咱們處理呈現,並在界面線程上執行。
使用 SurfaceView 實現該操做組件可能較爲困難。咱們不能只建立一對 SurfaceView 並將輸出引導至它們,由於 Surface 在屏幕方向改變期間會被銷燬。此外,這樣作會增長兩個層,而因爲可用疊加層的數量限制,咱們不得不盡可能將層數量減到最少。與上述方法不一樣,咱們但願建立一對 SurfaceTexture,以從視頻解碼器接收輸出,而後在應用中執行呈現,使用 GLES 將兩個紋理間隙呈現到 SurfaceView 的 Surface。
這個例子的效果是從MediaPlayer中拿到視頻幀,而後顯示在屏幕上,接着把屏幕上的內容dump到指定文件中。由於SurfaceTexture自己只產生紋理,因此這裏還須要GLSurfaceView配合來作最後的渲染輸出。
首先,VideoDumpView是GLSurfaceView的繼承類。在構造函數VideoDumpView()中會建立VideoDumpRenderer,也就是GLSurfaceView.Renderer的實例,而後調setRenderer()將之設成GLSurfaceView的Renderer。
109 public VideoDumpView(Context context) {
...
116 mRenderer = new VideoDumpRenderer(context);
117 setRenderer(mRenderer);
118 }
複製代碼
隨後,GLSurfaceView中的GLThread啓動,建立EGL環境後回調VideoDumpRenderer中的onSurfaceCreated()。
519 public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
...
551 // Create our texture. This has to be done each time the surface is created.
552 int[] textures = new int[1];
553 GLES20.glGenTextures(1, textures, 0);
554
555 mTextureID = textures[0];
556 GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);
...
575 mSurface = new SurfaceTexture(mTextureID);
576 mSurface.setOnFrameAvailableListener(this);
577
578 Surface surface = new Surface(mSurface);
579 mMediaPlayer.setSurface(surface);
複製代碼
這裏,首先經過GLES建立GL的外部紋理。外部紋理說明它的真正內容是放在ion分配出來的系統物理內存中,而不是GPU中,GPU中只是維護了其元數據。接着根據前面建立的GL紋理對象建立SurfaceTexture。
SurfaceTexture的參數爲GLES接口函數glGenTexture()獲得的紋理對象id。在初始化函數SurfaceTexture_init()中,先建立GLConsumer和相應的BufferQueue,再將它們的指針經過JNI放到SurfaceTexture的Java層對象成員中。
230 static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached,
231 jint texName, jboolean singleBufferMode, jobject weakThiz)
232 {
...
235 BufferQueue::createBufferQueue(&producer, &consumer);
...
242 sp<GLConsumer> surfaceTexture;
243 if (isDetached) {
244 surfaceTexture = new GLConsumer(consumer, GL_TEXTURE_EXTERNAL_OES,
245 true, true);
246 } else {
247 surfaceTexture = new GLConsumer(consumer, texName,
248 GL_TEXTURE_EXTERNAL_OES, true, true);
249 }
...
256 SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture);
257 SurfaceTexture_setProducer(env, thiz, producer);
...
266 sp<JNISurfaceTextureContext> ctx(new JNISurfaceTextureContext(env, weakThiz,
267 clazz));
268 surfaceTexture->setFrameAvailableListener(ctx);
269 SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);
複製代碼
因爲直接的Listener在Java層,而觸發者在Native層,所以須要從Native層回調到Java層。這裏經過JNISurfaceTextureContext當了跳板。JNISurfaceTextureContext的onFrameAvailable()起到了Native和Java的橋接做用:
180void JNISurfaceTextureContext::onFrameAvailable()
...
184 env->CallStaticVoidMethod(mClazz, fields.postEvent, mWeakThiz);
複製代碼
其中的fields.postEvent早在SurfaceTexture_classInit()中被初始化爲SurfaceTexture的postEventFromNative()函數。這個函數往所在線程的消息隊列中放入消息,異步調用VideoDumpRenderer的onFrameAvailable()函數,通知VideoDumpRenderer有新的數據到來。
回到onSurfaceCreated(),接下來建立供外部生產者使用的Surface類。Surface的構造函數之一帶有參數SurfaceTexture。
133 public Surface(SurfaceTexture surfaceTexture) {
...
140 setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture));
複製代碼
它其實是把SurfaceTexture中建立的BufferQueue的Producer接口實現類拿出來後建立了相應的Surface類。
135 static jlong nativeCreateFromSurfaceTexture(JNIEnv* env, jclass clazz,
136 jobject surfaceTextureObj) {
137 sp<IGraphicBufferProducer> producer(SurfaceTexture_getProducer(env, surfaceTextureObj));
...
144 sp<Surface> surface(new Surface(producer, true));
複製代碼
這樣,Surface爲BufferQueue的Producer端,SurfaceTexture中的GLConsumer爲BufferQueue的Consumer端。當經過Surface繪製時,SurfaceTexture能夠經過updateTexImage()來將繪製結果綁定到GL的紋理中。
回到onSurfaceCreated()函數,接下來調用setOnFrameAvailableListener()函數將VideoDumpRenderer(實現SurfaceTexture.OnFrameAvailableListener接口)做爲SurfaceTexture的Listener,由於它要監聽內容流上是否有新數據。接着將SurfaceTexture傳給MediaPlayer,由於這裏MediaPlayer是生產者,SurfaceTexture是消費者。後者要接收前者輸出的Video frame。這樣,就經過Observer pattern創建起了一條通知鏈:MediaPlayer -> SurfaceTexture -> VideDumpRenderer。在onFrameAvailable()回調函數中,將updateSurface標誌設爲true,表示有新的圖像到來,須要更新Surface了。爲毛不在這兒立刻更新紋理呢,由於當前可能不在渲染線程。SurfaceTexture對象能夠在任意線程被建立(回調也會在該線程被調用),但updateTexImage()只能在含有紋理對象的GL context所在線程中被調用。所以通常狀況下回調中不能直接調用updateTexImage()。
與此同時,GLSurfaceView中的GLThread也在運行,它會調用到VideoDumpRenderer的繪製函數onDrawFrame()。
372 public void onDrawFrame(GL10 glUnused) {
...
377 if (updateSurface) {
...
380 mSurface.updateTexImage();
381 mSurface.getTransformMatrix(mSTMatrix);
382 updateSurface = false;
...
394 // Activate the texture.
395 GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
396 GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTextureID);
...
421 // Draw a rectangle and render the video frame as a texture on it.
422 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
...
429 DumpToFile(frameNumber);
複製代碼
這裏,經過SurfaceTexture的updateTexImage()將內容流中的新圖像轉成GL中的紋理,再進行座標轉換。綁定剛生成的紋理,畫到屏幕上。
最後onDrawFrame()調用DumpToFile()將屏幕上的內容倒到文件中。在DumpToFile()中,先用glReadPixels()從屏幕中把像素數據存到Buffer中,而後用FileOutputStream輸出到文件。
它它能夠將Camera中的內容放在View中進行顯示。在onCreate()函數中首先建立TextureView,再將Activity(實現了TextureView.SurfaceTextureListener接口)傳給TextureView,用於監聽SurfaceTexture準備好的信號。
protected void onCreate(Bundle savedInstanceState) {
...
mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);
...
}
複製代碼
TextureView的構造函數並不作主要的初始化工做。主要的初始化工做是在getHardwareLayer()中,而這個函數是在其基類View的draw()中調用。TextureView重載了這個函數:
348 HardwareLayer getHardwareLayer() {
...
358 mLayer = mAttachInfo.mHardwareRenderer.createTextureLayer();
359 if (!mUpdateSurface) {
360 // Create a new SurfaceTexture for the layer.
361 mSurface = new SurfaceTexture(false);
362 mLayer.setSurfaceTexture(mSurface);
363 }
364 mSurface.setDefaultBufferSize(getWidth(), getHeight());
365 nCreateNativeWindow(mSurface);
366
367 mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
368
369 if (mListener != null && !mUpdateSurface) {
370 mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight());
371 }
...
390 applyUpdate();
391 applyTransformMatrix();
392
393 return mLayer;
394 }
複製代碼
由於TextureView是硬件加速層(類型爲LAYER_TYPE_HARDWARE),它首先經過HardwareRenderer建立相應的HardwareLayer類,放在mLayer成員中。而後建立SurfaceTexture類。以後將HardwareLayer與SurfaceTexture作綁定。接着調用Native函數nCreateNativeWindow,它經過SurfaceTexture中的BufferQueueProducer建立Surface類。注意Surface實現了ANativeWindow接口,這意味着它能夠做爲EGL Surface傳給EGL接口從而進行硬件繪製。而後setOnFrameAvailableListener()將監聽者mUpdateListener註冊到SurfaceTexture。這樣,當內容流上有新的圖像到來,mUpdateListener的onFrameAvailable()就會被調用。而後須要調用註冊在TextureView中的SurfaceTextureListener的onSurfaceTextureAvailable()回調函數,通知TextureView的使用者SurfaceTexture已就緒。
注意這裏這裏爲TextureView建立了DeferredLayerUpdater,而不是像Android 4.4(Kitkat)中返回GLES20TextureLayer。由於Android 5.0(Lollipop)中在App端分離出了渲染線程,並將渲染工做放到該線程中。這個線程還能接收VSync信號,所以它還能本身處理動畫。事實上,這裏DeferredLayerUpdater的建立就是經過同步方式在渲染線程中作的。DeferredLayerUpdater,顧名思義,就是將Layer的更新請求先記錄在這,當渲染線程真正要畫的時候,再進行真正的操做。其中的setSurfaceTexture()會調用HardwareLayer的Native函數nSetSurfaceTexture()將SurfaceTexture中的surfaceTexture成員(類型爲GLConsumer)傳給DeferredLayerUpdater,這樣以後要更新紋理時DeferredLayerUpdater就知道從哪裏更新了。
前面提到初始化中會調用onSurfaceTextureAvailable()這個回調函數。在它的實現中,TextureView的使用者就能夠將準備好的SurfaceTexture傳給數據源模塊,供數據源輸出之用。如:
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mCamera = Camera.open();
...
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
...
}
複製代碼
看一下setPreviewTexture()的實現,其中把SurfaceTexture中初始化時建立的GraphicBufferProducer拿出來傳給Camera模塊。
576static void android_hardware_Camera_setPreviewTexture(JNIEnv *env,
577 jobject thiz, jobject jSurfaceTexture)
...
585 producer = SurfaceTexture_getProducer(env, jSurfaceTexture);
...
594 if (camera->setPreviewTarget(producer) != NO_ERROR) {
複製代碼
接下來當內容流有新圖像可用,TextureView會被通知到(經過SurfaceTexture.OnFrameAvailableListener接口)。SurfaceTexture.OnFrameAvailableListener是SurfaceTexture有新內容來時的回調接口。TextureView中的mUpdateListener實現了該接口:
755 public void onFrameAvailable(SurfaceTexture surfaceTexture) {
756 updateLayer();
757 invalidate();
758 }
複製代碼
能夠看到其中會調用updateLayer()函數,而後經過invalidate()函數申請更新UI。updateLayer()會設置mUpdateLayer標誌位。這樣,當下次VSync到來時,Choreographer通知App經過重繪View hierachy。在UI重繪函數performTranversals()中,做爲View hierachy的一分子,TextureView的draw()函數被調用,其中便會相繼調用applyUpdate()和HardwareLayer的updateSurfaceTexture()函數。
138 public void updateSurfaceTexture() {
139 nUpdateSurfaceTexture(mFinalizer.get());
140 mRenderer.pushLayerUpdate(this);
141 }
複製代碼
updateSurfaceTexture()實際經過JNI調用到android_view_HardwareLayer_updateSurfaceTexture()函數。在其中會設置相應DeferredLayerUpdater的標誌位mUpdateTexImage,它表示在渲染線程中須要更新該層的紋理。
ThreadedRenderer做爲新的HardwareRenderer替代了Android 4.4中的Gl20Renderer。其中比較關鍵的是RenderProxy類,須要讓渲染線程幹活時就經過這個類往渲染線程發任務。RenderProxy中指向的RenderThread就是渲染線程的主體了,其中的threadLoop()函數是主循環,大多數時間它會poll在線程的Looper上等待,當有同步請求(或者VSync信號)過來,它會被喚醒,而後處理TaskQueue中的任務。TaskQueue是RenderTask的隊列,RenderTask表明一個渲染線程中的任務。如DrawFrameTask就是RenderTask的繼承類之一,它主要用於渲染當前幀。而DrawFrameTask中的DeferredLayerUpdater集合就存放着以前對硬件加速層的更新操做申請。
當主線程準備好渲染數據後,會以同步方式讓渲染線程完成渲染工做。其中會先調用processLayerUpdate()更新全部硬件加速層中的屬性,繼而調用到DeferredLayerUpdater的apply()函數,其中檢測到標誌位mUpdateTexImage被置位,因而會調用doUpdateTexImage()真正更新GL紋理和轉換座標。
SurfaceView是一個有本身Surface的View。它的渲染能夠放在單獨線程而不是主線程中。其缺點是不能作變形和動畫(這裏說的變形和動畫應該指的是View總體的變形和動畫, 由於SurfaceView內是徹底能夠實現動態的圖像變化的)。SurfaceTexture能夠用做非直接輸出的內容流,這樣就提供二次處理的機會。與SurfaceView直接輸出(直接顯示出來)相比,這樣會有若干幀的延遲。同時,因爲它自己管理BufferQueue,所以內存消耗也會稍微大一些。TextureView是一個能夠把內容流做爲外部紋理輸出在上面的View。它自己須要是一個硬件加速層。事實上TextureView自己也包含了SurfaceTexture。它與SurfaceView+SurfaceTexture組合相比能夠完成相似的功能(即把內容流上的圖像轉成紋理,而後輸出)。區別在於TextureView是在View hierachy中作繪製,所以通常它是在主線程上作的(在Android 5.0引入渲染線程後,它是在渲染線程中作的)。而SurfaceView+SurfaceTexture在單獨的Surface上作繪製,能夠是用戶提供的線程,而不是系統的主線程或是渲染線程。另外,與TextureView相比,它還有個好處是能夠用Hardware overlay進行顯示。
什麼是hardware overlay? 基本上就是說SurfaceView+SurfaceTexture能夠組合在一塊兒, TextureView和SurfaceTexture也能夠組合在一塊兒, 這兩種組合一個是在自定義線程中, 一個是個主線程(5.0以後也不是主線程而是渲染 線程)中。
實現遊戲循環的熱門方法以下所示:
while (playing) {
advance state by one frame
render the new frame
sleep until it’s time to do the next frame
}
複製代碼
此方法還存在一些問題,最根本的問題是遊戲能夠定義什麼是「幀」這一狀況。不一樣的顯示屏將以不一樣的速率刷新,而且該速率可能會隨時間變化。若是您生成幀的速度快於顯示屏可以顯示的速度,則您偶爾不得不丟掉一個幀。若是生成幀的速度過慢,則 SurfaceFlinger 會按期沒法找到新的緩衝區來獲取幀,並將從新顯示上一幀。這兩種狀況都會致使明顯異常。
您要作的是匹配顯示屏的幀速率,並根據從上一幀起通過的時間推動遊戲狀態。有兩種方法能夠實現這一點:(1) 將 BufferQueue 填滿並依賴「交換緩衝區」背壓;(2) 使用 Choreographer (API 16+)。
只需儘快交換緩衝區,便可輕鬆實現隊列填充。在 Android 的早期版本中,這樣作實際上可能會使您遭受處罰,其中 SurfaceView#lockCanvas() 會讓您休眠 100 毫秒。如今,它由 BufferQueue 調節,且 BufferQueue 的清空速度與 SurfaceFlinger 的消費能力相關。
可在 Android Breakout 中找到此方法的一個示例。它使用 GLSurfaceView,後者在一個循環中運行,而該循環會調用應用的 onDrawFrame() 回調,而後交換緩衝區。若是 BufferQueue 已滿,則直到緩衝區可用以後纔會調用 eglSwapBuffers()。當 SurfaceFlinger 獲取一個新的用於顯示的緩衝區後,便會釋放以前獲取的緩衝區,這時這些緩衝區就變爲可用狀態。由於這發生在 VSYNC 上,因此您的繪圖循環時間將與刷新率相匹配。大多數狀況下是這樣的。
此方法存在幾個問題。首先,應用與 SurfaceFlinger 操做組件關聯,後者所花費的時間各不相同,具體取決於要執行的工做量以及是否與其餘進程搶佔 CPU 時間。因爲您的遊戲狀態根據緩衝區交換的間隔時間推動,所以動畫不會以一致的速率更新。可是以 60fps 的速率運行時,不一致會在一段時間以後達到平衡,所以您可能不會注意到卡頓。
其次,因爲 BufferQueue 還沒有填滿,所以前幾回緩衝區交換的速度會很是快。所計算的幀間隔時間將接近於零,所以遊戲會生成幾個不會發生任何操做的幀。在 Breakout 這樣的遊戲(每次刷新都會更新屏幕)中,除了遊戲首次啓動(或取消暫停)時以外,隊列老是滿的,所以效果不明顯。偶爾暫停動畫,而後返回到快速模式的遊戲可能會出現異常問題。
經過 Choreographer,您能夠設置在下一個 VSYNC 上觸發的回調。實際的 VSYNC 時間以參數形式傳入。所以,即便您的應用不會當即喚醒,您仍然能夠準確瞭解顯示屏刷新週期什麼時候開始。使用此值(而非當前時間)可爲您的遊戲狀態更新邏輯產生一致的時間源。
遺憾的是,您在每一個 VSYNC 以後獲得回調這一事實並不能保證及時執行回調,也沒法保證您可以迅速高效地對其進行操做。您的應用須要手動檢測卡頓和丟幀的狀況。
Grafika 中的「記錄 GL 應用」操做組件提供了此狀況的示例。在某些設備(例如 Nexus 4 和 Nexus 5)上,若是您只是坐視不理,則操做組件會開始丟幀。GL 呈現微不足道,但有時會從新繪製 View 元素;若是設備已進入下降功耗模式,則測量/佈局傳遞可能須要很長時間(根據 systrace,Android 4.4 上的時鐘速度減慢以後,這一傳遞須要 28 毫秒,而不是 6 毫秒。若是您在屏幕上拖動手指,它會認爲您在與該操做組件互動,所以時鐘會保持高速狀態,您永遠不會丟幀)。
若是當前時間超過 VSYNC 時間後 N 毫秒,則簡單的解決辦法是在 Choreographer 回調中丟掉一幀。理想狀況下,N 的值取決於先前觀察到的 VSYNC 間隔。例如,若是刷新週期是 16.7 毫秒 (60fps),而您的運行時間延遲超過 15 毫秒,則可能會丟失一幀。
若是您查看「記錄 GL 應用」運行狀況,則會看到丟幀計數器計數增長了,甚至會在丟幀時在邊框中看到紅色閃爍狀況。除非您的觀察力很是強,不然不會看到動畫卡頓現象。以 60fps 的速率運行時,只要動畫以固定速率繼續播放,應用能夠偶爾丟幀,沒有任何人會注意到。您成功的概率在必定程度上取決於您正在繪製的內容、顯示屏的特性,以及使用該應用的用戶發現卡頓的擅長程度。
通常而言,若是您要呈現到 SurfaceView、GLSurfaceView 或 TextureView 上,則須要在專用線程中進行呈現。請勿在界面線程上進行任何「繁重」或花費時間不定的操做。
Breakout 和「記錄 GL 應用」使用專用呈現程序線程,而且也在該線程上更新動畫狀態。只要能夠快速更新遊戲狀態,這就是合理的方法。
其餘遊戲將遊戲邏輯和呈現徹底分開。若是您有一個簡單的遊戲,只是每 100 毫秒移動一個塊,則您可使用一個只進行如下操做的專用線程:
run() {
Thread.sleep(100);
synchronized (mLock) {
moveBlock();
}
}
複製代碼
(您可能須要讓休眠時間基於固定的時鐘,以防止漂移現象 - sleep() 不徹底一致,而且 moveBlock() 須要花費的時間不爲零 - 可是您瞭解就行。)
當繪圖代碼喚醒時,它就會抓住鎖,獲取塊的當前位置,釋放鎖,而後進行繪製。您無需基於幀間增量時間進行分數移動,只須要有一個移動對象的線程,以及另外一個在繪製開始時隨地繪製對象的線程。
對於任何複雜度的場景,都須要建立一個即將發生的事件的列表(按喚醒時間排序),並使繪製代碼保持休眠狀態,直到該發生下一個事件爲止。