Bitmap知识点集合

简介: 今天聊聊Bitmap相关的面试题/知识点,看看你是否都弄明白了呢?

前言


今天聊聊Bitmap相关的面试题/知识点,看看你是否都弄明白了呢?


  • Bitmap是什么,怎么存储图片?
  • Bitmap内存如何计算?
  • Bitmap内存 和drawable目录的关系。
  • Bitmap加载优化?不改变图片质量的情况下怎么优化?
  • inJustDecodeBounds是什么?
  • Bitmap内存复用怎么实现?
  • 高清大图加载该怎么处理?
  • 如何跨进程传递大图?


Bitmap是什么,怎么存储图片。


Bitmap,位图,本质上是一张图片的内容在内存中的表达形式。它将图片的内容看做是由存储数据的有限个像素点组成;每个像素点存储该像素点位置的ARGB值,每个像素点的ARGB值确定下来,这张图片的内容就相应地确定下来。其中,A代表透明度,RGB代表红绿蓝三种颜色通道值。


Bitmap内存如何计算


Bitmap一直都是Android中的内存大户,计算大小的方式有三种:


  • getRowBytes() 这个在API Level 1添加的,返回的是bitmap一行所占的大小,需要乘以bitmap的高,才能得出btimap的大小
  • getByteCount() 这个是在API Level 12添加的,其实是对getRowBytes()乘以高的封装
  • getAllocationByteCount() 这个是在API Level 19添加的


这里我将一张图片放到项目的drawable-xxhdpi文件夹中,然后通过方法获取图片所占的内存大小:


var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test)
    img.setImageBitmap(bitmap)
    Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}")
    Log.e(TAG,"size = ${bitmap.allocationByteCount}")


打印出来的结果是


size=1960000


具体是怎么计算的呢?


图片内存=宽 * 高 * 每个像素所占字节


这个像素所占字节又和Bitmap.Config有关,Bitmap.Config是个枚举类,用于描述每个像素点的信息,比如:


  • ARGB_8888。常用类型,总共32位,4个字节,分别表示透明度和RGB通道。
  • RGB_565。16位,2个字节,只能描述RGB通道。


所以我们这里的图片内存计算就得出:


宽700 * 高700 * 每个像素4字节=1960000


Bitmap内存 和drawable目录的关系


首先放一张drawable目录对应的屏幕密度对照表,来自郭霖的博客:


2.jpg


刚才的案例,我们是把图片放到drawable-xxhdpi文件夹,而drawable-xxhdpi文件夹对应的dpi就是我们测试手机的dpi—480。所以图片的内存就是我们所计算的宽 * 高 * 每个像素所占字节


如果我们把图片放到其他的文件夹,比如drawable-hdpi文件夹(对应的dpi是240),会发生什么呢?


再次打印结果:


size = 7840000


这是因为一张图片的实际占用内存大小计算公式是:


占用内存 = 宽 * 缩放比例 * 高 * 缩放比例 * 每个像素所占字节


这个缩放比例就跟屏幕密度DPI有关了:


缩放比例 = 设备dpi/图片所在目录的dpi


所以我们这张图片的实际占用内存位:


宽700 * (480/240) * 高700 * (480/240) * 每个像素4字节 = 7840000


Bitmap加载优化?不改变图片质量的情况下怎么优化?


常用的优化方式是两种:


  • 修改Bitmap.Config


这一点刚才也说过,不同的Conifg代表每个像素不同的占用空间,所以如果我们把默认的ARGB_8888改成RGB_565,那么每个像素占用空间就会由4字节变成2字节了,那么图片所占内存就会减半了。


可能一定程度上会降低图片质量,但是我实际测试看不出什么变化。


  • 修改inSampleSize


inSampleSize,采样率,这个参数是用于图片尺寸压缩的,他会在宽高的维度上每隔inSampleSize个像素进行一次采集,从而达到缩放图片的效果。这种方法只会改变图片大小,不会影响图片质量。


val options=BitmapFactory.Options()
    options.inSampleSize=2
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test2,options)
    img.setImageBitmap(bitmap)


实际项目中,我们可以设置一个与目标图像大小相近的inSampleSize,来减少实际使用的内存:


fun getImage(): Bitmap {
        var options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.drawable.test2, options)
        // 计算最佳采样率
        options.inSampleSize = getImageSampleSize(options.outWidth, options.outHeight)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, R.drawable.test2, options)
    }


inJustDecodeBounds是什么?


上面的例子大家应该发现了,其中有个inJustDecodeBounds,又设置为true,又设置成false的,总感觉多此一举,那么他到底是干嘛呢?


因为我们要获取图片本身的大小,如果直接decodeResource加载一遍的话,那么就会增加内存了,所以官方提供了这样一个参数inJustDecodeBounds。如果inJustDecodeBounds为ture,那么decodebitmap为null,也就是不返回实际的bitmap,只把图片的大小信息放到了options的值中。


所以这个参数就是用来获取图片的大小信息的同时不占用内存。


Bitmap内存复用怎么实现?


如果有个需求,是在同一个imageview中可以加载不同的图片,那我们需要每次都去新建一个Bitmap对象,占用新的内存空间吗?如果我们这样写的话:


override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.actvitiy_bitmap)
        btn1.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test))
        }
        btn2.setOnClickListener {
            img.setImageBitmap(getBitmap(R.drawable.test2))
        }
    }
    fun getBitmap(resId: Int): Bitmap {
        var options = BitmapFactory.Options()
        return BitmapFactory.decodeResource(resources, resId, options)
    }


这样就会Bitmap就会频繁去申请内存,释放内存,从而导致大量GC,内存抖动。


为了防止这种情况呢,我们就可以用到inBitmap参数,用于Bitmap的内存复用。这样同一块内存空间就可以被多个Bitmap对象复用,从而减少了频繁的GC。


val options by lazy {
        BitmapFactory.Options()
    }
    val reuseBitmap by lazy {
        options.inMutable = true
        BitmapFactory.decodeResource(resources, R.drawable.test, options)
    }
    fun getBitmap(resId: Int): Bitmap {
        options.inMutable = true
        options.inBitmap = reuseBitmap
        return BitmapFactory.decodeResource(resources, resId, options)
    }


这里有几个要注意的点


  • inBitmap要和inMutable属性配套使用,否则将无法复用。
  • Android 4.4之前,只能重用相同大小的 Bitmap 内存区域;4.4之后只要复用内存空间的Bitmap对象大小比inBitmap指向的内存空间要小即可。


所以一般在复用之前,还要判断下,新的Bitmap内存是不是小于可以复用的Bitmap内存,然后才能进行复用。


高清大图加载该怎么处理?


如果是高清大图,那就说明不允许进行图片压缩,比如微博长图,清明上河图。


所以我们就要对图片进行局部显示,这就用到BitmapRegionDecoder属性,主要用于显示图片的某一块矩形区域。


比如我要显示左上角的100 * 100区域:


fun setImagePart() {
        val inputStream: InputStream = assets.open("test.jpg")
        val bitmapRegionDecoder: BitmapRegionDecoder =
            BitmapRegionDecoder.newInstance(inputStream, false)
        val options = BitmapFactory.Options()
        val bitmap = bitmapRegionDecoder.decodeRegion(
            Rect(0, 0, 100, 100), options)
        image.setImageBitmap(bitmap)
    }


实际项目使用中,我们可以根据手势滑动,然后不断更新我们的Rect参数来实现具体的功能即可。


具体实现源码可以参考鸿洋的博客:


https://blog.csdn.net/lmj623565791/article/details/49300989


如何跨进程传递大图?


  • Bundle直接传递。bundle最常用于Activity间传递,也属于跨进程的一种方式,但是传递的大小有限制,一般为1M。


//intent.put的putExtra方法实质也是通过bundle
intent.putExtra("image",bitmap);
bundle.putParcelable("image",bitmap)


Bitmap之所以可以直接传递,是因为其实现了Parcelable接口进行了序列化。而Parcelable的传递原理是利用了Binder机制,将Parcel序列化的数据写入到一个共享内存(缓冲区)中,读取的时候也会从这个缓冲区中去读取字节流,然后再反序列化成对象使用。这个共享内存也就是缓存区有一个大小限制—1M,而且是公用的。所以传图片的话很容易就容易超过这个大小然后报错TransactionTooLargeException


所以这个方案不可靠。


  • 文件传输


将图片保存到文件,然后只传输文件路径,这样肯定是可以的,但是不高效。


  • putBinder


这个就是考点了。通过传递binder的方式传递bitmap。


//传递binder
val bundle = Bundle()
bundle.putBinder("bitmap", BitmapBinder(mBitmap))
//接收binder中的bitmap
val imageBinder: BitmapBinder = bundle.getBinder("bitmap") as BitmapBinder
val bitmap: Bitmap? = imageBinder.getBitmap()
//Binder子类
class BitmapBinder :Binder(){
    private var bitmap: Bitmap? = null
    fun ImageBinder(bitmap: Bitmap?) {
        this.bitmap = bitmap
    }
    fun getBitmap(): Bitmap? {
        return bitmap
    }
}


为什么用putBinder就没有大小限制了呢?


  • 因为putBinder中传递的其实是一个文件描述符fd,文件本身被放到一个共享内存中,然后获取到这个fd之后,只需要从共享内存中取出Bitmap数据即可,这样传输就很高效了。
  • 而用Intent/bundle直接传输的时候,会禁用文件描述符fd,只能在parcel的缓存区中分配空间来保存数据,所以无法突破1M的大小限制。


文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。第一个打开的文件是0,第二个是1,依此类推。


参考


https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1872


https://www.cnblogs.com/shakinghead/p/11025805.html


https://blog.csdn.net/lmj623565791/article/details/49300989


https://blog.csdn.net/ylyg050518/article/details/97671874

目录
相关文章
|
3月前
|
Java
【Java集合类面试二十二】、Map和Set有什么区别?
该CSDN博客文章讨论了Map和Set的区别,但提供的内容摘要并未直接解释这两种集合类型的差异。通常,Map是一种键值对集合,提供通过键快速检索值的能力,而Set是一个不允许重复元素的集合。
|
5月前
|
存储 Java 索引
JavaSE——集合框架一(5/7)-Set系列集合:Set集合的特点、底层原理、哈希表、去重复原理
JavaSE——集合框架一(5/7)-Set系列集合:Set集合的特点、底层原理、哈希表、去重复原理
50 1
|
5月前
|
存储
Map集合,集合嵌套知识点
Map集合,集合嵌套知识点
22 0
|
存储 算法
【面试题】位图
【面试题】位图
73 0
|
6月前
|
存储 算法 索引
白话BitMap\RoaringBitMap和BloomFilter
白话BitMap\RoaringBitMap和BloomFilter
|
C++ 容器
【C++进阶】十一、哈希的应用---位图(一)
目录 一、位图的引入 二、位图的应用 三、位图的使用(bitset的使用) 3.1 介绍 3.2 使用 四、bitset(位图模拟实现)
150 0
【C++进阶】十一、哈希的应用---位图(一)
|
C++ 容器
【C++进阶】七、使用红黑树对set和map进行封装
目录 前言 一、改造红黑树 1.1 红黑树迭代器相关 1.2 红黑树接口相关 二、set代码 三、map代码
91 0
【C++进阶】七、使用红黑树对set和map进行封装
|
存储 C++ 容器
C++进阶 红黑树封装map和set
C++进阶 红黑树封装map和set
142 1
C++进阶 红黑树封装map和set
|
安全 小程序 Java
面试官:怎么删除 HashMap 中的元素?我一行代码搞定,赶紧拿去用!
面试官:怎么删除 HashMap 中的元素?我一行代码搞定,赶紧拿去用!
221 0
|
Scala 开发者
Map 映射课堂练习 | 学习笔记
快速学习 Map 映射课堂练习