Add DataStorageScreen for managing media cache and storage: - Display cache statistics (image/video/audio counts and sizes) - Show AsyncStorage usage information - Add clear all cache functionality - Add cleanup expired cache option (7 days threshold) - Add storage usage breakdown by category - Add "Last cleaned" timestamp display Also fix MediaCacheManager to skip native cache operations on web platform: - Return original URI directly on web without caching - Skip directory creation on web - Skip file existence checks on web - Skip startup and periodic cleanup on web
738 lines
19 KiB
TypeScript
738 lines
19 KiB
TypeScript
/**
|
||
* 媒体缓存管理器
|
||
* @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<string, CacheEntry> = new Map();
|
||
private config: MediaCacheConfig;
|
||
private policy: CleanupPolicy;
|
||
private cleanupTimer: ReturnType<typeof setTimeout> | 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<MediaCacheConfig>,
|
||
policy?: Partial<CleanupPolicy>
|
||
) {
|
||
this.config = { ...DEFAULT_MEDIA_CACHE_CONFIG, ...config };
|
||
this.policy = { ...DEFAULT_CLEANUP_POLICY, ...policy };
|
||
}
|
||
|
||
/**
|
||
* 初始化缓存管理器
|
||
* 确保缓存目录存在并加载缓存记录
|
||
*/
|
||
public async initialize(): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<boolean> {
|
||
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<void> {
|
||
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<string> {
|
||
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<string | null> {
|
||
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<CleanupResult> {
|
||
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<CleanupResult> {
|
||
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<CleanupResult> {
|
||
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<CleanupResult> {
|
||
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<CleanupResult> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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();
|