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

相关文章
|
2月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
241 4
|
2月前
|
缓存 Java 数据库
Android的ANR原理
【10月更文挑战第18天】了解 ANR 的原理对于开发高质量的 Android 应用至关重要。通过合理的设计和优化,可以有效避免 ANR 的发生,提升应用的性能和用户体验。
131 56
|
1月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
66 14
|
1月前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
1月前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
1月前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
42 0
|
2月前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
2月前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
2月前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。
|
2月前
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
85 2