前言
今天聊聊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文件夹中,然后通过方法获取图片所占的内存大小:
代码语言:javascript复制var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test)
img.setImageBitmap(bitmap)
Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}")
Log.e(TAG,"size = ${bitmap.allocationByteCount}")
打印出来的结果是
代码语言:javascript复制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),会发生什么呢?
再次打印结果:
代码语言:javascript复制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区域:
代码语言:javascript复制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,依此类推。
面试前做好准备战!
接下来将分享面试的一个复习路线,如果你也在准备面试但是不知道怎么高效复习,可以参考一下我的复习路线,有任何问题也欢迎一起互相交流,加油吧!
这里给大家提供一个方向,进行体系化的学习:
1、看视频进行系统学习
前几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。
另外,我自己也珍藏了好几套视频,有需要的我也可以分享给你。
2、进行系统梳理知识,提升储备
客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
系统学习方向:
- 架构师筑基必备技能:深入Java泛型 注解深入浅出 并发编程 数据传输与序列化 Java虚拟机原理 反射与类加载 动态代理 高效IO
- Android高级UI与FrameWork源码:高级UI晋升 Framework内核解析 Android组件内核 数据持久化
- 360°全方面性能调优:设计思想与代码质量优化 程序性能优化 开发效率优化
- 解读开源框架设计思想:热修复设计 插件化框架解读 组件化框架设计 图片加载框架 网络访问框架设计 RXJava响应式编程框架设计 IOC架构设计 Android架构组件Jetpack
- NDK模块开发:NDK基础知识体系 底层图片处理 音视频开发
- 微信小程序:小程序介绍 UI开发 API操作 微信对接
- Hybrid 开发与Flutter:Html5项目实战 Flutter进阶
知识梳理完之后,就需要进行查漏补缺,所以针对这些知识点,我手头上也准备了不少的电子书和笔记,这些笔记将各个知识点进行了完美的总结。
3、读源码,看实战笔记,学习大神思路
“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。
主要内含微信 MMKV 源码、AsyncTask 源码、Volley 源码、Retrofit源码、OkHttp 源码等等。
4、面试前夕,刷题冲刺
面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。
关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三。