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。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值