8.5 资源管理

> **本节目标**:掌握 RML 应用中各类资源的管理——文件、网络、订阅、缓存的生命周期管理。

> **MVVM 数据驱动模式说明**:本章示例遵循 RML 的 MVVM 数据驱动模式。在 `#[command]` 方法中,对于直接字段赋值(如 `self.is_loading = true;`),`#[command]` 宏会自动注入 `bump_version("<field>")` 与 `cx.notify()`,因此通常无需手动调用 `cx.notify()`。以下三种例外情况仍需手动调用 `cx.notify()`: > 1. **间接修改**:通过局部借用的修改(如 `let p = &mut self.x; *p = 1;`),宏的 AST 模式匹配无法识别; > 2. **方法调用修改字段**:如 `self.items.push(...)`、`self.items.retain(...)`、`self.cache.update(...)` 等,宏不识别方法调用导致的字段变更; > 3. **嵌套异步闭包**:`cx.spawn(|this, mut cx| ...)` 与 `this.update(&mut cx, |this, cx| ...)` 等闭包内部,宏仅注入命令方法体本身,不进入嵌套闭包。 > > 详见 [命令系统](../04-code-behind/command-system.md) §4.4.0。

8.5.1 资源的类型

RML 应用中需要管理的资源:

| 资源类型 | 获取方式 | 释放方式 | | ----------- | ------------------ | ------------------ | | 文件句柄 | `File::open()` | `file.close()` | | 网络连接 | `connect()` | `conn.close()` | | 数据库连接 | `connect()` | `conn.close()` | | 订阅 | `cx.subscribe()` | `sub.unsubscribe()`| | 定时器 | `cx.spawn()` | `task.abort()` | | 缓存 | 自定义 | 手动清理 | | WebSocket | `connect()` | `ws.close()` |

8.5.2 资源管理的原则

1. 谁获取谁释放

#[derive(IModel)]
#[component]
pub struct DataView {
    file_handle: Option<FileHandle>,  // 我获取的,我负责释放
}

impl DataView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        self.file_handle = Some(FileHandle::open("data.txt"));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        if let Some(handle) = self.file_handle.take() {
            handle.close();  // 我释放
        }
    }
}

2. 及时释放

// ✅ 用完立即释放
pub fn process_file(&mut self, cx: &mut Context<Self>) {
    let handle = FileHandle::open("data.txt");
    let data = handle.read_all();
    handle.close();  // 用完立即关闭

    self.data = data;
    cx.notify();
}

// ❌ 持有不释放
pub fn process_file(&mut self, cx: &mut Context<Self>) {
    self.file_handle = Some(FileHandle::open("data.txt"));
    // 一直持有,直到视图卸载才释放
}

3. 错误时释放

pub fn load_data(&mut self, cx: &mut Context<Self>) {
    let handle = FileHandle::open("data.txt")
        .expect("无法打开文件");

    let result = handle.read_all();

    // 无论成功失败都关闭
    handle.close();

    match result {
        Ok(data) => {
            self.data = data;
        }
        Err(e) => {
            self.error = Some(e.to_string().into());
        }
    }
    cx.notify();
}

8.5.3 文件资源管理

读取文件

use std::fs::File;
use std::io::Read;

#[derive(IModel)]
#[component]
pub struct FileView {
    pub content: SharedString,
    pub is_loading: bool,
    pub error: Option<SharedString>,
}

impl FileView {
    #[command]
    pub fn open_file(&mut self, ev: &ChangeEvent, cx: &mut Context<Self>) {
        let path = ev.value.clone();

        self.is_loading = true;
        // 宏自动注入:bump_version("is_loading") + cx.notify()

        cx.spawn(|this, mut cx| async move {
            match tokio::fs::read_to_string(&path).await {
                Ok(content) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.content = content.into();
                        this.is_loading = false;
                        this.error = None;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.error = Some(e.to_string().into());
                        this.is_loading = false;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
            }
        }).detach();
    }
}

写入文件

#[command]
pub fn save_file(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
    let path = self.file_path.clone();
    let content = self.content.clone();

    cx.spawn(|this, mut cx| async move {
        match tokio::fs::write(&path, content.as_bytes()).await {
            Ok(_) => {
                let _ = this.update(&mut cx, |this, cx| {
                    this.is_saved = true;
                    // 异步闭包内,宏不注入,需手动 notify
                    cx.notify();
                });
            }
            Err(e) => {
                let _ = this.update(&mut cx, |this, cx| {
                    this.error = Some(e.to_string().into());
                    // 异步闭包内,宏不注入,需手动 notify
                    cx.notify();
                });
            }
        }
    }).detach();
}

8.5.4 网络资源管理

HTTP 请求

#[derive(IModel)]
#[component]
pub struct ApiView {
    pub data: Option<ApiResponse>,
    pub is_loading: bool,
    pub error: Option<SharedString>,
}

impl ApiView {
    #[command]
    pub fn fetch_data(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.is_loading = true;
        self.error = None;
        // 宏自动注入:bump_version("is_loading") + bump_version("error") + cx.notify()

        cx.spawn(|this, mut cx| async move {
            match reqwest::get("https://api.example.com/data").await {
                Ok(response) => {
                    match response.json::<ApiResponse>().await {
                        Ok(data) => {
                            let _ = this.update(&mut cx, |this, cx| {
                                this.data = Some(data);
                                this.is_loading = false;
                                // 异步闭包内,宏不注入,需手动 notify
                                cx.notify();
                            });
                        }
                        Err(e) => {
                            let _ = this.update(&mut cx, |this, cx| {
                                this.error = Some(format!("解析失败: {}", e).into());
                                this.is_loading = false;
                                // 异步闭包内,宏不注入,需手动 notify
                                cx.notify();
                            });
                        }
                    }
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.error = Some(format!("请求失败: {}", e).into());
                        this.is_loading = false;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
            }
        }).detach();
    }
}

WebSocket 连接

#[derive(IModel)]
#[component]
pub struct ChatView {
    pub messages: Vec<Message>,
    pub is_connected: bool,
    websocket: Option<WebSocketConnection>,
}

impl ChatView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        self.connect_websocket(cx);
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        // 关闭 WebSocket 连接
        if let Some(ws) = self.websocket.take() {
            ws.close();
        }
    }

    fn connect_websocket(&mut self, cx: &mut Context<Self>) {
        cx.spawn(|this, mut cx| async move {
            match WebSocketConnection::connect("wss://chat.example.com").await {
                Ok(ws) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.websocket = Some(ws);
                        this.is_connected = true;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
                Err(_) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.is_connected = false;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
            }
        }).detach();
    }
}

8.5.5 数据库连接管理

#[derive(IModel)]
pub struct DatabaseManager {
    connection: Option<DbConnection>,
}

impl DatabaseManager {
    pub fn new() -> Self {
        Self {
            connection: None,
        }
    }

    pub fn connect(&mut self, cx: &mut Context<Self>) {
        cx.spawn(|this, mut cx| async move {
            match DbConnection::connect("database.url").await {
                Ok(conn) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.connection = Some(conn);
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
                Err(e) => {
                    // 处理错误
                }
            }
        }).detach();
    }

    pub fn disconnect(&mut self, cx: &mut Context<Self>) {
        if let Some(conn) = self.connection.take() {
            conn.close();
            cx.notify();
        }
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        if let Some(conn) = self.connection.take() {
            conn.close();
        }
    }
}

8.5.6 订阅管理

订阅全局状态

#[derive(IModel)]
#[component]
pub struct NotificationView {
    pub notifications: Vec<Notification>,
    subscription: Option<Subscription>,
}

impl NotificationView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        let service = cx.global::<Entity<NotificationService>>();

        self.subscription = Some(cx.subscribe(
            &service,
            |this, _, event, cx| {
                this.handle_notification(event, cx);
            },
        ));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        if let Some(sub) = self.subscription.take() {
            sub.unsubscribe();
        }
    }

    fn handle_notification(&mut self, event: &NotificationEvent, cx: &mut Context<Self>) {
        self.notifications.push(event.notification.clone());
        // 订阅回调内,宏不注入,需手动 notify
        cx.notify();
    }
}

订阅多个源

#[derive(IModel)]
#[component]
pub struct DashboardView {
    pub user_updates: Vec<UserUpdate>,
    pub system_alerts: Vec<SystemAlert>,
    user_subscription: Option<Subscription>,
    system_subscription: Option<Subscription>,
}

impl DashboardView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        let user_service = cx.global::<Entity<UserService>>();
        let system_service = cx.global::<Entity<SystemService>>();

        self.user_subscription = Some(cx.subscribe(
            &user_service,
            |this, _, event, cx| {
                this.user_updates.push(event.clone());
                // 订阅回调内,宏不注入,需手动 notify
                cx.notify();
            },
        ));

        self.system_subscription = Some(cx.subscribe(
            &system_service,
            |this, _, event, cx| {
                this.system_alerts.push(event.clone());
                // 订阅回调内,宏不注入,需手动 notify
                cx.notify();
            },
        ));
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        if let Some(sub) = self.user_subscription.take() {
            sub.unsubscribe();
        }
        if let Some(sub) = self.system_subscription.take() {
            sub.unsubscribe();
        }
    }
}

8.5.7 缓存管理

简单缓存

use std::collections::HashMap;
use std::time::{Duration, Instant};

#[derive(IModel)]
pub struct Cache<K, V>
where
    K: std::hash::Hash + Eq + Clone,
    V: Clone,
{
    entries: HashMap<K, CacheEntry<V>>,
    ttl: Duration,
}

struct CacheEntry<V> {
    value: V,
    created_at: Instant,
}

impl<K, V> Cache<K, V>
where
    K: std::hash::Hash + Eq + Clone,
    V: Clone,
{
    pub fn new(ttl: Duration) -> Self {
        Self {
            entries: HashMap::new(),
            ttl,
        }
    }

    pub fn get(&self, key: &K) -> Option<V> {
        self.entries.get(key).and_then(|entry| {
            if entry.created_at.elapsed() < self.ttl {
                Some(entry.value.clone())
            } else {
                None
            }
        })
    }

    pub fn set(&mut self, key: K, value: V) {
        self.entries.insert(
            key,
            CacheEntry {
                value,
                created_at: Instant::now(),
            },
        );
    }

    pub fn cleanup_expired(&mut self) {
        self.entries.retain(|_, entry| entry.created_at.elapsed() < self.ttl);
    }

    pub fn clear(&mut self) {
        self.entries.clear();
    }
}

在视图中使用缓存

#[derive(IModel)]
#[component]
pub struct CachedDataView {
    pub data: Option<Data>,
    pub is_loading: bool,
    cache: Entity<Cache<String, Data>>,
}

impl CachedDataView {
    pub fn new(cache: Entity<Cache<String, Data>>) -> Self {
        Self {
            data: None,
            is_loading: false,
            cache,
        }
    }

    #[command]
    pub fn load_data(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        let cache_key = "data_key".to_string();

        // 先查缓存
        let cached = self.cache.read(cx).get(&cache_key);
        if let Some(data) = cached {
            self.data = Some(data);
            // 宏自动注入:bump_version("data") + cx.notify()
            return;
        }

        // 缓存未命中,从服务器加载
        self.is_loading = true;
        // 宏自动注入:bump_version("is_loading") + cx.notify()

        let cache = self.cache.clone();
        cx.spawn(|this, mut cx| async move {
            match fetch_data_from_api().await {
                Ok(data) => {
                    // 写入缓存
                    cache.update(&mut cx, |cache, cx| {
                        cache.set(cache_key, data.clone());
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });

                    let _ = this.update(&mut cx, |this, cx| {
                        this.data = Some(data);
                        this.is_loading = false;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.is_loading = false;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
            }
        }).detach();
    }
}

8.5.8 资源管理的最佳实践

1. 使用 RAII

// ✅ RAII:资源在 Drop 时自动释放
pub struct FileHandle {
    file: Option<File>,
}

impl FileHandle {
    pub fn open(path: &str) -> std::io::Result<Self> {
        Ok(Self {
            file: Some(File::open(path)?),
        })
    }

    pub fn read_all(&mut self) -> std::io::Result<String> {
        let mut content = String::new();
        self.file.as_mut().unwrap().read_to_string(&mut content)?;
        Ok(content)
    }
}

impl Drop for FileHandle {
    fn drop(&mut self) {
        // 自动关闭文件
        if let Some(file) = self.file.take() {
            drop(file);
        }
    }
}

2. 集中管理资源

#[derive(IModel)]
pub struct ResourceManager {
    connections: HashMap<String, Connection>,
    file_handles: HashMap<String, FileHandle>,
    subscriptions: Vec<Subscription>,
}

impl ResourceManager {
    pub fn new() -> Self {
        Self {
            connections: HashMap::new(),
            file_handles: HashMap::new(),
            subscriptions: Vec::new(),
        }
    }

    pub fn get_connection(&mut self, name: &str) -> Option<&Connection> {
        self.connections.get(name)
    }

    pub fn add_connection(&mut self, name: String, conn: Connection) {
        self.connections.insert(name, conn);
    }

    pub fn close_all(&mut self) {
        for (_, conn) in self.connections.drain() {
            conn.close();
        }
        for (_, handle) in self.file_handles.drain() {
            // handle 在 Drop 时自动关闭
        }
        for sub in self.subscriptions.drain() {
            sub.unsubscribe();
        }
    }
}

impl Drop for ResourceManager {
    fn drop(&mut self) {
        self.close_all();
    }
}

3. 资源池

pub struct ConnectionPool {
    connections: Vec<Connection>,
    max_size: usize,
}

impl ConnectionPool {
    pub fn new(max_size: usize) -> Self {
        Self {
            connections: Vec::new(),
            max_size,
        }
    }

    pub fn get(&mut self) -> Option<Connection> {
        self.connections.pop().or_else(|| {
            if self.connections.len() < self.max_size {
                Some(Connection::new())
            } else {
                None
            }
        })
    }

    pub fn return_connection(&mut self, conn: Connection) {
        if self.connections.len() < self.max_size {
            self.connections.push(conn);
        } else {
            conn.close();
        }
    }
}

8.5.9 完整示例:图片查看器

use std::collections::HashMap;
use std::time::{Duration, Instant};
use rml::prelude::*;

#[derive(IModel)]
pub struct ImageCache {
    entries: HashMap<String, CacheEntry>,
    max_size: usize,
}

struct CacheEntry {
    image: ImageData,
    last_used: Instant,
}

impl ImageCache {
    pub fn new(max_size: usize) -> Self {
        Self {
            entries: HashMap::new(),
            max_size,
        }
    }

    pub fn get(&mut self, url: &str) -> Option<ImageData> {
        if let Some(entry) = self.entries.get_mut(url) {
            entry.last_used = Instant::now();
            Some(entry.image.clone())
        } else {
            None
        }
    }

    pub fn set(&mut self, url: String, image: ImageData) {
        if self.entries.len() >= self.max_size {
            self.evict_lru();
        }
        self.entries.insert(url, CacheEntry {
            image,
            last_used: Instant::now(),
        });
    }

    fn evict_lru(&mut self) {
        if let Some((lru_url, _)) = self.entries
            .iter()
            .min_by_key(|(_, entry)| entry.last_used)
            .map(|(k, _)| (k.clone(), ()))
        {
            self.entries.remove(&lru_url);
        }
    }

    pub fn clear(&mut self) {
        self.entries.clear();
    }
}

#[derive(IModel)]
#[component]
pub struct ImageViewer {
    pub current_image: Option<ImageData>,
    pub is_loading: bool,
    pub error: Option<SharedString>,
    pub image_url: SharedString,

    cache: Entity<ImageCache>,
    load_task: Option<Task<()>>,
}

impl ImageViewer {
    pub fn new(cache: Entity<ImageCache>) -> Self {
        Self {
            current_image: None,
            is_loading: false,
            error: None,
            image_url: SharedString::default(),
            cache,
            load_task: None,
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut Context<Self>) {
        if !self.image_url.is_empty() {
            self.load_image(cx);
        }
    }

    #[on_unloaded]
    pub fn on_unloaded(&mut self, _cx: &mut Context<Self>) {
        if let Some(task) = self.load_task.take() {
            task.abort();
        }
    }

    fn load_image(&mut self, cx: &mut Context<Self>) {
        // 取消之前的加载任务
        if let Some(task) = self.load_task.take() {
            task.abort();
        }

        let url = self.image_url.to_string();

        // 先查缓存
        let cached = self.cache.update(cx, |cache, cx| {
            cache.get(&url)
        });

        if let Some(image) = cached {
            self.current_image = Some(image);
            self.is_loading = false;
            cx.notify();
            return;
        }

        // 缓存未命中,从网络加载
        self.is_loading = true;
        self.error = None;
        cx.notify();

        let cache = self.cache.clone();
        self.load_task = Some(cx.spawn(|this, mut cx| async move {
            match fetch_image(&url).await {
                Ok(image) => {
                    // 写入缓存
                    cache.update(&mut cx, |cache, cx| {
                        cache.set(url.clone(), image.clone());
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });

                    let _ = this.update(&mut cx, |this, cx| {
                        this.current_image = Some(image);
                        this.is_loading = false;
                        this.error = None;
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
                Err(e) => {
                    let _ = this.update(&mut cx, |this, cx| {
                        this.is_loading = false;
                        this.error = Some(e.to_string().into());
                        // 异步闭包内,宏不注入,需手动 notify
                        cx.notify();
                    });
                }
            }
        }));
    }

    #[command]
    pub fn change_image(&mut self, ev: &ChangeEvent, cx: &mut Context<Self>) {
        self.image_url = ev.value.clone();
        self.load_image(cx);
    }

    #[command]
    pub fn reload(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.load_image(cx);
    }

    #[command]
    pub fn clear_cache(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.cache.update(cx, |cache, cx| {
            cache.clear();
            // 异步闭包内,宏不注入,需手动 notify
            cx.notify();
        });
    }
}

async fn fetch_image(url: &str) -> Result<ImageData, String> {
    // 模拟图片下载
    Ok(ImageData::default())
}

#[derive(Model, Clone, Default)]
pub struct ImageData {
    pub data: Vec<u8>,
}

8.5.10 小结

RML 的资源管理:

- **原则**:谁获取谁释放、及时释放、错误时释放 - **文件**:用 `tokio::fs` 异步读写 - **网络**:HTTP 请求、WebSocket 连接 - **数据库**:连接池管理 - **订阅**:`cx.subscribe()` + `sub.unsubscribe()` - **缓存**:LRU 缓存,定期清理 - **最佳实践**:RAII、集中管理、资源池

下一章 → [第 9 章 · 架构与最佳实践](../09-architecture/INDEX.md)