Android drawFunctor 原理及應用

語言: CN / TW / HK

:raising_hand|type_1_2:‍♀️ 編者按:本文作者是螞蟻集團客戶端開發工程師戰曲,drawFunctor 是 Android 提供的一種在 RenderThread 渲染流程中插入執行程式碼機制,本文將介紹如何基於 drawFunctor 實現 GL 注入 RenderThread 的功能,歡迎查閱~

    一. 背景

螞蟻 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 佔用上具有優勢, 可應用於區域性頁面的互動渲染、影片渲染等場景。

有點意思,那就點個關注唄 :information_desk_person|type_3:‍♀️

:point_down|type_5: 點選「閱讀原文」,在評論區與我們互動噢