文章目录
一、Bitmap 复用池
二、弱引用 Bitmap 内存释放
三、从 Bitmap 复用池中获取对应可以被复用的 Bitmap 对象
1、Android 2.3.3(API 级别 10)及以下的版本
2、Android 4.4(API 级别 19)以下的版本
2、在 Android 4.4(API 级别 19)及以上的版本
四、LruCache 内存缓存、内存复用工具类
1、工具类
2、工具类测试
3、执行结果
五、源码及资源下载
在上一篇博客 【Android 内存优化】Bitmap 内存缓存 ( Bitmap 缓存策略 | LruCache 内存缓存 | LruCache 常用操作 | 工具类代码 ) 中 , 使用 LruCache 缓存 Bitmap 数据到内存中 , 设置其最大缓存为应用可用内存的 1/8 , 将解码后的 Bitmap 对象缓存到 LruCache 中 , 避免重复使用该 Bitmap 对象时重复解码加载图片 ;
一、Bitmap 复用池
1 . Bitmap 复用池 : 加载图片时 , 使用 inBitmap 复用选项 , 需要获取图片时 , 优先从 Bitmap 复用池中查找复用已存在的 Bitmap 对象 ; 假如 Bitmap 对象长时间不使用 , 就会从 LruCache 内存缓存中移除 , 此时放入到 Bitmap 复用池中 ;
2 . 弱引用 : 这里使用弱引用保存该 Bitmap , 每次 GC 时都会回收没有被引用的 Bitmap , 需要创建一个线程安全的 HashSet , 其中的元素是 Bitmap 弱引用 ;
Set<WeakReference<Bitmap>> bitmapReusePool;
二、弱引用 Bitmap 内存释放
有一点特别注意 , Java 中的弱引用 , 在 GC 时会回收没有使用到的内存 ; Bitmap 内存如果在 Java 层 , 可以将该内存回收 , 但是如果 Bitmap 内存在 Native 层 , 必须调用 Bitmap 对象的 recycle 方法 , 才能将内存释放 ;
1 . Bitmap 内存放置策略 :
3.0 以下系统中 , Bitmap 内存在 Native 层
3.0 以上系统中 , Bitmap 内存在 Java 层
8.0 及以上的系统中 , Bitmap 内存在 Native 层
为了适配所有手机 , 所有版本 , 不管 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<WeakReference<Bitmap>> bitmapReusePool 的内存是否被回收 * 需要维护一个线程 , 不断尝试从该引用队列中获取引用 * */ private ReferenceQueue<Bitmap> referenceQueue; /** * 监控 Set<WeakReference<Bitmap>> bitmapReusePool 的内存是否被回收 , * 调用 ReferenceQueue<Bitmap> 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(); }
三、从 Bitmap 复用池中获取对应可以被复用的 Bitmap 对象
根据不同系统版本进行不同处理 :
1、Android 2.3.3(API 级别 10)及以下的版本
Android 2.3.3(API 级别 10)及以下的版本 : 使用 Bitmap 对象的 recycle 方法回收内存 ; // Android 2.3.3(API 级别 10)及以下的版本中 , 使用 Bitmap 对象的 recycle 方法回收内存
2、Android 4.4(API 级别 19)以下的版本
Android 4.4(API 级别 19)以下的版本 : 复用的前提是必须同时满足以下 3 个条件 :
被解码的图像必须是 JPEG 或 PNG 格式
被复用的图像宽高必须等于 解码后的图像宽高
解码图像的 BitmapFactory.Options.inSampleSize 设置为 1 , 也就是不能缩放
才能复用成功 , 另外被复用的图像的像素格式 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(); } }
2、在 Android 4.4(API 级别 19)及以上的版本
在 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(); } }
四、LruCache 内存缓存、内存复用工具类
1、工具类
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<WeakReference<Bitmap>> bitmapReusePool 的内存是否被回收 * 需要维护一个线程 , 不断尝试从该引用队列中获取引用 * */ private ReferenceQueue<Bitmap> referenceQueue; /** * 监控 Set<WeakReference<Bitmap>> bitmapReusePool 的内存是否被回收 , * 调用 ReferenceQueue<Bitmap> 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<String, Bitmap> 的值 , 即 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(); } }
2、工具类测试
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()); }
3、执行结果
执行结果 : 第一次尝试从 LruCache 中获取图像 , 没有获取到 , 创建新的 Bitmap 放入 LruCache 中 , 第二次获取直接从 LruCache 中获取到了图像 ;
五、源码及资源下载
源码及资源下载地址 :
① GitHub 工程地址 : BitmapMemory
② BitmapLruCacheMemoryReuse.java 工具类地址 : BitmapLruCacheMemoryReuse.java