question
In Android
, get a 1000*20000
( 宽1000px,高20000px
) large image, how to load and display normally and not happen OOM
?
analyze
Android
The system will allocate a certain size of heap memory for the application
, and if it encounters a high-resolution image, if its configuration is ARGB
(each pixel occupies 4Byte
)
, then the memory it will consume 1000200004=800000000
is about 100, and 80MB
the memory is easily exhausted. appear OOM
.
Of course, this is loaded with the native method of the system Bitmap
. In most cases, we use third-party libraries like Glide
, Freso
, Picasso
, which do a certain amount of processing for large image loading, but we can’t just stop at using it, but also figure out how to solve it. Image loading OOM
issue.
There are two ways to solve this problem
- Image sampling rate scaling
- Use
BitmapRegionDecoder
part of the loaded image
Let's talk about it
Solution 1: Image sampling rate scaling
BitmapFactory.Options
The principle of this method is to scale the picture by a certain ratio and reduce the resolution, thereby reducing the memory usage. Objects are specifically used here .
BitmapFacotry.Options为BitmapFactory
An internal class, it is mainly used to set and store BitmapFactory
some information about loading pictures.
The following are Options
the attributes that need to be used in:
inJustDecodeBounds
: If set totrue
, the pixel array of the picture will not be loaded into memoryoutHeight
: the height of the imageoutWidth
: the width of the pictureinSampleSize
: After setting this value, the picture will be loaded according to this sampling rate. It cannot be set to a1
number smaller than that. For example, if it is set to4
, the resolution, width and height will be the original1/4
, and the overall memory occupied at this time will be the original1/16
It should be noted that
if itinSampleSize
is set to1
be less than it will be considered1
, and if it is2
a multiple ,
if it is not then it will be rounded down to2
a multiple of integers (but this is notAndroid
true for all versions)
code example
First, we need to put the long picture in assets
the folder
and then read the file stream through the code
var inputStream = assets.open("image.jpg")
Then, by BitmapFactory.Options
setting inJustDecodeBounds = true
, do not read the image to the memory, only read the image information (this can avoid the occurrence during the reading process OOM
), so that we can get the width and height of the image.
val opts = BitmapFactory.Options()
//注意这里是关键 --> 不读取图像到内存中,仅读取图片的信息
opts.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, opts)
//获取图片的宽高
val imgWidth = opts.outWidth
val imgHeight = opts.outHeight
Next, we need to calculate the appropriate sampling rate.
By passing 图片实际宽高 / 目标ImageView宽高
, we can get the appropriate sampling rate.
The final sampling rate is subject to the maximum direction.
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
}
Finally, we open the file stream of the long image again, and inJustDecodeBounds
set false
it inSampleSize
to the specified sampling rate
to get the final scaled Bitmap and display it in the ImageView
// false表示读取图片像素数组到内存中,依照指定的采样率
opts.inJustDecodeBounds = false
opts.inSampleSize = scale
//由于流只能被使用一次,所以需要再次打开
inputStream = assets.open("image.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
targetImageView.setImageBitmap(bitmap)
Let's take a look at the complete code
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)
}
The effect after running is as follows
Solution 2: Images are loaded by region
Sometimes it is not only required not to appear OOM
, but also not to be compressed and fully displayed. In this case, BitmapRegionDecoder
classes are used.
/**
* 传入图片
* 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
Mainly used to display a certain rectangular area of an image, this class is very suitable for loading partitioned areas and loading large images.
In order to display all of the big picture, along with the partial display, gestures must be added so that it can be dragged up and down to view.
In this way, you need to customize a control, and the idea of customizing this control is also very simple.
- Provide an entry to set the picture
- Rewrite
onTouchEvent
, and update the parameters of the display area according to the user's certain gestures. - After not updating the area parameters, call it
invalidate
, get itonDraw
inside , and draw it.regionDecoder.decodeRegion
Bitmap
draw
code example
First, we're going to create a custom View
:BigImageView
class BigImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
Next, declare some constants,
which rect
are used to represent the drawn area, which will be assigned later.
options
that isBitmapFactory.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
}
}
In onTouchEvent
the event,
when the sliding event is processed ACTION_DOWN
, it will be assigned downY
the current position .
When ACTION_MOVE
moving, call rect.offset
to rect
modify 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
}
Of course, it is also necessary to judge and process the boundary .
If it is rect.top
less than , 0
then it will rect.top
be assigned as the height of the component0
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
}
}
Then you need to pass the image into
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()
}
At onMeasure
the time, set it rect
to the size of the picture
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
}
}
In draw
the method, draw the specified area
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val bitmap = decoder?.decodeRegion(rect, options)
if (bitmap != null) {
canvas?.drawBitmap(bitmap, 0F, 0F, null)
}
}
Finally Activity
call in
val inputStream = assets.open("image.jpg")
binding.bigImageView.setBitmap(inputStream)
Take a look at the complete code
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()
}
}
The effect is as follows
summary
By now, we understand how to Android
load a large image in
- Solution 1: Image sampling rate scaling: Get the width and height of the image first, and then reduce the image size
BitmapFactory.Options.inJustDecodeBounds
by setting the sampling rate , so as to achieve the purpose of reducing the image sizeinSampleSize
- Solution 2: Images are loaded by region: By
bitmapRegionDecoder.decodeRegion
drawing the specified region of the image, it is avoided to load the entire image at one time, and only the images required by the screen are displayed, thereby avoidingOOM
Download the source code of this article: Demo of loading a large image in Android