注册

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 时加入了变换,变换采用标准 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 占用
基于 TextureView100 M ( Graphics 38 M )6%
基于 GLFunctor84 M ( Graphics 26 M )4%

从上述结果可得出结论,使用 drawFunctor 方式在内存、CPU 占用上具有优势, 可应用于局部页面的互动渲染、视频渲染等场景。


作者:支付宝体验科技
链接:https://juejin.cn/post/7130501902545977352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册