Android drawFunctor原理及应用

简介: 一. 背景AntGraphic项目Android平台中使用了基于TextureView环境实现GL渲染的技术方案,而TextureView需使用与Activity Window独立的GraphicBuffer,RenderThread在上屏TextureView内容时需要将GraphicBuffer封装为EGLImage上传为纹理再渲染,内存占用较高。为降低内存占用,经仔细调研Android源码,

一. 背景

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占用上具有优势, 可应用于局部页面的互动渲染、视频渲染等场景

相关文章
|
3天前
|
存储 Java Android开发
Android系统 设置第三方应用为默认Launcher实现和原理分析
Android系统 设置第三方应用为默认Launcher实现和原理分析
18 0
|
2天前
|
移动开发 Java Android开发
构建高效Android应用:采用Kotlin协程优化网络请求
【4月更文挑战第24天】 在移动开发领域,尤其是对于Android平台而言,网络请求是一个不可或缺的功能。然而,随着用户对应用响应速度和稳定性要求的不断提高,传统的异步处理方式如回调地狱和RxJava已逐渐显示出局限性。本文将探讨如何利用Kotlin协程来简化异步代码,提升网络请求的效率和可读性。我们将深入分析协程的原理,并通过一个实际案例展示如何在Android应用中集成和优化网络请求。
|
2天前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优势与实践
【4月更文挑战第24天】随着移动开发技术的不断演进,提升应用性能和用户体验已成为开发者的核心任务。在Android平台上,Kotlin语言凭借其简洁性和功能性成为主流选择之一。特别是Kotlin的协程功能,它为异步编程提供了一种轻量级的解决方案,使得处理并发任务更加高效和简洁。本文将深入探讨Kotlin协程在Android开发中的应用,通过实际案例分析协程如何优化应用性能,以及如何在项目中实现协程。
|
3天前
|
存储 缓存 安全
Android系统 应用存储路径与权限
Android系统 应用存储路径与权限
6 0
Android系统 应用存储路径与权限
|
3天前
|
存储 安全 Android开发
Android系统 自定义系统和应用权限
Android系统 自定义系统和应用权限
19 0
|
3天前
|
网络协议 Shell Android开发
Android 深入学习ADB调试原理(1)
Android 深入学习ADB调试原理(1)
20 1
|
3天前
|
存储 Java Linux
Android系统获取event事件回调等几种实现和原理分析
Android系统获取event事件回调等几种实现和原理分析
25 0
|
8天前
|
缓存 移动开发 Android开发
构建高效Android应用:从优化用户体验到提升性能表现
【4月更文挑战第18天】 在移动开发的世界中,打造一个既快速又流畅的Android应用并非易事。本文深入探讨了如何通过一系列创新的技术策略来提升应用性能和用户体验。我们将从用户界面(UI)设计的简约性原则出发,探索响应式布局和Material Design的实践,再深入剖析后台任务处理、内存管理和电池寿命优化的技巧。此外,文中还将讨论最新的Android Jetpack组件如何帮助开发者更高效地构建高质量的应用。此内容不仅适合经验丰富的开发者深化理解,也适合初学者构建起对Android高效开发的基础认识。
9 0
|
8天前
|
移动开发 Android开发 开发者
构建高效Android应用:采用Kotlin进行内存优化的策略
【4月更文挑战第18天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,由于设备和版本的多样性,确保应用流畅运行且占用资源少是一大挑战。本文将探讨使用Kotlin语言开发Android应用时,如何通过内存优化来提升应用性能。我们将从减少不必要的对象创建、合理使用数据结构、避免内存泄漏等方面入手,提供实用的代码示例和最佳实践,帮助开发者构建更加高效的Android应用。
14 0
|
10天前
|
缓存 移动开发 Java
构建高效的Android应用:内存优化策略
【4月更文挑战第16天】 在移动开发领域,尤其是针对资源有限的Android设备,内存优化是提升应用性能和用户体验的关键因素。本文将深入探讨Android应用的内存管理机制,分析常见的内存泄漏问题,并提出一系列实用的内存优化技巧。通过这些策略的实施,开发者可以显著减少应用的内存占用,避免不必要的后台服务,以及提高垃圾回收效率,从而延长设备的电池寿命并确保应用的流畅运行。