Bitmap使用中的兩級緩存,及內存重用

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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章