案例是測試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。