一. 背景
AntGraphic项目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渲染逻辑。
五. 效果
我们用基本渲染case,对使用TextureView渲染和使用drawFunctor渲染的方式进行了比较,结果如下:
Simple Case |
内存 |
CPU占用 |
基于TextureView |
100M (Graphics 38M) |
6% |
基于GLFunctor |
84M (Graphics 26M) |
4% |
从上述结果可得出结论,使用drawFunctor方式在内存、CPU占用上具有优势, 可应用于局部页面的互动渲染、视频渲染等场景