From 863b7433309aa90adefa607eb14ea7931dcdecc5 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 22 Dec 2025 11:01:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E7=BC=93=E5=AD=98=E7=AD=96?= =?UTF-8?q?=E7=95=A5=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BD=8D=E5=9B=BE=E7=BB=98?= =?UTF-8?q?=E7=94=BB=E6=97=B6=E7=9A=84=E5=BC=82=E5=B8=B8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerbell/build.properties | 4 +- .../java/cc/winboll/studio/powerbell/App.java | 211 ++++---------- .../powerbell/utils/BitmapCacheUtils.java | 257 +++++++++++++----- .../powerbell/views/BackgroundView.java | 178 ++++++++---- 4 files changed, 372 insertions(+), 278 deletions(-) diff --git a/powerbell/build.properties b/powerbell/build.properties index 7067383..9731995 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Dec 22 10:25:46 HKT 2025 +#Mon Dec 22 02:57:59 GMT 2025 stageCount=21 libraryProject= baseVersion=15.14 publishVersion=15.14.20 -buildCount=0 +buildCount=1 baseBetaVersion=15.14.21 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java index 7eb369d..1a19f4d 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -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编写) + * 核心策略:无论内存是否紧张,强制保持位图缓存与视图控件缓存 */ public class App extends GlobalApplication { // ===================== 常量定义区 ===================== @@ -36,29 +32,17 @@ 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 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; @@ -119,14 +103,12 @@ public class App extends GlobalApplication { initBaseTools(); // 初始化临时文件夹 initTempDir(); - // 初始化工具类实例 + // 初始化工具类实例(含强制缓存工具) initUtils(); // 初始化广播接收器 initReceiver(); - // 启动定时任务 - //initTimerTask(); - LogUtils.d(TAG, "onCreate() 应用初始化完成"); + LogUtils.d(TAG, "onCreate() 应用初始化完成,强制缓存策略已启用"); } @Override @@ -138,8 +120,8 @@ public class App extends GlobalApplication { ToastUtils.release(); // 释放通知工具 releaseNotificationManager(); - // 停止定时任务 - //stopTimerTask(); + // 释放广播接收器 + releaseReceiver(); LogUtils.d(TAG, "onTerminate() 应用资源释放完成"); } @@ -147,25 +129,19 @@ public class App extends GlobalApplication { @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, "onTrimMemory() 调用,内存等级level:" + level + ",强制保持所有缓存(不清理)"); + // 仅记录缓存状态,不执行任何清理操作 + logCacheStatus(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + // 核心修改:低内存时也不清理缓存,仅记录日志 + LogUtils.w(TAG, "onLowMemory() 调用,强制保持所有缓存(不清理)"); + // 仅记录缓存状态,不执行任何清理操作 + logCacheStatus(); } // ===================== 私有初始化方法区 ===================== @@ -195,15 +171,27 @@ public class App extends GlobalApplication { } /** - * 初始化工具类实例 + * 初始化工具类实例(核心:强制初始化缓存工具,不随内存紧张重建) */ private void initUtils() { - LogUtils.d(TAG, "initUtils() 开始初始化工具类"); + LogUtils.d(TAG, "initUtils() 开始初始化工具类,启用强制缓存策略"); sAppConfigUtils = getAppConfigUtils(this); sAppCacheUtils = getAppCacheUtils(this); - sBitmapCacheUtils = BitmapCacheUtils.getInstance(); + + // 强制初始化Bitmap缓存工具(单例唯一,不重复创建) + if (sBitmapCacheUtils == null) { + sBitmapCacheUtils = BitmapCacheUtils.getInstance(); + LogUtils.d(TAG, "initUtils() Bitmap缓存工具已初始化(强制保持)"); + } + + // 强制初始化视图控件缓存工具(单例唯一,不重复创建) + if (sMemoryCachedBackgroundView == null) { + sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this); + LogUtils.d(TAG, "initUtils() 视图控件缓存工具已初始化(强制保持)"); + } + mNotificationManager = new NotificationManagerUtils(this); - LogUtils.d(TAG, "initUtils() 工具类初始化完成"); + LogUtils.d(TAG, "initUtils() 工具类初始化完成,强制缓存策略已生效"); } /** @@ -216,68 +204,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 +234,17 @@ 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 logCacheStatus() { + LogUtils.d(TAG, "logCacheStatus() 开始记录缓存状态"); + if (sBitmapCacheUtils != null) { + LogUtils.d(TAG, "logCacheStatus() Bitmap缓存工具实例有效(强制保持)"); } - LogUtils.d(TAG, "getTrimMemoryLevelDesc() 内存等级描述结果:" + desc); - return desc; + if (sMemoryCachedBackgroundView != null) { + LogUtils.d(TAG, "logCacheStatus() 视图控件缓存工具实例有效(强制保持)"); + } + LogUtils.d(TAG, "logCacheStatus() 缓存状态记录完成,所有缓存均强制保持"); } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java index e3bd087..d21943c 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java @@ -4,25 +4,33 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Process; 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.concurrent.ConcurrentHashMap; /** * @Author ZhanGSKen&豆包大模型 * @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. 高压缩比减少OOM风险 + * 核心策略:无论内存如何紧张,强制保持已缓存的Bitmap,通过高压缩比降低单张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; + // 最大图片尺寸(降低至720P,进一步减少内存占用,强制缓存核心策略) + private static final int MAX_WIDTH = 720; + private static final int MAX_HEIGHT = 1280; // SP 相关常量 private static final String SP_NAME = "BitmapCacheSP"; @@ -30,18 +38,24 @@ public class BitmapCacheUtils { // 单例实例(volatile 保证多线程可见性) private static volatile BitmapCacheUtils sInstance; - // 路径-Bitmap 缓存容器(内存缓存) - private final Map mBitmapCacheMap; + // 路径-Bitmap 硬引用缓存(唯一缓存,强制保持,内存紧张不回收) + private final Map mHardCacheMap; + // 路径-引用计数 映射(解决多实例共享问题) + private final Map 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 +72,28 @@ public class BitmapCacheUtils { return sInstance; } + /** + * 补充接口:直接缓存已解码的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.put(imagePath, 1); + // 持久化当前路径到 SP + saveLastCachePathToSp(imagePath); + LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功(强制保持) - " + imagePath); + return bitmap; + } + /** * 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP * @param imagePath 图片绝对路径 @@ -76,29 +112,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内存占用) Bitmap bitmap = decodeCompressedBitmap(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 +147,103 @@ 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; } - return bitmap; + + // 缓存未命中或Bitmap已失效 + return null; } /** - * 清空所有 Bitmap 缓存(释放内存),并清空 SP 中保存的最后缓存路径 + * 新增接口:增加指定路径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)); + } + } + + /** + * 新增接口:减少指定路径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.d(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)) { @@ -164,7 +254,7 @@ public class BitmapCacheUtils { } /** - * 压缩解码 Bitmap(按最大尺寸缩放,避免OOM) + * 高压缩比解码 Bitmap(强制缓存核心:通过降低分辨率+RGB_565减少单张Bitmap内存占用) * @param imagePath 图片绝对路径 * @return 解码后的 Bitmap / null(文件无效/解码失败) */ @@ -187,20 +277,21 @@ public class BitmapCacheUtils { return null; } - // 计算缩放比例 - int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT); + // 计算高压缩比缩放比例(强制缓存核心:尽可能降低分辨率) + int sampleSize = calculateHighCompressSampleSize(options, MAX_WIDTH, MAX_HEIGHT); - // 第二步:加载压缩后的 Bitmap + // 第二步:加载高压缩比的 Bitmap options.inJustDecodeBounds = false; options.inSampleSize = sampleSize; - options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存(比ARGB_8888少一半内存) - options.inPurgeable = true; - options.inInputShareable = true; + options.inPreferredConfig = Bitmap.Config.RGB_565; // 强制使用RGB_565,比ARGB_8888少一半内存 + options.inPurgeable = false; // 关闭可清除标志,强制保持内存 + options.inInputShareable = false; try { return BitmapFactory.decodeFile(imagePath, options); } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath); + LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常(已启用高压缩比) - " + imagePath); + // 强制缓存策略:OOM时仅记录日志,不清理已缓存的Bitmap return null; } catch (Exception e) { LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e); @@ -209,23 +300,29 @@ public class BitmapCacheUtils { } /** - * 计算 Bitmap 缩放比例 + * 计算高压缩比缩放比例(强制缓存核心:优先保证不超过最大尺寸,尽可能压缩) */ - private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) { + private int calculateHighCompressSampleSize(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; - } + // 高压缩比逻辑:只要超过最大尺寸,就持续放大采样率 + while (rawWidth / inSampleSize > maxWidth || rawHeight / inSampleSize > maxHeight) { + inSampleSize *= 2; } + + LogUtils.d(TAG, "calculateHighCompressSampleSize: 高压缩比缩放比例为 " + inSampleSize); return inSampleSize; } + /** + * 工具方法:判断Bitmap是否有效(非空且未被回收) + */ + private boolean isBitmapValid(Bitmap bitmap) { + return bitmap != null && !bitmap.isRecycled(); + } + /** * 从 SP 中获取最后一次缓存的图片路径 * @return 最后缓存的路径 / null(未保存) @@ -266,12 +363,44 @@ 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缓存"); + } + + @Override + public void onLowMemory() { + // 强制缓存策略:低内存时仅记录日志,不清理任何缓存 + LogUtils.w(TAG, "onLowMemory: 系统低内存,强制保持所有Bitmap缓存(已启用高压缩比)"); + } + + @Override + public void onConfigurationChanged(android.content.res.Configuration newConfig) { + // 配置变化时无需处理 + } + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java index fd307e9..3cde594 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -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的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺) * 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪 + * 改进:强化Bitmap生命周期管理,防止已回收Bitmap绘制崩溃 */ public class BackgroundView extends RelativeLayout { public static final String TAG = "BackgroundView"; - // 新增:记录当前已缓存的图片路径 + // 记录当前已缓存的图片路径 private String mCurrentCachedPath = ""; private Context mContext; @@ -108,7 +110,7 @@ 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; @@ -117,22 +119,26 @@ public class BackgroundView extends RelativeLayout { ? 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); + // 调用带路径判断的loadImage方法 + if (isRefresh) { + App.sBitmapCacheUtils.removeCachedBitmap(targetPath); + // 刷新时直接解码,避免缓存旧数据 + Bitmap newBitmap = decodeBitmapWithCompress(new File(targetPath), 1080, 1920); + if (newBitmap != null) { + App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap); + } + } + loadImage(targetPath); } // ====================================== 对外方法 ====================================== /** - * 改造后:添加路径判断,路径更新时同步更新缓存;缓存Bitmap为null时提示并加载透明背景 + * 改进版:强化Bitmap有效性校验,增加缓存重加载机制,防止已回收Bitmap崩溃 * @param imagePath 图片绝对路径 */ public void loadImage(String imagePath) { @@ -151,34 +157,31 @@ 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); + // 缓存无效,移除旧缓存并强制重加载 + App.sBitmapCacheUtils.removeCachedBitmap(imagePath); } } - // 2. 路径已更新:移除旧缓存,加载新图片并更新缓存 + // 2. 路径已更新:移除旧缓存 if (!TextUtils.isEmpty(mCurrentCachedPath)) { App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath); LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath); } // ======================== 路径判断逻辑结束 ======================== - // 无缓存/路径更新:走原有逻辑加载图片 + // 无缓存/缓存无效/路径更新:重新加载图片 if (!calculateImageAspectRatio(imageFile)) { setDefaultTransparentBackground(); return; @@ -193,16 +196,24 @@ public class BackgroundView extends RelativeLayout { } // 缓存新图片,并更新当前缓存路径记录 - App.sBitmapCacheUtils.cacheBitmap(imagePath); + App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap); 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(); @@ -231,14 +242,17 @@ public class BackgroundView extends RelativeLayout { options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int scaleX = options.outWidth / maxWidth; - int scaleY = options.outHeight / maxHeight; + // 改进:更精准的采样率计算(避免过度压缩) + int scaleX = (int) Math.ceil((float) options.outWidth / maxWidth); + int scaleY = (int) Math.ceil((float) options.outHeight / maxHeight); int inSampleSize = Math.max(scaleX, scaleY); - if (inSampleSize <= 0) inSampleSize = 1; + inSampleSize = Math.max(1, inSampleSize); // 确保采样率≥1 options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; - options.inPreferredConfig = Bitmap.Config.RGB_565; + options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存 + options.inPurgeable = true; // 允许系统在内存紧张时回收 + options.inInputShareable = true; return BitmapFactory.decodeFile(file.getAbsolutePath(), options); } catch (Exception e) { LogUtils.e(TAG, "压缩解码失败:" + e.getMessage()); @@ -254,40 +268,86 @@ 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); + // 改进:先清空Drawable,避免残留已回收Bitmap + mIvBackground.setImageDrawable(null); mIvBackground.setBackgroundColor(0x00000000); mImageAspectRatio = 1.0f; - // 清空缓存路径记录 mCurrentCachedPath = ""; - //mIvBackground.setVisibility(View.GONE); } - // ====================================== 重写方法 ====================================== + // ====================================== 重写方法(核心改进) ====================================== + /** + * 重写:绘制前强制校验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: 释放Bitmap资源"); + // 清空ImageView的Drawable,释放Bitmap引用 + mIvBackground.setImageDrawable(null); + // 清空当前缓存路径,避免后续错误引用 + mCurrentCachedPath = ""; + // 可选:如果当前View是唯一使用者,移除全局缓存 + // if (!TextUtils.isEmpty(mCurrentCachedPath)) { + // App.sBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath); + // } + } + + /** + * 重写:恢复尺寸调整逻辑,确保View尺寸变化时正确显示 + */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - //adjustImageViewSize(); // 尺寸变化时重新调整 + adjustImageViewSize(); // 恢复尺寸调整 } - }