feat(cache): 启动时异步清理过期及超限磁盘缓存

This commit is contained in:
2026-07-27 19:02:54 +08:00
parent de1ce76fdf
commit e8be46f1a3
3 changed files with 105 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.config.SystemIntentAction;
import com.ttstd.dialer.data.cache.CacheManager;
import com.ttstd.dialer.manager.AppManager;
import com.ttstd.dialer.manager.MapManager;
import com.ttstd.dialer.manager.WeatherManager;
@@ -137,6 +138,9 @@ public class BaseApplication extends Application {
}
registerReceivers();
// 启动时异步清理磁盘缓存(过期淘汰 + 容量上限),不阻塞启动
CacheManager.getInstance(this).cleanupAsync();
}
}

View File

@@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.lang.reflect.Type;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多级缓存统一入口L1 内存(LruCache) → L2 磁盘(文件+Gson)。
@@ -19,6 +21,8 @@ public final class CacheManager {
private static volatile CacheManager sInstance;
private static final ExecutorService sCleanupExecutor = Executors.newSingleThreadExecutor();
private final MemoryCache mMemoryCache;
private final DiskCache mDiskCache;
@@ -105,4 +109,20 @@ public final class CacheManager {
mMemoryCache.clear();
mDiskCache.clear();
}
/**
* 异步清理磁盘缓存(过期淘汰 + 容量上限),可在主线程调用,内部切到工作线程。
* 建议在 Application 启动时调用一次。
*/
public void cleanupAsync() {
sCleanupExecutor.execute(this::cleanup);
}
/**
* 同步清理磁盘缓存,必须在工作线程调用。
*/
@WorkerThread
public void cleanup() {
mDiskCache.cleanup();
}
}

View File

@@ -24,6 +24,10 @@ import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* L2 磁盘缓存:文件 + Gson 序列化App 冷启动后仍可秒出数据。
@@ -41,6 +45,12 @@ public final class DiskCache {
private static final String CACHE_DIR_NAME = "api_disk_cache";
private static final String TMP_SUFFIX = ".tmp";
/**
* 容量上限:最多保留的缓存条目数(超出按文件修改时间淘汰最旧)。
* API 响应体普遍较小300 条足以覆盖常用接口并约束磁盘占用,可按需调整。
*/
private static final int MAX_ENTRIES = 300;
private static final String KEY_SAVE_TIME = "saveTime";
private static final String KEY_SOFT_TTL = "softTtl";
private static final String KEY_HARD_TTL = "hardTtl";
@@ -115,6 +125,7 @@ public final class DiskCache {
if (!tmp.renameTo(file)) {
Log.w(TAG, "Rename tmp cache file failed, key=" + key);
}
maybeEnforceCapacity();
} catch (IOException e) {
Log.w(TAG, "Write disk cache failed, key=" + key, e);
//noinspection ResultOfMethodCallIgnored
@@ -140,6 +151,76 @@ public final class DiskCache {
}
}
/**
* 启动 / 低内存时调用:删除已硬过期的条目,并强制容量上限。
* 必须在工作线程调用。
*/
@WorkerThread
public void cleanup() {
deleteExpired();
enforceCapacity();
}
/**
* 删除所有已硬过期的缓存文件(解析失败/损坏的也一并淘汰)。
*/
private void deleteExpired() {
File[] files = mCacheDir.listFiles();
if (files == null) {
return;
}
long now = System.currentTimeMillis();
for (File f : files) {
if (f.isFile() && isFileExpired(f, now)) {
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}
}
/**
* 仅解析头部的 saveTime/hardTtl 判断硬过期,避免反序列化整个 value。
*/
private boolean isFileExpired(@NonNull File file, long now) {
try (Reader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
long saveTime = root.get(KEY_SAVE_TIME).getAsLong();
long hardTtl = root.get(KEY_HARD_TTL).getAsLong();
return now - saveTime > hardTtl;
} catch (Exception e) {
// 解析失败视为损坏,直接淘汰
return true;
}
}
/**
* 超出 {@link #MAX_ENTRIES} 时,按文件修改时间(最旧优先)淘汰多余条目。
*/
private void enforceCapacity() {
File[] files = mCacheDir.listFiles();
if (files == null || files.length <= MAX_ENTRIES) {
return;
}
List<File> sorted = new ArrayList<>(Arrays.asList(files));
sorted.sort(Comparator.comparingLong(File::lastModified));
int toRemove = sorted.size() - MAX_ENTRIES;
for (int i = 0; i < toRemove; i++) {
//noinspection ResultOfMethodCallIgnored
sorted.get(i).delete();
}
}
/**
* 写入后轻量检查:仅在可能超限时才真正排序淘汰,避免每次 put 都遍历目录。
*/
private void maybeEnforceCapacity() {
File[] files = mCacheDir.listFiles();
if (files != null && files.length > MAX_ENTRIES) {
enforceCapacity();
}
}
private File fileFor(@NonNull String key) {
return new File(mCacheDir, md5(key) + ".json");
}