
簡(jiǎn)介針對(duì) Android 平臺(tái)上將 OpenGLES3 紋理 ID 渲染到 ImageReader Surface 的需求這份資源以完整可運(yùn)行的 Android 工程形式提供了從紋理加載、EGLDisplay 虛擬屏幕綁定、eglSwapBuffers 緩沖交換到 ImageReader 回調(diào)讀取數(shù)據(jù)并生成 Bitmap 的端到端參考實(shí)現(xiàn)。源碼按功能拆分結(jié)構(gòu)清晰適合需要對(duì)接相機(jī)/視頻幀數(shù)據(jù)或做離屏渲染的 Android 中高級(jí)開(kāi)發(fā)者參考。包含 89 個(gè)文件壓縮包僅 879KB。工程采用 Android Gradle 標(biāo)準(zhǔn)結(jié)構(gòu)java 文件承載渲染線程與回調(diào)核心邏輯xml 完成界面布局及清單配置gradle 與 properties 管理依賴和構(gòu)建參數(shù)webp 等用于界面輔助展示bin、lock 等則屬于工程編譯緩存。代碼中對(duì) EGL 環(huán)境搭建、紋理加載、Surface 數(shù)據(jù)通路和 onImageAvailable 取幀等關(guān)鍵節(jié)點(diǎn)均有直接體現(xiàn)便于對(duì)照項(xiàng)目進(jìn)行流程梳理、移植或性能調(diào)優(yōu)。目前已有 731 人學(xué)習(xí)/下載適合正在鉆研 Android 圖形管線、希望打通 OpenGLES 與 Surface 之間數(shù)據(jù)鏈路的開(kāi)發(fā)者。1. 為什么要把 GL_TEXTURE_2D 紋理 ID 渲染到 ImageReader 的 Surface 上在 Android 圖形鏈路里紋理 ID 往往是離屏渲染鏈尾部的產(chǎn)物FBO 完成特效合成、第三方 SDK 輸出一幀經(jīng)過(guò) GL_TEXTURE_2D 的紋理下一步要么送編碼器要么給系統(tǒng)拍下來(lái)。但紋理 ID 只是 GPU 里的一個(gè)“名字”MediaCodec、ImageReader 不認(rèn)識(shí)它它們認(rèn)識(shí)的是 Surface——或者說(shuō)得更準(zhǔn)確一點(diǎn)認(rèn)識(shí)一塊 BufferQueue 的生產(chǎn)端。把 GL_TEXTURE_2D 紋理 ID 畫到 ImageReader 提供的 Surface 上本質(zhì)是在 GPU 里把一張紋理完整搬一次家完成一次低成本的顏色緩沖交換而不是把圖像數(shù)據(jù)讀回 CPU 再寫出去。這個(gè)需求常見(jiàn)于視頻編輯、老照片修復(fù)、平臺(tái)美顏抽幀和低延遲串流。它比 glReadPixels 高效得多比使用 SurfaceTexture 多一層可讀性也比直接向 MediaCodec 編碼器 Surface 輸出更能兼容后續(xù)的 CPU 讀幀邏輯。適合兩類人一類是想把 OpenGL ES 3.0 離屏渲染結(jié)果交給系統(tǒng)編碼、錄屏、拍照的工程師另一類是手頭有一個(gè)來(lái)自第三方庫(kù)的 GL_TEXTURE_2D 紋理 ID不知道如何不碰像素就轉(zhuǎn)型成 Android 可消費(fèi)幀的人。這篇文章按我的慣用方案把 EGL 環(huán)境、ImageReader Surface 初始化、渲染代碼、同步柵欄和編碼器接入全部拆開(kāi)。2. 用 EGL 把 ImageReader 的 Surface 接進(jìn) OpenGL ES 3.0先解決“畫到哪”幾乎所有 Android 開(kāi)發(fā)者看到 ImageReader 的第一反應(yīng)都是調(diào)onImageAvailable后拿Image對(duì)象逐一讀 Plane。這個(gè)思路只覆蓋了消費(fèi)端忽略了 ImageReader 另一個(gè)更重要的身份它的getSurface()返回值是一個(gè)標(biāo)準(zhǔn) Surface而 Surface 背后是 BufferQueue 的生產(chǎn)端。換句話說(shuō)GPU 可以把它當(dāng)作一塊可繪制的緩沖區(qū)域只要 EGL 愿意認(rèn)領(lǐng)這塊 Surface。2.1 ImageReader 的 Surface 不是 View渲染路徑完全不同TextureView、SurfaceView 的 Surface 與 ImageReader 的 Surface 在底層都關(guān)聯(lián) BufferQueue但在 API 表現(xiàn)上差異很大。SurfaceView 上可以用 OpenGL 繪制TextureView 本身是 View 層級(jí)里的一個(gè)圖層而 ImageReader 的 Surface 只能作為 ImageReader 的消費(fèi)緩沖入口。它沒(méi)有窗口句柄也不能被 attach 到窗口系統(tǒng)唯一的接入方式就是通過(guò) EGL 的eglCreateWindowSurface。很多人在這一步選錯(cuò) API把 ImageReader 的 Surface 當(dāng) SurfaceTexture 傳進(jìn)setPreviewTexture或者嘗試Canvas.lockCanvas后手動(dòng)編解碼都繞了遠(yuǎn)路。常規(guī)做法是創(chuàng)建 ImageReader取出其 Surface然后在 EGL 層創(chuàng)建 EGLSurface之后所有 GL 繪制命令都會(huì)落到這塊 Surface 上eglSwapBuffers之后 ImageReader 就能異步收到一張全新 Image。下面是四種常見(jiàn) Surface 消費(fèi)者的對(duì)比對(duì)象可被 GL 繪制能否 CPU 讀像素典型用途SurfaceView 的 Surface可以需要額外處理游戲、視頻播放TextureView 的 SurfaceTexture可以需要額外處理相機(jī)預(yù)覽、UI 合成MediaCodec 的 input Surface可以不可以直接讀硬編碼輸入ImageReader 的 Surface可以通過(guò) EGL 接入可以且是設(shè)計(jì)目標(biāo)拍幀、讀紋理、YUV 轉(zhuǎn)換2.2 EGL Surface 與 ImageReader 的像素格式必須匹配EGL 是 OpenGL ES 和窗口系統(tǒng)之間的橋梁。把 ImageReader 的 Surface 接進(jìn)來(lái)核心是eglGetDisplay、eglInitialize、eglChooseConfig、eglCreateWindowSurface這條鏈路。其中最容易出問(wèn)題的是 EGLConfig 的選擇ImageReader 的緩沖格式由構(gòu)造時(shí)的 pixelFormat 決定而 EGLWindowSurface 創(chuàng)建時(shí)不能直接指定格式它必須從 EGLConfig 中推導(dǎo)。如果 EGLConfig 的 RGBA 位數(shù)與 ImageReader 不一致輕則色彩偏移重則eglCreateWindowSurface直接返回EGL_BAD_MATCH。以下是我在實(shí)際項(xiàng)目里穩(wěn)定使用的 EGL 初始化片段EGLDisplay display EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); int[] version new int[2]; EGL14.eglInitialize(display, version, 0, version, 1); int[] configAttrs { EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT | 0x0040, // 0x0040 對(duì)應(yīng) EGL_OPENGL_ES3_BIT EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_NONE }; EGLConfig[] configs new EGLConfig[1]; int[] numConfigs new int[1]; EGL14.eglChooseConfig(display, configAttrs, 0, configs, 0, 1, numConfigs, 0); int[] surfaceAttrs { EGL14.EGL_NONE }; EGLSurface eglSurface EGL14.eglCreateWindowSurface(display, configs[0], imageReaderSurface, surfaceAttrs, 0);這段代碼里第二行的0x0040很關(guān)鍵。Android 的 EGL 頭文件對(duì)EGL_OPENGL_ES3_BIT_KHR的定義是 0x0040而EGL14.EGL_OPENGL_ES2_BIT的值是 0x0004兩者并不相同。如果只寫入 ES2 bit某些機(jī)型上依然能創(chuàng)建成功但后續(xù)eglCreateContext請(qǐng)求的 OpenGL ES 3.0 context 與 EGLConfig 的 renderable type 不匹配會(huì)得到EGL_BAD_MATCH或EGL_BAD_CONFIG。2.3 共享上下文紋理 ID 在新 EGLContext 里“仍然有效”EGLSurface 創(chuàng)建完成后下一步是創(chuàng)建 EGLContext。如果你已經(jīng)有另一個(gè)線程在渲染紋理比如相機(jī)模塊或第三方濾鏡庫(kù)在某個(gè) GL 線程上生成了紋理 ID那么新的渲染線程必須創(chuàng)建共享上下文。EGLContext 默認(rèn)擁有獨(dú)立的資源命名空間紋理 ID 只是一個(gè)小整數(shù)在不同 context 里如果不同步它在 GL 側(cè)會(huì)被當(dāng)作“不存在的對(duì)象”即使調(diào)用glBindTexture也不會(huì)報(bào)錯(cuò)繪制結(jié)果只會(huì)是黑色。創(chuàng)建共享上下文的方法是先拿到當(dāng)前線程的活躍 contextEGL14.eglGetCurrentContext()然后把它傳給eglCreateContext的第二個(gè)參數(shù)。這樣兩個(gè) context 共享紋理、FBO 等對(duì)象紋理 ID 才能跨線程使用。但如果原紋理所在線程根本沒(méi)有 EGLContext而是通過(guò)其他方式生成的那問(wèn)題就不一樣了——那種情況通常要用擴(kuò)展接口導(dǎo)出內(nèi)存句柄這里不展開(kāi)。3. 渲染 GL_TEXTURE_2D 到 ImageReader Surface 的完整實(shí)現(xiàn)有了 EGL Surface 和共享上下文渲染核心就只剩三件事創(chuàng)建 Shader 程序、綁定紋理、提交繪制。這一章給出可以直接抄進(jìn)工程的 Java 實(shí)現(xiàn)用的是 OpenGL ES 3.0 API沒(méi)有引入任何第三方依賴Android Studio 里新建工程即可運(yùn)行。3.1 初始化 EGL 環(huán)境與創(chuàng)建 EGLSurface 的代碼把上一章的邏輯收攏成一個(gè)初始化方法注意imageReader必須提前創(chuàng)建并且寬高要與紋理一致public void init(ImageReader imageReader, EGLContext sharedContext, int textureId) { mImageReader imageReader; mTextureId textureId; mWidth imageReader.getWidth(); mHeight imageReader.getHeight(); mDisplay EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); int[] version new int[2]; EGL14.eglInitialize(mDisplay, version, 0, version, 1); int[] configAttrs { EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT | 0x0040, EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_NONE }; EGLConfig[] configs new EGLConfig[1]; int[] numConfigs new int[1]; EGL14.eglChooseConfig(mDisplay, configAttrs, 0, configs, 0, 1, numConfigs, 0); mSurface EGL14.eglCreateWindowSurface(mDisplay, configs[0], imageReader.getSurface(), new int[]{EGL14.EGL_NONE}, 0); int[] contextAttrs { EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE }; mContext EGL14.eglCreateContext(mDisplay, configs[0], sharedContext, contextAttrs, 0); EGL14.eglMakeCurrent(mDisplay, mSurface, mSurface, mContext); setupShaders(); }參數(shù)說(shuō)明sharedContext是紋理來(lái)源線程的 EGLContext如果傳入的 textureId 就在當(dāng)前線程創(chuàng)建這里可以直接傳EGL14.EGL_NO_CONTEXT。EGL_CONTEXT_CLIENT_VERSION, 3決定創(chuàng)建的是 OpenGL ES 3.0 context注意 EGLConfig 里必須帶上 ES3 bit否則這里報(bào)錯(cuò)。eglMakeCurrent的作用是把 EGLSurface 綁定到當(dāng)前線程的渲染目標(biāo)之后所有 GLES3 指令都作用于這塊 ImageReader Surface。3.2 Shader 與繪制命令如何用三角形覆蓋紋理ImageReader 的 Surface 是一塊矩形緩沖渲染時(shí)最常見(jiàn)的方式是畫兩個(gè)三角形拼成一個(gè)全屏四邊形。頂點(diǎn)著色器負(fù)責(zé)接收頂點(diǎn)坐標(biāo)和紋理坐標(biāo)片段著色器負(fù)責(zé)從 GL_TEXTURE_2D 上采樣顏色。頂點(diǎn)著色器#version 300 es layout(location 0) in vec4 aPosition; layout(location 1) in vec2 aTexCoord; out vec2 vTexCoord; void main() { gl_Position aPosition; vTexCoord aTexCoord; }片段著色器#version 300 es precision mediump float; uniform sampler2D uTexture; in vec2 vTexCoord; out vec4 fragColor; void main() { fragColor texture(uTexture, vTexCoord); }這個(gè)著色器沒(méi)有做任何顏色變換和矩陣運(yùn)算保留紋理原始顏色。sampler2D默認(rèn)綁定紋理單元 0所以繪制前不需要顯式glActiveTexture只要把紋理 ID bind 到GL_TEXTURE_2D上即可。如果將來(lái)要接入 OES 外部紋理需要將采樣器類型換成samplerExternalOES同時(shí)把紋理目標(biāo)改成GL_TEXTURE_EXTERNAL_OES與本文標(biāo)題鎖定的 GL_TEXTURE_2D 場(chǎng)景不是同一套配置。3.3 draw 幀的調(diào)用時(shí)機(jī)與紋理參數(shù)頂點(diǎn)數(shù)據(jù)和繪制代碼private void drawFrame() { EGL14.eglMakeCurrent(mDisplay, mSurface, mSurface, mContext); GLES30.glViewport(0, 0, mWidth, mHeight); GLES30.glUseProgram(mProgram); GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVbo); GLES30.glEnableVertexAttribArray(0); GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 5 * 4, 0); GLES30.glEnableVertexAttribArray(1); GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 5 * 4, 3 * 4); // 關(guān)鍵綁定紋理 ID 到 GL_TEXTURE_2D 目標(biāo) GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId); GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); EGL14.eglSwapBuffers(mDisplay, mSurface); }glVertexAttribPointer(1, 2, ..., 5 * 4, 3 * 4)表示頂點(diǎn)數(shù)據(jù)每 5 個(gè) float 為一組前 3 個(gè)是位置后 2 個(gè)是紋理坐標(biāo)stride 為 20 字節(jié)。繪制的 4 個(gè)頂點(diǎn)可以用(-1,-1,0,0,0)到(1,1, ..., 1,1)的三角形帶覆蓋全屏。紋理綁定后建議檢查紋理對(duì)象的過(guò)濾參數(shù)。外部庫(kù)生成的紋理 ID 不一定設(shè)置了GL_TEXTURE_MIN_FILTER如果默認(rèn)值是GL_NEAREST_MIPMAP_LINEAR而紋理沒(méi)有生成 mipmap畫面會(huì)出現(xiàn)模糊接縫或黑邊??稍赿rawFrame里每次綁定時(shí)執(zhí)行一次glTexParameteriGLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE); GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);GL_CLAMP_TO_EDGE可以避免紋理邊緣采樣到透明或黑色區(qū)域。如果紋理 ID 來(lái)源是最新一幀相機(jī) YUV 轉(zhuǎn)成的 GL_TEXTURE_2D換幀時(shí) GPU 和 CPU 之間可能還有未完成寫入需要在 draw 之前插入glFinish()或使用同步柵欄否則第一幀可能出現(xiàn)紋素錯(cuò)亂。4. 共享上下文、同步與格式最常出問(wèn)題的 3 個(gè)位置EGL 初始化完成后渲染邏輯本身很短真正消耗時(shí)間的是排錯(cuò)。這一章只討論最常導(dǎo)致黑屏、花屏、綠屏的三個(gè)因素上下文共享失效、GPU 繪制與 ImageReader 消費(fèi)之間的時(shí)序、像素格式不匹配。4.1 紋理 ID 不是“全局身份證”必須用共享上下文許多人拿到外部傳入的 textureId 后既不詢問(wèn)它的 EGLContext也不做共享直接在新線程里glBindTexture后繪制得到的結(jié)果是黑屏但不報(bào)錯(cuò)。原因是 GL 紋理 ID 在 context 的資源表里只是一個(gè)索引新 context 里沒(méi)有這張表的注冊(cè)信息。共享上下文的創(chuàng)建方法在 2.3 節(jié)已經(jīng)說(shuō)明這里補(bǔ)充一個(gè)常見(jiàn)誤用只共享 EGLDisplay卻不共享 EGLContext這種代碼在開(kāi)發(fā)機(jī)上偶爾能跑因?yàn)槟承?qū)動(dòng)廠商把資源表做成了全局的但壓測(cè)和真機(jī)更換設(shè)備后立刻崩潰。推薦的做法是在紋理生產(chǎn)方初始化時(shí)記錄一個(gè)全局變量sSharedContext EGL14.eglGetCurrentContext();然后在渲染線程 init 時(shí)把它作為創(chuàng)建參數(shù)傳進(jìn)去。如果紋理生產(chǎn)方?jīng)]有 EGLContext直接傳EGL_NO_CONTEXT。注意共享 context 只共享對(duì)象不共享 GL 狀態(tài)機(jī)所以紋理參數(shù)、Shader、VBO 都要在新 context 里重新設(shè)置。4.2 EGL 渲染先行ImageReader 異步消費(fèi)同步在哪做ImageReader 的 Image 是異步到達(dá)的onImageAvailable觸發(fā)時(shí)并不代表 GPU 繪制已經(jīng)完成。eglSwapBuffers只表示緩沖被送交 BufferQueue不保證 GPU 已結(jié)束渲染。在部分設(shè)備上ImageReader 拿到幀時(shí)紋理內(nèi)容還處于可寫狀態(tài)后面消費(fèi)線程讀取時(shí)會(huì)出現(xiàn)半幀或內(nèi)容撕裂。如果追求穩(wěn)妥最直接的做法是在eglSwapBuffers后插入 GPU 等待EGL14.eglSwapBuffers(mDisplay, mSurface); // 等待 GPU 完成所有繪制指令確保 ImageReader 能拿到已完成的幀 GLES30.glFinish();glFinish會(huì)讓 CPU 阻塞直到 buffer 可復(fù)用。對(duì)于 30fps 的抽幀需求影響不大但如果要跑 60fps建議換成 fenceEGLSyncKHR fence EGLExt.eglCreateSyncKHR(mDisplay, EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, new int[]{ EGLExt.EGL_SYNC_NATIVE_FENCE_ANDROID, 0, EGL14.EGL_NONE }, 0); // 從 fence 里取出 native fence fd傳給消費(fèi)端等待完成后 close這是 Android 推薦的低延遲方案把 fence fd 一并傳給消費(fèi)者而不是讓 CPU 在 GPU 完成后才繼續(xù)跑。如果當(dāng)前只做總結(jié)輸出glFinish簡(jiǎn)單不易錯(cuò)。4.3 像素格式與 Size 對(duì)齊ImageReader 不是隨便創(chuàng)建的ImageReader 的構(gòu)造參數(shù)pixelFormat必須與 EGLConfig 的 RGBA 位數(shù)一致否則畫面顏色會(huì)偏移或者完全失真。RGB_565、RGBA_8888、YUV_420_888 在 BufferQueue 里是不同格式EGL 無(wú)法對(duì)一個(gè) YUV 格式的 Surface 直接執(zhí)行 RGBA 的渲染操作。標(biāo)題鎖定的場(chǎng)景是 OpenGL ES 3.0 輸出所以 ImageReader 通常這樣創(chuàng)建mImageReader ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);注意最后那個(gè)maxImages參數(shù)。如果設(shè)成 1渲染頻率高時(shí)消費(fèi)者來(lái)不及 closeImageReader 會(huì)停止向 BufferQueue 請(qǐng)求新緩沖eglSwapBuffers會(huì)卡死設(shè)成 4 又會(huì)成倍占用內(nèi)存。2 是絕大多數(shù)抽幀和編碼場(chǎng)景下的默認(rèn)值。寬度和高度也需要對(duì)齊。給 ImageReader 傳奇數(shù)尺寸部分設(shè)備在eglCreateWindowSurface階段就失敗部分在ImageReader.getPlanes()階段因?yàn)?stride 和 width 不一致返回錯(cuò)誤的 Buffer 布局。常規(guī)做法是先對(duì)尺寸做對(duì)齊再創(chuàng)建int alignedWidth (width 15) / 16 * 16; int alignedHeight (height 15) / 16 * 16;紋理渲染到對(duì)齊后的 Surface 上如果 UVC 相機(jī)原始尺寸是 640x480對(duì)齊后依然是這兩個(gè)數(shù)字因?yàn)?16 的倍數(shù)對(duì)齊不會(huì)改變這兩個(gè)值。5. 接入 MediaCodec 編碼器與 30fps 幀率控制一個(gè)可收尾的技巧渲染到 ImageReader 的最終目的大多離不開(kāi)編碼。一個(gè)更高效的做法是讓 OpenGL ES 3.0 直接繪制到 MediaCodec 編碼器的輸入 Surface省掉 ImageReader 這個(gè)中間層。但有時(shí)業(yè)務(wù)需要同時(shí)保留 CPU 讀幀能力那就必須雙 Surface 并行。5.1 如果你要編碼別讓 ImageReader 成為中間跳板常見(jiàn)錯(cuò)誤寫法是先渲染到 ImageReader再在onImageAvailable里把Image的 buffer 拷貝出來(lái)通過(guò)MediaCodec.queueInputBuffer喂給編碼器。這不叫 GPU 渲染等于把 GPU 畫的幀又回到了 CPU再交給編碼器性能損耗巨大。如果不需要 CPU 讀幀直接創(chuàng)建編碼器輸入 SurfacemEncoder MediaCodec.createEncoderByType(video/avc); MediaFormat format MediaFormat.createVideoFormat(video/avc, alignedWidth, alignedHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface encoderSurface mEncoder.createInputSurface(); mEncoder.start();然后把encoderSurface傳給第 3 章的init方法ImageReader 那條路徑留作旁路抽幀。5.2 兩個(gè) EGLSurface 交替渲染同時(shí)保留讀像素能力為了同時(shí)滿足編碼和讀幀可以創(chuàng)建兩個(gè) EGLSurface一個(gè)綁定 ImageReader一個(gè)綁定編碼器輸入 Surface。每次drawFrame時(shí)按當(dāng)前消費(fèi)者選擇目標(biāo)EGL14.eglMakeCurrent(mDisplay, chooseSurface(), chooseSurface(), mContext); GLES30.glFinish(); EGL14.eglSwapBuffers(mDisplay, chooseSurface());chooseSurface()根據(jù)當(dāng)前是否處于檢測(cè)階段返回對(duì)應(yīng) EGLSurface。這樣編碼鏈路的每一幀都是 GPU 直接寫入不經(jīng)過(guò) CPUImageReader 路徑只在真正需要抽幀的瞬間才切換過(guò)去。幀率控制可以用Choreographer在 VSYNC 信號(hào)上回調(diào)或者用循環(huán)控制long startTime System.nanoTime(); long frameInterval 33_000_000L; // 30fps單位納秒 while (running) { drawFrame(); long elapsed System.nanoTime() - startTime; if (elapsed frameInterval) { SystemClock.sleep((frameInterval - elapsed) / 1_000_000L); } startTime System.nanoTime(); }SystemClock.sleep比Thread.sleep更精確它不受 Java 層時(shí)間調(diào)整影響適合在 GL 線程里做節(jié)奏控制。5.3 用 adb 驗(yàn)證 ImageReader 是否真的在刷新接入完成后如果無(wú)法確定 ImageReader 是否收到幀可以用 adb 查看 SurfaceFlinger 的圖層狀態(tài)。先列出所有 Surface 名稱找到 ImageReader 對(duì)應(yīng)的項(xiàng)adb shell dumpsys SurfaceFlinger --list對(duì)比渲染前后同一 Surface 的z-order或visible region變化可以確認(rèn)是否提交了緩沖。但要注意一點(diǎn)ImageReader 表面在eglSwapBuffers后不會(huì)永遠(yuǎn)可見(jiàn)如果消費(fèi)者沒(méi)有及時(shí) close 圖像Surface 圖層可能被 SurfaceFlinger 判定為無(wú)內(nèi)容而在--list中消失。遇到這種情況優(yōu)先檢查onImageAvailable是否執(zhí)行以及Image.close()是否及時(shí)調(diào)用。本文還有配套的精品資源點(diǎn)擊獲取