在Android
中,获取一个1000*20000
(宽1000px,高20000px
)的大图,如何正常加载显示且不发生OOM
呢?
Android
系统会为应用分配一定大小的堆内存
而如果遇到高分辨率图片时,如果它的配置为ARGB
(每个像素占4Byte
)
那么它要消耗的内存为1000200004=800000000
,大约是80MB
这样轻而易举的就耗尽了内存,出现OOM
。
当然这是用系统原生方法来加载Bitmap
,多数情况下我们都是使用第三方库像Glide
、Freso
、Picasso
,它们对大图加载做了一定处理,但我们不能仅仅停留在会用,更要搞清楚如何解决大图加载OOM
问题。
解决这个问题的方法有两种
BitmapRegionDecoder
加载图片的一部分下面我们都来讲一下
这种方法的原理是,将图片按一定比例缩放,降低分辨率,从而减少内存占用,这里具体用到了BitmapFactory.Options
对象。
BitmapFacotry.Options为BitmapFactory
的一个内部类,它主要用于设定和存储BitmapFactory
加载图片的一些信息。
下面是Options
中需要用到的属性:
inJustDecodeBounds
:如果设置为true
,将不把图片的像素数组加载到内存中outHeight
:图片的高度outWidth
:图片的宽度inSampleSize
:设置此值后,图片将依据此采样率进行加载,不能设置为小于1
的数,例如设置为4
,分辨率宽和高将为原来的1/4
,这个时候整体所占内存将是原来的1/16
需要注意的是 :
如果inSampleSize
设置为小于1
则会被认为是1
,而且它如果为2
的倍数
如果不是那么会向下取整2
的倍数 (但是这个并不是所有Android
版本都成立的)
首先,我们需要将长图放到assets
文件夹下
然后通过代码,读取到文件流
var inputStream = assets.open("image.jpg")
接着,通过BitmapFactory.Options
,设置inJustDecodeBounds = true
,不读取图像到内存,仅读取图片信息 (这样子可以避免读取过程中就发生OOM
),这样,我们就可以获取到图片的宽高了。
val opts = BitmapFactory.Options()
//注意这里是关键 --> 不读取图像到内存中,仅读取图片的信息
opts.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, opts)
//获取图片的宽高
val imgWidth = opts.outWidth
val imgHeight = opts.outHeight
接着,我们要计算出合适的采样率
通过图片实际宽高 / 目标ImageView宽高
,可以得到合适的采样率
最终的采样率以最大的方向为准
val targetImageWidth = targetImageView.width
val targetImageHeight = targetImageView.height
//计算采样率
val scaleX = imgWidth / targetImageWidth
val scaleY = imgHeight / targetImageHeight
//采样率依照最大的方向为准
var scale = max(scaleX, scaleY)
if (scale < 1) {
scale = 1
}
最后,我们再次打开长图的文件流,并将inJustDecodeBounds
设为false
,inSampleSize
赋值为指定的采样率
得到最终缩放后的Bitmap,展示到ImageView中
// false表示读取图片像素数组到内存中,依照指定的采样率
opts.inJustDecodeBounds = false
opts.inSampleSize = scale
//由于流只能被使用一次,所以需要再次打开
inputStream = assets.open("image.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
targetImageView.setImageBitmap(bitmap)
再来看一下完整的代码
private fun loadBigImage(targetImageView: ImageView) {
var inputStream = assets.open("image.jpg")
val opts = BitmapFactory.Options()
//注意这里是关键 --> 不读取图像到内存中,仅读取图片的信息
opts.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, opts)
//获取图片的宽高
val imgWidth = opts.outWidth
val imgHeight = opts.outHeight
val targetImageWidth = targetImageView.width
val targetImageHeight = targetImageView.height
//计算采样率
val scaleX = imgWidth / targetImageWidth
val scaleY = imgHeight / targetImageHeight
//采样率依照最大的方向为准
var scale = max(scaleX, scaleY)
if (scale < 1) {
scale = 1
}
Log.i(TAG, "loadBigImage:$scale")
// false表示读取图片像素数组到内存中,依照指定的采样率
opts.inJustDecodeBounds = false
opts.inSampleSize = scale
//由于流只能被使用一次,所以需要再次打开
inputStream = assets.open("image.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
targetImageView.setImageBitmap(bitmap)
}
运行后效果如下
有时候不仅要求不出现OOM
,还要求不能压缩,完全展示,这种情况下就要用到BitmapRegionDecoder
类了。
/**
* 传入图片
* BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,
* 支持传入文件路径,文件描述符,文件的inputStream等
*/
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream,false);
Rect rect = Rect();
BitmapFactory.Options opetion = BitmapFactory.Options();
/**
* 指定显示图片的区域
* 参数一很明显的rect //参数二是BitmapFactory.Options,
* 你可以告诉图片的inSampleSize,inPreferredConfig等
*/
bitmapRegionDecoder.decodeRegion(rect, opetion);
BitmapRegionDecoder
主要用于显示图片的某一块矩形区域,这个类非常适合加载分区区域加载大图。
为了显示大图的全部,那么伴随着部分显示,必将要添加手势,使其可以上下拖动查看。
这样就需要自定义一个控件了,而自定义这个控件思想也很简单。
onTouchEvent
,在里面根据用户一定的手势,去更新显示区域的参数。invalidate
,onDraw
里面再去regionDecoder.decodeRegion
拿到Bitmap
,去draw
绘制,就可以了。首先,我们要创建一个自定义View
: BigImageView
class BigImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {}
接着,声明一些常量
其中rect
用来表示绘制的区域,后面会对其进行赋值。
options
就是BitmapFactory.Options
companion object {
private var decoder: BitmapRegionDecoder? = null
//图片的宽度和高度
private var imageWidth: Int = 0
private var imageHeight: Int = 0
//绘制的区域
private var rect = Rect()
private val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565
}
}
在onTouchEvent
事件里,处理滑动事件
当ACTION_DOWN
的时候都会赋值downY
为当前位置
当ACTION_MOVE
移动的时候,调用rect.offset
,来修改rect
的top
、left
、right
、bottom
var downX = 0F
var downY = 0F
override fun onTouchEvent(event: MotionEvent): Boolean {
super.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_MOVE -> {
val dY = (event.y - downY).toInt()
if (imageHeight > height) {
rect.offset(0, -dY)
checkHeight()
invalidate()
}
}
MotionEvent.ACTION_DOWN -> {}
}
return true
}
当然,还要判断和处理边界
如果rect.top
小于0
,那么就将rect.top
赋值为0
,rect.bottom
赋值为组件的高度
private fun checkHeight() {
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight
rect.top = imageHeight - height
}
if (rect.top < 0) {
rect.top = 0
rect.bottom = height
}
}
然后需要将图片传入
fun setBitmap(inputStream: InputStream) {
var tempOptions = BitmapFactory.Options()
tempOptions.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, tempOptions)
imageWidth = tempOptions.outWidth
imageHeight = tempOptions.outHeight
decoder = BitmapRegionDecoder.newInstance(inputStream, false)
requestLayout()
invalidate()
}
在onMeasure
的时候,设置rect
为图片的大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (rect.right == 0 && rect.bottom == 0) {
val width = measuredWidth
val height = measuredHeight
rect.left = 0
rect.top = 0
rect.right = rect.left + width
rect.bottom = rect.top + height
}
}
在draw
方法的时候,绘制指定区域
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val bitmap = decoder?.decodeRegion(rect, options)
if (bitmap != null) {
canvas?.drawBitmap(bitmap, 0F, 0F, null)
}
}
最后在Activity
中调用
val inputStream = assets.open("image.jpg")
binding.bigImageView.setBitmap(inputStream)
来看下完整的代码
class BigImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
companion object {
private var decoder: BitmapRegionDecoder? = null
//图片的宽度和高度
private var imageWidth: Int = 0
private var imageHeight: Int = 0
//绘制的区域
private var rect = Rect()
private val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565
}
}
var downX = 0F
var downY = 0F
override fun onTouchEvent(event: MotionEvent): Boolean {
super.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_MOVE -> {
val dY = (event.y - downY).toInt()
if (imageHeight > height) {
rect.offset(0, -dY)
checkHeight()
invalidate()
}
}
MotionEvent.ACTION_DOWN -> {}
}
return true
}
private fun checkHeight() {
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight
rect.top = imageHeight - height
}
if (rect.top < 0) {
rect.top = 0
rect.bottom = height
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val bitmap = decoder?.decodeRegion(rect, options)
if (bitmap != null) {
canvas?.drawBitmap(bitmap, 0F, 0F, null)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (rect.right == 0 && rect.bottom == 0) {
val width = measuredWidth
val height = measuredHeight
rect.left = 0
rect.top = 0
rect.right = rect.left + width
rect.bottom = rect.top + height
}
}
fun setBitmap(inputStream: InputStream) {
var tempOptions = BitmapFactory.Options()
tempOptions.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, tempOptions)
imageWidth = tempOptions.outWidth
imageHeight = tempOptions.outHeight
decoder = BitmapRegionDecoder.newInstance(inputStream, false)
requestLayout()
invalidate()
}
}
效果如下所示
到现在,我们就明白如何在Android
中加载一张大图了
BitmapFactory.Options.inJustDecodeBounds
先获取图片宽高,在通过设置采样率inSampleSize
来缩小图片尺寸,从而达到减小图片大小的目的bitmapRegionDecoder.decodeRegion
来绘制图片指定区域,来避免一次性加载整张图片,只显示屏幕需要的图片,从而避免OOM
本文源码下载 : Android中加载一张大图示例Demo