[shine_image开发进度-第二篇]:内存缓存与持久化缓存

3,286 阅读8分钟

大家好,我又回来了,时隔近一个月,我又来更新文章了。

至于为什么这么久才来更新,主要是因为这段时间在研究"区块链开发","深度学习","准备圣诞礼物"和"戏弄chat gpt"之类的杂事啦~ 绝对不是在摸鱼,绝对不是!!!

今天主要是更新shine_image的开发进度,第二篇章,顺便展望下第三篇,图片处理。

首先先说说,在第一篇的shine_image其实是一个很初级的阶段,它每次都进行下载原图,解析图片字节,返回。大家知道在实际应用中,这样频繁的进行网络,内存,io操作,是非常耗费性能的。所以这里就会有一个烂大街的优化方案了,那就是。

加缓存!

flutter的image组件也都有内置使用cache优化性能的实现,但是它会有一点点小小的问题就是了,由于我们的shine是通过ffi开发可供flutter组件使用的跨平台rust逻辑系列。

所以缓存我就做在了rust侧,好处是和解析不必分离,且具有更高的可操作空间。

话不多说,我们进入代码:

首先。我在rust增加了两个结构体,这两个结构体构成了我们的缓存核心数据结构:

#[derive(Debug, Default)]
pub struct ImageStorage {
    pub bytes_size: usize,
    pub image_collection: HashMap<String, ImageMap>,
}

#[derive(Debug)]
pub struct ImageMap {
    pub once_image_size: usize,
    pub once_image_type: i64,
    pub once_image_data: Vec<u8>,
}

简单的解释下代码,ImageStorage是一个主要数据结构,它包含了一个bytes_size,和一个image_collectionHashMap的数据结构。

而这个HashMap则是再包一个ImageMap的数据结构。

在我们缓存的时候,我们会对bytes_size进行增加相应的字节,将实际的图片数据,塞入ImageMap

这样的目的是在图片不断缓存的过程中,对总缓存的内存占用做个记录,方便设置一个上限,在达到上限的时候进行移除和回收。

由于会对ImageStorage进行大量的读写,在使用的时候我们需要一个高效的方式来设计它,传统的OOP编程会使用单例模式,只对它初始化一次,未来每次都拿到这同一个实例。

我们便也使用这个方法,通过rust的once_cell我们将设置一个全局的静态变量来管理ImageStorage

如下面的代码:

static IMGINSTANCE: Lazy<Arc<RwLock<ImageStorage>>> = Lazy::new(|| Arc::new(RwLock::new(ImageStorage::default())));

这里由于rust对数据竞争与内存安全的严格,所以我加上了读写锁。

它是一种可以提供多个读权限,单个写权限的一种锁结构。

这样我们就可以编写一个add数据进结构体的方法了。

这里我编写了一个add_storage()的方法。

fn add_storage(args_size: &usize, args_key: String, args_type: i64, args_data: &Vec<u8>) {
    
    let mut img_ins = IMGINSTANCE.write().expect("写入锁不可用");

    if !img_ins.image_collection.contains_key(&args_key) {
        if img_ins.bytes_size >= 100000000 {
            let mut count_size = 0;
            let mut count_key: Vec<String> = vec![];
            for (i, m) in &img_ins.image_collection {
                if count_size >= *args_size {
                    img_ins.bytes_size += args_size;
                    img_ins.image_collection.insert(
                        args_key,
                        ImageMap {
                            once_image_size: *args_size,
                            once_image_type: args_type,
                            once_image_data: args_data.to_vec(),
                        },
                    );
                    break;
                }
                if m.once_image_type != 0 {
                    count_key.push(i.to_string());
                    count_size += m.once_image_size;
                }
            }
            for key in count_key {
                if let Some(map) = img_ins.image_collection.get(&key) {
                    img_ins.bytes_size -= map.once_image_size;
                    img_ins.image_collection.remove(&key);
                }
            }
        } else {
            img_ins.bytes_size += args_size;
            img_ins.image_collection.insert(
                args_key,
                ImageMap {
                    once_image_size: *args_size,
                    once_image_type: args_type,
                    once_image_data: args_data.to_vec(),
                },
            );
        }
    }
    drop(img_ins);
}

代码比较长,这里简单的解释下。

因为要写入数据,我们肯定要获取写锁。

然后我们查询,我们的key,是否在 ImageStorage -> image_collection 中。

如果已经存在,我们就丢弃我们的方法,并drop掉我们的锁。

如果不存在,我们再判断 bytes_size 是否超过了100000000字节,如果超过了,我们会根据需要存储的字节长度,查询足够的缓存空间的数据并记录它们的key,将它们移除。

否则,我们将直接插入缓存数据。

另外,考虑到图片列表会大量的调用它,这样会导致,前一个逻辑还没执行完毕,写入锁还没释放,下一个逻辑又挤进来,产生panic

所以我这里又设置了一个队列,将数据丢进来,排队进行处理。

fn image_add_storage_queue() -> Sender<(usize, String, i64, Vec<u8>)> {
    let (sender, receiver) = unbounded::<(usize, String, i64, Vec<u8>)>();
    thread::spawn(move || {
        while let Ok(data) = receiver.recv() {
            add_storage(&data.0, data.1, data.2, &data.3);
        }
    });
    sender
}

在成熟的应用和成熟的图片组件中,仅仅是实现了图片的缓存,并不足以实际支持应用的可靠性与高性能。 这里就又要引入新的补充能力了,一般优质的第三方组件都会实现本地的持久化存储。 比较知名的例如:extended_imagecached_network_image

有种做法是将图片缓存到本地图片文件,在解析的时候判断本地文件是否存在。

这里我考虑到未来应用可能会需要统计持久化存储占用的io体积,以及清理的逻辑,我使用了本地数据库的方式,通过rusqlite创建一个sqlite数据库进行持久化管理。

代码大概是这样的:

static IMAGECONNECTION: OnceCell<Arc<Mutex<Connection>>> = OnceCell::new();

#[tokio::main(flavor = "current_thread")]
pub async fn init_image_db(app_path:String){
    let conn = Connection::open(app_path).expect("数据库初始化失败");
    conn.execute(
        "CREATE TABLE IF NOT EXISTS image_db_storage (
            image_key TEXT PRIMARY KEY,
            image_value BLOB NOT NULL,
            image_pallet TEXT NOT NULL DEFAULT 'download',
            image_group INTEGER NOT NULL DEFAULT 0,
            image_send TEXT NOT NULL DEFAULT '0',
            image_receive TEXT NOT NULL DEFAULT '0',
            image_time INTEGER NOT NULL
        )",
        (),
    ).expect("建表失败");
    IMAGECONNECTION.get_or_init(|| {
        Arc::new(Mutex::new(conn))
    });
}

这里没有使用读写锁,而用了互斥锁,是因为Connection它是被一个引用计数RefCell包裹的数据结构,在未实现指定Trait前,就无法使用读写锁。

我图方便就还是使用互斥锁了。

我们有了数据库,和数据表,不能没有数据吧,这里是我们存储进数据库的数据结构,以及执行方法。

#[derive(Deserialize, Serialize)] 
pub struct ImageDbData {
    pub image_key:String,
    pub image_value:Vec<u8>,
    pub image_pallet:String,
    pub image_group:i64,
    pub image_send:String,
    pub image_receive:String,
    pub image_time:i64,
}

fn set_image_db(key:&str,value:&[u8],get_pallet:Option<String>,get_group:Option<i64>,get_send:Option<String>,get_receive:Option<String>){
    if let Some(mutex_conn) = IMAGECONNECTION.get() {
        let conn = mutex_conn.lock().expect("获取锁失败");
        let value_as_blob = value.to_sql().expect("blob数据转换错误");
        let time = create_the_time();
        let pallet:String;
        let group:i64;
        let send:String;
        let receive:String;
        if let Some(p) = get_pallet{
            pallet = p;
        }else{
            pallet = "download".to_owned();
        }
        if let Some(g) = get_group{
            group = g;
        }else{
            group = 0;
        }
        if let Some(s) = get_send{
            send = s;
        }else{
            send = "0".to_owned();
        }
        if let Some(r) = get_receive{
            receive = r;
        }else{
            receive = "0".to_owned();
        }
        conn.execute("INSERT OR IGNORE INTO image_db_storage (image_key,image_value,image_pallet,image_group,image_send,image_receive,image_time) VALUES (?,?,?,?,?,?,?)", (key,value_as_blob,&pallet,group,&send,&receive,time)).expect("查询失败");
        drop(mutex_conn);
    }
}

这里也简单的说明一下。

ImageDbData

image_key是唯一主键。

image_value是图片的矩阵数据。

image_pallet是来自哪个功能模块,默认是download,如果你有需要,也可以指定成其他模块,如电商,文章,聊天之类的。

image_group确定是否来自群组,因为如果是聊天功能,对人与对群是不同的,要整理图片数据,对群一般没有对人重要。

image_send图片的发送者,image_receive图片的接收者。通过发送者与接收者的设置,可以在一对一聊天的时候,整理与指定人的图片数据。

image_time记录一下图片的存入时间,也方便根据时间整理图片的思路。

毕竟图片的用处很广泛,还是设置细致些的好,也免得未来又要去改表。

然后修改我们的解析图片的代码它就变成了这样。对了,之前方法叫analyze_image,我把它改为了network_analyze_image

#[tokio::main(flavor = "current_thread")]
pub async fn network_analyze_image(url: String) -> Result<ZeroCopyBuffer<Vec<u8>>> {

    let img_ins = IMGINSTANCE.read().expect("获取读写锁错误");

    if !img_ins.image_collection.contains_key(&url) {
        let db_res_get = get_image_db_by_key(url.as_ref());
        match db_res_get {
            Ok(bytes) => {
                drop(img_ins);
                let sender = image_add_storage_queue();
                let image_type = 1; 
                let image_vec = bytes.clone();
                let data = (image_vec.len(), url, image_type, image_vec);
                sender.send(data).expect("Failed to send data");
                Ok(ZeroCopyBuffer(bytes))
            }
            Err(_) => {
                let res = HTTPCLIENT.get(&url).send().await?;

                if !res.status().is_success() {
                    return Err(anyhow!("下载图片失败: {}", res.status()));
                }

                let bytes = res.bytes().await?.to_vec();

                drop(img_ins);

                let senderdb = image_add_db_queue();
                let image_db_vec = bytes.clone();
                let db_url = url.clone();
                let db_data = (db_url,image_db_vec,None,None,None,None);
                senderdb.send(db_data).expect("Failed to send db_data");

                let sender = image_add_storage_queue();
                let image_type = 1; 
                let image_vec = bytes.clone();
                let data = (image_vec.len(), url, image_type, image_vec);
                sender.send(data).expect("Failed to send data");

                Ok(ZeroCopyBuffer(bytes))
            }
        }
    } else {
        if let Some(res_image) = img_ins.image_collection.get(&url) {
            let bytes = res_image.once_image_data.clone();
            Ok(ZeroCopyBuffer(bytes))
        } else {
            Err(anyhow!("缓存中获取数据失败"))
        }
    }
}

我们可以试试,我们的新图片组件的表现如何了!

下面是一些截图:

shine_image在与sdk的Image.network的对比:

QQ视频20221226141525 -original-original (1).gif

shine_image在与extended_image的对比:

QQ视频20221226142509 -original-original.gif

题外话

看到最后结果,有人可能觉得shine_image在与extended_image的差距基本等于没有,费这么大力气做这玩意干嘛,吃饱了闲的。

首先,shine_image在rust侧处理,它更安全,不容易引发内存问题。

其次,shine_image它是可跨平台的,rust打包了so库,天然的可跨平台性,而不需要去编写双端代码。

最后,shine_image可以更容易支持更新的图片格式,兼容rust未来的数据结构优化,也可以对图片矩阵进行编程,裁剪,缩放,水印自然不在话下,而一些像素计算也是拿手好戏,比如flutter做一个音乐背景图模糊的效果。

QQ图片20221226145701.jpg

像QQ音乐的上下模糊效果,在dart里制作效果并不还原,性能也有所影响。而在rust中,我们可以通过编写图像元素内部的算法来实现。并交给缓存来保证加载等。

在下一篇,我们的目标是创建上传方法与本地图片管理方法。

通常来说,我是比较建议在上传的时候对图像进行编码的改变,resize,以及压缩的。

这样可以避免在使用图片的时候因为过大的图片尺寸导致应用崩溃的情况。

如果你对shine_image的说明有什么不懂或者有什么建议,也可以评论一下,我看看我是否哪里考虑的不周全。

今天就到这里,谢谢观看~