Compare commits

...

2 Commits

3 changed files with 61 additions and 91 deletions

View File

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

View File

@@ -16,17 +16,16 @@ 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. 硬引用唯一缓存(极致强制保持,任何情况不自动回收) 3. 路径-Bitmap 映射 4. 线程安全
* 5. SP 持久化最后缓存路径 6. 构造时预加载 7. 引用计数防误回收 8. 高压缩比减少OOM风险
* 核心策略无论内存如何紧张强制保持已缓存的Bitmap通过高压缩比降低单张Bitmap内存占用,永不自动清理
* 5. SP 持久化最后缓存路径 6. 构造时预加载 7. 引用计数防误回收 8. 无图片压缩,保留原始品质
* 核心策略无论内存如何紧张强制保持已缓存的Bitmap保留图片原始品质,永不自动清理
*/
public class BitmapCacheUtils {
public static final String TAG = "BitmapCacheUtils";
// 最大图片尺寸降低至720P进一步减少内存占用强制缓存核心策略
private static final int MAX_WIDTH = 720;
private static final int MAX_HEIGHT = 1280;
// 移除最大图片尺寸限制(不压缩图片
// 若需限制尺寸而非压缩品质,可保留此常量用于校验,不参与采样率计算
// SP 相关常量
private static final String SP_NAME = "BitmapCacheSP";
@@ -68,7 +67,7 @@ public class BitmapCacheUtils {
return sInstance;
}
// ====================================== 新增核心方法解决App类调用问题 ======================================
// ====================================== 保留核心监控方法保证与App类兼容 ======================================
/**
* 获取当前缓存的Bitmap数量App类调用专用
* @return 缓存的Bitmap数量
@@ -106,7 +105,7 @@ public class BitmapCacheUtils {
return totalSize;
}
// ====================================== 核心接口:缓存操作 ======================================
// ====================================== 核心接口:缓存操作(无压缩) ======================================
/**
* 补充接口直接缓存已解码的Bitmap适配BackgroundView改进需求
* @param imagePath 图片绝对路径
@@ -125,7 +124,7 @@ public class BitmapCacheUtils {
mRefCountMap.putIfAbsent(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功极致强制保持 - " + imagePath);
LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功极致强制保持,无压缩 - " + imagePath);
return bitmap;
}
@@ -157,8 +156,8 @@ public class BitmapCacheUtils {
return hardCacheBitmap;
}
// 压缩比加载 Bitmap强制缓存核心通过降低分辨率减少单张Bitmap内存占用
Bitmap bitmap = decodeCompressedBitmap(imagePath);
// 压缩解码 Bitmap保留原始品质
Bitmap bitmap = decodeOriginalBitmap(imagePath);
if (bitmap != null) {
// 极致强制:存入硬引用缓存,永不自动回收
mHardCacheMap.put(imagePath, bitmap);
@@ -166,7 +165,7 @@ public class BitmapCacheUtils {
mRefCountMap.put(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(极致强制保持) - " + imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(极致强制保持,无压缩 - " + imagePath);
} else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
}
@@ -291,75 +290,54 @@ public class BitmapCacheUtils {
}
}
// ====================================== 内部工具方法 ======================================
// ====================================== 内部工具方法(无压缩解码) ======================================
/**
* 压缩解码 Bitmap极致强制缓存核心:通过降低分辨率+RGB_565减少单张Bitmap内存占用
* 压缩解码 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 = calculateHighCompressSampleSize(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; // 强制使用RGB_565比ARGB_8888少一半内存
options.inSampleSize = 1; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 保留全彩品质如需节省内存可改为RGB_565不影响品质
options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存
options.inInputShareable = false;
options.inDither = false; // 关闭抖动,减少内存占用
options.inScaled = 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;
}
}
/**
* 计算高压缩比缩放比例(极致强制缓存核心:优先保证不超过最大尺寸,尽可能压缩)
*/
private int calculateHighCompressSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
// 高压缩比逻辑:只要超过最大尺寸,就持续放大采样率
while (rawWidth / inSampleSize > maxWidth || rawHeight / inSampleSize > maxHeight) {
inSampleSize *= 2;
}
// 额外压缩即使未超过最大尺寸也强制至少压缩1倍进一步减少内存占用
inSampleSize = Math.max(inSampleSize, 2);
LogUtils.d(TAG, "calculateHighCompressSampleSize: 高压缩比缩放比例为 " + inSampleSize);
return inSampleSize;
}
/**
* 工具方法判断Bitmap是否有效非空且未被回收
*/
@@ -409,7 +387,7 @@ 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 中无效路径
@@ -435,7 +413,7 @@ public class BitmapCacheUtils {
@Override
public void onTrimMemory(int level) {
// 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + "极致强制保持所有Bitmap缓存");
LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + "极致强制保持所有Bitmap缓存(无压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@@ -443,7 +421,7 @@ public class BitmapCacheUtils {
@Override
public void onLowMemory() {
// 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onLowMemory: 系统低内存极致强制保持所有Bitmap缓存已启用高压缩");
LogUtils.w(TAG, "onLowMemory: 系统低内存极致强制保持所有Bitmap缓存压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}

View File

@@ -21,8 +21,8 @@ import java.io.File;
/**
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪、无压缩
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
*/
public class BackgroundView extends RelativeLayout {
@@ -100,7 +100,7 @@ public class BackgroundView extends RelativeLayout {
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 完成 ===");
@@ -124,11 +124,11 @@ public class BackgroundView extends RelativeLayout {
return;
}
// 核心修改:刷新时不删除旧缓存,仅重新解码并更新缓存(强制保持策略)
// 核心修改:刷新时不删除旧缓存,仅重新解码原始品质图片并更新缓存(强制保持策略)
if (isRefresh) {
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码并更新缓存 - " + targetPath);
// 刷新时直接解码,更新缓存(不删除旧缓存)
Bitmap newBitmap = decodeBitmapWithCompress(new File(targetPath), 1080, 1920);
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存 - " + targetPath);
// 刷新时直接解码原始品质图片,更新缓存(不删除旧缓存)
Bitmap newBitmap = decodeOriginalBitmap(new File(targetPath));
if (newBitmap != null) {
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
// 增加引用计数
@@ -140,7 +140,7 @@ public class BackgroundView extends RelativeLayout {
// ====================================== 对外方法 ======================================
/**
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
@@ -164,14 +164,14 @@ public class BackgroundView extends RelativeLayout {
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(cachedBitmap)) {
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap");
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap(原始品质)");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
return;
} else {
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载 - " + imagePath);
// 缓存无效,直接重加载(不删除旧缓存,强制保持策略)
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载原始品质图片 - " + imagePath);
// 缓存无效,直接重加载原始品质图片(不删除旧缓存,强制保持策略)
}
}
@@ -182,7 +182,7 @@ public class BackgroundView extends RelativeLayout {
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/缓存无效/路径更新:重新加载图片
// 无缓存/缓存无效/路径更新:重新加载原始品质图片
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
@@ -191,19 +191,19 @@ public class BackgroundView extends RelativeLayout {
// 先尝试从缓存获取
Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(bitmap)) {
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap - " + imagePath);
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap(原始品质) - " + imagePath);
} else {
// 缓存无效应解码
bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
// 缓存无效应解码原始品质图片
bitmap = decodeOriginalBitmap(imageFile);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 缓存新图片(强制保持)
// 缓存新图片(强制保持,原始品质
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
LogUtils.d(TAG, "loadImage: 加载新图片并缓存 - " + imagePath);
LogUtils.d(TAG, "loadImage: 加载新图片并缓存(原始品质) - " + imagePath);
}
// 增加引用计数(配合全局缓存工具的引用计数机制)
@@ -248,29 +248,21 @@ public class BackgroundView extends RelativeLayout {
}
/**
* 改进版解码方法强制缓存策略关闭可回收标志使用高压缩比减少OOM风险
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
*/
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 = (int) Math.ceil((float) options.outWidth / maxWidth);
int scaleY = (int) Math.ceil((float) options.outHeight / maxHeight);
int inSampleSize = Math.max(scaleX, scaleY);
inSampleSize = Math.max(1, inSampleSize); // 确保采样率≥1
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存
// 核心修改:关闭可回收标志,防止系统主动回收(强制缓存策略)
options.inPurgeable = false;
// 核心配置:无缩放、全彩品质、关闭可回收标志(强制保持)
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;
}
}
@@ -308,7 +300,7 @@ public class BackgroundView extends RelativeLayout {
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 仅居中平铺,无缩放
mIvBackground.setVisibility(View.VISIBLE);
}
@@ -360,7 +352,7 @@ public class BackgroundView extends RelativeLayout {
}
/**
* 重写恢复尺寸调整逻辑确保View尺寸变化时正确显示
* 重写恢复尺寸调整逻辑确保View尺寸变化时正确显示(无压缩)
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {