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.alarmclock.AlarmManagerHelper;
|
||||||
import com.ttstd.dialer.config.CommonConfig;
|
import com.ttstd.dialer.config.CommonConfig;
|
||||||
import com.ttstd.dialer.config.SystemIntentAction;
|
import com.ttstd.dialer.config.SystemIntentAction;
|
||||||
|
import com.ttstd.dialer.data.cache.CacheManager;
|
||||||
import com.ttstd.dialer.manager.AppManager;
|
import com.ttstd.dialer.manager.AppManager;
|
||||||
import com.ttstd.dialer.manager.MapManager;
|
import com.ttstd.dialer.manager.MapManager;
|
||||||
import com.ttstd.dialer.manager.WeatherManager;
|
import com.ttstd.dialer.manager.WeatherManager;
|
||||||
@@ -137,6 +138,9 @@ public class BaseApplication extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerReceivers();
|
registerReceivers();
|
||||||
|
|
||||||
|
// 启动时异步清理磁盘缓存(过期淘汰 + 容量上限),不阻塞启动
|
||||||
|
CacheManager.getInstance(this).cleanupAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多级缓存统一入口:L1 内存(LruCache) → L2 磁盘(文件+Gson)。
|
* 多级缓存统一入口:L1 内存(LruCache) → L2 磁盘(文件+Gson)。
|
||||||
@@ -19,6 +21,8 @@ public final class CacheManager {
|
|||||||
|
|
||||||
private static volatile CacheManager sInstance;
|
private static volatile CacheManager sInstance;
|
||||||
|
|
||||||
|
private static final ExecutorService sCleanupExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
private final MemoryCache mMemoryCache;
|
private final MemoryCache mMemoryCache;
|
||||||
private final DiskCache mDiskCache;
|
private final DiskCache mDiskCache;
|
||||||
|
|
||||||
@@ -105,4 +109,20 @@ public final class CacheManager {
|
|||||||
mMemoryCache.clear();
|
mMemoryCache.clear();
|
||||||
mDiskCache.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.lang.reflect.Type;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L2 磁盘缓存:文件 + Gson 序列化,App 冷启动后仍可秒出数据。
|
* 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 CACHE_DIR_NAME = "api_disk_cache";
|
||||||
private static final String TMP_SUFFIX = ".tmp";
|
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_SAVE_TIME = "saveTime";
|
||||||
private static final String KEY_SOFT_TTL = "softTtl";
|
private static final String KEY_SOFT_TTL = "softTtl";
|
||||||
private static final String KEY_HARD_TTL = "hardTtl";
|
private static final String KEY_HARD_TTL = "hardTtl";
|
||||||
@@ -115,6 +125,7 @@ public final class DiskCache {
|
|||||||
if (!tmp.renameTo(file)) {
|
if (!tmp.renameTo(file)) {
|
||||||
Log.w(TAG, "Rename tmp cache file failed, key=" + key);
|
Log.w(TAG, "Rename tmp cache file failed, key=" + key);
|
||||||
}
|
}
|
||||||
|
maybeEnforceCapacity();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, "Write disk cache failed, key=" + key, e);
|
Log.w(TAG, "Write disk cache failed, key=" + key, e);
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//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) {
|
private File fileFor(@NonNull String key) {
|
||||||
return new File(mCacheDir, md5(key) + ".json");
|
return new File(mCacheDir, md5(key) + ".json");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user