首页 > 其他分享 >eframe 持久化实现

eframe 持久化实现

时间:2023-11-05 17:34:56浏览次数:33  
标签:mut 持久 实现 eframe storage value MyApp

在 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 生成

标签:mut,持久,实现,eframe,storage,value,MyApp
From: https://www.cnblogs.com/zhongdongy/p/eframe-persistence.html

相关文章

  • 基础排序——数组和链表实现(C)
    一、数组实现①选择排序从index=0开始,每一轮找到一个最小的元素,然后交换num[index]和num[min]的位置,直至数组遍历完。得到一个升序数组。voidselectSort(int*num,intn){for(inti=0;i<n;i++){intmin=i;for(intj=i+1;j<n;j++){......
  • 《信息安全系统设计与实现》第九周学习笔记
      《信息安全系统设计与实现》第九周学习笔记第五章定时器及时钟服务硬件定时器定时器是由时钟源和可编程计数器组成的硬件设备。时钟源通常是一个晶体振荡器,会产生周期性电信号,以精确的频率驱动计数器。使用一个倒计时值对计数器进行编程,每个时钟信号减1。当计数减为0时......
  • 《信息安全系统设计与实现》第八次学习笔记
    第五章:定时器及时钟服务硬件定时器定时器是由时钟源和可编程计数器组成的硬件设备。时钟源通常是一个晶体振荡器,会产生周期性电信号,以精确的频率驱动计数器。使用一个倒计时值对计数器进行编程,每个时钟信号减1。当计数减为0时,计数器向CPU生成一个定时器中断,将计数值重新加载到......
  • 《信息安全系统设计与实现》第九周学习笔记
    硬件定时器定时器是由时钟源和可编程计数器组成的硬件设备。时钟源通常是一个晶体振荡器,会产生周期性电信号,以精确的频率驱动计数器。使用一个倒计时值对计数器进行编程,每个时钟信号减1。当计数减为0时,计数器向CPU生成一个定时器中断,将计数值重新加载到计数器中,并重复倒计时。......
  • 信息安全系统设计与实现学习笔记8
    学习笔记8-重点总结1.定时器及时钟服务1.1硬件定时器由时钟源和可编程计数器组成的硬件设备。时钟源通常是晶体振荡器,驱动计数器以精确的频率。计数器周期称为定时器刻度,是系统的基本计时单元。1.2个人计算机定时器实时时钟(RTC)提供时间和日期信息,即使在关机时也能......
  • python实现PDF文件指定页码号裁剪
    代码importPyPDF2out_pdf=PyPDF2.PdfFileWriter()dst_file=f'output.pdf'withopen('input.pdf','rb')assrc_file: reader=PyPDF2.PdfFileReader(src_file) pages=reader.numPages forpinrange(12): #这里是获取源PDF前12页,......
  • fibnacci数列递归实现
    1.什么是fibnacci数列斐波那契数列(Fibonaccisequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(LeonardoFibonacci)以兔子繁殖为例子而引入,故又称“兔子数列”,其数值为:1、1、2、3、5、8、13、21、34……不难发现,前两项的值各为1,从第三项起,每一项都是前两项的和。2.fibnacci数......
  • 如何实现乐观锁
    乐观锁一般会使用版本号机制或CAS算法实现,CAS算法相对来说更多一些,这里需要格外注意。#版本号机制一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在......
  • 自然语言处理历史史诗:NLP的范式演变与Python全实现
    本文全面回顾了自然语言处理(NLP)从20世纪50年代至今的历史发展。从初创期的符号学派和随机学派,到理性主义时代的逻辑和规则范式,再到经验主义和深度学习时代的数据驱动方法,以及最近的大模型时代,NLP经历了多次技术革新和范式转换。文章不仅详细介绍了每个阶段的核心概念和技术,还提供......
  • 高精度减法(C语言实现)
    高精度减法(C语言实现)介绍众所周知,整数在C和C++中以int,long,longlong三种不同大小的数据存储,数据大小最大可达2^64,但是在实际使用中,我们仍不可避免的会遇到爆longlong的超大数运算,这个时候,就需要我们使用高精度算法,来实现巨大数的运算。高精度的本质是将数字以字符串的形式......