【Rive】rive-android源码分析

news/2025/10/4 12:09:14/文章来源:https://www.cnblogs.com/zhyan8/p/19123014

1 前言

​ 本文基于 rive-android 10.1.0 进行源码分析,主要介绍 Rive 的渲染类型、RendererType 透传流程、Surface 透传流程、渲染流程、启动渲染流程、暂停渲染流程等内容。

​ rive-android 类图框架如下。图中,蓝色的类表示 rive-android 中 Kotlin 代码,绿色的类表示 rive-android 中 C++ 代码,橙色的类表示 rive-runtime 中代码(下同)。本文只解读 rive-android 的源码,不解读 rive-runtime 的源码。

img

​ Rive 相关应用参考以下内容。

  • Rive在Android上的简单应用
  • 动画
  • 混合动画
  • Android与Rive交互
  • 事件
  • 波动文字
  • 骨骼动画
  • 眼睛互动动画

2 渲染类型

​ rive-android 10.0.0 之前的版本,有三种渲染类型,分别是:Skia、Rive、Canvas,默认是 Skia 类型。10.0.0 版本开始舍弃了 Skia 渲染类型,默认是 Rive 类型。Skia 和 Rive 渲染类型底层都是基于 EGL 环境进行离屏渲染。

​ RenderType 类源码如下。

​ app.rive.runtime.kotlin.core.RendererType.kt

enum class RendererType(val value: Int) {Rive(0),Canvas(1);companion object {fun fromIndex(index: Int): RendererType {val maxIndex = entries.size...return entries[index]}}
}

3 RendererType 透传流程

​ 本节将介绍 RendererType 如何一步一步透传下去,直到最终创建 DrawableThreadState 对象。本节中,读者需要重点关注 RendererType 是如何在各个类之间传递的。RendererType 透传流程图如下。

img

3.1 设置 RendererType 的源头

​ 用户在初始化 Rive 环境时,可以指定 RenderType,如下。

Rive.init(applicationContext, defaultRenderer = RendererType.Rive)

点击并拖拽以移动

​ 也可以在布局文件中指定 RenderType(如果想使用 Winscope 或 Perfetto 查看 Trace,可以配置 riveTraceAnimations 参数),如下。

<app.rive.runtime.kotlin.RiveAnimationViewapp:riveRenderer="Rive"app:riveTraceAnimations="true"… />

​ 无论是哪种方式,在 RiveAnimationView 对象被创建时,渲染类型会保存在 rendererAttributes 对象的 rendererType 属性中(rendererAttributes 是 RendererAttributes 类型)。

3.2 创建 RiveArtboardRenderer

​ RiveAnimationView 继承了 RiveTextureView,RiveTextureView 继承了 TextureView,并重写了其 onAttachedToWindow 函数,如下。

​ app.rive.runtime.kotlin.RiveTextureView.kt

@CallSuper
override fun onAttachedToWindow() {super.onAttachedToWindow()surfaceTextureListener = thisisOpaque = falserenderer = createRenderer().apply { make() }
}

​ 在 RiveAnimationView 中重写了 createRenderer 函数,如下。

​ app.rive.runtime.kotlin.RiveAnimationView.kt

override fun createRenderer(): Renderer {return RiveArtboardRenderer(trace = rendererAttributes.riveTraceAnimations,controller = controller,rendererType = rendererAttributes.rendererType,)
}

​ RiveArtboardRenderer 只有主构造函数,没有次要构造函数,如下。关于主构造函数和次要构造函数的介绍详见 → 类和对象。

​ app.rive.runtime.kotlin.renderers.RiveArtboardRenderer.kt

open class RiveArtboardRenderer(trace: Boolean = false,rendererType: RendererType = Rive.defaultRendererType,private var controller: RiveFileController,
) : Renderer(rendererType, trace)

​ Renderer 的也只有主构造函数,没有次要构造函数,如下。

​ app.rive.runtime.kotlin.renderers.Renderer.kt

abstract class Renderer(@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)var type: RendererType = Rive.defaultRendererType,val trace: Boolean = false
) : NativeObject(NULL_POINTER), Choreographer.FrameCallback

3.3 创建 JNIRenderer

​ 我们再回到 RiveTextureView 的 onAttachedToWindow 函数中,在 createRenderer 之后,调用了 Renderer 的 make 函数。make 函数的源码如下。cppPointer 是在 NativeObject 中定义的 Long 类型变量,对应 C++ 中的指针变量,指向的是 JNI 中 JNIRenderer 对象。在 make 函数中又调用了 constructor 函数,它在 JNI 中有实现。(注意:这里的 constructor 不是 Renderer 的构造函数,因为其前面加了 fun,构造函数前面不能加 fun)

​ app.rive.runtime.kotlin.renderers.Renderer.kt

@CallSuper
open fun make() {if (!hasCppObject) {cppPointer = constructor(trace, type.value)refs.incrementAndGet()}
}/** Instantiates JNIRenderer in C++ */
private external fun constructor(trace: Boolean, type: Int): Long

​ 全局搜索 "Renderer_constructor",找到 JNI 中的 constructor 函数的实现如下。

​ bindings/bindings_renderer.cpp

JNIEXPORT jlong JNICALL
Java_app_rive_runtime_kotlin_renderers_Renderer_constructor(JNIEnv* env, jobject ktRenderer,jboolean trace, jint type)
{RendererType rendererType = static_cast<RendererType>(type);JNIRenderer* renderer = new JNIRenderer(ktRenderer, trace, rendererType);...return (jlong)renderer;
}

​ JNIRenderer 的构造函数如下,这里会创建 RefWorker 对象保存在 m_worker 中,创建 ITracer 对象保存在 m_tracer 中。如果 trace 为 false,将创建 NoopTracer 对象,其 beginSection、endSection 函数都是空实现;如果 trace 为 true,将创建 Tracer 对象,其 beginSection 函数将调用 ATrace_beginSection,endSection 函数将调用 ATrace_endSection(详见 aosp 中的 frameworks/base/native/android/trace.cpp),这时用户就可以使用 Winscope 或 Perfetto 查看 Trace 了。

​ models/jni_renderer.cpp

JNIRenderer::JNIRenderer(jobject ktRenderer, bool trace, const RendererType rendererType) :m_worker(RefWorker::CurrentOrFallback(rendererType)),m_ktRenderer(GetJNIEnv()->NewGlobalRef(ktRenderer)),m_tracer(getTracer(trace))
{}

3.4 创建 RefWorker

​ RefWorker::CurrentOrFallback 的源码如下。

​ helpers/work_ref.cpp

rcp<RefWorker> RefWorker::CurrentOrFallback(RendererType rendererType)
{rcp<RefWorker> currentOrFallback;switch (rendererType){case RendererType::None:assert(false);break;case RendererType::Rive:currentOrFallback = RiveWorker();break;case RendererType::Canvas:currentOrFallback = CanvasWorker();break;}if (currentOrFallback == nullptr){currentOrFallback = CanvasWorker();}return currentOrFallback;
}

​ RiveWorker 和 CanvasWorker 函数实现如下。

​ helpers/work_ref.cpp

rcp<RefWorker> RefWorker::RiveWorker()
{static enum class RiveRendererSupport { unknown, no, yes } s_isSupported;static std::unique_ptr<RefWorker> s_riveWorker;...if (s_isSupported == RiveRendererSupport::unknown){std::unique_ptr<RefWorker> candidateWorker(new RefWorker(RendererType::Rive));candidateWorker->runAndWait([](rive_android::DrawableThreadState* threadState) {PLSThreadState* plsThreadState = static_cast<PLSThreadState*>(threadState);s_isSupported = plsThreadState->renderContext() != nullptr ? RiveRendererSupport::yes : RiveRendererSupport::no;});if (s_isSupported == RiveRendererSupport::yes){s_riveWorker = std::move(candidateWorker);}...}if (s_riveWorker != nullptr){++s_riveWorker->m_externalRefCount;}return rcp(s_riveWorker.get());
}rcp<RefWorker> RefWorker::CanvasWorker()
{if (s_canvasWorker == nullptr){s_canvasWorker = std::unique_ptr<RefWorker>(new RefWorker(RendererType::Canvas));}++s_canvasWorker->m_externalRefCount;return rcp(s_canvasWorker.get());
}

​ RefWorker 继承 WorkerThread,如下。

​ helpers/work_ref.hpp

explicit RefWorker(const RendererType rendererType) :WorkerThread(RendererName(rendererType), Affinity::None, rendererType)
{}

3.5 创建 WorkThread

​ WorkThread 的构造函数如下,可以看到这里创建了一个线程,用于处理渲染任务。

​ helpers/worker_thread.hpp

WorkerThread(const char* name, Affinity affinity, const RendererType rendererType) :m_RendererType(rendererType),mName(name),mAffinity(affinity),mWorkMutex{}
{mThread = std::thread([this]() { threadMain(); });
}

​ threadMain 函数的实现如下。首先通过 MakeThreadState 函数创建 DrawableThreadState 对象;接着进入 for 无限循环体中,如果 mWorkQueue 为空(mWorkQueue 中的元素是 function<void(DrawableThreadState*) 类型的函数指针),线程将处于 wait 状态,否则从工作队列中取出函数指针元素,并执行函数;如果从工作队列中取出的函数指针为空,将结束线程。

​ helpers/worker_thread.hpp

void threadMain()
{setAffinity(mAffinity);...m_threadState = MakeThreadState(m_RendererType);std::unique_lock lock(mWorkMutex);for (;;){while (mWorkQueue.empty()){m_workPushedCondition.wait(mWorkMutex);}Work work = mWorkQueue.front();mWorkQueue.pop();if (!work){// A null function is a special token that tells the thread to terminate.break;}lock.unlock();work(m_threadState.get());lock.lock();++m_lastCompletedWorkID;m_workedCompletedCondition.notify_all();}m_threadState.reset();DetachThread();
}

3.6 创建 DrawableThreadState

​ MakeThreadState 函数的实现如下。

​ helpers/worker_thread.cpp

std::unique_ptr<DrawableThreadState> WorkerThread::MakeThreadState(const RendererType type)
{switch (type){case RendererType::Canvas:return std::make_unique<CanvasThreadState>();default:case RendererType::Rive:return std::make_unique<PLSThreadState>();}
}

​ CanvasThreadState 继承 DrawableThreadState,PLSThreadState 继承 EGLThreadState,EGLThreadState 继承 DrawableThreadState(在 rive-android 10.0.0 之前的版本,还有 SkiaThreadState 也继承 EGLThreadState,它对应的 RenderType 是 Skia)。

3.6.1 DrawableThreadState、EGLThreadState、CanvasThreadState 接口定义

​ DrawableThreadState、EGLThreadState、CanvasThreadState 的接口定义如下。

​ helpers/thread_state_egl.hpp

class DrawableThreadState
{
public:virtual ~DrawableThreadState(){};virtual void swapBuffers() = 0;
};class EGLThreadState : public DrawableThreadState
{
public:EGLThreadState();virtual ~EGLThreadState() = 0;EGLSurface createEGLSurface(ANativeWindow*);virtual void destroySurface(EGLSurface) = 0;virtual void makeCurrent(EGLSurface) = 0;void swapBuffers() override;protected:EGLSurface m_currentSurface = EGL_NO_SURFACE;EGLDisplay m_display = EGL_NO_DISPLAY;EGLContext m_context = EGL_NO_CONTEXT;EGLConfig m_config = static_cast<EGLConfig>(0);
};class CanvasThreadState : public DrawableThreadState
{
public:void swapBuffers() override {}
};

3.6.2 创建 EGLThreadState

​ EGLThreadState 的构造函数源码如下,它实现了 EGL 环境创建 5 步中的前 3步,剩下 2 步在 PLSThreadState 的构造函数中实现。EGL 环境创建详见 → EGL+FBO离屏渲染。

​ helpers/thread_state_egl.cpp

EGLThreadState::EGLThreadState()
{// 创建 EGLDisplaym_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);...if (!eglInitialize(m_display, 0, 0)){...return;}// 创建 EGLConfigconst EGLint configAttributes[] = {...};...eglChooseConfig(m_display, configAttributes, supportedConfigs.data(), num_configs, &num_configs);...// 创建 EGLContextm_context = eglCreateContext(m_display, m_config, nullptr, contextAttributes);...
}

3.6.3 创建 PLSThreadState

​ PLSThreadState 的构造函数源码如下,它实现了 EGL 环境创建 5 步中的后 2步。eglCreatePbufferSurface 用于创建一个离屏渲染的 EGL 表面,Pbuffer 表面是一个虚拟的离屏缓冲区,可以在其中进行渲染操作,而不直接与屏幕交互。rive::gpu::RenderContextGLImpl::MakeContext 的源码不在 rive-android 代码库中,在 rive-runtime 代码库中,详见 → render_context_gl_impl.cpp。

​ helpers/thread_state_psl.cpp

PLSThreadState::PLSThreadState()
{// 创建 EGLSurfaceconst EGLint PbufferAttrs[] = {...};m_backgroundSurface = eglCreatePbufferSurface(m_display, m_config, PbufferAttrs);...// 绑定 EGLSurface 和 EGLContext 到显示设备 (EGLDisplay)eglMakeCurrent(m_display, m_backgroundSurface, m_backgroundSurface, m_context);m_currentSurface = m_backgroundSurface;m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext();
}

4 Surface 透传流程

​ 本节将介绍 Surface 如何一步一步透传下去,直到最终创建 rive::RiveRenderer 对象或 CanvasRenderer 对象。本节中,读者需要重点关注 Surface 是如何在各个类之间传递的。Surface 透传流程图如下。

img

4.1 Surface 创建的源头

​ 在 RiveTextureView 的 onSurfaceTextureAvailable 方法中,创建了 Surface 如下。

​ app.rive.runtime.kotlin.RiveTextureView.kt

@CallSuper
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {if (this::viewSurface.isInitialized) {viewSurface.release()}viewSurface = Surface(surfaceTexture)renderer?.apply {stop()setSurface(viewSurface)}
}

4.2 Renderer.setSurface

​ Renderer 的 setSurface 函数源码如下。cppPointer 是在 NativeObject 中定义的 Long 类型变量,对应 C++ 中的指针变量,指向的是 JNI 中 JNIRenderer 对象。

​ app.rive.runtime.kotlin.renderers.Renderer.kt

fun setSurface(surface: Surface) {cppSetSurface(surface, cppPointer)isAttached = truestart()
}

4.3 JNI cppSetSurface

​ 全局搜索 Renderer_cppSetSurface,找到 JNI 层 cppSetSurface 函数的实现如下。对于 Rive 渲染类型,将创建一个 ANativeWindow 指针(ANativeWindow_fromSurface 函数详见 aosp 中的 frameworks/base/native/android/native_window_jni.cpp),并将 ANativeWindow 指针传递到 JNIRenderer 中;对于 Canvas 渲染类型,直接将 Surface 传递到 JNIRenderer 中。

​ bindings/bindings_renderer.cpp

JNIEXPORT void JNICALL
Java_app_rive_runtime_kotlin_renderers_Renderer_cppSetSurface(JNIEnv* env, jobject,jobject surface, jlong rendererRef)
{JNIRenderer* renderer = reinterpret_cast<JNIRenderer*>(rendererRef);if (renderer->rendererType() != RendererType::Canvas){ANativeWindow* surfaceWindow = ANativeWindow_fromSurface(env, surface);reinterpret_cast<JNIRenderer*>(rendererRef) ->setSurface(surfaceWindow);if (surfaceWindow){ANativeWindow_release(surfaceWindow);}}else{renderer->setSurface(surface);}
}

4.4 JNIRenderer::setSurface

​ JNIRenderer::setSurface 的源码如下。m_worker 是 RefWorker 类的实例,RefWorker 继承 WorkThread,WorkThread 中创建了工作线程(见 3.5 节),并且有个 run 函数,m_worker->run 表示把任务提交到 WorkThread 中的工作线程中执行。

​ models/jni_renderer.cpp

void JNIRenderer::setSurface(SurfaceVariant surface)
{SurfaceVariant oldSurface = m_surface;acquireSurface(surface);m_worker->run([this, oldSurface](DrawableThreadState* threadState) mutable {m_workerThreadID = std::this_thread::get_id();...if (m_surface.index() > 0){m_workerImpl = WorkerImpl::Make(m_surface, threadState, m_worker->rendererType());}});
}

4.5 WorkerImpl::Make

​ models/worker_impl.cpp

std::unique_ptr<WorkerImpl> WorkerImpl::Make(SurfaceVariant surface,DrawableThreadState* threadState, const RendererType type)
{...bool success = false;std::unique_ptr<WorkerImpl> impl;switch (type){case RendererType::Rive:{ANativeWindow* window = std::get<ANativeWindow*>(surface);impl = std::make_unique<PLSWorkerImpl>(window, threadState, &success);break;}case RendererType::Canvas:{jobject ktSurface = std::get<jobject>(surface);impl = std::make_unique<CanvasWorkerImpl>(ktSurface, &success);}default:break;}...return impl;
}

4.6 创建 WorkerImpl

​ PLSWorkerImpl 继承 EGLWorkerImpl,EGLWorkerImpl 和 CanvasWorkerImpl 继承 WorkerImpl。

4.6.1 WorkerImpl 接口定义

​ models/worker_impl.hpp

class WorkerImpl
{
public:static std::unique_ptr<WorkerImpl> Make(SurfaceVariant, DrawableThreadState*, const RendererType);void start(jobject ktRenderer, std::chrono::high_resolution_clock::time_point);void stop();void doFrame(ITracer*, DrawableThreadState*, jobject ktRenderer, std::chrono::high_resolution_clock::time_point);virtual void prepareForDraw(DrawableThreadState*) const = 0;virtual void destroy(DrawableThreadState*) = 0;virtual void flush(DrawableThreadState*) const = 0;virtual rive::Renderer* renderer() const = 0;protected:jclass m_ktRendererClass = nullptr;jmethodID m_ktDrawCallback = nullptr;jmethodID m_ktAdvanceCallback = nullptr;std::chrono::high_resolution_clock::time_point m_lastFrameTime;bool m_isStarted = false;
};

4.6.2 创建 EGLWorkerImpl

​ EGLWorkerImpl 的构造函数如下。

​ models/worker_impl.hpp

class EGLWorkerImpl : public WorkerImpl
{
...
protected:EGLWorkerImpl(struct ANativeWindow* window, DrawableThreadState* threadState, bool* success){*success = false;auto eglThreadState = static_cast<EGLThreadState*>(threadState);m_eglSurface = eglThreadState->createEGLSurface(window);if (m_eglSurface == EGL_NO_SURFACE)return;*success = true;}
...
};

​ EGLThreadState::createEGLSurface 函数源码如下。eglCreateWindowSurface 用于创建一个与屏幕窗口相关的 EGL 表面,这个表面通常与设备的窗口系统交互,使得 OpenGL ES 渲染的内容能够显示在屏幕上(补充:eglCreatePbufferSurface 用于创建一个离屏渲染的 EGL 表面)。

​ helpers/thread_state_egl.cpp

EGLSurface EGLThreadState::createEGLSurface(ANativeWindow* window)
{if (!window){return EGL_NO_SURFACE;}...// 创建一个与屏幕窗口相关的 EGL 表面, 这个表面通常与设备的窗口系统交, 使得 OpenGL ES 渲染的内容能够显示在屏幕上auto res = eglCreateWindowSurface(m_display, m_config, window, nullptr);...return res;
}

4.6.3 创建 PLSWorkerImpl

​ PLSWorkerImpl 的构造函数如下,最终创建了 rive::RiveRenderer 对象,其源码不在 rive-android 代码库中,在 rive-runtime 代码库中,详见 → rive_renderer.cpp。

​ models/worker_impl.cpp

PLSWorkerImpl::PLSWorkerImpl(struct ANativeWindow* window, DrawableThreadState* threadState, bool* success) :EGLWorkerImpl(window, threadState, success)
{if (!success){return;}auto eglThreadState = static_cast<EGLThreadState*>(threadState);eglThreadState->makeCurrent(m_eglSurface);rive::gpu::RenderContext* renderContext = PLSWorkerImpl::PlsThreadState(eglThreadState)->renderContext();if (renderContext == nullptr){return; // PLS was not supported.}int width = ANativeWindow_getWidth(window); // 获取窗口宽度int height = ANativeWindow_getHeight(window); // 获取窗口高度GLint sampleCount; // 多重采样数glBindFramebuffer(GL_FRAMEBUFFER, 0); // 绑定默认帧缓冲区glGetIntegerv(GL_SAMPLES, &sampleCount); // 查询当前的多重采样数// 创建一个基于 GLES 的帧缓冲区渲染目标,用于离屏渲染m_renderTarget = rive::make_rcp<rive::gpu::FramebufferRenderTargetGL>(width, height, 0, sampleCount);// 初始化 Rive 的 PLS 渲染器m_plsRenderer = std::make_unique<rive::RiveRenderer>(renderContext);*success = true;
}

​ PLSThreadState::makeCurrent 函数的实现如下,eglMakeCurrent 是 EGL 环境创建的最后一步,作用是绑定 EGLSurface 和 EGLContext 到显示设备(EGLDisplay)。

​ helpers/thread_state_pls.cpp

void PLSThreadState::makeCurrent(EGLSurface eglSurface)
{if (eglSurface == m_currentSurface){return;}...// 绑定 EGLSurface 和 EGLContext 到显示设备 (EGLDisplay)eglMakeCurrent(m_display, eglSurface, eglSurface, m_context);m_currentSurface = eglSurface;...
}

4.6.4 创建 CanvasWorkerImpl

​ CanvasWorkerImpl 的构造函数如下。这里主要创建了 CanvasRenderer 对象,保存在 m_canvasRenderer 中,CanvasRenderer 继承 rive::Renderer,其源码不在 rive-android 代码库中,在 rive-runtime 代码库中,详见 → renderer.hpp、renderer.cpp;接着通过 NewGlobalRef(ktSurface) 将传入的局部引用 ktSurface 转换为全局引用,并存储在 m_ktSurface 中(全局引用会阻止 Java 垃圾回收器回收该对象,直到显式释放)。

​ models/worker_impl.hpp

class CanvasWorkerImpl : public WorkerImpl
{
public:CanvasWorkerImpl(jobject ktSurface, bool* success) :m_canvasRenderer{std::make_unique<CanvasRenderer>()}{m_ktSurface = GetJNIEnv()->NewGlobalRef(ktSurface);*success = true;}private:std::unique_ptr<CanvasRenderer> m_canvasRenderer;jobject m_ktSurface = nullptr;
};

5 渲染流程

​ 渲染流程图如下。

img

5.1 Renderer.doFrame

​ Renderer 实现了 Choreographer.FrameCallback 接口,重写了其 doFrame 方法,其 doFrame 和 scheduleFrame 方法源码如下。

​ app.rive.runtime.kotlin.renderers.Renderer.kt

@CallSuper
override fun doFrame(frameTimeNanos: Long) {if (isPlaying) {cppDoFrame(cppPointer)scheduleFrame()}
}open fun scheduleFrame() {Handler(Looper.getMainLooper()).post {Choreographer.getInstance().postFrameCallback(this@Renderer)}
}

5.2 JNI cppDoFrame

​ 全局搜索 "Renderer_cppDoFrame",找到 JNI 中的 cppDoFrame 函数的实现如下。

​ bindings/bindings_renderer.cpp

JNIEXPORT void JNICALL
Java_app_rive_runtime_kotlin_renderers_Renderer_cppDoFrame(JNIEnv*, jobject, jlong rendererRef)
{reinterpret_cast<JNIRenderer*>(rendererRef)->doFrame();
}

5.3 JNIRenderer::doFrame

​ JNIRenderer::doFrame 的源码如下。m_worker 是 RefWorker 类的实例,RefWorker 继承 WorkThread,WorkThread 中创建了工作线程(见 3.5 节),并且有个 run 函数,m_worker->run 表示把任务提交到 WorkThread 中的工作线程中执行。m_workerImpl 是 4.6 节创建的 WorkerImpl 对象(可能是 PLSWorkerImpl 或 CanavsWorkerImpl)。

​ models/jni_renderer.cpp

void JNIRenderer::doFrame()
{if (m_numScheduledFrames >= kMaxScheduledFrames){return;}m_worker->run([this](DrawableThreadState* threadState) {if (!m_workerImpl)return;auto now = std::chrono::high_resolution_clock::now();m_workerImpl->doFrame(m_tracer, threadState, m_ktRenderer, now);m_numScheduledFrames--;calculateFps(now);});m_numScheduledFrames++;
}

5.4 WorkerImpl::doFrame

5.4.1 doFrame

​ WorkerImpl::doFrame 的源码如下。tracer 是性能追踪器,用于测量各阶段耗时,它在 JNIRenderer 中创建(见 3.3 节),这里有 3 个追踪标签,分别是 "draw()"、"flush()"、"swapBuffers()"。m_ktAdvanceCallback 在 start 函数中定义,指向 kotlin 中 Renderer.advance 函数;m_ktDrawCallback 在 start 函数中定义,指向 kotlin 中 Renderer.draw 函数。

​ models/worker_impl.cpp

void WorkerImpl::doFrame(ITracer* tracer, DrawableThreadState* threadState, jobject ktRenderer,std::chrono::high_resolution_clock::time_point frameTime)
{if (!m_isStarted){return;}float fElapsedMs = std::chrono::duration<float>(frameTime - m_lastFrameTime).count();m_lastFrameTime = frameTime;auto env = GetJNIEnv();// m_ktAdvanceCallback 在 start 函数中定义, 指向 kotlin 中 Renderer.advance 函数JNIExceptionHandler::CallVoidMethod(env, ktRenderer, m_ktAdvanceCallback, fElapsedMs);tracer->beginSection("draw()");// 准备渲染状态, 调用 eglMakeCurrent 函数, 或 surface.lockCanvas 函数prepareForDraw(threadState);// m_ktDrawCallback 在 start 函数中定义, 指向 kotlin 中 Renderer.draw 函数JNIExceptionHandler::CallVoidMethod(env, ktRenderer, m_ktDrawCallback);tracer->beginSection("flush()");flush(threadState); // 提交渲染指令tracer->endSection(); // flushtracer->beginSection("swapBuffers()");threadState->swapBuffers(); // 交换缓冲区tracer->endSection(); // swapBufferstracer->endSection(); // draw()
}

​ 接下来,分别介绍 prepareForDraw、flush、swapBuffers。

5.4.2 prepareForDraw

1)EGLWorkerImpl::prepareForDraw

​ EGLWorkerImpl 的 prepareForDraw 函数源码如下。PLSThreadState 中实现了 makeCurrent 函数(见 2.6.3 节),eglThreadState->makeCurrent 中 调用了 eglMakeCurrent 函数,eglMakeCurrent 是 EGL 环境创建的最后一步,作用是绑定 EGLSurface 和 EGLContext 到显示设备(EGLDisplay)。

​ models/worker_impl.hpp

virtual void prepareForDraw(DrawableThreadState* threadState) const override
{auto eglThreadState = static_cast<EGLThreadState*>(threadState);// 里面调用了 eglMakeCurrent, 用于绑定 EGLSurface 和 EGLContext 到显示设备 (EGLDisplay)eglThreadState->makeCurrent(m_eglSurface);clear(threadState);
}

2)CanvasWorkerImpl::prepareForDraw

​ CanvasWorkerImpl 的 prepareForDraw 函数源码如下。其作用是通过 surface.lockCanvas 方法拿到 Canvas 对象。

​ models/worker_impl.cpp

void CanvasWorkerImpl::prepareForDraw(DrawableThreadState*) const
{m_canvasRenderer->bindCanvas(m_ktSurface);
}

​ bindCanvas 函数源码如下,m_ktCanvas 是通过 surface.lockCanvas 函数获取的 Canvas 对象。

​ models/canvas_renderer.hpp

void bindCanvas(jobject ktSurface)
{...JNIEnv* env = GetJNIEnv();// 通过 surface.lockCanvas 函数获取 Canvas 对象m_ktCanvas = env->NewGlobalRef(GetCanvas(ktSurface));// 通过 canvas.getWidth 函数获取 Canvas 的宽度m_width = JNIExceptionHandler::CallIntMethod(env, m_ktCanvas, GetCanvasWidthMethodId());// 通过 canvas.getHeight 函数获取 Canvas 的高度m_height = JNIExceptionHandler::CallIntMethod(env, m_ktCanvas, GetCanvasHeightMethodId());Clear(m_ktCanvas);
}static jobject GetCanvas(jobject ktSurface)
{return GetJNIEnv()->CallObjectMethod(ktSurface, GetSurfaceLockCanvasMethodId(), nullptr);
}

​ GetSurfaceLockCanvasMethodId 用于获取 surface.lockCanvas 的方法 id,具体实现如下。这里没有调用 surface.lockHardwareCanvas 方法,说明 Canvas 渲染方式是软渲染(即 CPU 渲染,而不是 GPU 渲染)。

​ models/jni_refs.cpp

jmethodID GetSurfaceLockCanvasMethodId()
{return GetMethodId(GetAndroidSurfaceClass(), "lockCanvas", "(Landroid/graphics/Rect;)Landroid/graphics/Canvas;");
}jmethodID GetMethodId(jclass clazz, const char* name, const char* sig)
{JNIEnv* env = GetJNIEnv();jmethodID output = env->GetMethodID(clazz, name, sig);env->DeleteLocalRef(clazz);return output;
}jclass GetAndroidSurfaceClass() { return GetClass("android/view/Surface"); }

5.4.3 flush

1)PLSWorkerImpl::flush

​ PLSWorkerImpl 的 flush 函数如下。renderContext->flush 函数的源码不在 rive-android 代码库中,在 rive-runtime 代码库中,详见 → render_context.cpp。

​ models/worker_impl.cpp

void PLSWorkerImpl::flush(DrawableThreadState* threadState) const
{PLSThreadState* plsThreadState = PLSWorkerImpl::PlsThreadState(threadState);rive::gpu::RenderContext* renderContext = plsThreadState->renderContext();renderContext->flush({.renderTarget = m_renderTarget.get()});
}

2)CanvasWorkerImpl::flush

​ CanvasWorkerImpl 的 flush 函数如下,最终会调用 surface.unlockCanvasAndPost 函数。

​ models/worker_impl.cpp

void CanvasWorkerImpl::flush(DrawableThreadState*) const
{m_canvasRenderer->unlockAndPost(m_ktSurface);
}

​ models/canvas_renderer.hpp

void unlockAndPost(jobject ktSurface)
{JNIEnv* env = GetJNIEnv();JNIExceptionHandler::CallVoidMethod(env, ktSurface, GetSurfaceUnlockCanvasAndPostMethodId(), m_ktCanvas);m_width = -1;m_height = -1;env->DeleteGlobalRef(m_ktCanvas);m_ktCanvas = nullptr;
}

​ GetSurfaceUnlockCanvasAndPostMethodId 函数调用了 surface.unlockCanvasAndPost 函数,具体实现如下。

​ models/jni_refs.cpp

jmethodID GetSurfaceUnlockCanvasAndPostMethodId()
{return GetMethodId(GetAndroidSurfaceClass(), "unlockCanvasAndPost", "(Landroid/graphics/Canvas;)V");
}jmethodID GetMethodId(jclass clazz, const char* name, const char* sig)
{JNIEnv* env = GetJNIEnv();jmethodID output = env->GetMethodID(clazz, name, sig);env->DeleteLocalRef(clazz);return output;
}jclass GetAndroidSurfaceClass() { return GetClass("android/view/Surface"); }

5.4.4 EGLThreadState::swapBuffers

​ DrawableThreadState 的子类中,只有 EGLThreadState 重写了 swapBuffers 函数,如下。eglSwapBuffers 函数用于交换缓冲区。

​ helpers/thread_state_egl.cpp

void EGLThreadState::swapBuffers()
{eglSwapBuffers(m_display, m_currentSurface); // 交换缓冲区EGL_ERR_CHECK();
}

5.5 RiveArtboardRenderer.draw

​ 5.4.1 节 WorkerImpl::doFrame 函数中,在 prepareForDraw 之后,调用了 m_ktDrawCallback,它指向的是 kotlin 中 Renderer.draw 函数,它是个抽象函数,RiveArtboardRenderer 中实现了该函数,如下。controller 是 RiveFileController 对象,它在 RiveAnimationView 的 init 代码块中创建,在 createRenderer 函数中传递给 RiveArtboardRenderer。activeArtboard 是 Artboard 对象,

​ app.rive.runtime.kotlin.renderers.RiveArtboardRenderer.kt

@WorkerThread
override fun draw() {synchronized(controller.file?.lock ?: this) {...controller.activeArtboard?.draw(cppPointer, fit, alignment, scaleFactor = scaleFactor)}
}

5.6 Artboard.draw

​ app.rive.runtime.kotlin.core.Artboard.kt

@WorkerThread
fun draw(rendererAddress: Long, fit: Fit, alignment: Alignment, scaleFactor: Float = 1.0f) {synchronized(lock) {cppDrawAligned(cppPointer, rendererAddress, fit, alignment, scaleFactor)}
}

5.7 JNI cppDrawAligned

​ 全局搜索 "Artboard_cppDrawAligned",找到 JNI 中的 cppDrawAligned 函数的实现如下。首先获取 rive::ArtboardInstance 对象,接着通过 getRendererOnWorkerThread 函数获取 rive::RiveRenderer 或 CanavsRenderer 对象,最后调用 artboard->draw(renderer) 函数渲染一帧画面。rive::ArtboardInstance 的源码不在 rive-android 代码库中,在 rive-runtime 代码库中,详见 → artboard.hpp、artboard.cpp。

​ bindings/bindings_artboard.cpp

JNIEXPORT void JNICALL
Java_app_rive_runtime_kotlin_core_Artboard_cppDrawAligned(JNIEnv* env, jobject, jlong artboardRef,jlong rendererRef, jobject ktFit, jobject ktAlignment, jfloat scaleFactor)
{auto artboard = reinterpret_cast<rive::ArtboardInstance*>(artboardRef); // 获取 ArtboardInstance 对象(未开放源码)auto jniWrapper = reinterpret_cast<JNIRenderer*>(rendererRef); // 获取 JNIRenderer 对象// 获取 rive::RiveRenderer 对象或 CanvasRenderer 对象rive::Renderer* renderer = jniWrapper->getRendererOnWorkerThread();rive::Fit fit = GetFit(env, ktFit);rive::Alignment alignment = GetAlignment(env, ktAlignment);renderer->save(); // 如果 renderer 是 CanvasRenderer, 将调用 canvas.save 函数renderer->align(fit, alignment,rive::AABB(0, 0, jniWrapper->width(), jniWrapper->height()),artboard->bounds(),scaleFactor);artboard->draw(renderer);renderer->restore(); // 如果 renderer 是 CanvasRenderer, 将调用 canvas.restore 函数
}

6 启动渲染流程

​ 启动渲染流程图如下。

img

6.1 启动渲染的源头

​ 在 Rive 的 kotlin 源码中,通过调用 Renderer.start 函数启动渲染,但是,用户无法直接调用该函数,调用该函数的地方非常多,分成以下几类。

1)直接调用

​ 以下函数中会直接调用 Renderer.start 函数启动渲染。

  • RiveAnimationView.onAttachedToWindow
  • RiveTextureView.onVisibilityChanged
  • Renderer.setSurface

2)RiveFileController.onStart

​ RiveFileController.onStart 也指向了 Renderer.start 函数,在 RiveFileController 的 fit、alignment、layoutScaleFactor、layoutScaleFactorAutomatic 等属性变化、以及 autoplay 属性变为 true 时,会调用 onStart.invoke 启动渲染。

3)间接调用

​ 在 RiveAnimationView 的以下函数中,经过多步调用,最终会调用到 Renderer.start 函数启动渲染。

  • play
  • reset
  • fireState
  • setBooleanState
  • setNumberState
  • setTextRunValue
  • onTouchEvent

6.2 Renderer.start

​ Renderer 的 start 函数如下,通过 Choreographer.getInstance().postFrameCallback 函数让 doFrame 函数每帧调用一次,通过 cppStart 函数启动 Rive 引擎(rive::AudioEngine::RuntimeEngine)。

​ app.rive.runtime.kotlin.renderers.Renderer.kt

abstract class Renderer(@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)var type: RendererType = Rive.defaultRendererType,val trace: Boolean = false
) : NativeObject(NULL_POINTER), Choreographer.FrameCallbackfun start() {if (isPlaying) returnif (!isAttached) returnif (!hasCppObject) returnisPlaying = truecppStart(cppPointer)scheduleFrame()}open fun scheduleFrame() {Handler(Looper.getMainLooper()).post {Choreographer.getInstance().postFrameCallback(this@Renderer)}}@CallSuperoverride fun doFrame(frameTimeNanos: Long) {if (isPlaying) {cppDoFrame(cppPointer)scheduleFrame()}}
}

6.3 JNI cppStart

​ 全局搜索 "Renderer_cppStart",找到 JNI 中的 cppStart 函数的实现如下。

​ bindings/bindings_renderers.cpp

JNIEXPORT void JNICALL
Java_app_rive_runtime_kotlin_renderers_Renderer_cppStart(JNIEnv*, jobject, jlong rendererRef)
{reinterpret_cast<JNIRenderer*>(rendererRef)->start();
}

6.4 JNIRenderer::start

​ JNIRenderer::start 的源码如下。m_worker 是 RefWorker 类的实例,RefWorker 继承 WorkThread,WorkThread 中创建了工作线程(见 3.5 节),并且有个 run 函数,m_worker->run 表示把任务提交到 WorkThread 中的工作线程中执行。m_workerImpl 是 4.6 节创建的 WorkerImpl 对象(可能是 PLSWorkerImpl 或 CanavsWorkerImpl)。

​ models/jni_renderer.cpp

void JNIRenderer::start()
{m_worker->run([this](DrawableThreadState* threadState) {if (!m_workerImpl)return;auto now = std::chrono::steady_clock::now();m_fpsLastFrameTime = now;m_workerImpl->start(m_ktRenderer, now);});
}

6.5 WorkerImpl::start

​ WorkerImpl::start 函数源码如下。这里主要初始化 m_ktRendererClass 、m_ktDrawCallback、m_ktAdvanceCallback,并启动 Rive 引擎。

​ models/worker_impl.cpp

void WorkerImpl::start(jobject ktRenderer, std::chrono::high_resolution_clock::time_point frameTime)
{auto env = GetJNIEnv();jclass ktClass = env->GetObjectClass(ktRenderer);m_ktRendererClass = reinterpret_cast<jclass>(env->NewWeakGlobalRef(ktClass)); // 获取 kotlin 中 Renderer 对象m_ktDrawCallback = env->GetMethodID(m_ktRendererClass, "draw", "()V"); // 指向 Renderer.draw 方法m_ktAdvanceCallback = env->GetMethodID(m_ktRendererClass, "advance", "(F)V");  // 指向 Renderer.advance 方法m_lastFrameTime = frameTime;m_isStarted = true;if (auto engine = rive::AudioEngine::RuntimeEngine(false)){engine->start(); // 启动 Rive 引擎}
}

7 暂停渲染流程

​ 暂停渲染流程图如下。

img

7.1 暂停渲染的源头

​ 在 Rive 的 kotlin 源码中,通过调用 Renderer.stop 函数暂停渲染,但是,用户无法直接调用该函数,调用该函数的地方非常多,主要有以下几处。

  • RiveAnimationView.pause
  • RiveAnimationView.onDetachedFromWindow
  • RiveTextureView.onVisibilityChanged

7.2 Renderer.stop

​ Renderer 的 stop 函数如下,通过 cppStop 函数暂停 Rive 引擎(rive::AudioEngine::RuntimeEngine),通过 Choreographer.getInstance().removeFrameCallback 函数移除 doFrame 回调。

​ app.rive.runtime.kotlin.renderers.Renderer.kt

abstract class Renderer(@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)var type: RendererType = Rive.defaultRendererType,val trace: Boolean = false
) : NativeObject(NULL_POINTER), Choreographer.FrameCallback@CallSuperfun stop() {stopThread()Handler(Looper.getMainLooper()).post { // postFrameCallback must be called from the main looperChoreographer.getInstance().removeFrameCallback(this@Renderer)}}@CallSuperinternal fun stopThread() {if (!isPlaying) returnif (!hasCppObject) returnisPlaying = falsecppStop(cppPointer)}open fun scheduleFrame() {Handler(Looper.getMainLooper()).post {Choreographer.getInstance().postFrameCallback(this@Renderer)}}@CallSuperoverride fun doFrame(frameTimeNanos: Long) {if (isPlaying) {cppDoFrame(cppPointer)scheduleFrame()}}
}

7.3 JNI cppStop

​ 全局搜索 "Renderer_cppStop",找到 JNI 中的 cppStop 函数的实现如下。

​ bindings/bindings_renderers.cpp

JNIEXPORT void JNICALL
Java_app_rive_runtime_kotlin_renderers_Renderer_cppStop(JNIEnv*, jobject, jlong rendererRef)
{reinterpret_cast<JNIRenderer*>(rendererRef)->stop();
}

7.4 JNIRenderer::stop

​ JNIRenderer::stop 的源码如下。m_worker 是 RefWorker 类的实例,RefWorker 继承 WorkThread,WorkThread 中创建了工作线程(见 3.5 节),并且有个 run 函数,m_worker->run 表示把任务提交到 WorkThread 中的工作线程中执行。m_workerImpl 是 4.6 节创建的 WorkerImpl 对象(可能是 PLSWorkerImpl 或 CanavsWorkerImpl)。

​ models/jni_renderer.cpp

void JNIRenderer::stop()
{m_worker->run([this](DrawableThreadState* threadState) {if (!m_workerImpl)return;m_workerImpl->stop();});
}

7.5 WorkerImpl::stop

​ WorkerImpl::stop 函数源码如下。这里主要暂停 Rive 引擎,并将 m_ktRendererClass 、m_ktDrawCallback、m_ktAdvanceCallback 设置为空。

​ models/worker_impl.cpp

void WorkerImpl::stop()
{if (auto engine = rive::AudioEngine::RuntimeEngine(false)){engine->stop(); // 暂停 Rive 引擎}auto env = GetJNIEnv();if (m_ktRendererClass != nullptr){env->DeleteWeakGlobalRef(m_ktRendererClass);}m_ktRendererClass = nullptr;m_ktDrawCallback = nullptr;m_ktAdvanceCallback = nullptr;m_isStarted = false;
}

声明:本文转自【Rive】rive-android源码分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/927116.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

惠州专业网站建设价格wordpress网站维护教程

技术复盘--git 资料地址原理图安装配置基本命令分支命令对接gitee练习:远程仓库操作 资料地址 学习地址-B站黑马&#xff1a;https://www.bilibili.com/video/BV1MU4y1Y7h5 git官方&#xff1a;https://git-scm.com/ gitee官网&#xff1a;https://gitee.com/ 原理图 说明&am…

zkSync Era主网上线:首个zkEVM全面开放的技术突破

zkSync Era主网正式对外开放,这是全球首个完全开放的zkEVM解决方案。文章详细介绍了其独特的技术架构,包括原生账户抽象、LLVM编译器、数据压缩和超扩展性设计,以及经过多重安全审计的系统安全保障机制。gm zkEVM!…

企业网站开发知名品牌有哪些建设银行网站点击次数

公司简介 陕西集群物联网服务管理股份有限公司旗下的“集群e家”是专注于社区商圈O2O服务的平台&#xff0c;为社区&#xff08;乡村&#xff09;家庭提供创新的家庭消费服务及消费体验。集群e家智慧生活是以社区&#xff08;乡村&#xff09;为中心&#xff0c;以“互联网”的…

免费建商城网站快速网站seo效果

【Java】全套云HIS&#xff08;医院信息管理系统&#xff09;可对接医保 采用云端SaaS模式部署 SaaS 模式的云 HIS 更适用于基层医疗机构&#xff0c;而传统的 HIS 已经在大中型医疗机构大规模应用。过去&#xff0c;国内的大中型医疗机构投入了大量的资金来进行信息化系统建设…

建站推广网站收费做网站

#基础概念# #入门 数据库的主要分类 关系型数据库&#xff08;RDBMS&#xff09; 数据以表格形式存储&#xff0c;通过预定义的关系模型建立数据间的连接&#xff0c;使用SQL作为查询语言。常见的例子包括MySQL、Oracle、SQL Server、PostgreSQL、IBM DB2等。 非关系型数据库…

鄂尔多斯网站建设公司小程序模板源码免费下载

1. opencv概述 OpenCV是一个开源的计算机视觉库&#xff0c;它提供了一系列丰富的图像处理和计算机视觉算法&#xff0c;包括图像读取、显示、滤波、特征检测、目标跟踪等功能。 opencv官网&#xff1a;https://opencv.org/ opencv官网文档&#xff1a;https://docs.opencv.or…

完整教程:基于Spring Boot的爱琴海购物公园网上商城系统的设计与实现

完整教程:基于Spring Boot的爱琴海购物公园网上商城系统的设计与实现2025-10-04 11:55 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto…

Microsoft Access SQL 查询中的通配符 - 详解

Microsoft Access SQL 查询中的通配符 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &qu…

洛谷P11738 [集训队互测 2015] 未来程序改

这道题很显然是一道编译原理的题…… 本文简单的介绍了下Lexer, Parser和Interpreter的写法(实际上写编译器需要的是CodeGen) 可以看看,但是对OI似乎没什么用懒得写这么大的模拟了,想学的可以去看看我的项目QAQ很显…

mcp 面试题

什么是 MCP(Model Context Protocol) MCP 是 OpenAI 推出的 大模型上下文交互协议。它的作用是标准化 LLM 与外部工具、数据源、事件系统的交互方式。 在没有 MCP 之前,开发者需要为每个插件单独设计接口,成本高且…

做qq空间动态皮肤网站网络营销常用的方法

个人博客&#xff1a;代码菌-CSDN博客 专栏&#xff1a;C杂货铺_代码菌的博客-CSDN博客 目录 &#x1f308;前言&#x1f308; &#x1f4c1; 初始化列表&#xff08;灰常重要&#xff09; &#x1f4c2; 引入 &#x1f4c2; 概念 &#x1f4c2; 特性 &#x1f4c1; 拓展构…

6_什么是知识图谱

知识图谱(Knowledge Graph)是一种用于表示和存储知识的结构化数据模型。它以图的形式组织信息,其中实体(entities)作为节点,关系(relationships)作为边,形成一个相互连接的知识网络。 知识图谱的核心特点:实…

实用指南:[创业之路-645]:手机属于通信?还是属于消费类电子?还是移动互联网?

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

【开题答辩过程】以《基于SpringBoot+Vue+uni-app的智慧校园服务系统的设计与搭建》为例,不会开题答辩的可能进来看看

【开题答辩过程】以《基于SpringBoot+Vue+uni-app的智慧校园服务系统的设计与搭建》为例,不会开题答辩的可能进来看看pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !importan…

c 网站开发案例专业网络推广方案

C语言acm竞赛习题集锦.doc杭州电子科技大学 acm 习题精选 第 1 页 共 21 页 目录 1、 数塔问题 2 2、 并查集类问题 4 3、 递推类问题 9 4、 动态规划系列 10 5、 概率类题型 13 6、 组合数学类题型 15 7、 贪心策略 16 8、 几何问题 .19 杭州电子科技大学 acm 习题精选 第 2 页…

丽水做网站公司本地电脑做服务器建网站

0x00 简介本期主要会教大家如何从流量中还原出来文件。下面我将会用多种办法来讲解。使用系统&#xff1a;Kali Linux0x01 tcpxtract工具网络流量提取文件(方法1)Kali Linux默认没有安装该工具&#xff0c;需要自己安装安装命令&#xff1a;sudo apt install tcpxtract使用方法…

微信ipad协议个微机器人开发API

微信ipad协议个微机器人开发API,微信群机器人API 微信iPad协议,采用最新的ASE加密,以及最新的算法,iPad协议是一套微信个人号接口,基于web开发,它能实现微信中的百分之八十的功能,并辅助微信执行各种操作,提供…

做乒乓球网站的图片网络建设与维护是什么工作

2.1 数组 &#xff08;1) 概述 定义 在计算机科学中&#xff0c;数组是由一组元素&#xff08;值或变量&#xff09;组成的数据结构&#xff0c;每个元素有至少一个索引或键来标识 因为数组内的元素是连续存储的&#xff0c;所以数组中元素的地址&#xff0c;可以通过其索引…

手机可以搭建网站么页面设计模板怎么写

谈一谈BEV和Transformer在自动驾驶中的应用 BEV和Transformer都这么火&#xff0c;这次就聊一聊。 结尾有资料连接 一 BEV有什么用 首先&#xff0c;鸟瞰图并不能带来新的功能&#xff0c;对规控也没有什么额外的好处。 从鸟瞰图这个名词就可以看出来&#xff0c;本来摄像头…

做 网站 技术支持 抓获seo优化工具推荐

1、模块化编程 c语言模块化编程实现思路设计代码 具体的程序实现代码如下所示 1&#xff1a;程序的头文件 2&#xff1a;程序的函数文件 3&#xff1a;程序的主文件控制函数的实现 持续更新中......