Compare commits

...

6 Commits

5 changed files with 558 additions and 373 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Dec 22 10:25:46 HKT 2025
stageCount=21
#Mon Dec 22 13:00:54 HKT 2025
stageCount=24
libraryProject=
baseVersion=15.14
publishVersion=15.14.20
publishVersion=15.14.23
buildCount=0
baseBetaVersion=15.14.21
baseBetaVersion=15.14.24

View File

@@ -1,15 +1,11 @@
package cc.winboll.studio.powerbell;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
@@ -17,10 +13,10 @@ import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
import java.io.File;
import java.util.concurrent.TimeUnit;
/**
* 应用全局入口类适配Android API 30基于Java 7编写
* 核心策略:极致强制缓存 - 无论内存紧张程度永不自动清理任何缓存Bitmap/视图控件/路径记录)
*/
public class App extends GlobalApplication {
// ===================== 常量定义区 =====================
@@ -36,29 +32,20 @@ public class App extends GlobalApplication {
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
// 内存紧张通知文案常量
private static final String TRIM_MEMORY_NOTIFY_TITLE = "应用使用时内存紧张提醒";
private static final String TRIM_MEMORY_NOTIFY_CONTENT = "由于本应用使用时,系统通知内存紧张程度级别较高,图片缓存功能暂时不启用。";
// 定时任务间隔常量(分钟)
//private static final long TIMER_INTERVAL_MINUTES = 1;
// 缓存防护常量
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
// ===================== 静态属性区 =====================
// 数据配置工具
private static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils;
// 全局Bitmap缓存工具
// 全局Bitmap缓存工具(极致强制保持:一旦初始化,永不销毁)
public static BitmapCacheUtils sBitmapCacheUtils;
// 全局视图控件缓存工具
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
public static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 临时文件夹路径
private static String sTempDirPath = "";
// 定时任务静态属性(全局唯一)
// private static Handler sTimerHandler;
// private static Runnable sTimerRunnable;
// private static boolean sIsTimerRunning = false;
//
// ===================== 成员属性区 =====================
// 全局广播接收器
private GlobalApplicationReceiver mGlobalReceiver;
@@ -105,6 +92,25 @@ public class App extends GlobalApplication {
sAppCacheUtils.clearBatteryHistory();
}
/**
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
*/
public static void manualClearAllCache() {
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存调用(极致强制缓存策略下,需谨慎使用)");
// 清理Bitmap缓存
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存已手动清理");
}
// 清理视图控件缓存(仅清除静态引用,不销毁实例)
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存实例保持,仅清除静态引用");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 手动清理缓存完成(部分缓存实例仍可能保留在内存中)");
}
// ===================== 生命周期方法区 =====================
@Override
public void onCreate() {
@@ -119,53 +125,48 @@ public class App extends GlobalApplication {
initBaseTools();
// 初始化临时文件夹
initTempDir();
// 初始化工具类实例
// 初始化工具类实例(核心:极致强制缓存,永不销毁)
initUtils();
// 初始化广播接收器
initReceiver();
// 启动定时任务
//initTimerTask();
LogUtils.d(TAG, "onCreate() 应用初始化完成");
LogUtils.d(TAG, "onCreate() 应用初始化完成,极致强制缓存策略已启用");
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放资源");
LogUtils.d(TAG, "onTerminate() 应用终止,开始释放非缓存资源");
// 释放Toast工具
ToastUtils.release();
// 释放通知工具
releaseNotificationManager();
// 停止定时任务
//stopTimerTask();
// 释放广播接收器
releaseReceiver();
LogUtils.d(TAG, "onTerminate() 应用资源释放完成");
// 核心修改:应用终止时也不清理缓存,保持静态实例
LogUtils.w(TAG, CACHE_PROTECT_TAG + " 应用终止,极致强制缓存策略生效,不清理任何缓存");
LogUtils.d(TAG, "onTerminate() 非缓存资源释放完成,缓存实例保持");
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
LogUtils.d(TAG, "onTrimMemory() 调用内存等级level" + level);
sMemoryCachedBackgroundView.clearAllCache();
sBitmapCacheUtils.clearAllCache();
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
sMemoryCachedBackgroundView.getLastInstance(this);
//
// // 初始化通知工具(若未初始化)
// if (mNotificationManager == null) {
// mNotificationManager = new NotificationManagerUtils(this);
// LogUtils.d(TAG, "onTrimMemory()NotificationManagerUtils实例已初始化");
// }
//
// // 内存紧张等级判断
// if (level > ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// sendTrimMemoryNotification(level);
// } else {
// sBitmapCacheUtils = BitmapCacheUtils.getInstance();
// LogUtils.d(TAG, "onTrimMemory()Bitmap缓存已启用");
// }
// 极致强制缓存:禁止任何缓存清理操作,仅记录日志
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onTrimMemory() 调用内存等级level" + level + ",极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
logDetailedCacheStatus();
}
@Override
public void onLowMemory() {
super.onLowMemory();
// 极致强制缓存:低内存时也不清理任何缓存
LogUtils.w(TAG, CACHE_PROTECT_TAG + " onLowMemory() 调用,极致强制保持所有缓存");
// 记录详细缓存状态,不执行任何清理
logDetailedCacheStatus();
}
// ===================== 私有初始化方法区 =====================
@@ -195,15 +196,23 @@ public class App extends GlobalApplication {
}
/**
* 初始化工具类实例
* 初始化工具类实例(核心:极致强制缓存,一旦初始化永不销毁)
*/
private void initUtils() {
LogUtils.d(TAG, "initUtils() 开始初始化工具类");
LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用极致强制缓存策略");
sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this);
// 极致强制初始化Bitmap缓存工具必初始化永不销毁
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化极致强制保持永不销毁");
// 极致强制初始化视图控件缓存工具(必初始化,永不销毁)
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(极致强制保持,永不销毁)");
mNotificationManager = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initUtils() 工具类初始化完成");
LogUtils.d(TAG, "initUtils() 工具类初始化完成,极致强制缓存策略已生效");
}
/**
@@ -216,68 +225,21 @@ public class App extends GlobalApplication {
LogUtils.d(TAG, "initReceiver() 广播接收器注册完成");
}
/**
* 初始化定时任务(全局唯一实例)
*/
// private void initTimerTask() {
// LogUtils.d(TAG, "initTimerTask() 开始初始化定时任务,当前运行状态:" + sIsTimerRunning);
//
// // 已运行则直接返回
// if (sIsTimerRunning) {
// LogUtils.d(TAG, "initTimerTask() 定时任务已在运行,无需重复启动");
// return;
// }
//
// // 初始化Handler
// if (sTimerHandler == null) {
// sTimerHandler = new Handler(Looper.getMainLooper());
// LogUtils.d(TAG, "initTimerTask() 定时任务Handler已初始化");
// }
//
// // 初始化Runnable
// if (sTimerRunnable == null) {
// sTimerRunnable = new Runnable() {
// @Override
// public void run() {
// try {
// LogUtils.d(TAG, "定时任务执行,间隔:" + TIMER_INTERVAL_MINUTES + "分钟");
// sBitmapCacheUtils = BitmapCacheUtils.getInstance();
// LogUtils.d(TAG, "定时任务Bitmap缓存已重新初始化");
// } catch (Exception e) {
// LogUtils.e(TAG, "定时任务执行异常:" + e.getMessage());
// } finally {
// if (sIsTimerRunning) {
// long delayMillis = TimeUnit.MINUTES.toMillis(TIMER_INTERVAL_MINUTES);
// sTimerHandler.postDelayed(this, delayMillis);
// LogUtils.d(TAG, "定时任务已预约下次执行,延迟:" + delayMillis + "ms");
// }
// }
// }
// };
// LogUtils.d(TAG, "initTimerTask() 定时任务Runnable已初始化");
// }
//
// // 启动任务
// sTimerHandler.post(sTimerRunnable);
// sIsTimerRunning = true;
// LogUtils.d(TAG, "initTimerTask() 定时任务已启动,间隔:" + TIMER_INTERVAL_MINUTES + "分钟");
// }
// ===================== 私有工具方法区 =====================
/**
* 停止定时任务
* 释放广播接收器资源
*/
// private void stopTimerTask() {
// LogUtils.d(TAG, "stopTimerTask() 开始停止定时任务");
// if (sTimerHandler != null && sTimerRunnable != null) {
// sTimerHandler.removeCallbacks(sTimerRunnable);
// sIsTimerRunning = false;
// LogUtils.d(TAG, "stopTimerTask() 定时任务已停止运行状态重置为false");
// } else {
// LogUtils.d(TAG, "stopTimerTask() 定时任务未初始化,无需停止");
// }
// }
//
private void releaseReceiver() {
LogUtils.d(TAG, "releaseReceiver() 开始释放广播接收器");
if (mGlobalReceiver != null) {
mGlobalReceiver.unregisterAction();
mGlobalReceiver = null;
LogUtils.d(TAG, "releaseReceiver() 广播接收器资源已释放");
} else {
LogUtils.d(TAG, "releaseReceiver() 广播接收器未初始化,无需释放");
}
}
/**
* 释放通知管理工具资源
*/
@@ -293,53 +255,29 @@ public class App extends GlobalApplication {
}
/**
* 发送内存紧张通知
* 记录详细缓存状态(用于调试,监控极致强制缓存效果)
*/
private void sendTrimMemoryNotification(int level) {
LogUtils.d(TAG, "sendTrimMemoryNotification() 调用内存等级level" + level);
NotificationMessage message = new NotificationMessage();
message.setTitle(TRIM_MEMORY_NOTIFY_TITLE);
String content = String.format("%s [ 缓存紧张级别描述: Level %d | %s ]",
TRIM_MEMORY_NOTIFY_CONTENT, level, getTrimMemoryLevelDesc(level));
message.setContent(content);
mNotificationManager.showConfigNotification(this, message);
LogUtils.d(TAG, "sendTrimMemoryNotification() 内存紧张通知已发送,内容" + content);
}
/**
* 转换内存等级为可读描述
*/
private String getTrimMemoryLevelDesc(int level) {
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 调用传入level" + level);
String desc;
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
desc = "TRIM_MEMORY_COMPLETE应用内存完全紧张";
break;
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
desc = "MODERATE中等内存紧张";
break;
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
desc = "BACKGROUND应用进入后台";
break;
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
desc = "BACKGROUND应用UI隐藏";
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
desc = "RUNNING_CRITICAL应用运行关键级紧张";
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
desc = "RUNNING_LOW应用运行低内存";
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
desc = "RUNNING_MODERATE应用运行中等内存紧张";
break;
default:
desc = "UNKNOWN(" + level + ")";
break;
private void logDetailedCacheStatus() {
LogUtils.d(TAG, "logDetailedCacheStatus() 开始记录详细缓存状态");
// Bitmap缓存状态
if (sBitmapCacheUtils != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存工具实例有效极致强制保持");
// 假设BitmapCacheUtils有获取缓存数量的方法
try {
int cacheCount = sBitmapCacheUtils.getCacheCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量" + cacheCount);
} catch (Exception e) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " Bitmap缓存数量获取失败不影响缓存");
}
}
LogUtils.d(TAG, "getTrimMemoryLevelDesc() 内存等级描述结果:" + desc);
return desc;
// 视图控件缓存状态
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件缓存工具实例有效(极致强制保持)");
// 记录视图实例总数
int viewInstanceCount = MemoryCachedBackgroundView.getInstanceCount();
LogUtils.d(TAG, CACHE_PROTECT_TAG + " 视图控件实例总数:" + viewInstanceCount);
}
LogUtils.d(TAG, "logDetailedCacheStatus() 详细缓存状态记录完成,所有缓存均极致强制保持");
}
}

View File

@@ -4,25 +4,28 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 01:57
* @Describe 单例 Bitmap 缓存工具类Java 7 兼容)
* @Describe 单例 Bitmap 缓存工具类Java 7 兼容)- 极致强制缓存版(无图片压缩)
* 功能:内存缓存 Bitmap支持路径关联缓存、全局获取、缓存清空、SP 持久化最后缓存路径、构造时预加载
* 特点1. 单例模式 2. 压缩加载避免OOM 3. 路径-Bitmap 映射 4. 线程安全 5. SP 持久化最后缓存路径 6. 构造时预加载
* 特点1. 单例模式 2. 硬引用唯一缓存(极致强制保持,任何情况不自动回收) 3. 路径-Bitmap 映射 4. 线程安全
* 5. SP 持久化最后缓存路径 6. 构造时预加载 7. 引用计数防误回收 8. 无图片压缩,保留原始品质
* 核心策略无论内存如何紧张强制保持已缓存的Bitmap保留图片原始品质永不自动清理
*/
public class BitmapCacheUtils {
public static final String TAG = "BitmapCacheUtils";
// 最大图片尺寸适配1080P屏幕可根据需求调整
private static final int MAX_WIDTH = 1080;
private static final int MAX_HEIGHT = 1920;
// 移除最大图片尺寸限制(不压缩图片
// 若需限制尺寸而非压缩品质,可保留此常量用于校验,不参与采样率计算
// SP 相关常量
private static final String SP_NAME = "BitmapCacheSP";
@@ -30,18 +33,24 @@ public class BitmapCacheUtils {
// 单例实例volatile 保证多线程可见性)
private static volatile BitmapCacheUtils sInstance;
// 路径-Bitmap 缓存容器(内存缓存
private final Map<String, Bitmap> mBitmapCacheMap;
// 路径-Bitmap 硬引用缓存(唯一缓存,极致强制保持,任何情况不自动回收
private final Map<String, Bitmap> mHardCacheMap;
// 路径-引用计数 映射(解决多实例共享问题,仅用于统计,不影响缓存生命周期)
private final Map<String, Integer> mRefCountMap;
// SP 实例(用于持久化最后缓存路径)
private final SharedPreferences mSp;
// 私有构造器(单例模式)
private BitmapCacheUtils() {
mBitmapCacheMap = new HashMap<>();
// 使用ConcurrentHashMap保证线程安全避免手动同步
mHardCacheMap = new ConcurrentHashMap<>();
mRefCountMap = new ConcurrentHashMap<>();
// 初始化 SP使用 App 全局上下文,避免内存泄漏)
mSp = App.getInstance().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
// 构造时自动预加载 SP 中保存的最后一次缓存路径的图片
preloadLastCachedBitmap();
// 注册内存状态监听(仅记录日志,不清理缓存)
registerMemoryStatusListener();
}
/**
@@ -58,6 +67,67 @@ public class BitmapCacheUtils {
return sInstance;
}
// ====================================== 保留核心监控方法保证与App类兼容 ======================================
/**
* 获取当前缓存的Bitmap数量App类调用专用
* @return 缓存的Bitmap数量
*/
public int getCacheCount() {
int count = mHardCacheMap.size();
LogUtils.d(TAG, "getCacheCount: 当前缓存Bitmap数量 - " + count);
return count;
}
/**
* 获取当前缓存的所有图片路径集合
* @return 路径集合
*/
public Set<String> getCachedPaths() {
return mHardCacheMap.keySet();
}
/**
* 估算当前缓存的总内存占用(单位:字节)
* @return 总内存占用
*/
public long getTotalCacheSize() {
long totalSize = 0;
for (Bitmap bitmap : mHardCacheMap.values()) {
if (isBitmapValid(bitmap)) {
if (Build.VERSION.SDK_INT >= 12) {
totalSize += bitmap.getByteCount();
} else {
totalSize += bitmap.getRowBytes() * bitmap.getHeight();
}
}
}
LogUtils.d(TAG, "getTotalCacheSize: 当前缓存总内存占用 - " + totalSize + " 字节");
return totalSize;
}
// ====================================== 核心接口:缓存操作(无压缩) ======================================
/**
* 补充接口直接缓存已解码的Bitmap适配BackgroundView改进需求
* @param imagePath 图片绝对路径
* @param bitmap 已解码的有效Bitmap
* @return 缓存后的Bitmap / null参数无效
*/
public Bitmap cacheBitmap(String imagePath, Bitmap bitmap) {
if (TextUtils.isEmpty(imagePath) || !isBitmapValid(bitmap)) {
LogUtils.e(TAG, "cacheBitmap: 路径或Bitmap无效");
return null;
}
// 极致强制:直接存入硬引用缓存,覆盖旧值(若存在)
mHardCacheMap.put(imagePath, bitmap);
// 初始化引用计数为1若不存在
mRefCountMap.putIfAbsent(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功极致强制保持无压缩 - " + imagePath);
return bitmap;
}
/**
* 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP
* @param imagePath 图片绝对路径
@@ -76,29 +146,26 @@ public class BitmapCacheUtils {
}
// 已缓存则直接返回,避免重复加载
if (mBitmapCacheMap.containsKey(imagePath)) {
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath);
// 额外校验缓存的Bitmap是否有效
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
// 持久化当前路径到 SP(更新最后缓存路径)
saveLastCachePathToSp(imagePath);
return cachedBitmap;
} else {
// 缓存的Bitmap已失效移除后重新加载
mBitmapCacheMap.remove(imagePath);
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效移除后重新加载 - " + imagePath);
}
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
if (isBitmapValid(hardCacheBitmap)) {
LogUtils.d(TAG, "cacheBitmap: 硬引用缓存命中,引用计数+1 - " + imagePath);
// 引用计数+1
increaseRefCount(imagePath);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
return hardCacheBitmap;
}
// 压缩加载 Bitmap避免OOM
Bitmap bitmap = decodeCompressedBitmap(imagePath);
// 压缩解码 Bitmap保留原始品质
Bitmap bitmap = decodeOriginalBitmap(imagePath);
if (bitmap != null) {
// 存入缓存容器
mBitmapCacheMap.put(imagePath, bitmap);
// 持久化当前路径到 SP更新最后缓存路径
// 极致强制:存入硬引用缓存,永不自动回收
mHardCacheMap.put(imagePath, bitmap);
// 初始化引用计数为1
mRefCountMap.put(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径 - " + imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(极致强制保持,无压缩) - " + imagePath);
} else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
}
@@ -114,46 +181,106 @@ public class BitmapCacheUtils {
if (TextUtils.isEmpty(imagePath)) {
return null;
}
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
// 校验Bitmap是否有效
if (bitmap != null && bitmap.isRecycled()) {
mBitmapCacheMap.remove(imagePath);
return null;
// 仅从硬引用缓存获取,无任何 fallback
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
if (isBitmapValid(hardCacheBitmap)) {
return hardCacheBitmap;
}
// 缓存未命中或Bitmap已失效极致强制策略下理论上不会出现已回收情况
LogUtils.w(TAG, "getCachedBitmap: 缓存未命中或Bitmap已失效 - " + imagePath);
return null;
}
// ====================================== 引用计数管理(仅统计,不影响缓存) ======================================
/**
* 新增接口增加指定路径Bitmap的引用计数
* @param imagePath 图片绝对路径
*/
public void increaseRefCount(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return;
}
synchronized (mRefCountMap) {
Integer count = mRefCountMap.get(imagePath);
if (count == null) {
mRefCountMap.put(imagePath, 1);
} else {
mRefCountMap.put(imagePath, count + 1);
}
LogUtils.d(TAG, "increaseRefCount: " + imagePath + " 引用计数变为 " + mRefCountMap.get(imagePath));
}
return bitmap;
}
/**
* 清空所有 Bitmap 缓存(释放内存),并清空 SP 中保存的最后缓存路径
* 新增接口减少指定路径Bitmap的引用计数计数为0时仅标记不回收极致强制缓存策略
* @param imagePath 图片绝对路径
*/
public void decreaseRefCount(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return;
}
synchronized (mRefCountMap) {
Integer count = mRefCountMap.get(imagePath);
if (count == null || count <= 0) {
return;
}
int newCount = count - 1;
if (newCount <= 0) {
// 极致强制缓存策略引用计数为0时仅移除计数绝对不回收Bitmap
mRefCountMap.remove(imagePath);
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数为0极致强制保持Bitmap");
} else {
mRefCountMap.put(imagePath, newCount);
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数变为 " + newCount);
}
}
}
// ====================================== 缓存清理(仅手动调用,永不自动执行) ======================================
/**
* 清空所有 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
*/
public void clearAllCache() {
synchronized (mBitmapCacheMap) {
for (Bitmap bitmap : mBitmapCacheMap.values()) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); // 主动回收 Bitmap
}
LogUtils.w(TAG, "clearAllCache: 手动清空所有缓存(极致强制缓存策略下,需谨慎使用)");
// 清空硬引用缓存并回收Bitmap
for (Bitmap bitmap : mHardCacheMap.values()) {
if (isBitmapValid(bitmap)) {
bitmap.recycle();
}
mBitmapCacheMap.clear();
}
mHardCacheMap.clear();
// 清空引用计数
mRefCountMap.clear();
// 清空 SP 中保存的最后缓存路径
clearLastCachePathInSp();
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空SP 路径已清除");
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
}
/**
* 移除指定路径的 Bitmap 缓存
* 移除指定路径的 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
* @param imagePath 图片绝对路径
*/
public void removeCachedBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return;
}
synchronized (mBitmapCacheMap) {
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
synchronized (mRefCountMap) {
// 手动移除时才回收Bitmap
Bitmap hardBitmap = mHardCacheMap.remove(imagePath);
if (isBitmapValid(hardBitmap)) {
hardBitmap.recycle();
LogUtils.d(TAG, "removeCachedBitmap: 手动回收硬引用缓存 - " + imagePath);
}
mRefCountMap.remove(imagePath);
// 若移除的是最后缓存的路径,清空 SP
String lastPath = getLastCachePathFromSp();
if (imagePath.equals(lastPath)) {
@@ -163,69 +290,62 @@ public class BitmapCacheUtils {
}
}
// ====================================== 内部工具方法(无压缩解码) ======================================
/**
* 压缩解码 Bitmap按最大尺寸缩放避免OOM
* 压缩解码 Bitmap保留原始品质
* @param imagePath 图片绝对路径
* @return 解码后的 Bitmap / null文件无效/解码失败)
*/
private Bitmap decodeCompressedBitmap(String imagePath) {
private Bitmap decodeOriginalBitmap(String imagePath) {
// 前置校验:确保文件有效
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath);
LogUtils.e(TAG, "decodeOriginalBitmap: 文件无效,跳过解码 - " + imagePath);
return null;
}
BitmapFactory.Options options = new BitmapFactory.Options();
// 第一步:只获取图片尺寸,不加载像素
// 仅获取尺寸用于日志记录,不参与解码逻辑
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options);
// 校验尺寸是否有效
if (options.outWidth <= 0 || options.outHeight <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath);
LogUtils.e(TAG, "decodeOriginalBitmap: 图片尺寸无效 - " + imagePath);
return null;
}
// 计算缩放比例
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
LogUtils.d(TAG, "decodeOriginalBitmap: 图片原始尺寸 - " + options.outWidth + "x" + options.outHeight);
// 第二步:加载压缩后的 Bitmap
// 无压缩解码配置
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存比ARGB_8888少一半内存
options.inPurgeable = true;
options.inInputShareable = true;
options.inSampleSize = 1; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 保留全彩品质(如需节省内存可改为RGB_565不影响品质
options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存
options.inInputShareable = false;
options.inDither = true; // 开启抖动,保证色彩还原
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
try {
return BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
LogUtils.e(TAG, "decodeOriginalBitmap: OOM异常(无压缩,图片尺寸过大) - " + imagePath);
// 极致强制缓存策略OOM时仅放弃当前解码绝对不清理已缓存的Bitmap
return null;
} catch (Exception e) {
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
LogUtils.e(TAG, "decodeOriginalBitmap: 解码异常 - " + imagePath, e);
return null;
}
}
/**
* 计算 Bitmap 缩放比例
* 工具方法判断Bitmap是否有效非空且未被回收
*/
private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > maxWidth || rawHeight > maxHeight) {
int halfWidth = rawWidth / 2;
int halfHeight = rawHeight / 2;
while ((halfWidth / inSampleSize) >= maxWidth && (halfHeight / inSampleSize) >= maxHeight) {
inSampleSize *= 2;
}
}
return inSampleSize;
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
}
// ====================================== SP 持久化相关 ======================================
/**
* 从 SP 中获取最后一次缓存的图片路径
* @return 最后缓存的路径 / null未保存
@@ -254,6 +374,7 @@ public class BitmapCacheUtils {
LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空");
}
// ====================================== 预加载相关 ======================================
/**
* 构造时预加载 SP 中保存的最后一次缓存路径的图片
*/
@@ -266,12 +387,57 @@ public class BitmapCacheUtils {
// 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断)
Bitmap bitmap = cacheBitmap(lastPath);
if (bitmap != null) {
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功 - " + lastPath);
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功(极致强制保持,无压缩) - " + lastPath);
} else {
LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath);
// 预加载失败,清空 SP 中无效路径
clearLastCachePathInSp();
}
}
// ====================================== 内存状态监听(仅记录日志) ======================================
/**
* 注册内存状态监听(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private void registerMemoryStatusListener() {
if (Build.VERSION.SDK_INT >= 14) {
App.getInstance().registerComponentCallbacks(new MemoryStatusCallback());
LogUtils.d(TAG, "registerMemoryStatusListener: 内存状态监听已注册(仅记录日志,不清理缓存)");
}
}
/**
* 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private class MemoryStatusCallback implements android.content.ComponentCallbacks2 {
@Override
public void onTrimMemory(int level) {
// 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + "极致强制保持所有Bitmap缓存无压缩");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onLowMemory() {
// 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onLowMemory: 系统低内存极致强制保持所有Bitmap缓存无压缩");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
// 配置变化时无需处理
}
}
/**
* 记录当前缓存状态(用于内存紧张时的调试)
*/
private void logCurrentCacheStatus() {
LogUtils.d(TAG, "logCurrentCacheStatus: 缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节");
LogUtils.d(TAG, "logCurrentCacheStatus: 缓存路径 - " + getCachedPaths().toString());
}
}

View File

@@ -3,7 +3,9 @@ package cc.winboll.studio.powerbell.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -15,17 +17,17 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File;
/**
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪、无压缩
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
*/
public class BackgroundView extends RelativeLayout {
public static final String TAG = "BackgroundView";
// 新增:记录当前已缓存的图片路径
// 记录当前已缓存的图片路径
private String mCurrentCachedPath = "";
private Context mContext;
@@ -78,8 +80,8 @@ public class BackgroundView extends RelativeLayout {
mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL);
@@ -94,11 +96,11 @@ public class BackgroundView extends RelativeLayout {
mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺(无拉伸裁剪)
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ===");
@@ -108,31 +110,37 @@ public class BackgroundView extends RelativeLayout {
loadByBackgroundBean(bean, false);
}
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
return;
}
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
return;
}
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
return;
}
// 调用带路径判断的loadImage方法
if (isRefresh) {
App.sBitmapCacheUtils.removeCachedBitmap(targetPath);
App.sBitmapCacheUtils.cacheBitmap(targetPath);
}
loadImage(targetPath);
// 核心修改:刷新时不删除旧缓存,仅重新解码原始品质图片并更新缓存(强制保持策略)
if (isRefresh) {
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存 - " + targetPath);
// 刷新时直接解码原始品质图片,更新缓存(不删除旧缓存)
Bitmap newBitmap = decodeOriginalBitmap(new File(targetPath));
if (newBitmap != null) {
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
// 增加引用计数
App.sBitmapCacheUtils.increaseRefCount(targetPath);
}
}
loadImage(targetPath);
}
// ====================================== 对外方法 ======================================
/**
* 改造后添加路径判断路径更新时同步更新缓存缓存Bitmap为null时提示并加载透明背景
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
@@ -151,58 +159,72 @@ public class BackgroundView extends RelativeLayout {
mIvBackground.setVisibility(View.GONE);
// ======================== 新增:路径判断逻辑 ========================
// 1. 路径未变化:直接使用缓存
// ======================== 路径判断逻辑(强制缓存版) ========================
// 1. 路径未变化:校验缓存有效性,无效则重加载(不删除旧缓存)
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
// 核心修改判断缓存Bitmap是否为null
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
return;
} else {
// 缓存Bitmap为空或已回收提示并加载透明背景
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
ToastUtils.show("全局位图缓存为空,无法加载图片");
setDefaultTransparentBackground();
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(cachedBitmap)) {
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap原始品质");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
return;
} else {
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载原始品质图片 - " + imagePath);
// 缓存无效,直接重加载原始品质图片(不删除旧缓存,强制保持策略)
}
}
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
// 2. 路径已更新:保留旧缓存,仅更新当前路径记录(核心修改:不删除旧缓存
if (!TextUtils.isEmpty(mCurrentCachedPath) && !mCurrentCachedPath.equals(imagePath)) {
LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,当前路径从 " + mCurrentCachedPath + " 切换到 " + imagePath);
// 仅更新当前路径记录,不删除旧缓存
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/路径更新:走原有逻辑加载图片
// 无缓存/缓存无效/路径更新:重新加载原始品质图片
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
// 先尝试从缓存获取
Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(bitmap)) {
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap原始品质 - " + imagePath);
} else {
// 缓存无效应解码原始品质图片
bitmap = decodeOriginalBitmap(imageFile);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 缓存新图片(强制保持,原始品质)
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
LogUtils.d(TAG, "loadImage: 加载新图片并缓存(原始品质) - " + imagePath);
}
// 缓存新图片,并更新当前缓存路径记录
App.sBitmapCacheUtils.cacheBitmap(imagePath);
// 增加引用计数(配合全局缓存工具的引用计数机制)
App.sBitmapCacheUtils.increaseRefCount(imagePath);
// 更新当前缓存路径记录
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
// 直接使用setImageBitmap避免BitmapDrawable包装的引用风险
mIvBackground.setImageBitmap(bitmap);
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成 ===");
}
// ====================================== 内部工具方法 ======================================
/**
* 工具方法判断Bitmap是否有效非空且未被回收
*/
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
}
private boolean calculateImageAspectRatio(File file) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
@@ -225,23 +247,22 @@ public class BackgroundView extends RelativeLayout {
}
}
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
/**
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
*/
private Bitmap decodeOriginalBitmap(File file) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int scaleX = options.outWidth / maxWidth;
int scaleY = options.outHeight / maxHeight;
int inSampleSize = Math.max(scaleX, scaleY);
if (inSampleSize <= 0) inSampleSize = 1;
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 核心配置:无缩放、全彩品质、关闭可回收标志(强制保持)
options.inSampleSize = 1; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 全彩品质,无色彩损失
options.inPurgeable = false; // 关闭可回收标志,防止系统主动回收
options.inInputShareable = false;
options.inDither = true; // 开启抖动,保证色彩还原精度
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (Exception e) {
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
LogUtils.e(TAG, "原始品质解码失败:" + e.getMessage());
return null;
}
}
@@ -254,40 +275,89 @@ public class BackgroundView extends RelativeLayout {
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth != 0 && llHeight != 0) {
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
ivHeight = (int) (ivWidth / mImageAspectRatio);
} else {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
if (llWidth == 0 || llHeight == 0) {
LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整");
// 延迟调整(容器尺寸未就绪时)
post(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
});
return;
}
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
ivHeight = (int) (ivWidth / mImageAspectRatio);
} else {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 仅居中平铺,无缩放
mIvBackground.setVisibility(View.VISIBLE);
}
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
// 清空ImageView的Drawable释放本地引用不影响全局缓存
mIvBackground.setImageDrawable(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
// 清空缓存路径记录
mCurrentCachedPath = "";
//mIvBackground.setVisibility(View.GONE);
// 核心修改:路径清空时减少引用计数,不删除缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
}
// ====================================== 重写方法 ======================================
// ====================================== 重写方法(核心改进) ======================================
/**
* 重写绘制前强制校验Bitmap有效性防止已回收Bitmap崩溃
*/
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = mIvBackground.getDrawable();
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
Bitmap bitmap = bitmapDrawable.getBitmap();
if (!isBitmapValid(bitmap)) {
LogUtils.e(TAG, "onDraw: 检测到已回收Bitmap清空本地绘制保留全局缓存");
mIvBackground.setImageDrawable(null);
return;
}
}
super.onDraw(canvas);
}
/**
* 重写View从窗口移除时仅减少引用计数不删除全局缓存强制保持策略
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,保留全局缓存");
// 清空ImageView的Drawable释放本地引用
mIvBackground.setImageDrawable(null);
// 核心修改:仅减少引用计数,不删除全局缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
}
/**
* 重写恢复尺寸调整逻辑确保View尺寸变化时正确显示无压缩
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//adjustImageViewSize(); // 尺寸变化时重新调整
adjustImageViewSize(); // 恢复尺寸调整
}
}

View File

@@ -10,15 +10,18 @@ import cc.winboll.studio.powerbell.models.BackgroundBean;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/21 20:43
* @Describe 单实例缓存版背景视图控件基于Java7
* @Describe 单实例缓存版背景视图控件基于Java7- 强制缓存版
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
* 新增SP持久化最后加载路径、获取最后加载实例功能
* 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录
*/
public class MemoryCachedBackgroundView extends BackgroundView {
public static final String TAG = "MemoryCachedBackgroundView";
// 静态属性:保存当前缓存的路径和实例(替代原Map仅维护单实例
// 静态属性:保存当前缓存的路径和实例(强制保持,不自动销毁
private static String sCachedImagePath;
private static MemoryCachedBackgroundView sCachedView;
// 新增:记录所有创建过的实例数量(用于强制缓存监控)
private static int sInstanceCount = 0;
// SP相关常量
private static final String SP_NAME = "MemoryCachedBackgroundView_SP";
private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path";
@@ -26,29 +29,32 @@ public class MemoryCachedBackgroundView extends BackgroundView {
// ====================================== 构造器(继承并兼容父类) ======================================
private MemoryCachedBackgroundView(Context context) {
super(context);
LogUtils.d(TAG, "构造器1创建MemoryCachedBackgroundView实例");
sInstanceCount++;
LogUtils.d(TAG, "构造器1创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
LogUtils.d(TAG, "构造器2创建MemoryCachedBackgroundView实例");
sInstanceCount++;
LogUtils.d(TAG, "构造器2创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "构造器3创建MemoryCachedBackgroundView实例");
sInstanceCount++;
LogUtils.d(TAG, "构造器3创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
// ====================================== 核心静态方法:获取/创建缓存实例 ======================================
// ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ======================================
/**
* 从缓存获取或创建MemoryCachedBackgroundView实例
* 从缓存获取或创建MemoryCachedBackgroundView实例(强制保持旧实例)
* @param context 上下文
* @param imagePath 图片绝对路径(作为缓存标识)
* @param isReload 是否强制重新加载图片(路径匹配时仍刷新)
* @return 缓存/新创建的MemoryCachedBackgroundView实例
*/
public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) {
LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload);
LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload + " | 当前实例总数:" + sInstanceCount);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "getInstance():图片路径为空,创建空实例");
return new MemoryCachedBackgroundView(context);
@@ -66,24 +72,27 @@ public class MemoryCachedBackgroundView extends BackgroundView {
return sCachedView;
}
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存
LogUtils.d(TAG, "getInstance():路径未缓存,新建实例 | " + imagePath);
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心修改:保留旧实例,仅更新引用)
LogUtils.d(TAG, "getInstance():路径未缓存,新建实例(保留旧实例) | " + imagePath);
MemoryCachedBackgroundView oldView = sCachedView; // 保留旧实例引用,防止被销毁
String oldPath = sCachedImagePath;
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = imagePath;
sCachedView.loadImage(imagePath);
LogUtils.d(TAG, "getInstance():已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)");
return sCachedView;
}
// ====================================== 新增功能:获取最后加载的实例 ======================================
// ====================================== 新增功能:获取最后加载的实例(强制缓存版) ======================================
/**
* 获取最后一次loadImage的路径对应的实例
* 获取最后一次loadImage的路径对应的实例(强制保持所有实例)
* 无实例则创建并加载图片,同时更新静态缓存
* @param context 上下文
* @return 最后加载路径对应的实例
*/
public static MemoryCachedBackgroundView getLastInstance(Context context) {
LogUtils.d(TAG, "getLastInstance() 调用");
// 1. 从SP获取最后加载的路径
LogUtils.d(TAG, "getLastInstance() 调用 | 当前实例总数:" + sInstanceCount);
// 1. 从SP获取最后加载的路径(强制保持,不自动删除)
String lastPath = getLastLoadImagePath(context);
if (TextUtils.isEmpty(lastPath)) {
LogUtils.e(TAG, "getLastInstance():无最后加载路径,创建空实例");
@@ -96,17 +105,20 @@ public class MemoryCachedBackgroundView extends BackgroundView {
return sCachedView;
}
// 3. 路径不匹配 → 新建实例并更新缓存
LogUtils.d(TAG, "getLastInstance():最后路径未缓存,新建实例并加载 | " + lastPath);
// 3. 路径不匹配 → 新建实例并更新缓存(保留旧实例)
LogUtils.d(TAG, "getLastInstance():最后路径未缓存,新建实例并加载(保留旧实例) | " + lastPath);
MemoryCachedBackgroundView oldView = sCachedView;
String oldPath = sCachedImagePath;
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = lastPath;
sCachedView.loadImage(lastPath);
LogUtils.d(TAG, "getLastInstance():已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)");
return sCachedView;
}
// ====================================== 工具方法SP持久化最后加载路径 ======================================
// ====================================== 工具方法SP持久化最后加载路径(强制保持版) ======================================
/**
* 保存最后一次loadImage的路径到SP
* 保存最后一次loadImage的路径到SP(强制保持,不自动删除)
* @param context 上下文
* @param imagePath 图片路径
*/
@@ -116,11 +128,11 @@ public class MemoryCachedBackgroundView extends BackgroundView {
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply();
LogUtils.d(TAG, "saveLastLoadImagePath():已保存最后路径 | " + imagePath);
LogUtils.d(TAG, "saveLastLoadImagePath():已保存最后路径(强制保持) | " + imagePath);
}
/**
* 从SP获取最后一次loadImage的路径
* 从SP获取最后一次loadImage的路径(强制保持,不自动删除)
* @param context 上下文
* @return 最后加载的图片路径空则返回null
*/
@@ -130,49 +142,41 @@ public class MemoryCachedBackgroundView extends BackgroundView {
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null);
LogUtils.d(TAG, "getLastLoadImagePath():获取最后路径 | " + lastPath);
LogUtils.d(TAG, "getLastLoadImagePath():获取最后路径(强制保持) | " + lastPath);
return lastPath;
}
// ====================================== 工具方法:缓存管理 ======================================
// ====================================== 工具方法:缓存管理(强制缓存版 - 仅日志,不清理) ======================================
/**
* 清除当前缓存实例和路径
* 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理)
*/
public static void clearCache() {
LogUtils.d(TAG, "clearCache() 调用 | 当前缓存路径:" + sCachedImagePath);
sCachedView = null;
sCachedImagePath = null;
LogUtils.d(TAG, "clearCache():已清除当前缓存实例");
LogUtils.w(TAG, "clearCache() 调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath);
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearCache():强制缓存策略生效,未清除任何实例和路径");
}
/**
* 清除指定路径的缓存(仅当路径匹配当前缓存时生效
* 清除指定路径的缓存(强制缓存策略:仅日志,不实际清理
* @param imagePath 图片路径
*/
public static void removeCache(String imagePath) {
LogUtils.d(TAG, "removeCache() 调用 | 图片路径:" + imagePath);
LogUtils.w(TAG, "removeCache() 调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "removeCache():图片路径为空,清除失败");
return;
}
if (imagePath.equals(sCachedImagePath)) {
clearCache();
// 同步删除SP中最后路径记录
clearLastLoadImagePath(getContextFromCache());
LogUtils.d(TAG, "removeCache():已清除匹配路径的缓存 | " + imagePath);
} else {
LogUtils.d(TAG, "removeCache():路径不匹配当前缓存,无需清除 | " + imagePath);
}
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "removeCache():强制缓存策略生效,未清除任何实例和路径");
}
/**
* 清除所有缓存(同clearCache保持方法兼容性
* 清除所有缓存(强制缓存策略:仅日志,不实际清理
*/
public static void clearAllCache() {
LogUtils.d(TAG, "clearAllCache() 调用");
clearCache();
clearLastLoadImagePath(getContextFromCache());
LogUtils.d(TAG, "clearAllCache():已清除所有缓存及最后路径记录");
LogUtils.w(TAG, "clearAllCache() 调用(强制缓存策略:不实际清理缓存)");
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearAllCache()强制缓存策略生效未清除任何实例、路径和SP记录");
}
/**
@@ -184,16 +188,13 @@ public class MemoryCachedBackgroundView extends BackgroundView {
}
/**
* 清除SP中最后加载的路径记录
* 清除SP中最后加载的路径记录(强制缓存策略:仅日志,不实际清理)
* @param context 上下文
*/
public static void clearLastLoadImagePath(Context context) {
if (context == null) {
return;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().remove(KEY_LAST_LOAD_IMAGE_PATH).apply();
LogUtils.d(TAG, "clearLastLoadImagePath():已清除最后路径记录");
LogUtils.w(TAG, "clearLastLoadImagePath() 调用强制缓存策略不实际清理SP记录");
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearLastLoadImagePath()强制缓存策略生效未清除SP中最后路径记录");
}
// ====================================== 辅助方法:从缓存获取上下文 ======================================
@@ -205,12 +206,12 @@ public class MemoryCachedBackgroundView extends BackgroundView {
return sCachedView != null ? sCachedView.getContext() : null;
}
// ====================================== 重写父类方法:增强日志+SP持久化 ======================================
// ====================================== 重写父类方法:增强日志+SP持久化(强制保持版) ======================================
@Override
public void loadImage(String imagePath) {
LogUtils.d(TAG, "loadImage() 重载方法调用 | 图片路径:" + imagePath);
super.loadImage(imagePath);
// 保存最后加载路径到SP
// 保存最后加载路径到SP(强制保持,不自动删除)
saveLastLoadImagePath(getContext(), imagePath);
}
@@ -225,5 +226,15 @@ public class MemoryCachedBackgroundView extends BackgroundView {
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
super.loadByBackgroundBean(bean, isRefresh);
}
// ====================================== 新增:强制缓存监控方法 ======================================
/**
* 获取当前所有创建过的实例总数(用于监控强制缓存状态)
* @return 实例总数
*/
public static int getInstanceCount() {
LogUtils.d(TAG, "getInstanceCount() 调用 | 当前实例总数:" + sInstanceCount);
return sInstanceCount;
}
}