十八 Bitmap详解

概述

Bitmap 经常用于 安卓app中图片处理。同时它也是 内存消耗的大户,当bitmap要使用的内存空间超过了剩余可用内存,就会报OOM。

内存占用分析

Bitmap用来描述一张图片的宽高颜色等信息。 我们可以使用BitmapFactory来将某路径下的一张图片转化成bitmap对象。

那么具体如何转化,以及转化之后会占用多大内存呢?

将一张图片test.png放在 res/mipmap,然后用以下代码来加载图片并打印其占用空间:

java 复制代码
// 图片资源转化为内存中的bitmap对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", bitmap.getAllocationByteCount());
// 将bitmap对象设置给imageView
imageView.setImageBitmap(bitmap);

默认情况下,转化图片资源时采用的图片编码Config是 ARGB8888. 在这种情况下,每个像素占用的字节是4个

所以一个 440 x 423 的 图片,最终占用的内存空间会是 440 x 423 x 4 x 9 = 6700320 byte

屏幕自适应

在代码不动的前提下,如果将图片移动到 mipmap-xhdpi 下,再次计算图片大小:

同样还是这张图 : 440 x 423 的 图片,最终占用的内存空间会是 1676400 byte

实际上,BitmapFactory的图片解析过程,与图片所在目录dpi密度有关。

具体公式为:

  • 缩放比例 scale = 当前设备屏幕密度 / 图片所在dpi密度
  • bitmap实际大小 = 宽 * scale * 高 * scale * Config存储像素数(ARGB_8888 每个像素占用的字节是4个

加载assets中的图片

java 复制代码
InputStream open = getAssets().open("test.png");
Bitmap bitmap = BitmapFactory.decodeStream(open);
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));

打印结果为: 744480 (440 x 423 x 4)

由此可见,图片放在 drawable/mipmap 下 的任何目录,通过 bitmapFactory去加载时都会有 不同程度的缩放。 但是放在 assets中就能保持原图大小。

图片加载优化

由于上面所说的sacle参数的问题,比如一个65K的图,加载到内存之后,就有可能占用 2.5M的空间。此时,就要适当对bitmap的加载做出优化。有以下方式:

修改Config

默认的存储像素参数:

上面所说的 ARGB_8888 每个像素占用4个字节。 而 RGB_565 每个像素占用2个字节。

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));

这样就可以节省一半的空间。但是代价就是:

使得图像的质量下降。这是因为 RGB_565 只有16位,只有5位用于红色通道,6位用于绿色通道,5位用于蓝色通道。相比之下,ARGB_8888 则具有更高的色彩深度,即8位,每个颜色通道具有256个可能值。

另外,由于 RGB_565 无法支持透明度通道,所以对于需要使用透明度的图像来说,使用 RGB_565 格式就不再适用。

修改采样密度

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 修改采样密度,宽高上都每隔2个像素采样一次
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test);
// 打印bitmap占用的内存空间
Log.d("BitmapTag", String.valueOf(bitmap.getAllocationByteCount()));

options.inSampleSize = 2; 这句话,修改采样密度,宽高上都每隔2个像素采样一次。最终结果就是图片的大小缩小为原来的1/4

bitmap复用

Bitmap是吃内存的大户。所以,如果一个安卓的页面上存在大量的bitmap实例,并且存在频繁的内存申请和回收,就会造成app内存吃紧,内存抖动,交互卡顿。

比如下面的页面:

java 复制代码
public class MainActivity2 extends AppCompatActivity {

    private ActivityMainBinding binding;

    private int count;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        initView();
        load();
    }

    private void initView() {
        binding.imageView.setOnClickListener(v -> {
            count++;
            load();
        });
    }

    private void load() {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), count % 2 == 1 ? R.drawable.test : R.drawable.test2);
        binding.imageView.setImageBitmap(bitmap);
    }

}

频繁切换之后的内存情况如下:

此时,要做出优化,

java 复制代码
public class MainActivity2 extends AppCompatActivity {

    private ActivityMainBinding binding;

    private int count;
    private Bitmap lastBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        initView();
        load();
    }

    private void initView() {
        binding.imageView.setOnClickListener(v -> {
            count++;
            load();
        });
    }

    private void load() {

        // 根据 count 的奇偶性选择不同的资源
        int resId = count % 2 == 1 ? R.drawable.test : R.drawable.test2;

        // 复用上一次的 Bitmap
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        options.inBitmap = lastBitmap;

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);

        lastBitmap = bitmap; // 保存当前的 Bitmap

        binding.imageView.setImageBitmap(bitmap);
    }
}

复用的做法为:

  • 创建一个 BitmapFactory.Options,设置 isMutable=true,inBitmap=lastBitmap
  • 尝试加载 BitmapFactory.decodeResource 加载bitmap
  • 将加载后的bitmap赋值给 lastBitmap

优化之后,频繁切换bitmap,内存情况如下:

但是这种复用有一定的条件,那就是:

  • 新的bitmap所占用的空间一定要不能大于 已有的bitmap。否则就会重新创建bitmap。
  • isMutable必须是true

超大图的加载

有的长图,或者特别精密的图片,不建议一次性全部加载到内存,否则用户会感觉到等待很久图片都没加载完毕。而是采用分片加载的方式,结合放大手势分区块加载。

此时就要用到 BitmapRegionDecoder.

基本使用如下:

java 复制代码
InputStream open = getAssets().open("test2.png");

BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(open, false);

BitmapFactory.Options option = new BitmapFactory.Options();
Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, 300, 100), option);

几个关键点:

  • 创建 BitmapRegionDecoder 时使用静态newInstance方法,支持传入 绝对路径,文件描述符,输入流的方式
  • BitmapRegionDecoder的decodeRegion方法的第一个参数是一个矩形 Rect实例,它确定了本次要加载的图片范围。

可以通过自定义View,结合 BitmapRegionDecoder来动态的 获取不同Rect范围内的bitmap对象。结合滑动手势,就能实现图片的分步加载。

图片缓存

如果在列表的item中要用到 bitmap,比如RecyclerView,上下滑动,不停地去bindViewHolder,就有可能重复创建bitmap,此时,引入缓存机制,就有可能造成大量的内存分配和回收。比较成熟的方式是LRU策略。

  • 手动指定Bitmap能占用的最大空间(上图中是20M)
  • 用 图片名 和bitmap对象的对应关系,在recyclerView中去调用 getBitmapFromCache来获取bitmap对象,避免重复创建
  • 当缓存空间满载之后,如果再次 addBitmapToCache,那么 Lru缓存池就会自动清除相对不常使用的bitmap

总结

本文讲述了

  • 一张图被加载成bitmap之后的实际占用内存的计算方式
  • 通过Bitmap.Option.isBitmap可以实现 bitmap对象的复用
  • 当图片超大的时候,采用 BitmapRegionDecoder 来分片分布加载
  • 当界面显示多张图或者列表中显示很多图的时候,可以采用缓存算法
相关推荐
睡觉然后上课4 小时前
c基础面试题
c语言·开发语言·c++·面试
xgq4 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
邵泽明6 小时前
面试知识储备-多线程
java·面试·职场和发展
夜流冰8 小时前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
杰哥在此14 小时前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
GISer_Jing20 小时前
【React】增量传输与渲染
前端·javascript·面试
Neituijunsir1 天前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
小飞猪Jay1 天前
面试速通宝典——10
linux·服务器·c++·面试
猿java1 天前
Cookie和Session的区别
java·后端·面试
数据分析螺丝钉1 天前
力扣第240题“搜索二维矩阵 II”
经验分享·python·算法·leetcode·面试