網站首頁 編程語言 正文
一. 背景
螞蟻 NativeCanvas 項目 Android 平臺中使用了基于 TextureView 環境實現 GL 渲染的技術方案,而 TextureView 需使用與 Activity Window 獨立的 GraphicBuffer,RenderThread 在上屏 TextureView 內容時需要將 GraphicBuffer 封裝為 EGLImage 上傳為紋理再渲染,內存占用較高。為降低內存占用,經仔細調研 Android 源碼,發現其中存在一種稱為 drawFunctor 的技術,用來將 WebView 合成后的內容同步到 Activity Window 內上屏。經過一番探索成功實現了基于 drawFunctor 實現 GL 注入 RenderThread 的功能,本文將介紹這是如何實現的。
二. drawFunctor 原理介紹
drawFunctor 是 Android 提供的一種在 RenderThread 渲染流程中插入執行代碼機制,Android 框架是通過以下三步來實現這個機制的:
- 在 UI 線程 View 繪制流程 onDraw 方法中,通過 RecordingCanvas.invoke 接口,將 functor 插入 DisplayList 中
- 在 RenderThread 渲染 frame 時執行 DisplayList,判斷如果是 functor 類型的 op,則保存當前部分 gl 狀態
- 在 RenderThread 中真正執行 functor 邏輯,執行完成后恢復 gl 狀態并繼續
目前只能通過 View.OnDraw 來注入 functor,因此對于非 attached 的 view 是無法實現注入的。Functor 對具體要執行的代碼并未限制,理論上可以插入任何代碼的,比如插入一些統計、性能檢測之類代碼。系統為了 functor 不影響當前 gl context,執行 functor 前后進行了基本的狀態保存和恢復工作。
另外,如果 View 設置了使用 HardwareLayer, 則 RenderThread 會單獨渲染此 View,具體做法是為 Layer 生成一塊 FBO,View 的內容渲染到此 FBO 上,然后再將 FBO 以 View 在 hierachy 上的變換繪制 Activity Window Buffer 上。 對 drawFunctor 影響的是, 會切換到 View 對應的 FBO 下執行 functor, 即 functor 執行的結果是寫入到 FBO 而不是 Window Buffer。
三. 利用 drawFunctor 注入 GL 渲染
根據上文介紹,通過 drawFunctor 可以在 RenderThread 中注入任何代碼,那么也一定可以注入 OpenGL API 來進行渲染。我們知道 OpenGL API 需要執行 EGL Context 上,所以就有兩種策略:一種是利用 RenderThread 默認的 EGL Context 環境,一種是創建與 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 執行 Functor 時將調用 operator()方法,what 表示 functor 的操作類型,常見的有同步和繪制, 而 data 是 RenderThread 執行 functor 時傳入的參數,根據源碼發現是 data 是 android::uirenderer::DrawGlInfo 類型指針,包含當前裁剪區域、變換矩陣、dirty 區域等等。
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 設計
operator()調用時傳入的 what 參數為 Mode 枚舉, 對于注入 GL 的場景只需處理 kModeDraw 即可,c++ 側類設計如下:
// 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實現
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) {
// 渲染實現
}
因為 functor 是 Java 層調度的,而真正實現是在 c++ 的,因此需要設計 java 側類并做 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 () 中調度 functor
框架在 java Canvas 類上提供了 API,可以在 onDraw () 時將 functor 記錄到 Canvas 的 DisplayList 中。不過由于版本迭代的原因 API 在各版本上稍有不同,經總結可采用如下代碼調用,兼容各版本區別:
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;
}
}
注意上述代碼反射系統內部 API,Android 10 之后做了 Hidden API 保護,直接反射會失敗,此部分可網上搜索解決方案,此處不展開。
四. 實踐中遇到的問題
GL 狀態保存&恢復
Android RenderThread 在執行 drawFunctor 前會保存部分 GL 狀態,如下源碼:
// Android 9.0 code
// 保存狀態
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);
}
}
// 恢復狀態
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 狀態,可以增加保存和恢復所有其他 GL 狀態的邏輯,也可以針對實際 functor 中改變的狀態進行保存和恢復;特別注意 functor 執行時的 GL 狀態是非初始狀態,例如 stencil、blend 等都可能被系統 RenderThread 修改,因此很多狀態需要重置到默認。
View變換處理
當承載 functor 的 View 外部套 ScrollView、ViewPager,或者 View 執行動畫時,渲染結果異常或者不正確。例如水平滾動條中 View 使用 functor 渲染,內容不會隨著滾動條移動調整位置。進一步研究源碼 Android 發現,此類問題原因都是 Android 在渲染 View 時加入了變換,變換采用標準 4x4 變換列矩陣描述,其值可以從 DrawGlInfo::transform 字段中獲取, 因此渲染時需要處理 transform,例如將 transform 作為模型變換矩陣傳入 shader。
ContextLost
Android framework 在 trimMemory 時在 RenderThread 中會銷毀當前 GL Context 并創建一個新 Context, 這樣會導致 functor 的 program、shader、紋理等 GL 資源都不可用,再去渲染的話可能會導致閃退、渲染異常等問題,因此這種情況必須處理。
首先,需要響應 lowMemory 事件,可以通過監聽 Application 的 trimMemory 回調實現:
activity.getApplicationContext().registerComponentCallbacks(
new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
if (level == 15) {
// 觸發functor重建
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
}
@Override
public void onLowMemory() {
}
});
然后,保存 & 恢復 functor 的 GL 資源和執行狀態,例如 shader、program、fbo 等需要重新初始化,紋理、buffer、uniform 數據需要重新上傳。注意由于無法事前知道 onTrimMemory 發生,上一幀內容是無法恢復的,當然知道完整的狀態是可以重新渲染出來的。
鑒于存在無法提前感知的 ContextLost 情況,建議采用基于 commandbuffer 的模式來實現 functor 渲染邏輯。
五. 效果
我們用一個 OpenGL 渲染的簡單 case (分辨率1080x1920),對使用 TextureView 渲染和使用 drawFunctor 渲染的方式進行了比較,
結果如下:
Simple Case | 內存 | CPU 占用 |
---|---|---|
基于 TextureView | 100 M ( Graphics 38 M ) | 6% |
基于 GLFunctor | 84 M ( Graphics 26 M ) | 4% |
從上述結果可得出結論,使用 drawFunctor 方式在內存、CPU 占用上具有優勢, 可應用于局部頁面的互動渲染、視頻渲染等場景。
原文鏈接:https://juejin.cn/post/7130501902545977352
相關推薦
- 2022-10-14 Ubuntu18.04使用Xorg創建虛擬屏幕
- 2022-04-25 搭建RocketMQ在本地IDEA開發調試環境教程_服務器其它
- 2022-04-01 HIVE str_to_map將字符串轉為map格式
- 2022-04-15 python機器學習MATLAB最小二乘法的兩種解讀_python
- 2022-08-10 Python多任務版靜態Web服務器實現示例_python
- 2022-03-29 帶你了解C++中vector的用法_C 語言
- 2022-05-20 Spring-實現AOP的三種方式演示
- 2022-05-21 Deployment副本無狀態服務創建及水平擴展_服務器其它
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支