Android?drawFunctor?原理及應(yīng)用詳情
一. 背景
螞蟻 NativeCanvas 項目 Android 平臺中使用了基于 TextureView 環(huán)境實現(xiàn) GL 渲染的技術(shù)方案,而 TextureView 需使用與 Activity Window 獨立的 GraphicBuffer,RenderThread 在上屏 TextureView 內(nèi)容時需要將 GraphicBuffer 封裝為 EGLImage 上傳為紋理再渲染,內(nèi)存占用較高。為降低內(nèi)存占用,經(jīng)仔細調(diào)研 Android 源碼,發(fā)現(xiàn)其中存在一種稱為 drawFunctor 的技術(shù),用來將 WebView 合成后的內(nèi)容同步到 Activity Window 內(nèi)上屏。經(jīng)過一番探索成功實現(xiàn)了基于 drawFunctor 實現(xiàn) GL 注入 RenderThread 的功能,本文將介紹這是如何實現(xiàn)的。
二. drawFunctor 原理介紹
drawFunctor 是 Android 提供的一種在 RenderThread 渲染流程中插入執(zhí)行代碼機制,Android 框架是通過以下三步來實現(xiàn)這個機制的:
- 在 UI 線程 View 繪制流程 onDraw 方法中,通過 RecordingCanvas.invoke 接口,將 functor 插入 DisplayList 中
- 在 RenderThread 渲染 frame 時執(zhí)行 DisplayList,判斷如果是 functor 類型的 op,則保存當(dāng)前部分 gl 狀態(tài)
- 在 RenderThread 中真正執(zhí)行 functor 邏輯,執(zhí)行完成后恢復(fù) gl 狀態(tài)并繼續(xù)
目前只能通過 View.OnDraw 來注入 functor,因此對于非 attached 的 view 是無法實現(xiàn)注入的。Functor 對具體要執(zhí)行的代碼并未限制,理論上可以插入任何代碼的,比如插入一些統(tǒng)計、性能檢測之類代碼。系統(tǒng)為了 functor 不影響當(dāng)前 gl context,執(zhí)行 functor 前后進行了基本的狀態(tài)保存和恢復(fù)工作。
另外,如果 View 設(shè)置了使用 HardwareLayer, 則 RenderThread 會單獨渲染此 View,具體做法是為 Layer 生成一塊 FBO,View 的內(nèi)容渲染到此 FBO 上,然后再將 FBO 以 View 在 hierachy 上的變換繪制 Activity Window Buffer 上。 對 drawFunctor 影響的是, 會切換到 View 對應(yīng)的 FBO 下執(zhí)行 functor, 即 functor 執(zhí)行的結(jié)果是寫入到 FBO 而不是 Window Buffer。
三. 利用 drawFunctor 注入 GL 渲染
根據(jù)上文介紹,通過 drawFunctor 可以在 RenderThread 中注入任何代碼,那么也一定可以注入 OpenGL API 來進行渲染。我們知道 OpenGL API 需要執(zhí)行 EGL Context 上,所以就有兩種策略:一種是利用 RenderThread 默認的 EGL Context 環(huán)境,一種是創(chuàng)建與 RenderThread EGL Context share 的 EGL Context。本文重點介紹第一種,第二種方法大同小異。
Android Functor 定義
首先找到 Android 源碼中 Functor 的頭文件定義并引入項目:
namespace android { class Functor { public: Functor() {} virtual ~Functor() {} virtual int operator()(int /*what*/, void * /*data*/) { return 0; } }; }
RenderThread 執(zhí)行 Functor 時將調(diào)用 operator()方法,what 表示 functor 的操作類型,常見的有同步和繪制, 而 data 是 RenderThread 執(zhí)行 functor 時傳入的參數(shù),根據(jù)源碼發(fā)現(xiàn)是 data 是 android::uirenderer::DrawGlInfo 類型指針,包含當(dāng)前裁剪區(qū)域、變換矩陣、dirty 區(qū)域等等。
DrawGlInfo 頭文件定義如下:
namespace android { namespace uirenderer { /** * Structure used by OpenGLRenderer::callDrawGLFunction() to pass and * receive data from OpenGL functors. */ struct DrawGlInfo { // Input: current clip rect int clipLeft; int clipTop; int clipRight; int clipBottom; // Input: current width/height of destination surface int width; int height; // Input: is the render target an FBO bool isLayer; // Input: current transform matrix, in OpenGL format float transform[16]; // Input: Color space. // const SkColorSpace* color_space_ptr; const void* color_space_ptr; // Output: dirty region to redraw float dirtyLeft; float dirtyTop; float dirtyRight; float dirtyBottom; /** * Values used as the "what" parameter of the functor. */ enum Mode { // Indicates that the functor is called to perform a draw kModeDraw, // Indicates the the functor is called only to perform // processing and that no draw should be attempted kModeProcess, // Same as kModeProcess, however there is no GL context because it was // lost or destroyed kModeProcessNoContext, // Invoked every time the UI thread pushes over a frame to the render thread // *and the owning view has a dirty display list*. This is a signal to sync // any data that needs to be shared between the UI thread and the render thread. // During this time the UI thread is blocked. kModeSync }; /** * Values used by OpenGL functors to tell the framework * what to do next. */ enum Status { // The functor is done kStatusDone = 0x0, // DisplayList actually issued GL drawing commands. // This is used to signal the HardwareRenderer that the // buffers should be flipped - otherwise, there were no // changes to the buffer, so no need to flip. Some hardware // has issues with stale buffer contents when no GL // commands are issued. kStatusDrew = 0x4 }; }; // struct DrawGlInfo } // namespace uirenderer } // namespace android
Functor 設(shè)計
operator()調(diào)用時傳入的 what 參數(shù)為 Mode 枚舉, 對于注入 GL 的場景只需處理 kModeDraw 即可,c++ 側(cè)類設(shè)計如下:
// MyFunctor定義 namespace android { class MyFunctor : Functor { public: MyFunctor(); virtual ~MyFunctor() {} virtual void onExec(int what, android::uirenderer::DrawGlInfo* info); virtual std::string getFunctorName() = 0; int operator()(int /*what*/, void * /*data*/) override; private: }; } // MyFunctor實現(xiàn) int MyFunctor::operator() (int what, void *data) { if (what == android::uirenderer::DrawGlInfo::Mode::kModeDraw) { auto info = (android::uirenderer::DrawGlInfo*)data; onExec(what, info); } return android::uirenderer::DrawGlInfo::Status::kStatusDone; } void MyFunctor::onExec(int what, android::uirenderer::DrawGlInfo* info) { // 渲染實現(xiàn) }
因為 functor 是 Java 層調(diào)度的,而真正實現(xiàn)是在 c++ 的,因此需要設(shè)計 java 側(cè)類并做 JNI 橋接:
// java MyFunctor定義 class MyFunctor { private long nativeHandle; public MyFunctor() { nativeHandle = createNativeHandle(); } public long getNativeHandle() { return nativeHanlde; } private native long createNativeHandle(); } // jni 方法: extern "C" JNIEXPORT jlong JNICALL Java_com_test_MyFunctor_createNativeHandle(JNIEnv *env, jobject thiz) { auto p = new MyFunctor(); return (jlong)p; }
在 View.onDraw () 中調(diào)度 functor
框架在 java Canvas 類上提供了 API,可以在 onDraw () 時將 functor 記錄到 Canvas 的 DisplayList 中。不過由于版本迭代的原因 API 在各版本上稍有不同,經(jīng)總結(jié)可采用如下代碼調(diào)用,兼容各版本區(qū)別:
public class FunctorView extends View { ... private static Method sDrawGLFunction; private MyFunctor myFunctor = new MyFunctor(); @Override public void onDraw(Canvas cvs) { super.onDraw(cvs); getDrawFunctorMethodIfNot(); invokeFunctor(cvs, myFunctor); } private void invokeFunctor(Canvas canvas, MyFunctor functor) { if (functor.getNativeHandle() != 0 && sDrawGLFunction != null) { try { sDrawGLFunction.invoke(canvas, functor.getNativeHandle()); } catch (Throwable t) { // log } } } public synchronized static Method getDrawFunctorMethodIfNot() { if (sDrawGLFunction != null) { return sDrawGLFunction; } hasReflect = true; String className; String methodName; Class<?> paramClass = long.class; try { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { className = "android.graphics.RecordingCanvas"; methodName = "callDrawGLFunction2"; } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { className = "android.view.DisplayListCanvas"; methodName = "callDrawGLFunction2"; } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { className = "android.view.HardwareCanvas"; methodName = "callDrawGLFunction"; } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) { className = "android.view.HardwareCanvas"; methodName = "callDrawGLFunction2"; } else { className = "android.view.HardwareCanvas"; methodName = "callDrawGLFunction"; paramClass = int.class; } Class<?> canvasClazz = Class.forName(className); sDrawGLFunction = SystemApiReflector.getInstance(). getDeclaredMethod(SystemApiReflector.KEY_GL_FUNCTOR, canvasClazz, methodName, paramClass); } catch (Throwable t) { // 異常 } if (sDrawGLFunction != null) { sDrawGLFunction.setAccessible(true); } else { // (異常) } return sDrawGLFunction; } }
注意上述代碼反射系統(tǒng)內(nèi)部 API,Android 10 之后做了 Hidden API 保護,直接反射會失敗,此部分可網(wǎng)上搜索解決方案,此處不展開。
四. 實踐中遇到的問題
GL 狀態(tài)保存&恢復(fù)
Android RenderThread 在執(zhí)行 drawFunctor 前會保存部分 GL 狀態(tài),如下源碼:
// Android 9.0 code // 保存狀態(tài) void RenderState::interruptForFunctorInvoke() { mCaches->setProgram(nullptr); mCaches->textureState().resetActiveTexture(); meshState().unbindMeshBuffer(); meshState().unbindIndicesBuffer(); meshState().resetVertexPointers(); meshState().disableTexCoordsVertexArray(); debugOverdraw(false, false); // TODO: We need a way to know whether the functor is sRGB aware (b/32072673) if (mCaches->extensions().hasLinearBlending() && mCaches->extensions().hasSRGBWriteControl()) { glDisable(GL_FRAMEBUFFER_SRGB_EXT); } } // 恢復(fù)狀態(tài) void RenderState::resumeFromFunctorInvoke() { if (mCaches->extensions().hasLinearBlending() && mCaches->extensions().hasSRGBWriteControl()) { glEnable(GL_FRAMEBUFFER_SRGB_EXT); } glViewport(0, 0, mViewportWidth, mViewportHeight); glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer); debugOverdraw(false, false); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); scissor().invalidate(); blend().invalidate(); mCaches->textureState().activateTexture(0); mCaches->textureState().resetBoundTextures(); }
可以看出并沒有保存所有 GL 狀態(tài),可以增加保存和恢復(fù)所有其他 GL 狀態(tài)的邏輯,也可以針對實際 functor 中改變的狀態(tài)進行保存和恢復(fù);特別注意 functor 執(zhí)行時的 GL 狀態(tài)是非初始狀態(tài),例如 stencil、blend 等都可能被系統(tǒng) RenderThread 修改,因此很多狀態(tài)需要重置到默認。
View變換處理
當(dāng)承載 functor 的 View 外部套 ScrollView、ViewPager,或者 View 執(zhí)行動畫時,渲染結(jié)果異?;蛘卟徽_。例如水平滾動條中 View 使用 functor 渲染,內(nèi)容不會隨著滾動條移動調(diào)整位置。進一步研究源碼 Android 發(fā)現(xiàn),此類問題原因都是 Android 在渲染 View 時加入了變換,變換采用標(biāo)準 4x4 變換列矩陣描述,其值可以從 DrawGlInfo::transform 字段中獲取, 因此渲染時需要處理 transform,例如將 transform 作為模型變換矩陣傳入 shader。
ContextLost
Android framework 在 trimMemory 時在 RenderThread 中會銷毀當(dāng)前 GL Context 并創(chuàng)建一個新 Context, 這樣會導(dǎo)致 functor 的 program、shader、紋理等 GL 資源都不可用,再去渲染的話可能會導(dǎo)致閃退、渲染異常等問題,因此這種情況必須處理。
首先,需要響應(yīng) lowMemory 事件,可以通過監(jiān)聽 Application 的 trimMemory 回調(diào)實現(xiàn):
activity.getApplicationContext().registerComponentCallbacks( new ComponentCallbacks2() { @Override public void onTrimMemory(int level) { if (level == 15) { // 觸發(fā)functor重建 } } @Override public void onConfigurationChanged(Configuration newConfig) { } @Override public void onLowMemory() { } });
然后,保存 & 恢復(fù) functor 的 GL 資源和執(zhí)行狀態(tài),例如 shader、program、fbo 等需要重新初始化,紋理、buffer、uniform 數(shù)據(jù)需要重新上傳。注意由于無法事前知道 onTrimMemory 發(fā)生,上一幀內(nèi)容是無法恢復(fù)的,當(dāng)然知道完整的狀態(tài)是可以重新渲染出來的。
鑒于存在無法提前感知的 ContextLost 情況,建議采用基于 commandbuffer 的模式來實現(xiàn) functor 渲染邏輯。
五. 效果
我們用一個 OpenGL 渲染的簡單 case (分辨率1080x1920),對使用 TextureView 渲染和使用 drawFunctor 渲染的方式進行了比較,
結(jié)果如下:
Simple Case | 內(nèi)存 | CPU 占用 |
---|---|---|
基于 TextureView | 100 M ( Graphics 38 M ) | 6% |
基于 GLFunctor | 84 M ( Graphics 26 M ) | 4% |
從上述結(jié)果可得出結(jié)論,使用 drawFunctor 方式在內(nèi)存、CPU 占用上具有優(yōu)勢, 可應(yīng)用于局部頁面的互動渲染、視頻渲染等場景。
到此這篇關(guān)于Android drawFunctor 原理及應(yīng)用詳情的文章就介紹到這了,更多相關(guān)Android drawFunctor 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android保存的文件顯示到文件管理的最近文件和下載列表中的方法
這篇記錄的是Android中如何把我們往存儲中寫入的文件,如何顯示到文件管理的下載列表、最近文件列表中,需要的朋友可以參考下2020-01-01Android開發(fā)中日期工具類DateUtil完整實例
這篇文章主要介紹了Android開發(fā)中日期工具類DateUtil,結(jié)合完整實例形式分析了Android針對日期與時間的計算、轉(zhuǎn)換、格式化、獲取等相關(guān)操作技巧,需要的朋友可以參考下2017-11-11Android關(guān)于Glide的使用(高斯模糊、加載監(jiān)聽、圓角圖片)
這篇文章主要為大家詳細介紹了Android關(guān)于Glide的使用,內(nèi)容豐富,高斯模糊、加載監(jiān)聽、圓角圖片希望大家可以掌握,感興趣的小伙伴們可以參考一下2016-11-11Android?雙屏異顯自適應(yīng)Dialog的實現(xiàn)
Android 多屏互聯(lián)的時代,必然會出現(xiàn)多屏連接的問題,本文主要介紹了Android?雙屏異顯自適應(yīng)Dialog的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-12-12Android使用acoco統(tǒng)計代碼行覆蓋率介紹
大家好,本篇文章主要講的是Android使用acoco統(tǒng)計代碼行覆蓋率介紹,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12Android控件gridview實現(xiàn)單行多列橫向滾動效果
這篇文章主要為大家詳細介紹了Android控件gridview實現(xiàn)單行多列橫向滾動效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12