如何用 Rust 做 Android UI 渲染

背景

Rust 優秀的安全性、媲美 C++ 的性能以及對跨平臺編譯和外部語言(ffi)的支持使得其成爲高性能跨平臺庫的上佳實現語言。然而,Rust 是否可以在邏輯層之上進一步服務於一些通用性的 UI 渲染?我們大力智能客戶端團隊針對開源項目 rust-windowing( https://github.com/rust-windowing )中幾個核心工程進行剖析,並結合在 Android 系統的接入對此進行探索。

Rust UI 渲染:

Android 系統上使用 Rust 渲染核心圍繞 ANativeWindow 類展開,ANativeWindow 位於 android ndk 中,是 egl 跨平臺 EGLNativeWindowType 窗口類型在 Android 架構下的特定實現,因而基於 ANativeWindow 就可以創建一個 EglSurface 並通過 GLES 進行繪製和渲染。另一方面,ANativeWindow 可以簡單地與 Java 層的 Surface 相對應,因而將 Android 層需要繪製的目標轉換爲 ANativeWindow 是使用 Rust 渲染的關鍵,這一部分可以通過 JNI 完成。首先,我們先看一下 rust-windowing 對 UI 渲染的支持。

1 軟件繪製:

在 rust-windowing 項目中,android-ndk-rs 提供了 rust 與 android ndk 之間的膠水層,其中與 UI 渲染最相關的就是 NativeWindow 類,NativeWindow 在 Rust 上下文實現了對 ANativeWindow 的封裝,支持通過**「ffi」**對 ANativeWindow 進行操作,達到與在 java 層使用 lockCanvas() 和 unlockCanvasAndPost() 進行繪製相同的效果,基於這些 api,我們可以實現在 (A)NativeWindow 上的指定區域繪製一個長方形:

unsafe fn draw_rect_on_window(nativewindow: &NativeWindow, colors: Vec<u8>, rect: ndk_glue::Rect) {
    let height = nativewindow.height();
    let width = nativewindow.width();
    let color_format = get_color_format();
    let format = color_format.0;
    let bbp = color_format.1;
    nativewindow.set_buffers_geometry(width, height, format);
    nativewindow.acquire();
    let mut buffer = NativeWindow::generate_epmty_buffer(width, height, width, format);
    let locked = nativewindow.lock(&mut buffer, &mut NativeWindow::generate_empty_rect(0, 0, width, height));
    if locked < 0 {
        nativewindow.release();
        return;
    }

    draw_rect_into_buffer(buffer.bits, colors, rect, width, height);
    let result = nativewindow.unlock_and_post();
    nativewindow.release();
}

unsafe fn draw_rect_into_buffer(bits: *mut ::std::os::raw::c_void, colors: Vec<u8>, rect: ndk_glue::Rect, window_width: i32, window_height: i32) {
    let bbp = colors.len() as u32;
    let window_width = window_width as u32;
    for i in rect.top+1..=rect.bottom {
        for j in rect.left+1..=rect.right {
            let cur = (j + (i-1) * window_width - 1) * bbp;
            for k in 0..bbp {
                *(bits.offset((cur + (k as u32)) as isize) as *mut u8) = colors[k as usize];
            }
        }
    }
}

這樣就通過提交一個純色像素填充的 Buffer 在指定的位置成功渲染出了一個長方形,不過這種方式本質上是軟件繪製,性能欠佳,更好的方式是通過在 Rust 層封裝 GL 在 ANativeWindow 上使能硬件繪製。

2 硬件繪製:

2.1 跨平臺窗口系統:winit

2.1.1 Window:窗口

窗口系統最主要的目的是提供平臺無關的 Window 抽象,提供一系列通用的基礎方法、屬性方法、遊標相關方法、監控方法。winit 以 Window 類抽象窗口類型並持有平臺相關的 window 實現,通過 WindowId 唯一識別一個 Window 用於匹配後續產生的所有窗口事件 WindowEvent,最後通過建造者模式對外暴露實例化的能力,支持在 Rust 側設置一些平臺無關的參數(大小、位置、標題、是否可見等)以及平臺相關的特定參數,基本結構如下:

// src/window.rs
pub struct Window {
    pub(crate) window: platform_impl::Window,
}

impl Window {
    #[inline]
    pub fn request_redraw(&self) {
        self.window.request_redraw()
    }

    pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
        self.window.inner_position()
    }

    pub fn current_monitor(&self) -> Option<MonitorHandle> {
        self.window.current_monitor()
    }
}

pub struct WindowId(pub(crate) platform_impl::WindowId);

pub struct WindowBuilder {
    /// The attributes to use to create the window.
    pub window: WindowAttributes,

    // Platform-specific configuration.
    pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes,
}

impl WindowBuilder {
    #[inline]
    pub fn build<T: 'static>(
        self,
        window_target: &EventLoopWindowTarget<T>,
    ) -> Result<Window, OsError> {
        platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map(
            |window| {
                window.request_redraw();
                Window { window }
            },
        )
    }
}

在 Android 平臺,winit 暫時不支持使用給定的屬性構建一個 “Window”,大部分方法給出了空實現或者直接 panic,僅保留了一些事件循環相關的能力,真正的窗口實現仍然從 android-ndk-rs 膠水層獲得:當前的 android-ndk-rs 僅針對 ANativeActivity 進行了適配,通過屬性宏代理了unsafe extern "C" fn ANativeActivity_onCreate(...)方法,在獲得 ANativeActivity 指針 * activity 後,注入自定義的生命週期回調,在 onNativeWindowCreated 回調中獲得 ANativeWindow(封裝爲 NativeWindow)作爲當前上下文活躍的窗口。當然,android-ndk-rs 的能力也支持我們在任意一個 ANativeWindow 上生成對應的上層窗口。

2.1.2 EventLoop:事件循環 - 上層

事件循環是整個窗口系統行爲的驅動,統一響應拋出的系統任務和用戶交互並將反饋渲染到窗口上形成閉環,當你需要合理地觸發渲染時,最好的方式就是將指令發送給事件循環。winit 中,將事件循環封裝爲 EventLoop,使用 ControlFlow 控制 EventLoop 如何獲取、消費循環中的事件,並對外提供一個 EventLoopProxy 代理用於作爲與用戶交互的媒介,支持用戶通過 proxy 向 EventLoop 發送用戶自定義的事件:

Android 平臺的事件循環建立在 ALooper 之上,通過 android-ndk-rs 提供的膠水層注入的回調處理生命週期行爲和窗口行爲,通過代理 InputQueue 處理用戶手勢,同時支持響應用戶自定義事件和內部事件。一次典型的循環根據當前 first_event 的類型分發處理,一次處理一個主要事件;當 first_event 處理完成後,觸發一次 MainEventsCleared 事件回調給業務方,並判斷是否需要觸發 Resized 和 RedrawRequested,最後觸發 RedrawEventsCleared 事件標識所有事件處理完畢。

單次循環處理完所有事件後進入控制流,決定下一次處理事件的行爲,控制流支持 Android epoll 多路複用,在必要時喚醒循環處理後續事件,此外,控制流提供了強制執行、強制退出的能力。事實上,android-ndk-rs 就是通過添加 fd 的方式將窗口行爲拋到 EventLoop 中包裝成 Callback 事件處理:

// <--1--> 掛載fd
// ndk-glue/src/lib.rs
lazy_static! {
    static ref PIPE: [RawFd; 2] = {
        let mut pipe: [RawFd; 2] = Default::default();
        unsafe { libc::pipe(pipe.as_mut_ptr()) };
        pipe
    };
}

{
    ...
    thread::spawn(move || {
        let looper = ThreadLooper::prepare();
        let foreign = looper.into_foreign();
        foreign
            .add_fd(
                PIPE[0],
                NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,
                FdEvent::INPUT,
                std::ptr::null_mut(),
            )
            .unwrap();
    });
    ...
}

// <--2--> 向fd寫入數據
// ndk-glue/src/lib.rs
unsafe fn wake(_activity: *mut ANativeActivity, event: Event) {
    log::trace!("{:?}", event);
    let size = std::mem::size_of::<Event>();
    let res = libc::write(PIPE[1]&event as *const _ as *const _, size);
    assert_eq!(res, size as _);
}

// <--3--> 喚醒事件循環讀出事件
// src/platform_impl/android/mod.rs
fn poll(poll: Poll) -> Option<EventSource> {
    match poll {
        Poll::Event { ident, .. } => match ident {
            ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback),
            ...
        },
        ...
    }
}

// ndk-glue/src/lib.rs
pub fn poll_events() -> Option<Event> {
    unsafe {
        let size = std::mem::size_of::<Event>();
        let mut event = Event::Start;
        if libc::read(PIPE[0]&mut event as *mut _ as *mut _, size) == size as _ {
            Some(event)
        } else {
            None
        }
    }
}

2.2 跨平臺 egl 上下文:glutin

我們有了跨平臺的 OpenGL(ES) 用於描述圖形對象,也有了跨平臺的窗口系統 winit 封裝窗口行爲,但是如何理解圖形語言並將其渲染到各個平臺的窗口上?這就是 egl 發揮的作用,它實現了 OpenGL(ES) 和底層窗口系統之間的接口層。在 rust-windowing 項目中,glutin 工程承接了這個職責,以上下文的形式把窗口系統 winit 和 gl 關聯了起來。

Context 是 gl 的上下文環境,全局可以有多個 gl 上下文,但是一個線程同時只能有一個活躍的上下文,使用 ContextCurrentState 區分這一狀態。glutin 中 Context 可關聯零個或多個 Window,當 Context 與 Window 相關聯時,使用 ContextWrapper 類,ContextWrapper 使得可以方便地在上下文中同時操作 gl 繪製以及 Window 渲染。在其上衍生出兩個類型:(1)RawContext:Context 與 Window 雖然關聯但是分開存儲;(2)WindowedContext:同時存放相互關聯的一組 Context 和 Window。常見的場景下 WindowedContext 更加適用,通過 ContextBuilder 指定所需的 gl 屬性和像素格式就可以構造一個 WindowedContext,內部會初始化 egl 上下文,並基於持有的 EglSurfaceType 類型的 window 創建一個 eglsurface 作爲後續 gl 指令繪製(draw)、回讀(read)的作用目標(指定使用該 surface 上的緩衝)。

2.3 硬件繪製的例子:

基於 winit 和 glutin 提供的能力,使用 Rust 進行渲染的準備工作只需基於特定業務需求去創建一個 glutin 的 Context,通過 Context 中創建的 egl 上下文可以調用 gl api 進行繪製,而 window 讓我們可以掌控渲染流程,在需要的時候(比如基於 EventLoop 重繪指令或者一個簡單的無限循環)下發繪製指令。簡單地實現文章開頭的三角形 demo 動畫效果如下:

fn render(&mut self, gl: &Gl) {
    let time_elapsed = self.startTime.elapsed().as_millis();
    let percent = (time_elapsed % 5000) as f32 / 5000f32;
    let angle = percent * 2f32 * std::f32::consts::PI;

    unsafe {
        let vs = gl.CreateShader(gl::VERTEX_SHADER);
        gl.ShaderSource(vs, 1, [VS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
        gl.CompileShader(vs);
        let fs = gl.CreateShader(gl::FRAGMENT_SHADER);
        gl.ShaderSource(fs, 1, [FS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
        gl.CompileShader(fs);
        let program = gl.CreateProgram();
        gl.AttachShader(program, vs);
        gl.AttachShader(program, fs);
        gl.LinkProgram(program);
        gl.UseProgram(program);
        gl.DeleteShader(vs);
        gl.DeleteShader(fs);
        let mut vb = std::mem::zeroed();
        gl.GenBuffers(1, &mut vb);
        gl.BindBuffer(gl::ARRAY_BUFFER, vb);
        let vertex = [
            SIDE_LEN * (BASE_V_LEFT+angle).cos(), SIDE_LEN * (BASE_V_LEFT+angle).sin(),  0.0,  0.4,  0.0,
            SIDE_LEN * (BASE_V_TOP+angle).cos(),  SIDE_LEN * (BASE_V_TOP+angle).sin(),  0.0,  0.4,  0.0,
            SIDE_LEN * (BASE_V_RIGHT+angle).cos(), SIDE_LEN * (BASE_V_RIGHT+angle).sin(),  0.0,  0.4,  0.0,
        ];

        gl.BufferData(
            gl::ARRAY_BUFFER,
            (vertex.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
            vertex.as_ptr() as *const _,
            gl::STATIC_DRAW,
        );

        if gl.BindVertexArray.is_loaded() {
            let mut vao = std::mem::zeroed();
            gl.GenVertexArrays(1, &mut vao);
            gl.BindVertexArray(vao);
        }

        let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
        let color_attrib = gl.GetAttribLocation(program, b"color\0".as_ptr() as *const _);
        gl.VertexAttribPointer(
            pos_attrib as gl::types::GLuint,
            2,
            gl::FLOAT,
            0,
            5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
            std::ptr::null(),
        );

        gl.VertexAttribPointer(
            color_attrib as gl::types::GLuint,
            3,
            gl::FLOAT,
            0,
            5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
            (2 * std::mem::size_of::<f32>()) as *const () as *const _,
        );

        gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);
        gl.EnableVertexAttribArray(color_attrib as gl::types::GLuint);
        gl.ClearColor(1.3 * (percent-0.5).abs(), 0., 1.3 * (0.5 - percent).abs(), 1.0); 
        gl.Clear(gl::COLOR_BUFFER_BIT);
        gl.DrawArrays(gl::TRIANGLES, 0, 3);
    }
}

3 Android - Rust JNI 開發

以上 Rust UI 渲染部分完全運行在 Rust 上下文中(包括對 c++ 的封裝),而實際渲染場景下很難完全脫離 Android 層進行 UI 的渲染或不與 Activity 等容器進行交互。所幸 Rust UI 渲染主要基於 (A)NativeWindow,而 Android Surface 在 c++ 的對應類實現了 ANativeWindow,ndk 也提供了 ANativeWindow_fromSurface 方法從一個 surface 獲得 ANativeWindow 對象,因而我們可以通過 JNI 的方式使用 Rust 在 Android 層的 Surface 上進行 UI 渲染:

// Android

surface_view.holder.addCallback(object : SurfaceHolder.Callback2 {

    override fun surfaceCreated(p0: SurfaceHolder) {
        RustUtils.drawColorTriangle(surface, Color.RED)
    }

    override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}

    override fun surfaceDestroyed(p0: SurfaceHolder) {}

    override fun surfaceRedrawNeeded(p0: SurfaceHolder) {}

})

// Rust
pub unsafe extern fn Java_com_example_rust_1demo_RustUtils_drawColorTriangle__Landroid_view_Surface_2I(env: *mut JNIEnv, _: JClass, surface: jobject, color: jint) -> jboolean {
    println!("call Java_com_example_rust_1demo_RustUtils_drawColor__Landroid_view_Surface_2I"); 
    ndk_glue::set_native_window(NativeWindow::from_surface(env, surface));
    runner::start();
    0
}

需要注意,由於 EventLoop 是基於 ALooper 的封裝,調用 Rust 實現渲染時需要確保調用在有 Looper 的線程(比如 HandlerThread 中),或者在 Rust 渲染前初始化時爲當前線程準備 ALooper。

總結

使用 Rust 在 Android 上進行 UI 渲染的可行性已經得證,但是它的性能表現究竟如何?未來又將在哪些業務上落地?這些仍待進一步探索。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NIHGXXWEB1QoLXyRZUO7Nw