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

相关文章
|
20天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
21天前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
24天前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。
|
28天前
|
缓存 Java 数据库
Android的ANR原理
【10月更文挑战第18天】了解 ANR 的原理对于开发高质量的 Android 应用至关重要。通过合理的设计和优化,可以有效避免 ANR 的发生,提升应用的性能和用户体验。
56 8
|
26天前
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
38 2
|
30天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
48 5
|
30天前
|
移动开发 Dart 搜索推荐
打造个性化安卓应用:从零开始的Flutter之旅
【10月更文挑战第20天】本文将引导你开启Flutter开发之旅,通过简单易懂的语言和步骤,让你了解如何从零开始构建一个安卓应用。我们将一起探索Flutter的魅力,实现快速开发,并见证代码示例如何生动地转化为用户界面。无论你是编程新手还是希望扩展技能的开发者,这篇文章都将为你提供价值。
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
49 4
|
1月前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
51 2
|
1月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
39 3
下一篇
无影云桌面