前言
今天聊聊Bitmap相关的面试题/知识点,看看你是否都弄明白了呢?html
-
Bitmap是什么,怎么存储图片?面试
-
Bitmap内存如何计算?缓存
-
Bitmap内存 和drawable目录的关系。socket
-
Bitmap加载优化?不改变图片质量的状况下怎么优化?ide
-
inJustDecodeBounds是什么?学习
-
Bitmap内存复用怎么实现?测试
-
高清大图加载该怎么处理?优化
-
如何跨进程传递大图?this
Bitmap是什么,怎么存储图片。
Bitmap
,位图,本质上是一张图片的内容在内存中的表达形式。它将图片的内容看作是由存储数据的有限个像素点组成;每一个像素点存储该像素点位置的ARGB
值,每一个像素点的ARGB
值肯定下来,这张图片的内容就相应地肯定下来。其中,A表明透明度,RGB表明红绿蓝三种颜色通道值。spa
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
目录对应的屏幕密度对照表,来自郭霖的博客:
对照表
刚才的案例,咱们是把图片放到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,那么decode
的bitmap
为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
拜拜
有一块儿学习的小伙伴能够关注下❤️ 个人公众号——码上积木,天天剖析一个知识点,咱们一块儿积累知识。公众号回复111可得到面试题《思考与解答》以往期刊。