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

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

热门文章

最新文章