feat(cache): 启动时异步清理过期及超限磁盘缓存
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user