在上一篇博客 【Android 内存优化】Bitmap 内存缓存 ( Bitmap 缓存策略 | LruCache 内存缓存 | LruCache 常用操作 | 工具类代码 ) 中 , 使用 LruCache 缓存 Bitmap 数据到内存中 , 设置其最大缓存为应用可用内存的 1/8 , 将解码后的 Bitmap 对象缓存到 LruCache 中 , 避免重复使用该 Bitmap 对象时重复解码加载图片 ;
1 . Bitmap 复用池 : 加载图片时 , 使用 inBitmap 复用选项 , 需要获取图片时 , 优先从 Bitmap 复用池中查找复用已存在的 Bitmap 对象 ; 假如 Bitmap 对象长时间不使用 , 就会从 LruCache 内存缓存中移除 , 此时放入到 Bitmap 复用池中 ;
2 . 弱引用 : 这里使用弱引用保存该 Bitmap , 每次 GC 时都会回收没有被引用的 Bitmap , 需要创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用 ;
Set<WeakReference<Bitmap>> bitmapReusePool;
有一点特别注意 , Java 中的弱引用 , 在 GC 时会回收没有使用到的内存 ; Bitmap 内存如果在 Java 层 , 可以将该内存回收 , 但是如果 Bitmap 内存在 Native 层 , 必须调用 Bitmap 对象的 recycle 方法 , 才能将内存释放 ;
1 . Bitmap 内存放置策略 :
为了适配所有手机 , 所有版本 , 不管 GC 是否自动释放 Bitmap 内存 , 在弱引用对象被回收时 , 必须手动调用一下 Bitmap 对象的 recycle 方法 ;
2 . 兼容弱引用释放方法 : 使用引用队列 ReferenceQueue 监控该弱引用 Bitmap 的 Set 集合元素 , 当有 Bitmap 被回收后 , 就会将其放入 ReferenceQueue 中 , 此时开启一个线程 , 不断从 ReferenceQueue 调用 remove 方法获取被释放的内存对象 , 如果获取到了非空内容 , 说明有一个 Bitmap 弱引用对象被释放了 , 拿到该对象引用 Reference 后 , 获取其对应的 Bitmap 对象 , 手动调用 Bitmap 对象的 recycle 方法 , 即可完成对应操作 ;
代码示例 :
/**
* Bitmap 复用池
* 使用 inBitmap 复用选项
* 需要获取图片时 , 优先从 Bitmap 复用池中查找
* 这里使用弱引用保存该 Bitmap , 每次 GC 时都会回收该 Bitmap
* 创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用
*
* 该 Bitmap 复用池的作用是 , 假如 Bitmap 对象长时间不使用 , 就会从内存缓存中移除
*
* 因此这里需要处理 Bitmap 内存在 Native 层的情况 , 监控到 Java 层的弱引用被释放了
* 需要调用 Bitmap 对象的 recycle 方法 , 释放 Native 层的内存
*
* 需要使用引用队列监控弱引用的释放情况
*/
Set<WeakReference<Bitmap>> bitmapReusePool;
/**
* 引用队列 , 用于监控 Set> bitmapReusePool 的内存是否被回收
* 需要维护一个线程 , 不断尝试从该引用队列中获取引用
*
*/
private ReferenceQueue<Bitmap> referenceQueue;
/**
* 监控 Set> bitmapReusePool 的内存是否被回收 ,
* 调用 ReferenceQueue referenceQueue 的 remove 方法 ,
* 查看是否存在被回收的弱引用 , 如果存在 , 直接回收该弱引用对应的 Bitmap 对象
*/
private Thread referenceQueueMonitorThread;
/**
* 是否持续监控引用队列 ReferenceQueue
*/
private boolean isMonitorReferenceQueue = true;
/**
* 初始化引用队列
*/
private void initBitmapReusePool(){
// 创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用
bitmapReusePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
// 引用队列 , 当弱引用被 GC 扫描后 , 需要回收 , 会将该弱引用放入队列
// 一直不断的尝试从该引用队列中获取数据 , 如果获取到数据 , 就要回收该对象
referenceQueue = new ReferenceQueue<>();
// 定义监控线程
referenceQueueMonitorThread = new Thread(){
@Override
public void run() {
while (isMonitorReferenceQueue){
try {
Reference<Bitmap> reference = (Reference<Bitmap>) referenceQueue.remove();
Bitmap bitmap = reference.get();
// 不为空 , 且没有被回收 , 回收 Bitmap 内存
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 启动引用队列监控线程
referenceQueueMonitorThread.start();
}
根据不同系统版本进行不同处理 :
Android 2.3.3(API 级别 10)及以下的版本 : 使用 Bitmap 对象的 recycle 方法回收内存 ;
// Android 2.3.3(API 级别 10)及以下的版本中 , 使用 Bitmap 对象的 recycle 方法回收内存
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1){
// 如果 API 级别小于等于 10 , 不启用 Bitmap 内存复用机制 , 返回 null 即可
return null;
}
Android 4.4(API 级别 19)以下的版本 : 复用的前提是必须同时满足以下 3 个条件 :
才能复用成功 , 另外被复用的图像的像素格式 Config ( 如 RGB_565 ) 会覆盖设置的 BitmapFactory.Options.inPreferredConfig 参数 ;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2){
/*
Android 4.4(API 级别 19)以下的版本 : 在 Android 4.4(API 级别 19) 之前的代码中 ,
复用的前提是必须同时满足以下 3 个条件 :
1. 被解码的图像必须是 JPEG 或 PNG 格式
2. 被复用的图像宽高必须等于 解码后的图像宽高
3. 解码图像的 BitmapFactory.Options.inSampleSize 设置为 1 , 也就是不能缩放
才能复用成功 , 另外被复用的图像的像素格式 Config ( 如 RGB_565 ) 会覆盖设置的
BitmapFactory.Options.inPreferredConfig 参数 ;
*/
if(bitmap.getWidth() == width &&
bitmap.getHeight() == height && //被复用的图像宽高必须等于 解码后的图像宽高
inSampleSize == 1){// 图像的 BitmapFactory.Options.inSampleSize 设置为 1
//符合要求
inBitmap = bitmap;
iterator.remove();
}
}
在 Android 4.4(API 级别 19)及以上的版本 : 只要被解码后的 Bitmap 对象的字节大小 , 小于等于 inBitmap 的字节大小 , 就可以复用成功 ; 解码后的乳香可以是缩小后的 , 即 BitmapFactory.Options.inSampleSize 可以大于1 ;
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
/*
在 Android 4.4(API 级别 19)及以上的版本中 ,
只要被解码后的 Bitmap 对象的字节大小 , 小于等于 inBitmap 的字节大小 , 就可以复用成功 ;
解码后的乳香可以是缩小后的 , 即 BitmapFactory.Options.inSampleSize 可以大于1 ;
*/
// 首先要计算图像的内存占用 , 先要计算出图像的宽高 , 如果图像需要缩放 , 计算缩放后的宽高
if(inSampleSize > 1){
width = width / inSampleSize ;
height = height / inSampleSize;
}
// 计算内存占用 , 默认 ARGB_8888 格式
int byteInMemory = width * height * 4;;
if(bitmap.getConfig() == Bitmap.Config.ARGB_8888){
// 此时每个像素占 4 字节
byteInMemory = width * height * 4;
}else if(bitmap.getConfig() == Bitmap.Config.RGB_565){
// 此时每个像素占 2 字节
byteInMemory = width * height * 2;
}
// 如果解码后的图片内存小于等于被复用的内存大小 , 可以复用
if(byteInMemory <= bitmap.getAllocationByteCount()){
//符合要求
inBitmap = bitmap;
iterator.remove();
}
}
BitmapLruCacheMemoryReuse.java 工具类地址 : BitmapLruCacheMemoryReuse.java
package kim.hsl.bm.utils;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.LruCache;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* Bitmap 内存缓存
* 在将图片缓存到 LruCache 内存中基础上 ,
* 将从 LruCache 中移除的最近没有使用的 Bitmap 对象的内存复用
* 这样能最大限度减少内存抖动
*/
public class BitmapLruCacheMemoryReuse {
private static final String TAG = "BitmapMemoryCache";
/**
* 应用上下文对象
*/
private Context mContext;
/**
* 缓存图片的 LruCache
*/
private LruCache<String, Bitmap> mLruCache;
/**
* Bitmap 复用池
* 使用 inBitmap 复用选项
* 需要获取图片时 , 优先从 Bitmap 复用池中查找
* 这里使用弱引用保存该 Bitmap , 每次 GC 时都会回收该 Bitmap
* 创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用
*
* 该 Bitmap 复用池的作用是 , 假如 Bitmap 对象长时间不使用 , 就会从内存缓存中移除
*
* Bitmap 回收策略 :
* 3.0 以下系统中 , Bitmap 内存在 Native 层
* 3.0 以上系统中 , Bitmap 内存在 Java 层
* 8.0 及以上的系统中 , Bitmap 内存在 Native 层
*
* 因此这里需要处理 Bitmap 内存在 Native 层的情况 , 监控到 Java 层的弱引用被释放了
* 需要调用 Bitmap 对象的 recycle 方法 , 释放 Native 层的内存
*
* 需要使用引用队列监控弱引用的释放情况
*/
Set<WeakReference<Bitmap>> bitmapReusePool;
/**
* 引用队列 , 用于监控 Set> bitmapReusePool 的内存是否被回收
* 需要维护一个线程 , 不断尝试从该引用队列中获取引用
*
*/
private ReferenceQueue<Bitmap> referenceQueue;
/**
* 监控 Set> bitmapReusePool 的内存是否被回收 ,
* 调用 ReferenceQueue referenceQueue 的 remove 方法 ,
* 查看是否存在被回收的弱引用 , 如果存在 , 直接回收该弱引用对应的 Bitmap 对象
*/
private Thread referenceQueueMonitorThread;
/**
* 是否持续监控引用队列 ReferenceQueue
*/
private boolean isMonitorReferenceQueue = true;
/**
* 单例实现
*/
private static BitmapLruCacheMemoryReuse INSTANCE;
private BitmapLruCacheMemoryReuse(){}
public static BitmapLruCacheMemoryReuse getInstance(){
if(INSTANCE == null){
INSTANCE = new BitmapLruCacheMemoryReuse();
}
return INSTANCE;
}
/**
* 使用时初始化
* @param context
*/
public void init(Context context){
// 初始化内存缓存
initLruCache(context);
// 初始化弱引用队列
initBitmapReusePool();
}
/**
* 不使用时释放
*/
public void release(){
isMonitorReferenceQueue = false;
}
private void initLruCache(Context context){
// 为成员变量赋值
this.mContext = context;
// 获取 Activity 管理器
ActivityManager activityManager = (ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE);
// 获取应用可用的最大内存
int maxMemory = activityManager.getMemoryClass();
// 获取的 maxMemory 单位是 MB , 将其转为字节 , 除以 8
int lruCacheMemoryByte = maxMemory / 8 * 1024 * 1024;
// 设置的内存 , 一般是 APP 可用内存的 1/8
mLruCache = new LruCache<String, Bitmap>(lruCacheMemoryByte){
/**
* 返回 LruCache 的键和值的大小 , 单位使用用户自定义的单位
* 默认的实现中 , 返回 1 ; size 是 键值对个数 , 最大的 size 大小是最多键值对个数
* 键值对条目在 LruCache 中缓存时 , 其大小不能改变
* @param key
* @param value
* @return 返回 LruCache 的值 , 即 Bitmap 占用内存
*/
@Override
protected int sizeOf(String key, Bitmap value) {
// 如果使用的是复用的 Bitmap 对象 , 其占用内存大小是之前的图像分配的内存大小
// 大于等于当前图像的内存占用大小
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return value.getAllocationByteCount();
}
return value.getByteCount();
}
/**
* 从 LruCache 缓存移除 Bitmap 时会回调该方法
* @param evicted
* @param key
* @param oldValue
* @param newValue
*/
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue,
Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
/*
如果从 LruCache 内存缓存中移除的 Bitmap 是可变的
才能被复用 , 否则只能回收该 Bitmap 对象
Bitmap 回收策略 :
3.0 以下系统中 , Bitmap 内存在 Native 层
3.0 以上系统中 , Bitmap 内存在 Java 层
8.0 及以上的系统中 , Bitmap 内存在 Native 层
因此这里需要处理 Bitmap 内存在 Native 层的情况 , 监控到 Java 层的弱引用被释放了
需要调用 Bitmap 对象的 recycle 方法 , 释放 Native 层的内存
*/
if(oldValue.isMutable()){ // 可以被复用
// 将其放入弱引用中 , 每次 GC 启动后 , 如果该弱引用没有被使用 , 都会被回收
bitmapReusePool.add(new WeakReference<Bitmap>(oldValue, referenceQueue));
}else{ // 不可被复用 , 直接回收
oldValue.recycle();
}
}
};
}
private void initBitmapReusePool(){
// 创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用
bitmapReusePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
// 引用队列 , 当弱引用被 GC 扫描后 , 需要回收 , 会将该弱引用放入队列
// 一直不断的尝试从该引用队列中获取数据 , 如果获取到数据 , 就要回收该对象
referenceQueue = new ReferenceQueue<>();
// 定义监控线程
referenceQueueMonitorThread = new Thread(){
@Override
public void run() {
while (isMonitorReferenceQueue){
try {
Reference<Bitmap> reference = (Reference<Bitmap>) referenceQueue.remove();
Bitmap bitmap = reference.get();
// 不为空 , 且没有被回收 , 回收 Bitmap 内存
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 启动引用队列监控线程
referenceQueueMonitorThread.start();
}
/**
* 获取一个可以被复用的 Bitmap 对象
*
* 与 BitmapFactory 配合使用 :
*
* Android 4.4 以后的 Bitmap 复用情况 :
* 在 KITKAT ( Android 4.4 , 19 平台 ) 以后的代码中 ,
* 只要被解码生成的 Bitmap 对象的字节大小 ( 缩放后的 )
* 小于等于 inBitmap 的字节大小 , 就可以复用成功 ;
*
* Android 4.4 之前的 Bitmap 复用情况 : ( 比较苛刻 )
* 在 KITKAT 之前的代码中 , 被解码的图像必须是
* - JPEG 或 PNG 格式 ,
* - 并且 图像大小必须是相等的 ,
* - inssampleSize 设置为 1 ,
* 才能复用成功 ;
* 另外被复用的图像的 像素格式 Config ( 如 RGB_565 ) 会覆盖设置的 inPreferredConfig 参数
*
* @param width
* @param height
* @param inSampleSize
* @return
*/
public Bitmap getReuseBitmap(int width,int height,int inSampleSize){
// Android 2.3.3(API 级别 10)及以下的版本中 , 使用 Bitmap 对象的 recycle 方法回收内存
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1){
// 如果 API 级别小于等于 10 , 不启用 Bitmap 内存复用机制 , 返回 null 即可
return null;
}
// 获取准备复用的 Bitmap , 之后设置到 Options 中
Bitmap inBitmap = null;
// 使用迭代器遍历该 Set 集合 , 如果遍历中涉及到删除 , 就要使用迭代器遍历
Iterator<WeakReference<Bitmap>> iterator = bitmapReusePool.iterator();
//迭代查找符合复用条件的Bitmap
while (iterator.hasNext()){
// 循环遍历 Bitmap 对象
Bitmap bitmap = iterator.next().get();
if (bitmap != null){
/*
检查该 Bitmap 对象是否可以达到复用要求 ,
如果达到复用要求 , 就取出这个 Bitmap 对象 , 并将其从队列中移除
*/
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2){
/*
Android 4.4(API 级别 19)以下的版本 : 在 Android 4.4(API 级别 19) 之前的代码中 ,
复用的前提是必须同时满足以下 3 个条件 :
1. 被解码的图像必须是 JPEG 或 PNG 格式
2. 被复用的图像宽高必须等于 解码后的图像宽高
3. 解码图像的 BitmapFactory.Options.inSampleSize 设置为 1 , 也就是不能缩放
才能复用成功 , 另外被复用的图像的像素格式 Config ( 如 RGB_565 ) 会覆盖设置的
BitmapFactory.Options.inPreferredConfig 参数 ;
*/
if(bitmap.getWidth() == width &&
bitmap.getHeight() == height && //被复用的图像宽高必须等于 解码后的图像宽高
inSampleSize == 1){// 图像的 BitmapFactory.Options.inSampleSize 设置为 1
//符合要求
inBitmap = bitmap;
iterator.remove();
}
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
/*
在 Android 4.4(API 级别 19)及以上的版本中 ,
只要被解码后的 Bitmap 对象的字节大小 , 小于等于 inBitmap 的字节大小 , 就可以复用成功 ;
解码后的乳香可以是缩小后的 , 即 BitmapFactory.Options.inSampleSize 可以大于1 ;
*/
// 首先要计算图像的内存占用 , 先要计算出图像的宽高 , 如果图像需要缩放 , 计算缩放后的宽高
if(inSampleSize > 1){
width = width / inSampleSize ;
height = height / inSampleSize;
}
// 计算内存占用 , 默认 ARGB_8888 格式
int byteInMemory = width * height * 4;;
if(bitmap.getConfig() == Bitmap.Config.ARGB_8888){
// 此时每个像素占 4 字节
byteInMemory = width * height * 4;
}else if(bitmap.getConfig() == Bitmap.Config.RGB_565){
// 此时每个像素占 2 字节
byteInMemory = width * height * 2;
}
// 如果解码后的图片内存小于等于被复用的内存大小 , 可以复用
if(byteInMemory <= bitmap.getAllocationByteCount()){
//符合要求
inBitmap = bitmap;
iterator.remove();
}
}
}else if( bitmap == null ){
// 如果 bitmap 为空 , 直接从复用 Bitmap 集合中移除
iterator.remove();
}
}
return inBitmap;
}
/*
下面的 3 个方法是提供给用户用于操作 LruCache 的接口
*/
/**
* 将键值对放入 LruCache 中
* @param key
* @param value
*/
public void putBitmapToLruCache(String key, Bitmap value){
mLruCache.put(key, value);
}
/**
* 从 LruCache 中获取 Bitmap 对象
* @param key
* @return
*/
public Bitmap getBitmapFromLruCache(String key){
return mLruCache.get(key);
}
/**
* 清除 LruCache 缓存
*/
public void clearLruCache(){
mLruCache.evictAll();
}
}
package kim.hsl.bm;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import kim.hsl.bm.utils.BitmapLruCacheMemoryReuse;
import kim.hsl.bm.utils.BitmapSizeReduce;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
// 内存缓存
memoryCache();
}
/**
* 图像缓存
*/
private void memoryCache(){
// 初始化 LruCache 内存缓存 , 与引用队列 , 一般在 onCreate 方法中初始化
// 这里为了演示 , 放在方法的开头位置
BitmapLruCacheMemoryReuse.getInstance().init(this);
// 第一次从 LruCache 内存中获取 Bitmap 数据
Bitmap bitmap = BitmapLruCacheMemoryReuse.getInstance().
getBitmapFromLruCache(R.drawable.blog + "");
/*
如果从内存中获取 Bitmap 对象失败 , 这里就需要创建该图片 , 并放入 LruCache 内存中
*/
if(bitmap == null){
// 要复用内存的 Bitmap 对象 , 将新的 Bitmap 写入到该 Bitmap 内存中
Bitmap inBitmap = null;
// 尝试获取复用对象
BitmapLruCacheMemoryReuse.getInstance().
getReuseBitmap(200, 200, 1);
// 加载指定大小格式的图像
bitmap = BitmapSizeReduce.getResizedBitmap(this, R.drawable.blog,
200, 200, false, inBitmap);
// 将新的 bitap 放入 LruCache 内存缓存中
BitmapLruCacheMemoryReuse.getInstance().
putBitmapToLruCache(R.drawable.blog + "", bitmap);
Log.i("Bitmap 没有获取到创建新的", "blog : " + bitmap.getWidth() + " , " +
bitmap.getHeight() + " , " +
bitmap.getByteCount());
}else{
Log.i("Bitmap 内存中获取数据", "blog : " + bitmap.getWidth() + " , " +
bitmap.getHeight() + " , " +
bitmap.getByteCount());
}
// 第一次从 LruCache 内存中获取 Bitmap 数据
Bitmap bitmap2 = BitmapLruCacheMemoryReuse.getInstance().
getBitmapFromLruCache(R.drawable.blog + "");
Log.i("Bitmap 第二次内存中获取数据", "blog : " + bitmap2.getWidth() + " , " +
bitmap2.getHeight() + " , " +
bitmap2.getByteCount());
}
执行结果 : 第一次尝试从 LruCache 中获取图像 , 没有获取到 , 创建新的 Bitmap 放入 LruCache 中 , 第二次获取直接从 LruCache 中获取到了图像 ;
2020-07-02 15:15:48.300 5133-5133/kim.hsl.bm W/BitmapSizeReduce: getResizedBitmap options.outWidth=1990 , options.outHeight=1020
2020-07-02 15:15:48.300 5133-5133/kim.hsl.bm W/BitmapSizeReduce: getResizedBitmap inSampleSize=16
2020-07-02 15:15:48.327 5133-5133/kim.hsl.bm I/Bitmap 没有获取到创建新的: blog : 124 , 63 , 15624
2020-07-02 15:15:48.328 5133-5133/kim.hsl.bm I/Bitmap 第二次内存中获取数据: blog : 124 , 63 , 15624
源码及资源下载地址 :
① GitHub 工程地址 : BitmapMemory
② BitmapLruCacheMemoryReuse.java 工具类地址 : BitmapLruCacheMemoryReuse.java