/** * 媒体缓存管理器 * @module src/infrastructure/cache/MediaCacheManager * @description 管理图片、视频、音频缓存,支持多种清理策略 */ import { File, Paths } from 'expo-file-system'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; import { MediaType, CleanupTrigger, CacheEntry, CacheStats, CleanupPolicy, CleanupResult, MediaCacheConfig, DEFAULT_MEDIA_CACHE_CONFIG, DEFAULT_CLEANUP_POLICY, } from './types'; /** * 缓存记录存储键前缀 */ const CACHE_RECORD_PREFIX = 'media_cache_record_'; /** * 获取缓存目录路径 */ function getCacheDir(): string { const cachePath = Paths.cache; return `${cachePath}media_cache/`; } /** * 获取图片缓存目录 */ function getImageDir(): string { return `${getCacheDir()}images/`; } /** * 获取视频缓存目录 */ function getVideoDir(): string { return `${getCacheDir()}videos/`; } /** * 获取音频缓存目录 */ function getAudioDir(): string { return `${getCacheDir()}audios/`; } /** * 媒体缓存管理器类 * @class MediaCacheManager * @description 单例模式,管理所有媒体文件的缓存 */ export class MediaCacheManager { private static instance: MediaCacheManager; private cache: Map = new Map(); private config: MediaCacheConfig; private policy: CleanupPolicy; private cleanupTimer: ReturnType | null = null; private lastCleanupTime: number = 0; private isInitialized: boolean = false; /** * 获取单例实例 */ public static getInstance(): MediaCacheManager { if (!MediaCacheManager.instance) { MediaCacheManager.instance = new MediaCacheManager(); } return MediaCacheManager.instance; } /** * 私有构造函数 */ private constructor( config?: Partial, policy?: Partial ) { this.config = { ...DEFAULT_MEDIA_CACHE_CONFIG, ...config }; this.policy = { ...DEFAULT_CLEANUP_POLICY, ...policy }; } /** * 初始化缓存管理器 * 确保缓存目录存在并加载缓存记录 */ public async initialize(): Promise { if (this.isInitialized) return; try { // 确保缓存目录存在 await this.ensureDirectories(); // 加载缓存记录 await this.loadCacheRecords(); // 启动时清理 (非web平台) if (this.policy.cleanupOnStart && Platform.OS !== 'web') { await this.cleanup(CleanupTrigger.STARTUP); } // 启动定期清理 (非web平台) if (Platform.OS !== 'web') { this.startPeriodicCleanup(); } this.isInitialized = true; } catch (error) { console.error('[MediaCacheManager] 初始化失败:', error); throw error; } } /** * 确保缓存目录存在 */ private async ensureDirectories(): Promise { if (Platform.OS === 'web') return; try { const dirs = [getImageDir(), getVideoDir(), getAudioDir()]; for (const dir of dirs) { const dirFile = new File(dir); if (!await dirFile.exists) { await dirFile.create(); } } } catch (error) { console.error('[MediaCacheManager] 创建缓存目录失败:', error); } } /** * 从持久化存储加载缓存记录 */ private async loadCacheRecords(): Promise { try { const keys = await AsyncStorage.getAllKeys(); const recordKeys = keys.filter(k => k.startsWith(CACHE_RECORD_PREFIX)); if (recordKeys.length === 0) return; const records = await AsyncStorage.multiGet(recordKeys); for (const [key, value] of records) { if (value) { const entry: CacheEntry = JSON.parse(value); // 验证文件是否存在 const exists = await this.checkFileExists(entry.localPath); if (exists) { this.cache.set(entry.key, entry); } else { // 文件不存在,删除记录 await AsyncStorage.removeItem(key); } } } } catch (error) { console.error('[MediaCacheManager] 加载缓存记录失败:', error); } } /** * 检查文件是否存在 */ private async checkFileExists(path: string): Promise { if (Platform.OS === 'web') return false; try { const file = new File(path); return await file.exists; } catch { return false; } } /** * 记录缓存访问(用于LRU) */ public async recordAccess(key: string): Promise { const entry = this.cache.get(key); if (entry) { entry.lastAccessedAt = Date.now(); await this.saveCacheRecord(entry); } } /** * 缓存媒体文件 */ public async cacheMedia( uri: string, type: MediaType, options: { conversationId?: string; messageId?: string; size?: number; } = {} ): Promise { if (Platform.OS === 'web') { return uri; } const { conversationId, messageId, size = 0 } = options; // 生成唯一键 const key = this.generateCacheKey(uri, type); const localPath = this.getMediaPath(type, key); // 检查是否已缓存 const existingEntry = this.cache.get(key); if (existingEntry) { await this.recordAccess(key); return existingEntry.localPath; } try { // 下载文件 const destination = new File(localPath); const downloaded = await File.downloadFileAsync(uri, destination); // 获取实际文件大小 let fileSize = size; // fileSize 为 0 时使用默认大小,实际应用中可以通过其他方式获取 // 创建缓存条目 const entry: CacheEntry = { key, type, uri, localPath: downloaded.uri, size: fileSize, conversationId, messageId, createdAt: Date.now(), lastAccessedAt: Date.now(), }; // 保存记录 this.cache.set(key, entry); await this.saveCacheRecord(entry); // 检查是否需要清理 await this.checkAndCleanup(); return downloaded.uri; } catch (error) { console.error('[MediaCacheManager] 缓存媒体失败:', error); throw error; } } /** * 获取缓存的媒体路径 */ public async getCachedMedia(uri: string, type: MediaType): Promise { const key = this.generateCacheKey(uri, type); const entry = this.cache.get(key); if (entry) { const exists = await this.checkFileExists(entry.localPath); if (exists) { await this.recordAccess(key); return entry.localPath; } else { // 文件不存在,删除记录 this.cache.delete(key); await this.removeCacheRecord(key); } } return null; } /** * 获取缓存统计信息 */ public getCacheStats(): CacheStats { const stats: CacheStats = { totalSize: 0, imageCount: 0, videoCount: 0, audioCount: 0, totalEntries: this.cache.size, oldestItem: Date.now(), newestItem: 0, }; for (const entry of this.cache.values()) { stats.totalSize += entry.size; stats.oldestItem = Math.min(stats.oldestItem, entry.createdAt); stats.newestItem = Math.max(stats.newestItem, entry.createdAt); switch (entry.type) { case MediaType.IMAGE: stats.imageCount++; break; case MediaType.VIDEO: stats.videoCount++; break; case MediaType.AUDIO: stats.audioCount++; break; } } return stats; } /** * 清理操作入口 */ public async cleanup(trigger: CleanupTrigger): Promise { const errors: string[] = []; let deletedCount = 0; let freedSize = 0; const deletedItems: string[] = []; try { // 1. 清理过期缓存 const expiredResult = await this.cleanupExpired(); deletedCount += expiredResult.deletedCount; freedSize += expiredResult.freedSize; deletedItems.push(...expiredResult.deletedItems || []); errors.push(...expiredResult.errors); // 2. LRU清理(如果启用) if (this.policy.enableLRU) { const lruResult = await this.cleanupLRU(); deletedCount += lruResult.deletedCount; freedSize += lruResult.freedSize; deletedItems.push(...lruResult.deletedItems || []); errors.push(...lruResult.errors); } // 3. 阈值清理 const thresholdResult = await this.cleanupByThreshold(); deletedCount += thresholdResult.deletedCount; freedSize += thresholdResult.freedSize; deletedItems.push(...thresholdResult.deletedItems || []); errors.push(...thresholdResult.errors); this.lastCleanupTime = Date.now(); } catch (error) { errors.push(`清理失败: ${error instanceof Error ? error.message : '未知错误'}`); } return { trigger, timestamp: Date.now(), deletedCount, freedSize, errors, deletedItems, }; } /** * 清理过期缓存 */ public async cleanupExpired(): Promise { const now = Date.now(); const maxAge = this.policy.maxAge; const errors: string[] = []; let deletedCount = 0; let freedSize = 0; const deletedItems: string[] = []; const entriesToDelete: string[] = []; for (const [key, entry] of this.cache.entries()) { if (now - entry.lastAccessedAt > maxAge) { entriesToDelete.push(key); } } for (const key of entriesToDelete) { try { const entry = this.cache.get(key); if (entry) { const file = new File(entry.localPath); if (await file.exists) { await file.delete(); } this.cache.delete(key); await this.removeCacheRecord(key); deletedCount++; freedSize += entry.size; deletedItems.push(key); } } catch (error) { errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`); } } return { trigger: CleanupTrigger.MANUAL, timestamp: Date.now(), deletedCount, freedSize, errors, deletedItems, }; } /** * LRU清理 - 删除最近最少使用的缓存 */ public async cleanupLRU(targetSize?: number): Promise { const errors: string[] = []; let deletedCount = 0; let freedSize = 0; const deletedItems: string[] = []; // 如果没有指定目标大小,使用最大缓存大小的一半 const target = targetSize || Math.floor(this.policy.maxSize / 2); const currentStats = this.getCacheStats(); if (currentStats.totalSize <= target) { return { trigger: CleanupTrigger.MANUAL, timestamp: Date.now(), deletedCount: 0, freedSize: 0, errors: [], deletedItems: [], }; } // 按最后访问时间排序 const sortedEntries = Array.from(this.cache.entries()) .sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt); let currentSize = currentStats.totalSize; for (const [key, entry] of sortedEntries) { if (currentSize <= target) break; try { const file = new File(entry.localPath); if (await file.exists) { await file.delete(); } this.cache.delete(key); await this.removeCacheRecord(key); deletedCount++; freedSize += entry.size; deletedItems.push(key); currentSize -= entry.size; } catch (error) { errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`); } } return { trigger: CleanupTrigger.MANUAL, timestamp: Date.now(), deletedCount, freedSize, errors, deletedItems, }; } /** * 按阈值清理 - 超过最大大小时清理 */ private async cleanupByThreshold(): Promise { const currentStats = this.getCacheStats(); const errors: string[] = []; let deletedCount = 0; let freedSize = 0; const deletedItems: string[] = []; // 检查总大小是否超过限制 if (currentStats.totalSize <= this.policy.maxSize) { // 检查条目数是否超过限制 if (currentStats.totalEntries <= this.policy.maxEntries) { return { trigger: CleanupTrigger.MANUAL, timestamp: Date.now(), deletedCount: 0, freedSize: 0, errors: [], deletedItems: [], }; } } // 需要清理 const targetSize = Math.floor(this.policy.maxSize * 0.8); // 清理到80% const targetEntries = Math.floor(this.policy.maxEntries * 0.8); // 按LRU排序 const sortedEntries = Array.from(this.cache.entries()) .sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt); let currentSize = currentStats.totalSize; let currentEntries = currentStats.totalEntries; for (const [key, entry] of sortedEntries) { // 两个条件:大小或条目数 if (currentSize <= targetSize && currentEntries <= targetEntries) break; try { const file = new File(entry.localPath); if (await file.exists) { await file.delete(); } this.cache.delete(key); await this.removeCacheRecord(key); deletedCount++; freedSize += entry.size; deletedItems.push(key); currentSize -= entry.size; currentEntries--; } catch (error) { errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`); } } return { trigger: CleanupTrigger.MANUAL, timestamp: Date.now(), deletedCount, freedSize, errors, deletedItems, }; } /** * 删除指定会话的所有媒体缓存 */ public async clearConversationMedia(conversationId: string): Promise { const errors: string[] = []; let deletedCount = 0; let freedSize = 0; const deletedItems: string[] = []; const entriesToDelete: string[] = []; for (const [key, entry] of this.cache.entries()) { if (entry.conversationId === conversationId) { entriesToDelete.push(key); } } for (const key of entriesToDelete) { try { const entry = this.cache.get(key); if (entry) { const file = new File(entry.localPath); if (await file.exists) { await file.delete(); } this.cache.delete(key); await this.removeCacheRecord(key); deletedCount++; freedSize += entry.size; deletedItems.push(key); } } catch (error) { errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`); } } return { trigger: CleanupTrigger.CONVERSATION_DELETED, timestamp: Date.now(), deletedCount, freedSize, errors, deletedItems, }; } /** * 清空所有缓存 */ public async clearAll(): Promise { for (const entry of this.cache.values()) { try { const file = new File(entry.localPath); if (await file.exists) { await file.delete(); } } catch (error) { console.warn(`[MediaCacheManager] 删除文件失败: ${entry.localPath}`, error); } } this.cache.clear(); // 清除所有记录 const keys = await AsyncStorage.getAllKeys(); const recordKeys = keys.filter(k => k.startsWith(CACHE_RECORD_PREFIX)); if (recordKeys.length > 0) { await AsyncStorage.multiRemove(recordKeys); } } /** * 启动定期清理 */ public startPeriodicCleanup(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(async () => { // 检查是否需要清理 if (Date.now() - this.lastCleanupTime >= this.policy.cleanupInterval) { await this.cleanup(CleanupTrigger.SCHEDULED); } }, this.policy.cleanupInterval); } /** * 停止定期清理 */ public stopPeriodicCleanup(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } } /** * 检查是否需要清理 */ private async checkAndCleanup(): Promise { const stats = this.getCacheStats(); // 检查大小 if (stats.totalSize > this.policy.maxSize) { await this.cleanupLRU(); } // 检查条目数 if (stats.totalEntries > this.policy.maxEntries) { await this.cleanupLRU(); } } /** * 保存缓存记录到持久化存储 */ private async saveCacheRecord(entry: CacheEntry): Promise { try { const key = `${CACHE_RECORD_PREFIX}${entry.key}`; await AsyncStorage.setItem(key, JSON.stringify(entry)); } catch (error) { console.error('[MediaCacheManager] 保存缓存记录失败:', error); } } /** * 删除缓存记录 */ private async removeCacheRecord(key: string): Promise { try { const storageKey = `${CACHE_RECORD_PREFIX}${key}`; await AsyncStorage.removeItem(storageKey); } catch (error) { console.error('[MediaCacheManager] 删除缓存记录失败:', error); } } /** * 生成缓存键 */ private generateCacheKey(uri: string, type: MediaType): string { const hash = this.hashString(uri); return `${type.toLowerCase()}_${hash}_${Date.now()}`; } /** * 获取媒体文件存储路径 */ private getMediaPath(type: MediaType, key: string): string { const ext = this.getExtension(key, type); switch (type) { case MediaType.IMAGE: return `${getImageDir()}${key}.${ext}`; case MediaType.VIDEO: return `${getVideoDir()}${key}.${ext}`; case MediaType.AUDIO: return `${getAudioDir()}${key}.${ext}`; } } /** * 根据类型获取文件扩展名 */ private getExtension(key: string, type: MediaType): string { if (type === MediaType.IMAGE) { if (key.includes('gif')) return 'gif'; if (key.includes('webp')) return 'webp'; return 'jpg'; } if (type === MediaType.VIDEO) return 'mp4'; if (type === MediaType.AUDIO) return 'mp3'; return 'bin'; } /** * 字符串哈希函数 */ private hashString(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16); } } /** * 导出单例实例 */ export const mediaCacheManager = MediaCacheManager.getInstance();