案例是测试Bitmap使用过程中,如何使用二级缓存,及重用bitmap的内存
这里的二级缓存,一是内存缓存,而是磁盘缓存。
代码中已加注释,所以可以直接看代码:
一,首先是主Activity,其中会设置recyclerView的布局类型,适配器,设置磁盘缓存的路径。
public class MainActivity extends AppCompatActivity {
private final String TAG = MainActivity.class.getName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView rv = findViewById(R.id.rv);
LinearLayoutManager llm = new LinearLayoutManager(this);
rv.setLayoutManager(llm);
BitmapAdapter bitmapAdapter = new BitmapAdapter(this);
rv.setAdapter(bitmapAdapter);
ImageCache.getInstance().init(this, this.getExternalCacheDir() +"/bitmap");
Log.d(TAG,"this.getExternalCacheDir()="+this.getExternalCacheDir());
}
@Override
protected void onResume() {
super.onResume();
ImageCache.getInstance().setExit(false);
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
ImageCache.getInstance().interruptThread();
}
@Override
protected void onDestroy() {
super.onDestroy();
ImageCache.getInstance().interruptThread();
}
}
二,RecyclerView适配器,为测试方便,显示条目写了固定值,在加载每项数据时,先从内存缓存获取,然后是磁盘缓存,
这个过程中考虑了bitmap的内存重用。
public class BitmapAdapter extends RecyclerView.Adapter<BitmapAdapter.BitmapViewHolder> {
private Context mContext;
private final String TAG = BitmapAdapter.class.getName();
public BitmapAdapter(Context context) {
mContext = context;
}
@NonNull
@Override
public BitmapViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View item = LayoutInflater.from(mContext).inflate(
R.layout.rv_item, null, false);
BitmapViewHolder bitmapViewHolder = new BitmapViewHolder(item);
return bitmapViewHolder;
}
@Override
public void onBindViewHolder(@NonNull BitmapViewHolder holder, int position) {
Log.d("BitmapAdapter","position="+position);
//应用缓存,及复用Bitmap,首先从内存缓存读取
Bitmap bitmap = ImageCache.getInstance().getBitmapFromMemory(String.valueOf(position));
if (bitmap != null) {
Log.d(TAG, "memory cache in use..." + bitmap);
}
if (bitmap == null) {
//先检查是否有可复用内存,为方便测试,宽高比实际加载的bitmap稍小一点
Bitmap reusable = ImageCache.getInstance().getReusable(298, 298, 1);
if (reusable != null) {
Log.d(TAG, "reusable bitmap in use..." + reusable);
}
//如果reusable是null,在BitmapFactory.cpp的处理中会忽略掉重用
bitmap = ImageCache.getInstance().getBitmapFromDisk(String.valueOf(position), reusable);
if (bitmap != null) {
Log.d(TAG, "disk bitmap in use..." + bitmap);
}
//初次使用,肯定要先加载,从本地或者网络,然后放入缓存,磁盘
if (bitmap == null) {
bitmap = ImageResize.resizeBitmap(mContext, R.drawable.jpgfile,
300, 300, false ,reusable);
ImageCache.getInstance().putBitmap2Memory(String.valueOf(position) , bitmap);
ImageCache.getInstance().putBitmap2Disk(String.valueOf(position), bitmap);
}
}
if (bitmap != null) {
holder.iv.setImageBitmap(bitmap);
}
}
@Override
public int getItemCount() {
//仅做测试,总共500条数据,实际根据列表size来定
return 500;
}
class BitmapViewHolder extends RecyclerView.ViewHolder {
private ImageView iv;
public BitmapViewHolder(@NonNull View itemView) {
super(itemView);
iv = itemView.findViewById(R.id.iv);
}
}
}
三,内存缓存,磁盘缓存,对象复用的代码实现
其中LRUCache调用entryRemoved方法,移除图片时,需要注意下:
当bitmap.isMutable()为true可以复用,就放入复用池,这种情况就不能调用recycle,否则在后面去判断Bitmap使用复用池时,
会报错(can't access free/invalide bitmap).
如果不能复用,直接recycle掉,
public class ImageCache {
private final String TAG= ImageCache.class.getName();
private static ImageCache imageCache = new ImageCache();
private Context mContext;
//bitmap对象复用池
private Set<WeakReference<Bitmap>> mReusablePool;
private LruCache<String, Bitmap> mMemoryCache;
//先要下载DiskLruCache的源码
private DiskLruCache mDiskLruCache;
private ReferenceQueue<Bitmap> mReferenceQueue;
private boolean mExit;
private Thread mRqThread;
public static ImageCache getInstance() {
return imageCache;
}
public void init(Context context, String diskCacheDir) {
mContext = context;
mReusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
//计算当前进程的内存大小
ActivityManager am = (ActivityManager)mContext.getSystemService(Context.ACTIVITY_SERVICE);
//单位是megabytes
int memory = am.getMemoryClass();
Log.d(TAG,"memory="+memory);
//参数size单位是byte,
mMemoryCache = new LruCache<String, Bitmap>(memory * 1024 * 1024 / 10) {
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
//当一个bitmap被lrucache移除时,考虑复用这个bitmap对象
Log.d(TAG,"key="+key+",evicted="+evicted+",oldValue="+oldValue);
if (oldValue.isMutable()) {
//把引用添加到复用池,同时注册到一个引用队列上,在这个引用被引用的对象被GC时,
// 这个引用会被追加到这个队列中
mReusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
} else {
oldValue.recycle();
}
}
@Override
protected int sizeOf(String key, Bitmap value) {
//一张图片的大小
Log.d(TAG,"value.getAllocationByteCount()="+value.getAllocationByteCount());
return value.getAllocationByteCount();
}
};
try {
mDiskLruCache = DiskLruCache.open(new File(diskCacheDir),
BuildConfig.VERSION_CODE,
1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
//使用这个队列的目标,就是跟踪对象的回收,来检测内存泄漏
private ReferenceQueue<Bitmap> getReferenceQueue() {
if (mReferenceQueue == null) {
mReferenceQueue = new ReferenceQueue<>();
mRqThread = new Thread(new Runnable() {
@Override
public void run() {
//确保线程能正常退出。
while (!isExit() &&
!Thread.currentThread().isInterrupted()) {
try {
Reference<? extends Bitmap> rmove = mReferenceQueue.remove();
//如果一个引用-引用的对象被回收了,那么这个引用本身也应该释放掉。
Bitmap bitmap = rmove.get();
Log.d(TAG,"getReferenceQueue,bitmap="+bitmap);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
}
});
mRqThread.start();
}
return mReferenceQueue;
}
public boolean isExit() {
return mExit;
}
public void setExit(boolean mExit) {
this.mExit = mExit;
}
public void interruptThread() {
setExit(true);
mRqThread.interrupt();
Log.d(TAG,"isInterrupted="+mRqThread.isInterrupted());
}
//把bitmap放入缓存
public void putBitmap2Memory(String key, Bitmap bitmap) {
mMemoryCache.put(key, bitmap);
}
//从缓存读取bitmap
public Bitmap getBitmapFromMemory(String key) {
return mMemoryCache.get(key);
}
//清空缓存
public void clearMemory() {
mMemoryCache.evictAll();
}
//把bitmap放入磁盘
public void putBitmap2Disk(String key, Bitmap bitmap) {
DiskLruCache.Snapshot snapshot = null;
OutputStream os = null;
//先判断磁盘中有没有,如果没有就写入
try {
snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
//写入文件前,做压缩
os = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG, 60, os);
editor.commit();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (snapshot != null) {
snapshot.close();
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//从磁盘读取bitmap
public Bitmap getBitmapFromDisk(String key, Bitmap reusable) {
DiskLruCache.Snapshot snapshot = null;
Bitmap bitmap = null;
try {
snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
return null;
}
InputStream is = snapshot.getInputStream(0);
BitmapFactory.Options options = new BitmapFactory.Options();
//重用bitmap的内存
options.inMutable = true;
options.inBitmap = reusable;
bitmap = BitmapFactory.decodeStream(is, null, options);
if (bitmap != null) {
mMemoryCache.put(key, bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
//判断bitmap的内存是否可以重用,根据inBitmap的注释,将要解码的bitmap的内存要小于等于
// 被重用bitmap的内存。
public Bitmap getReusable(int width, int height, int inSampleSize) {
Bitmap reusable = null;
Iterator<WeakReference<Bitmap>> iterator = mReusablePool.iterator();
while (iterator.hasNext()) {
Bitmap bitmap = iterator.next().get();
if (bitmap != null) {
if (checkReusableBitmap(bitmap, width, height, inSampleSize)) {
reusable = bitmap;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}
return reusable;
}
private boolean checkReusableBitmap (Bitmap bitmap, int width, int height, int inSampleSize) {
if (inSampleSize > 1) {
width /= inSampleSize;
height /= inSampleSize;
}
int byteCount = width * height * getBytesPerPixel(bitmap.getConfig());
return byteCount <= bitmap.getAllocationByteCount();
}
//单个像素占的字节数
private int getBytesPerPixel(Bitmap.Config config) {
if (config == Bitmap.Config.ARGB_8888) {
return 4;
}
return 2;
}
}
在android8.0之前,Bitmap的内存是在java层分配的,JVM在GC时会去调用bitmap.recycle,所以8.0通常不用主动去调用recycle,但是建议还是主动去recycle,以便告诉jvm这块空间不需要了,可以及时回收.
在android8.0之后,可能因为在java层分配太占空间,所以Bitmap内存又在native分配,这时GC就管不了这块空间了,所以要主动recycle.
在复用池中,对bitmap对象的引用使用的是弱引用,在应用弱引用时,同时关联了引用队列,这样在 ,被弱引用引用的bitmap对象被回收时,可以通过引用队列的操作,回收到引用本身,因为引用本身也是bitmap类型的变量,也需要手动执行bitmap.recyle(),给一个适当的清理机制,避免大量的WeakReference对象带来内存泄漏。
另外,因为ReferenceQueue的remove操作是阻塞的,所以要放在一个单独的线程去完成.
四,在第一次加载图片时,根据需要的宽高简单做个缩放
public class ImageResize {
public static Bitmap resizeBitmap(Context context,int id, int desiredWidth, int desireHeight,
boolean alpha, Bitmap reusable) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
//仅解析out参数
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources,id, options);
int width = options.outWidth;
int height = options.outHeight;
//计算缩放比例
options.inSampleSize = calculateInSampleSize(width,height,desiredWidth,desireHeight);
//如果不需要透明,重新设置颜色配置
if (!alpha) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds = false;
//复用Bitmap
options.inMutable = true;
options.inBitmap = reusable;
return BitmapFactory.decodeResource(resources, id, options);
}
public static int calculateInSampleSize(int originalW, int originalH, int desiredW, int desiredH) {
int inSampleSize = 1;
//inSampleSize是2的指数次幂
if (originalW > desiredW && originalH > desiredH) {
inSampleSize = 2;
while (originalW / inSampleSize > desiredW && originalH / inSampleSize > desiredH) {
inSampleSize *=2;
}
}
return inSampleSize;
}
}
五,布局文件不粘贴了,就是一个RecyclerView。