性能优化--bitmap

830 阅读8分钟

Bitmap内存模型

  • Android 2.3之前

Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中,Native内存中的像素数据并不会以可预测的方式进行同步回收,有可能会导致Native内存升高

  • Android 3.0-8.0

Bitmap的像素数据也被放在了Dalvik Heap中,这样Bitmap 内存也会随着对象一起被回收。

  • Android 8.0 之后

Bitmap的像素数据又被放到 Native 内存中。当然此时Google做了改进,在Native层的Bitmap的像素数据可以做到和Java层的对象一起快速释放。

Bitmap内存回收

  • Android 2.3之前

调用Bitmap.recycle()方法,进行bitmap的内存回收

  • Android 3之后

有强调Bitmap.recycle();而是强调Bitmap的复用:

Bitmap占有多大内存

  • 1: getByteCount()
  • 2:getAllocationByteCount()

getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。

两者区别:

1:一般情况下两者是相等的;

2: 通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。

如何计算Bitmap的内存

Bitmap它占用的内存 = width * height * (nTargetDensity/inDensity)* 一个像素所占的内存。

  • inDensity:默认为图片所在文件夹对应的密度
  • nTargetDensity: 为当前系统密度

一个像素占用多大内存?

  • ARGB_8888: 每个像素4字节. 共32位,默认设置。
  • Alpha_8: 只保存透明度,共8位,1字节。
  • ARGB_4444: 共16位,2字节。
  • RGB_565:共16位,2字节,只存储RGB值。

看BitmapFactory.decodeResource()源码

    BitmapFactory.java
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts) {
        if (opts == null) {
            opts = new Options();
        }
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                //inDensity默认为图片所在文件夹对应的密度
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        if (opts.inTargetDensity == 0 && res != null) {
            //inTargetDensity为当前系统密度。
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        return decodeStream(is, pad, opts);
    }
    
    BitmapFactory.cpp 此处只列出主要代码。
    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
        //初始缩放系数
        float scale = 1.0f;
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                //缩放系数是当前系数密度/图片所在文件夹对应的密度;
                scale = (float) targetDensity / density;
            }
        }
        //原始解码出来的Bitmap;
        SkBitmap decodingBitmap;
        if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
            return nullObjectReturn("decoder->decode returned false");
        }
        //原始解码出来的Bitmap的宽高;
        int scaledWidth = decodingBitmap.width();
        int scaledHeight = decodingBitmap.height();
        //要使用缩放系数进行缩放,缩放后的宽高;
        if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
            scaledWidth = int(scaledWidth * scale + 0.5f);
            scaledHeight = int(scaledHeight * scale + 0.5f);
        }    
        //源码解释为因为历史原因;sx、sy基本等于scale。
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());
        canvas.scale(sx, sy);
        canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
        // now create the java bitmap
        return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }

Bitmap如何复用?

1:使用LruCache和DiskLruCache做内存和磁盘缓存;

2:对用Bitma内存块进行复用

注意:对内存块进行复用时

1:inMutable为true

2: Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;这时候:getByteCount() getAllocationByteCount()大小一样

3:Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig

4:Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;

BitmapFactory.Options options = new BitmapFactory.Options();
// 图片复用,这个属性必须设置;
options.inMutable = true;
// 手动设置缩放比例,使其取整数,方便计算、观察数据;
options.inDensity = 320;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
// 对象内存地址;
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());

// 使用inBitmap属性,这个属性必须设置;
options.inBitmap = bitmap;
options.inDensity = 320;
// 设置缩放宽高为原始宽高一半;
options.inTargetDensity = 160;
options.inMutable = true;
Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
// 复用对象的内存地址;
Log.i(TAG, "bitmapReuse = " + bitmapReuse);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());

输出:
I/lz: bitmap = android.graphics.Bitmap@35ac9dd4
I/lz: width:1024:::height:594
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse = android.graphics.Bitmap@35ac9dd4 // 两个对象的内存地址一致
I/lz: width:512:::height:297
I/lz: bitmap:ByteCount = 608256:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse:ByteCount = 608256:::bitmapReuse:AllocationByteCount = 2433024 // ByteCount比AllocationByteCount小

可以看出:

  • 从内存地址的打印可以看出,两个对象其实是一个对象,Bitmap复用成功;

  • bitmapReuse占用的内存(608256)正好是bitmap占用内存(2433024)的四分之一;

  • getByteCount()获取到的是当前图片应当所占内存大小,getAllocationByteCount()获取到的是被复用Bitmap真实占用内存大小。虽然bitmapReuse的内存只有608256,但是因为是复用的bitmap的内存,因而其真实占用的内存大小是被复用的bitmap的内存大小(2433024)。这也是getAllocationByteCount()可能比getByteCount()大的原因。

Bitmap如何压缩?

1: Bitmap.compress()

质量压缩: 它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。经过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。

2: BitmapFactory.Options.inSampleSize

  • 解码图片时,设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。而设置BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap

  • 设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。例如:一张宽高为2048x1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512x384。占有的内存就是0.75M而不是12M,足足节省了15倍。

  • inSampleSize取值一般时2的倍数

public class ImageResize {

    /**
     *  缩放bitmap
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @return
     */
    public static Bitmap resizeBitmap(Context context,int id,int maxW,int maxH,boolean hasAlpha,Bitmap reusable){
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 只解码出 outxxx参数 比如 宽、高
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources,id,options);
        //根据宽、高进行缩放
        int w = options.outWidth;
        int h = options.outHeight;
        //设置缩放系数
        options.inSampleSize = calcuteInSampleSize(w,h,maxW,maxH);
        if (!hasAlpha){
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false;
        //设置成能复用
        options.inMutable=true;
        options.inBitmap=reusable;
        return BitmapFactory.decodeResource(resources,id,options);
    }

    /**
     * 计算缩放系数
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return 缩放的系数
     */
    private static int calcuteInSampleSize(int w,int h,int maxW,int maxH) {
        int inSampleSize = 1;
        if (w > maxW && h > maxH){
            inSampleSize = 2;
            //循环 使宽、高小于 最大的宽、高
            while (w /inSampleSize > maxW && h / inSampleSize > maxH){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

如何设计一个图片缓存框架?

加载一张图片,我们基本上会经历以下几个步骤:

  • 从内存中获取,如果获取到则返回,如果获取不到
  • 从磁盘中获取,如果获取到则返回,如果获取不到
  • 从网络获取,然后依次保存到内存、磁盘中去

1:内存缓存

使用LruCache算法,LruCache算法:也就是最近最少使用算法,就是当缓存空间满了的时候,将最近最少使用的数据从缓存空间中删除以增加可用的缓存空间来缓存新内容。 LruCache算法底层是利用LinkedHashMap数据结构。LinkedHashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null.

为什么要用LinkedHashMap来存缓存呢,这个跟算法有关,LinkedHashMap刚好能提供LRUCache需要的算法。 这个集合内部本来就有个排序功能,当第三个参数是true的时候,数据在被访问的时候就会排序,这个排序的结果就是把最近访问的数据放到集合的最后面。


memoryCache=new LruCache<String,Bitmap>(memoryClass/8*1024*1024){
    /**
     * @return value占用的内存大小
     */
    @Override
    protected int sizeOf(String key, Bitmap value) {
        //19之前   必需同等大小,才能复用  inSampleSize=1
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT){
            return value.getAllocationByteCount();
        }
        return value.getByteCount();
    }
    /**
     * 当lru满了,bitmap从lru中移除对象时,会回调
     */
    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        if(oldValue.isMutable()){//如果是设置成能复用的内存块,拉到java层来管理
            //3.0以下   Bitmap   native
            //3.0以后---8.0之前  java
            //8。0开始      native
            //把这些图片放到一个复用沲中
            reuseablePool.add(new WeakReference<Bitmap>(oldValue,referenceQueue));
        }else{
            //oldValue就是移出来的对象
            oldValue.recycle();
        }


    }
};

注意: entryRemoved()方法中,判断移除的Bitmap对象是否可以复用,如果不可以直接回收,如果可以则将最近未使用的Bitmap对象放到一个复用沲中。同时单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放。 这也是Glide中的四级缓存原理。(复用池中缓存的是bitmap内存块),利用复用池缓存,不仅可以减小内存开销,还可以提高响应速度。

private ReferenceQueue<Bitmap> getReferenceQueue(){
    if(null==referenceQueue){
        //当弱用引需要被回收的时候,会进到这个队列中
        referenceQueue=new ReferenceQueue<Bitmap>();
        //单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放
        clearReferenceQueue=new Thread(new Runnable() {
            @Override
            public void run() {
                while(!shutDown){
                    try {
                        //remove是阻塞式的
                        Reference<Bitmap> reference=referenceQueue.remove();
                        Bitmap bitmap=reference.get();
                        if(null!=bitmap && !bitmap.isRecycled()){
                            bitmap.recycle();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        clearReferenceQueue.start();
    }
    return referenceQueue;
}

2:磁盘缓存

/**
 * 加入磁盘缓存
 */
public void putBitMapToDisk(String key,Bitmap bitmap){
    DiskLruCache.Snapshot snapshot=null;
    OutputStream os=null;
    try {
        snapshot=diskLruCache.get(key);
        //如果缓存中已经有这个文件  不理他
        if(null==snapshot){
            //如果没有这个文件,就生成这个文件
            DiskLruCache.Editor editor=diskLruCache.edit(key);
            if(null!=editor){
                os=editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                editor.commit();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if(null!=snapshot){
            snapshot.close();
        }
        if(null!=os){
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

调用:

Bitmap bitmap=ImageCache.getInstance().getBitmapFromMemory(String.valueOf(position));
if(null==bitmap){
    //如果内存没数据,就去复用池找
    Bitmap reuseable=ImageCache.getInstance().getReuseable(60,60,1);
    //reuseable能复用的内存
    //从磁盘找
    bitmap = ImageCache.getInstance().getBitmapFromDisk(String.valueOf(position),reuseable);
    //如果磁盘中也没缓存,就从网络下载
    if(null==bitmap){
        bitmap=ImageResize.resizeBitmap(context,R.mipmap.wyz_p,80,80,false,reuseable);
        ImageCache.getInstance().putBitmapToMemeory(String.valueOf(position),bitmap);
        ImageCache.getInstance().putBitMapToDisk(String.valueOf(position),bitmap);
        Log.i("jimmy","从网络加载了数据");
    }else{
        Log.i("jimmy","从磁盘中加载了数据");
    }

}else{
    Log.i("jimmy","从内存中加载了数据");
}

参考

www.jianshu.com/p/7643c6aad… juejin.cn/post/684490…