/*
* Copyright (C) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import util from '@ohos.util';
import { FileUtils } from '../utils/FileUtils';
import fs from '@ohos.file.fs';
import { LogUtil } from '../utils/LogUtil';
const INT_MAX = 2147483647
/**
* 二级文件缓存
* 主线程通过lruCache管理缓存大小
* 子线程直接读写文件
*/
export class FileCache {
static CACHE_FOLDER: string = 'ImageKnife' // context cacheDir 的缓存文件目录
maxMemory: number = 0
currentMemory: number = 0
maxSize: number = 0
path: string = ''
private lruCache: util.LRUCache<string, number>
private isInited: boolean = false
public context?: Context
readonly defaultMaxMemorySize: number = 10 * 1024 * 1024 * 1024;
readonly defaultMemorySize: number = 1024 * 1024 * 1024;
constructor(context: Context, size: number, memory: number) {
if (size <= 0 || size > INT_MAX) {
size = INT_MAX;
}
if (memory <= 0 || memory > this.defaultMaxMemorySize) {
memory = this.defaultMemorySize;
}
this.lruCache = new util.LRUCache(size);
this.maxMemory = memory;
this.currentMemory = 0;
this.maxSize = size;
this.context = context;
}
/**
* 遍历缓存文件目录,初始化缓存
*/
public async initFileCache(path: string = FileCache.CACHE_FOLDER) {
if (this.isInited) {
return
}
let startTime = Date.now()
if (this.context && path.startsWith(this.context.cacheDir) === true) {
this.path = path
} else {
FileCache.CACHE_FOLDER = path
this.path = this.context?.cacheDir + FileUtils.SEPARATOR + FileCache.CACHE_FOLDER + FileUtils.SEPARATOR
}
await FileUtils.getInstance().createFolder(this.path)
// 遍历缓存目录下的文件,按照时间顺序加入缓存
let filenames: string[] = await FileUtils.getInstance().ListFile(this.path)
interface CacheFileInfo {
file: string;
ctime: number;
size: number;
}
// 按照上次访问该文件的时间排序
let cachefiles: CacheFileInfo[] = []
for (let i = 0; i < filenames.length; i++) {
let stat: fs.Stat | undefined = await FileUtils.getInstance().Stat(this.path + filenames[i])
cachefiles.push({
file: filenames[i],
ctime: stat === undefined ? 0 : stat.ctime,
size: stat?.size ?? 0
})
}
let sortedCachefiles: CacheFileInfo[] = cachefiles.sort((a, b) => a.ctime - b.ctime)
for (let i = 0; i < sortedCachefiles.length; i++) {
const fileSize: number = sortedCachefiles[i].size;
// 处理数量超过size的场景,移除即将排除的文件
if (this.lruCache.length == this.maxSize && !this.lruCache.contains(sortedCachefiles[i].file)) {
let remove: number | undefined = this.lruCache.remove(this.lruCache.keys()[0])
if (remove !== undefined) {
FileUtils.getInstance().deleteFile(this.path + this.lruCache.keys()[0])
this.removeMemorySize(fileSize)
}
}
this.lruCache.put(sortedCachefiles[i].file, fileSize)
this.addMemorySize(fileSize)
}
this.trimToSize();
LogUtil.info('image init initFileCache:' + (Date.now() - startTime) + ',num:' + filenames.length + ',nums:' + this.lruCache.length + ',size:' + this.currentMemory)
this.isInited = true
}
public isFileCacheInit():boolean {
return this.isInited
}
public getCacheFolder(): string {
return FileCache.CACHE_FOLDER
}
// 添加缓存键值对,同时写文件
put(key: string, value: ArrayBuffer): void {
if (key == null || value == null) {
throw new Error('key or value is invalid ');
}
if (!this.isInited) {
return
}
// 如果size满了的话,需要按照LRU的方式删除第一个
if (this.lruCache.length == this.maxSize && !this.lruCache.contains(key)) {
this.remove(this.lruCache.keys()[0])
} else if (this.lruCache.contains(key)) {
this.remove(key)
}
let pre = this.lruCache.put(key, value.byteLength)
FileUtils.getInstance().writeDataSync(this.path + key, value)
if (pre !== undefined) {
this.addMemorySize(value)
}
this.trimToSize()
}
// 添加缓存键值对,但不写文件(用于子线程已经写文件的场景)
putWithoutWriteFile(key: string, value: ArrayBuffer | number): void {
if (key == null || value == null) {
throw new Error('key or value is invalid ');
}
if (!this.isInited) {
return
}
// 如果size满了的话,需要按照LRU的方式删除第一个
if (this.lruCache.length == this.maxSize && !this.lruCache.contains(key)) {
this.remove(this.lruCache.keys()[0])
} else if (this.lruCache.contains(key)) {
this.lruCache.remove(key)
this.lruCache.put(key, typeof value == 'number' ? value : value.byteLength)
return
}
this.lruCache.put(key, typeof value == 'number' ? value : value.byteLength)
this.addMemorySize(value)
this.trimToSize()
}
get(key: string): ArrayBuffer | undefined {
if (!this.isInited) {
return
}
if (this.lruCache.get(key) !== undefined) {
// TODO 如何才能修改文件访问时间呢
return FileUtils.getInstance().readFileSync(this.path + key)
}
return undefined
}
// 移除键为key的缓存
remove(key: string): void {
if (key == null) {
throw new Error('key is null,checking the parameter');
}
if (!this.isInited) {
return
}
let remove: number | undefined = this.lruCache.remove(key)
if (remove !== undefined) {
FileUtils.getInstance().deleteFile(this.path + key)
this.removeMemorySize(remove)
}
}
async removeAll(): Promise<void> {
if (!this.isInited) {
return
}
this.lruCache.clear()
this.currentMemory = 0;
let filenames: string[] = await FileUtils.getInstance().ListFile(this.path)
for (let i = 0; i < filenames.length; i++) {
await FileUtils.getInstance().deleteFile(this.path + filenames[i])
}
}
size(): number {
return this.lruCache.length
}
// 移除较少使用的缓存数据
private trimToSize(): void {
while (true) {
if (this.currentMemory <= this.maxMemory || this.lruCache.isEmpty()) {
break
}
let delkey = this.lruCache.keys()[0]
let remove: number | undefined = this.lruCache.remove(delkey)
if (remove !== undefined) {
FileUtils.getInstance().deleteFile(this.path + delkey)
this.removeMemorySize(remove)
}
this.lruCache.remove(delkey)
}
}
private removeMemorySize(value: ArrayBuffer | number): void {
if (typeof value == 'number') {
this.currentMemory -= value
}
else if (value != undefined) {
this.currentMemory -= value.byteLength
LogUtil.debug('FileCache removeMemorySize: ' + value.byteLength + ' currentMemory:' + this.currentMemory)
}
}
private addMemorySize(value: ArrayBuffer | number): void {
if (typeof value == 'number') {
this.currentMemory += value
LogUtil.debug('FileCache addMemorySize: ' + value + ' currentMemory:' + this.currentMemory)
}
else if (value != undefined) {
this.currentMemory += value.byteLength
LogUtil.debug('FileCache addMemorySize: ' + value.byteLength + ' currentMemory:' + this.currentMemory)
}
}
/**
* 子线程里只写入缓存文件
* @param context
* @param key
* @param value
*/
static saveFileCacheOnlyFile(context: Context, key: string, value: ArrayBuffer, folder: string = FileCache.CACHE_FOLDER): boolean {
// 写文件
if (!FileUtils.getInstance()
.writeFileSync(context.cacheDir + FileUtils.SEPARATOR + folder + FileUtils.SEPARATOR + key, value)) {
FileUtils.getInstance().createFolderSync(context.cacheDir + FileUtils.SEPARATOR + folder + FileUtils.SEPARATOR)
FileUtils.getInstance()
.writeFileSync(context.cacheDir + FileUtils.SEPARATOR + folder + FileUtils.SEPARATOR + key, value)
}
return true
}
/**
* 子线程中,通过文件名,直接查找是否有文件缓存
* @param context
* @param key
* @returns
*/
static getFileCacheByFile(context: Context, key: string, folder: string = FileCache.CACHE_FOLDER): ArrayBuffer | undefined {
// 从文件获取查看是否有缓存
return FileUtils.getInstance()
.readFileSync(context.cacheDir + FileUtils.SEPARATOR + folder + FileUtils.SEPARATOR + key)
}
/**
* 获取key缓存数据绝对路径
*
* @params key 数值
*/
getFileToPath(key: string): string {
if(!!!key) {
throw new Error('key is null,checking the parameter')
}
let path = this.path + key
if(FileUtils.getInstance().exist(path)) {
return path
} else {
return ''
}
}
}