Android drawFunctor 原理及应用

简介: Android drawFunctor 原理及应用

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团客户端开发工程师战曲,drawFunctor 是 Android 提供的一种在 RenderThread 渲染流程中插入执行代码机制,本文将介绍如何基于 drawFunctor 实现 GL 注入 RenderThread 的功能,欢迎查阅~

  一. 背景

蚂蚁 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 占用
基于 TextureView 100 M ( Graphics 38 M ) 6%
基于 GLFunctor 84 M ( Graphics 26 M ) 4%

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

相关文章
|
9天前
|
安全 Android开发 Kotlin
Android经典实战之SurfaceView原理和实践
本文介绍了 `SurfaceView` 这一强大的 UI 组件,尤其适合高性能绘制任务,如视频播放和游戏。文章详细讲解了 `SurfaceView` 的原理、与 `Surface` 类的关系及其实现示例,并强调了使用时需注意的线程安全、生命周期管理和性能优化等问题。
40 7
|
5天前
|
编解码 前端开发 Android开发
Android经典实战之TextureView原理和高级用法
本文介绍了 `TextureView` 的原理和特点,包括其硬件加速渲染的优势及与其他视图叠加使用的灵活性,并提供了视频播放和自定义绘制的示例代码。通过合理管理生命周期和资源,`TextureView` 可实现高效流畅的图形和视频渲染。
36 12
|
3天前
|
开发框架 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的指南
在移动应用开发的广阔天地中,安卓与iOS两大平台各占半壁江山。本文将深入浅出地对比这两大操作系统的开发环境、工具和用户体验设计,揭示它们在编程语言、开发工具以及市场定位上的根本差异。我们将从开发者的视角出发,逐步剖析如何根据项目需求和目标受众选择适合的平台,同时探讨跨平台开发框架的利与弊,为那些立志于打造下一个热门应用的开发者提供一份实用的指南。
14 5
|
1天前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
7 1
|
9天前
|
XML Java Android开发
探索Android开发之旅:打造你的第一个应用
【9月更文挑战第4天】在这篇专为初学者设计的文章中,我们将一起踏上激动人心的Android开发之旅。从设置开发环境到实现一个简单的“Hello World”应用,每一步都充满了发现和学习。文章将引导你理解Android开发的基础知识,并鼓励你动手实践。让我们开始吧,创造你的第一款Android应用,开启技术世界的新篇章!
|
11天前
|
存储 缓存 搜索推荐
打造个性化天气应用:Android 平台上的天气预报小助手
【9月更文挑战第2天】在这篇文章中,我们将一起探索如何从零开始构建一个简单却功能强大的天气应用。通过这个指南,你将学会如何在 Android 平台上使用 Java 编程语言和相关 API 来创建你自己的天气预报小助手。文章不仅提供了代码示例,还深入讨论了设计思路、用户界面优化以及数据管理等关键方面,旨在帮助初学者理解并实现一个完整的应用项目。
|
14天前
|
搜索推荐 IDE 开发工具
打造个性化安卓应用:从零开始的Flutter之旅
在数字时代的浪潮中,拥有一款个性化且高效的移动应用已成为许多创业者和企业的梦想。本文将引导你使用Flutter框架,从零基础开始构建一个安卓应用,不仅涉及界面设计、功能实现,还包括性能优化的关键技巧。通过简洁易懂的语言和实用的代码示例,我们将一起探索如何让你的应用在众多竞争者中脱颖而出。 【8月更文挑战第31天】
|
14天前
|
存储 开发工具 Android开发
打造你的专属安卓应用:从零开始的Flutter之旅
【8月更文挑战第31天】在数字时代的浪潮中,拥有一款属于自己的应用不仅是梦想的启航,也是技术实力的展现。本文将引导你使用Flutter框架,轻松步入安卓应用的开发世界。无论你是编程新手还是希望拓展技能边界的开发者,跟随这篇指南,你将学会如何搭建开发环境、设计用户界面,并实现基本功能。让我们一起探索代码的力量,开启一段创造之旅吧!
|
5天前
|
开发工具 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的关键考量
在数字时代的浪潮中,安卓和iOS这两大操作系统如同双子星座般耀眼夺目,引领着移动应用的潮流。它们各自拥有独特的魅力和深厚的用户基础,为开发者提供了广阔的舞台。然而,正如每枚硬币都有两面,安卓与iOS在开发过程中也展现出了截然不同的特性。本文将深入剖析这两者在开发环境、编程语言、用户体验设计等方面的显著差异,并探讨如何根据目标受众和项目需求做出明智的选择。无论你是初涉移动应用开发的新手,还是寻求拓展技能边界的资深开发者,这篇文章都将为你提供宝贵的见解和实用的建议,帮助你在安卓与iOS的开发之路上更加从容自信地前行。