eframe 持久化实现

发布时间 2023-11-05 17:32:55作者: 那阵东风

在 Native 平台上, eframe 将应用程序的指定信息存储在文件系统中, 并由开发者决定启动时是否需要加载这些此前保存的应用程序信息. 在 Web 平台, 这些信息则存储在浏览器的 local-storage 中. 这篇文章将以 Native 平台为例, 探究以下几个问题:

  1. 持久化是怎样实现的?
  2. 持久化的时机是什么?
  3. 持久化的意义是什么?

持久化的实现

eframe 作为 egui 的一个渲染后台, 它同时具备了在桌面端进行原生渲染和在 Web 端使用 WebAssembly 调用 Canvas 渲染的能力. 为了对这两者进行兼容, eframe 封装了对底层存储设施的调用接口, 从而我们只需要实现 eframe::App trait 定义的 save() 方法即可. 这个过程可以用下图来表示:

eframe 持久化过程

具体到代码中, 我们最主要的处理集中在应用的启动组件, 比如下面代码示例中的 MyApp:

struct MyApp {}

impl MyApp {
	pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
		Self {}
	}
}

impl eframe::App for MyApp {
	fn update(&mut self, _: &egui::Context, _: &mut eframe::Frame) {
		// 绘制界面的代码.
	}
}

首先需要实现 eframe:App trait 的 save() 方法, 即:

// snip

fn save(&mut self, storage: &mut dyn eframe::Storage) {
	eframe::set_value(storage, eframe::APP_KEY, self);
}

// snip

这个方法就是上面流程图中的起点. 注意第三个参数 self, 它对应方法签名中的泛型 T. 而这个泛型要求必须实现 serde::Serialize trait. 说明, 我们的应用程序核心组件 MyApp 本身, 以及自己的所有属性都必须可以被序列化.

在了解具体代码之前, 我们还需要直到什么时候恢复这些存储 (持久化) 的应用状态. 没错, 就是 new() 方法! 当我们的程序首次启动时, 需要在 new 或者其它关联函数 (可以类比为构造函数) 中, 通过 eframe::get_value() 函数获得对应的持久化数据, 并转化为对应的实例. 因此, 我们的 MyApp 还必须能够被反序列化, 即实现 serde::Deserialize trait.

这样一来, 我们的代码就变成了下面的样子:

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct MyApp {}

impl MyApp {
    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
        if let Some(storage) = cc.storage {
            let stored_state: Self =
                eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
            return stored_state;
        }
        Self {}
    }
}
impl Default for MyApp {
    fn default() -> Self {
        Self {}
    }
}

impl eframe::App for MyApp {
    fn save(&mut self, storage: &mut dyn eframe::Storage) {
        eframe::set_value(storage, eframe::APP_KEY, self);
    }
    fn update(&mut self, _: &egui::Context, _: &mut eframe::Frame) {}
}

仔细观察 eframe::get_value()eframe::set_value() 的方法签名:

pub fn get_value<T>(storage: &dyn eframe::Storage, key: &str) -> Option<T>  
where  
T: serde::de::DeserializeOwned;

pub fn set_value<T>(storage: &mut dyn eframe::Storage, key: &str, value: &T)  
where  
T: serde::Serialize;

可以发现两者共同参数有是 storagekey. 不难理解, key 用于区分不同应用程序, 保证我们的程序数据不会与其他程序的混淆. 而 storage 则是一个 dyn eframe::Storage 的引用, 根据是否需要修改区分了可变引用 (set_value) 和不可变引用 (get_value). 但是为什么要用 dyn eframe::Storage?

原因就在刚才的持久化流程图和下面的逆持久化流程图中:

eframe 逆持久化过程

当我们使用 eframe 抽象出的函数接口 eframe::get_value()eframe::set_value() 时, 当然不希望再去处理复杂的存储后端判断逻辑. 因此, 就需要用一个抽象的概念——多态, 来解决不同运行时下相同的操作逻辑. dyn eframe::Storage 让我们能够无视当前所使用的存储后端, 直接调用由 eframe::Storage trait 定义的存储方法, 从而实现用统一的接口完成持久化和逆持久化.

持久化的时机

持久化什么时候发生? 发生之前我的数据是否及时保存? 频繁的数据持久化是否会降低应用程序性能? 要解决这些问题, 我们可以从函数调用的角度来了解什么时候会发生持久化. 当然, 逆持久化的过程只会在程序启动时发生一次, 因此不必在意.

eframe 启动和渲染过程的函数调用图 (节选)

从笔者此前分享的函数调用图可以看出, 每一次渲染 paint() 过程的最后, 都会通过 maybe_autosave() 进行判断, 从而执行一次保存操作. 另一方面, 当应用程序退出时, 退出过程中也会执行一次保存操作.

所以综上所述, 持久化的时机就在单次渲染完成后, 以及应用程序正常结束前. 但是, 需要注意不是所有情况下都能够执行持久化的. 从 eframe::native::epi_integration::EpiIntegrationsave() 方法源代码可以看出, 我们必须在 Cargo.toml 中为 eframe 开启 persistence feature 才会正常执行相关保存的代码.

持久化的意义

我想从几个角度浅浅聊一下这个问题.

首先, 从数据的角度, 持久化是进行数据保护的重要措施. 不论是在应用程序中独立实现的文件数据保存, 还是由框架实现的数据持久化存储, 本质上都是对数据进行保护. 只不过前者保存用户的数据信息, 后者保存应用程序的执行阶段, 有如检查点那样能够恢复到此前的执行过程.

其次, 从工程的角度, 持久化, 尤其是框架实现的持久化能够一定程度减轻调试时的复杂度. 比如当程序运行过程中崩溃时, 我们可以通过持久化快速恢复故障现场, 便于调试和改进. 持久化的文件我们可以进行备份, 便于调试完成后替换检查.

最后, 从用户的角度, 持久化能够让一些用户的个人偏好得以保存下来, 而不需要开发人员将这些数据上传到服务器或者单独实现一套保存逻辑.


本文插图由 Midjourney 生成