Android drawFunctor 原理及應用詳情

一. 背景

螞蟻 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 時加入瞭變換,變換采用標準 4×4 變換列矩陣描述,其值可以從 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 (分辨率1080×1920),對使用 TextureView 渲染和使用 drawFunctor 渲染的方式進行瞭比較,

結果如下:

Simple Case 內存 CPU 占用
基於 TextureView 100 M ( Graphics 38 M ) 6%
基於 GLFunctor 84 M ( Graphics 26 M ) 4%

從上述結果可得出結論,使用 drawFunctor 方式在內存、CPU 占用上具有優勢, 可應用於局部頁面的互動渲染、視頻渲染等場景。

到此這篇關於Android drawFunctor 原理及應用詳情的文章就介紹到這瞭,更多相關Android drawFunctor 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: