十八 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 来分片分布加载
  • 当界面显示多张图或者列表中显示很多图的时候,可以采用缓存算法
相关推荐
聪明的笨猪猪1 小时前
Java Redis “缓存设计”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
聪明的笨猪猪2 小时前
Java Redis “运维”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
苏打水com2 小时前
JavaScript 面试题标准答案模板(对应前文核心考点)
javascript·面试
南北是北北6 小时前
JetPack WorkManager
面试
uhakadotcom7 小时前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
想想就想想7 小时前
线程池执行流程详解
面试
程序员清风8 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
南北是北北9 小时前
JetPack ViewBinding
面试
南北是北北9 小时前
jetpack ViewModel
面试
渣哥9 小时前
Lazy能否有效解决循环依赖?答案比你想的复杂
javascript·后端·面试