Bitmap之内存管理

前言

在Android应用程序内存问题中,Bitmap内存问题,占了相当的比重,所以弄清楚Bitmap内存管理的机制原理就尤为重要。下面就分析总结一下,Bitmap内存相关的问题。

1.Bitmap简介

Bitmap 是 Android 系统中的图像处理中最重要类之一。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。

有两种方法可以创建 Bitmap 对象,分别是通过 Bitmap.createBitmap() 和 BitmapFactory 的 decode 系列静态方法创建 Bitmap 对象。

下面我们主要介绍 BitmapFactory 的 decode 方式创建 Bitmap 对象。

  • decodeFile 从文件系统中加载
    • 通过 Intent 打开本地图片或照片
    • 根据 uri 获取图片的路径
    • 根据路径解析 Bitmap:Bitmap bm = BitmapFactory.decodeFile(path);
  • decodeResource 以 R.drawable.xxx 的形式从本地资源中加载
    • Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.icon);
  • decodeStream 从输入流加载
    • Bitmap bm = BitmapFactory.decodeStream(stream);
  • decodeByteArray 从字节数组中加载
    • Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);

2.BitmapFactory.Options控制图片解码的参数

  • inSampleSize:这是表示采样大小。用于将图片缩小加载出来的,以免占用太大内存,适合缩略图。

  • inJustDecodeBounds:当 inJustDecodeBounds 为 true 时,执行 decodexxx 方法时,BitmapFactory 只会解析图片的原始宽高信息,并不会真正的加载图片

  • inPreferredConfig:用于配置图片解码方式,对应的类型 Bitmap.Config。如果非null,则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888

  • inBitmap:在 Android 3.0 开始引入了 inBitmap 设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个Bitmap。在 Android4.4,新增了允许 inBitmap 设置的图片与需要加载的图片的大小不同的情况,只要 inBitmap 的图片比当前需要加载的图片大就好了。

  • inDendity

    表示这个bitmap的像素密度,根据drawable目录, 图片放在不同的资源目录,会有不同程度缩放

  • inTargetDensity

    表示要被画出来时的目标(屏幕)的像素密度,
    代码中获取的方式getResources().getDisplayMetrics().densityDpi

通过 BitmapFactory.Options 的这些参数,我们就可以按一定的采样率来加载缩小后的图片,然后在 ImageView 中使用缩小的图片这样就会降低内存占用避免【OOM】,提高了 Bitamp 加载时的性能。

这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素,一张图片所占内存大小的计算方式: 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。

3.Bitmap的内存占用

我们在使用图片的时候,选择 jpg、png或者webp,对内存会不会有影响呢?对同一张图片而言,更改jpg为png或其它格式,并不会改变它实际占用的内存大小。Bitmap的大小(在本地磁盘通过属性查看)与Bitmap在内存中占用的大小是两个概念,值也不相同。Bitmap内存占用大小为:width * height * 一个像素点占用的字节数(由Bitmap的像素存储格式决定),只要这些因素没变,占用内存是不会改变的。可通过调用getByteCount()来获取Bitmap占用内存的大小,该方法返回的是可用于存储此位图像素的最小字节数

在这里插入图片描述
将这三张图片拷贝至drawable-xxhdpi目录,调用getByteCount方法获取占用内存:

	/**
     * 解析图片
     */
    private void decodeBitmap() {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv);
        Log.e("xpf", "decodeBitmap: icon_mv.jpg " + bitmap.getWidth() + "x" + bitmap.getHeight() + "x"
                + bitmap.getConfig() + ",内存总大小" + bitmap.getByteCount());
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv_p);
        Log.e("xpf", "decodeBitmap: icon_mv_p.png " + bitmap1.getWidth() + "x" + bitmap1.getHeight() + "x"
                + bitmap1.getConfig() + ",内存总大小" + bitmap1.getByteCount());
        Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv_w);
        Log.e("xpf", "decodeBitmap: icon_mv_w.webp " + bitmap2.getWidth() + "x" + bitmap2.getHeight() + "x"
                + bitmap2.getConfig() + ",内存总大小" + bitmap2.getByteCount());

    }

打印日志如下:

在这里插入图片描述

我们知道由于Android的资源加载机制,图片放在不同的drawable目录,会对图片进行不同程度的拉伸或缩放,如果将图片放到drawable-hdpi目录,图片内存占用内存信息如下:

在这里插入图片描述

很明显图片尺寸被放大/拉伸了,内存占用就变大了,与Android资源加载机制相关。有一定的优先级,与设备dpi有关,适配时需要注意。

匹配的优先级:

  • 如果在最匹配的目录没有找到对应图片,就会向更高密度的目录查找,直到没有更高密度的目录
  • 如果一直往高密度目录均没有查找,Android就会查找drawable-nodpi目录。drawable-nodpi目录中的资源适用于所有密度的设备,不管当前屏幕的密度如何,系统都不会缩放此目录中的资源。因此,对于永远不希望系统缩放的资源,最简单的方法就是放在此目录中;同时,放在该目录中的资源最好不要再放到其他drawable目录下了,避免得到非预期的效果
  • 如果在drawable-nodpi目录也没有查找到,系统就会向比最匹配目录密度低的目录依次查找,直到没有更低密度的目录。例如,最匹配目录是xxhdpi,更高密度的目录和nodpi目录查找不到后,就会依次查找drawable-xhdp、drawable-hdpi、drawable-mdpi、drawable-ldpi。

放大还是缩小:

  • 如果图片所在目录为匹配目录,则图片会根据设备dpi做适当的缩放调整。
  • 如果图片所在目录dpi低于匹配目录,那么该图片被认为是为低密度设备需要的,现在要显示在高密度设备上,图片会被放大。
  • 如果图片所在目录dpi高于匹配目录,那么该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小。
  • 如果图片所在目录为drawable-nodpi,则无论设备dpi为多少,保留原图片大小,不进行缩放。

像素存储格式中A代表透明度;R代表红色;G代表绿色;B代表蓝色,单个像素点占用的字节数如下表:

格式 意义 单个像素占用字节数
ALPHA_8 表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度 1
ARGB_4444 表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位 2
ARGB_8888 表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位 4
RGB_565 表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位 2

3.Bitmap的高效加载/内存压缩/采样率压缩

想象一下如果一张图片为800 * 800 的RGB_565加载到内存,占用内存大小为800 * 800 * 2=1.22M, 比如显示的Imageview大小为80 * 80 ,一个应用有数不清的图片,如果直接加载到内存,对内存的压力太大了,显然是不合理的。此时,就根据采样率来加载图片。采样率inSampleSize = n, 表示采样后的图片的宽高均为图片原始宽高的1/n, 其中n一般为2的指数,比如1,2,4,8,16等等。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整一个最接近2的指数来代替,比如3,系统会选择2来代替。通过采用率来加载图片,本质是根据一定比例对图片进行尺寸压缩,其流程大致如下:

  • 将BitmapFactory.Options的inJustDecodeBounds参数设为true,并加载图片
  • 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数
  • 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
  • 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片

需要注意的是inJustDecodeBounds参数设为true,BitmapFactory只会解析图片的的原始宽高信息,并不会去真正的加载图片。得到采样率后,inJustDecodeBounds参数设为false,再去真正加载图片,加载的图片就是最终缩放后的图片。

/**
 * Bitmap高效加载,采样率压缩,本质是调整bitmap的尺寸
 */
public class ImageResize {

    /**
     * 返回压缩的图片
     * @param context
     * @param id
     * @param reqWidth
     * @param reqHeight
     * @param hasAlpha
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int id, int reqWidth, int reqHeight, boolean hasAlpha) {
        Resources resources = context.getApplicationContext().getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        //设置inJustDecodeBounds为true时,并不会真正加载图片,只是解析bitmap的原始宽高信息,轻量级操作
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources, id, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        if (!hasAlpha) {
            //不需要透明度
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        //设置inJustDecodeBounds为false,真正去加载图片
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(resources, id, options);

    }



    /**
     * 获取采样率/缩放系数
     * @param options
     * @param reqWidth 需要显示的宽度/xml定义的宽度
     * @param reqHeight 需要显示的高度/xml定义的高度
     * @return
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        //bitmap的原始宽高(测量宽高)
        int width = options.outWidth;
        int height = options.outHeight;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfWidth = width / 2;
            int halfHeight = height / 2;
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *=2;
            }
        }
        return inSampleSize;
    }
}

这就是Bitmap的内存优化角度之一,内存压缩即采样率压缩,本质是通过调整图片的尺寸,前面提到过,图片的占用内存为:宽 * 高 * 单个像素占用字节 ,这三个因素不变,内存占用就不会改变。在BitmapAdapter类的onBindViewHolder方法中,分别采用以下两种方式加载bitmap,对比内存占用情况

@Override
    public void onBindViewHolder(@NonNull BitmapViewHolder bitmapViewHolder, int i) {

        //原始方法获取bitmap
//        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_mv_w);

        // 优化角度一:压缩图片
        Bitmap bitmap = ImageResize.resizeBitmap(context, R.drawable.icon_mv, 80, 80, false);

        bitmapViewHolder.iv.setImageBitmap(bitmap);
    }

2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,当java层bitmap被回收后,能及时回收native层的像素数据。8.0之后图像资源的管理更加优秀,极大降低了OOM的发生。

4.Bitmap的缓存策略

主要就是内存缓存和磁盘缓存,内存缓存主要就是利用LruCache通过最近最少使用算法来实现,是基于LinkedHashMap的封装。通过下面简单的例子,便于更形象的理解。

public class LRU {
	public static main(String[] args) {
		//true 访问排序
		LinkedHashMap<String, Integer> map = new LinkedHashMap(0, 0.75F, true);
		
		map.put("一", 1);// 最开始添加(访问)的,它的LRU算法移除概率是最高的(越容易被移除)
		map.put("二", 2);
		map.put("三", 3);
		map.put("四", 4);
		map.put("五", 5);// 最后添加(访问)的,它的LRU算法移除概率是最低的(越难被移除)
		
		for(Map.Entry<String, Integer> value : map.entrySet()){
			System.out.print(value.getValue);
		}
		
		//12345
		
		//使用了某个元素
		map.get("三")//最近被访问,就越不可能被回收
		
		for(Map.Entry<String, Integer> value : map.entrySet()){
			System.out.print(value.getValue);
		}
		//12453
		
	}
}

遍历打印集合的value,当达到容量上限时,优先移除最近最少使用,即最先添加或者最先访问的元素会被优先移除。在例子中,元素3最近被访问了,从频率和时间上看,元素1就是最近最少使用,如果达到容量上限,会优先移除元素1,再添加元素。

内存缓存是通过LrcCache来实现,磁盘缓存是通过DiskLruCache实现,下面是内存缓存和磁盘缓存的初始化

public void init(Context context, String dir) {

        //初始化复用池
        reusePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
        
        ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
        if (activityManager == null) {
            return;
        }
		//内存大小
        int memorySize = activityManager.getMemoryClass();

        //返回一张图片占用内存大小,单位:byte
        lruCache = new LruCache<String, Bitmap>(memorySize / 8 * 1024 * 1024) {
            //返回一张图片占用内存大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    return value.getAllocationByteCount();
                }
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //判断是否采用复用内存
                if (oldValue.isMutable()) {
                    //3.0~7.1 bitmap 像素存储在 Java 堆
                    //8.0及之后,bitmap 像素存储在 Native 堆
                    reusePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {
                    //如果没有开启bitmap内存复用,那么就回收释放
                    oldValue.recycle();
                }
            }
        };
        try {
            //初始化磁盘缓存
            diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

添加bitmap缓存的get、put方法

/**
     * 内存缓存,将bitmap放入内存
     * @param key
     * @param bitmap
     */
    public void putBitmapToMemory(String key, Bitmap bitmap) {
        lruCache.put(key, bitmap);
    }

    /**
     * 从内存缓存中取出Bitmap
     * @param key
     * @return
     */
    public Bitmap getBitmapFromMemory(String key) {
        return lruCache.get(key);
    }

    /**
     * 清除内存缓存
     */
    public void clearMemory() {
        lruCache.evictAll();
    }

    /**
     * 将bitmap放入磁盘缓存
     * @param key
     * @param bitmap
     */
    public void putBitmapToDisk(String key, Bitmap bitmap) {
        DiskLruCache.Snapshot snapshot = null;
        OutputStream os = null;
        try {
            snapshot = diskLruCache.get(key);
            if (snapshot == null) {
                DiskLruCache.Editor editor = diskLruCache.edit(key);
                if (editor != null) {
                    os = editor.newOutputStream(0);
                    //质量压缩
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 50, 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
     * @param key
     * @return
     */
    public Bitmap getBitmapFromDisk(String key, Bitmap resueable) {
        DiskLruCache.Snapshot snapshot = null;
        Bitmap bitmap = null;
        InputStream is = null;
        try {
            snapshot = diskLruCache.get(key);
            if (snapshot == null) {
                return null;
            }
            is = snapshot.getInputStream(0);
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inMutable = true;
            options.inBitmap = resueable;
            bitmap = BitmapFactory.decodeStream(is, null, options);
            if (bitmap != null) {
                lruCache.put(key, bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (snapshot != null) {
                snapshot.close();
            }
        }

        return bitmap;
    }

5.Bitmap的内存复用

如下图所示,在Android 3.0之前,为bitmap申请的内存是不能复用的,每次加载一张bitmap,都会重新为它分配一块内存,这显然是很耗内存的,而且频繁的加载图片,释放内存容易造成内存抖动;在3.0~4.4 版本,内存可以复用,但前提是,两张图片占用的内存大小必须相等;在4.4之后,只要加载的图片,比释放的图片占用内存小,就可以复用内存,大大降低了内存的消耗,也在一定程度上降低了内存抖动的发生的可能性。
在这里插入图片描述在这里插入图片描述

Bitmap的内存复用主要有几个细节要注意

  • 开启内存复用

    //bitmap内存复用,inMutable 为true,表示可变的,才能复用
    //下面两行代码就表示了开启了bitmap的内存复用
    options.inMutable = true;

    options.inBitmap = bitmap;

  • 判断是否满足内存复用条件

/**
     * bitmap内存复用条件判断
     * 3.0 之前不能复用
     * 3.0~4.4 宽高一样,inSampleSize = 1, 即内存占用一样才可以
     * 4.4 之后,只要小于等于可复用的内存就可以
     * @return
     */
    public Bitmap getReuseable(int width, int height, int inSampleSize) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return null;
        }

        Bitmap reuseable = null;
        Iterator<WeakReference<Bitmap>> iterator = reusePool.iterator();
        while (iterator.hasNext()) {
            Bitmap bitmap = iterator.next().get();
            if (bitmap != null) {
                if (checkBitmapMemory(bitmap, width, height, inSampleSize)) {
                    reuseable = bitmap;
                    iterator.remove();
                    break;
                }
            } else {
                iterator.remove();
            }
        }
        return reuseable;
    }

    /**
     * 校验bitmap是否满足内存复用条件
     * @param bitmap
     * @param width
     * @param height
     * @param inSampleSize
     * @return
     */
    private boolean checkBitmapMemory(Bitmap bitmap, int width, int height, int inSampleSize) {
        //3.0~4.4
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return bitmap.getWidth() == width && bitmap.getHeight() == height && inSampleSize == 1;
        }

        // 高于4.4
        if (inSampleSize > 1) {
            width /= inSampleSize;
            height /= inSampleSize;
        }

        int byteCount = width*height*getBytePerPixel(bitmap.getConfig());

        //bitmap.getByteCount() 图片内存
        //bitmap.getAllocationByteCount() 系统分配内存
        return byteCount <= bitmap.getAllocationByteCount();
    }

    /**
     * 通过像素格式计算每一个像素占用字节
     * @param config
     * @return
     */
    private int getBytePerPixel(Bitmap.Config config) {
        if (config == Bitmap.Config.ALPHA_8) {
            return 1;
        } else if (config == Bitmap.Config.ARGB_8888) {
            return 4;
        } else {
            return 2;
        }
    }
  • 如果开启了内存复用,满足复用条件,当从缓存移除元素时,就存放到复用池,否则就回收释放
	@Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue.isMutable()) {
                    //3.0~7.1 bitmap 像素存储在 Java 堆
                    //8.0及之后,bitmap 像素存储在 Native 堆
                    reusePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {
                    //bitmap回收
                    oldValue.recycle();
                }
            }

6.小结

Bitmap的内存管理主要就是这几个部分,不同层级的缓存,内存缓存和磁盘缓存,避免每次都要网络加载。通过采样率加载图片,达到压缩的效果,避免加载bitmap过大造成的内存压力;内存复用,避免了每次加载释放bitmap,都要分配和释放对应的内存空间,有效的缓解了内存抖动。有了对Bitmap内存管理的认识,就比较容易理解图片加载框架原理了。

原创文章 23 获赞 30 访问量 9565

猜你喜欢

转载自blog.csdn.net/my_csdnboke/article/details/104662808