Android中加载一张大图,如何正常显示且不发生OOM ?

这篇具有很好参考价值的文章主要介绍了Android中加载一张大图,如何正常显示且不发生OOM ?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

问题

Android中,获取一个1000*20000(宽1000px,高20000px)的大图,如何正常加载显示且不发生OOM呢?

分析

Android系统会为应用分配一定大小的堆内存
而如果遇到高分辨率图片时,如果它的配置为ARGB(每个像素占4Byte)
那么它要消耗的内存为1000200004=800000000,大约是80MB
这样轻而易举的就耗尽了内存,出现OOM

当然这是用系统原生方法来加载Bitmap,多数情况下我们都是使用第三方库像GlideFresoPicasso,它们对大图加载做了一定处理,但我们不能仅仅停留在会用,更要搞清楚如何解决大图加载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设为falseinSampleSize赋值为指定的采样率
得到最终缩放后的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)
}

运行后效果如下

Android中加载一张大图,如何正常显示且不发生OOM ?

解决方案二 : 图片按区域加载

有时候不仅要求不出现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,在里面根据用户一定的手势,去更新显示区域的参数。
  • 没有更新区域参数后,调用invalidateonDraw里面再去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,来修改recttopleftrightbottom

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赋值为0rect.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中加载一张大图,如何正常显示且不发生OOM ?

小结

到现在,我们就明白如何在Android中加载一张大图了

  • 方案一 : 图片采样率缩放 : 利用BitmapFactory.Options.inJustDecodeBounds先获取图片宽高,在通过设置采样率inSampleSize来缩小图片尺寸,从而达到减小图片大小的目的
  • 方案二 : 图片按区域加载 : 通过bitmapRegionDecoder.decodeRegion来绘制图片指定区域,来避免一次性加载整张图片,只显示屏幕需要的图片,从而避免OOM

本文源码下载 : Android中加载一张大图示例Demo文章来源地址https://www.toymoban.com/news/detail-500006.html

到了这里,关于Android中加载一张大图,如何正常显示且不发生OOM ?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java中加载图片并显示

    目录 一、首先获得图片 二、将图片显示在界面中 三、完整代码展示 关于图片的加载方式有很多种,这里我只介绍一种,毕竟太多记不住。 关于图片的显示,这里使用一个paint()的方法。 对于paint()方法我们需要自己去重写。paint()方法不需要我们调用,系统会自动调用,一定

    2024年02月11日
    浏览(29)
  • Android 在TextView前面添加多个任意View且不影响换行

    实现效果如下: 如上,将头像后面的东西看作一个整体,因为不能影响后面内容的换行,且前面控件的长度是可变的,所以采用自定义View的方法来实现: 使用举例 :  (😂抱歉啊使用这边没有用Java写,不会Kotlin的应该也能看懂啥意思) activity_main.xml: la: dataListType的 0,1,

    2024年02月09日
    浏览(55)
  • Mac下载的软件显示文件已损坏,如何解决文件已损坏问题,让文件可以正常运行

    设备/引擎:Mac(11.6)/Mac Mini 开发工具:终端 开发需求:让显示已损坏的文件顺利安装到电脑 大家肯定都遇到过下载的dmg文件安装时显示文件已损坏,让丢废纸篓,系统设置也没有需要去手动点击通过允许啥的,这种情况就很头疼,想用又一直用不了。今天就总结一下分享

    2024年02月04日
    浏览(50)
  • 在QGIS中加载显示3DTiles数据

    “我们最近有机会在QGIS 3.34中实现一个非常令人兴奋的功能–能够以“Cesium 3D Tiles”格式加载和查看3D内容!” ——QGIS官方的 宣传介绍。 体验一下,感觉就是 如芒刺背 、 如坐针毡 、 如鲠在喉 。 除非我电脑硬件有问题,要么QGIS的3Dtiles是真的垃。。 请大家赶紧下载试用,

    2024年02月04日
    浏览(38)
  • 如何在Qt Desginer中设置背景图片,且不覆盖其它控件

    正常情况,我们直接通过在样式表里设置背景图片会出现背景图片覆盖其它控件的情况,比如下面操作: 首先右击空白处,点击改变样式表。 然后选择background-image 然后点击铅笔图标 之后我们要先添加前缀,也就是我们的项目名,比如我的这个项目名为Snake,那就添加一个名

    2024年02月03日
    浏览(44)
  • ​第20课 在Android Native开发中加入新的C++类

    ​这节课我们开始利用ffmpeg和opencv在Android环境下来实现一个rtmp播放器,与第2课在PC端实现播放器的思路类似,只不过在处理音视频显示和播放的细节略有不同。 1.压缩备份上节课工程文件夹并修改工程文件夹为demo20,将demo20导入到Eclipse或者在原工程上继续下列的开发步骤。

    2024年01月25日
    浏览(43)
  • .net8+webapi+sqlsugar基本配置;“连接数据库过程中发生错误,检查服务器是否正常连接字符串是否正确”异常

    1、引入sqlsugar的nugat包 2、封装一个操作类(参考sqlsugar官方文档) 3、配置program.cs和appsettings program.cs加上下面代码  appsettings配置连接字符串 4、新建一个控制台生成实体类 5、添加测试的controller 6、swagger调试抛异常 7、修改csproj文件中,仅适用.net 8(参考sqlsugar官方文档)  8、

    2024年03月15日
    浏览(67)
  • 如何在jupyter中加入自己的环境

    jupyter是在深度学习过程中常用的东西,那么该如何引入我们自己的环境呢? 比如我们这里就引入了我们自己的环境 多了一个Python(one)这里面有我们所有下载的python包 我们这里用默认的创建一个文件,并且导入我们的pytorch包 运行 我们可以看见报错了,为什么呢? 因为默认的

    2024年01月20日
    浏览(34)
  • 如何在UnrealEngine虚幻引擎中加载Web页面

    对于非游戏开发团队来讲,在面向非游戏领域的UE项目中嵌入Web页面并实现交互无疑能充分利用现有开发资源和流程,WebUI插件能提供完整的Web页面加载及交互手段,让团队中的UE开发工程师和Web开发工程师能够各司其职、紧密配合。 WebUI的安装配置过程可详细参考官方文档。

    2024年02月06日
    浏览(42)
  • 如何在vue3中加入markdown语法

    yarn add md-editor-v3 或者是在vue图形化界面中直接搜索 md-editor-v3 进行安装。 引入可以参考这个,根据自己的需求进行修改和添加。 参考md-editor-v3,文档比较详细。可以参考一下。=

    2024年02月13日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包