Android Bitmap防止内存溢出

2023-05-16

1.Bitmap

在Android开发中经常会使用到Bitmap,而Bitmap使用不当很容易引发OOM。

Bitmap占用内存大小的计算公式为:图片宽度×图片高度×一个像素点所占字节数 ,因此减小这三个参数的任一值都可减小bitmap所占的内存大小(也可以通过Bitmap.getAllocationByteCount()方法来查看Bitmap所占内存大小)。

因此使用Bitmap时需要优化,防止引发内存溢出问题。优化方法有两种:①减少bitmap对内存的占用;②重用已经占用内存的bitmap空间或使用现有的bitmap,比如图片非常多时使用LruCache缓存机制。

 

2.减小内存占用

减小宽高BitmapFactory.Options.inSampleSize

inSampleSize是BitmapFactory.Options的一个属性,改变它即可改变图片的宽高。如果该值设置为大于1的值(小于1的值即为1),就会请求解码器对原始图像进行二次采样,返回较小的图像以节省内存。

比如inSampleSize = 4,则返回的图像宽度为原始宽度的1/4,高度为原始高度的1/4,像素数目为原始像素数目的1/16。

inSampleSize属性通常配合inJustDecodeBounds属性使用,如果inJustDecodeBounds设置为true,则解码器将返回null(无位图),但outWidth/outHeight仍会设置字段,从而允许调用者查询位图而不必为其像素分配内存

private fun sampleCompress(requestWidth: Int, requestHeight: Int) {

    val options = BitmapFactory.Options()

    options.inJustDecodeBounds = true  //不分配内存空间,仅计算图片尺寸

    BitmapFactory.decodeResource(resources, R.mipmap.timg, options)

    Log.d(TAG, "bitmap outWidth:${options.outWidth}") //原始图片宽

    Log.d(TAG, "bitmap outHeight:${options.outHeight}") //原始图片高

    // 根据宽高要求,计算缩放倍数

    var sampleSize = 1

    if (requestWidth < options.outWidth || requestHeight < options.outHeight) {

        sampleSize = max(options.outWidth * 1.0/requestWidth, options.outHeight * 1.0/requestHeight).toInt()

    }

    options.inJustDecodeBounds = false

    options.inSampleSize = sampleSize

    val bitmap = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

    logBmInfo(bitmap) //缩放后的图片

}

d1028b67a97940309e3c133866d5a0ed.png

原始Bitmap宽高各为1000,若要求宽高各为500 ,则得出inSampleSize为2。根据log可知,原始Bitmap占用内存大小为4000000B,但是经过使用inSampleSize属性压缩宽高,从而减小为原先1/4的内存占用。

减小每个像素占用的字节数BitmapFactory.Options.inPreferredConfig

inPreferredConfig是BitmapFactory.Options的一个属性,默认值为Bitmap.Config.ARGB_8888,改变该配置,可改变一个像素点占用的字节数

该属性中A代表透明度,R代表红色,G代表绿色,B代表蓝色。

位图使用像素的一格一格的小点来描述图像,计算机屏幕其实就是一张包含大量像素点的网格。在位图中,平时看到的图像是由每一个网格中的像素点的位置和色彩值决定的,每一点的色彩是固定的,而每个像素点色彩值的种类,产生了不同的位图Config,常见的有:

1)ALPHA_8:表示8位Alpha位图,A占8位,没有颜色,只有透明度 ,每个像素占用1个字节内存。

2)ARGB_4444(已废弃) :表示16位ARGB位图,即A占4位,R占4位,G占4位,B占4位,共占用2个字节 。

3)ARGB_8888:表示32位ARGB位图,即A占8位,R占8位,G占8位,B占8位,每个像素占用4个字节内存。

4)RGB_565:表示16位RGB位图,即R占5位,G占6位,B占5位,没有透明度,每个像素占用2个字节内存。

private fun argbCompress() {

    val bm1 = BitmapFactory.decodeResource( resources, R.mipmap.timg)

    logBmInfo(bm1)

    val options = BitmapFactory.Options()

    // 设配置为RGB_565

    options.inPreferredConfig = Bitmap.Config.RGB_565

    val bm2 = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

    logBmInfo(bm2)

从执行日志结果可以看到优化后的Bitmap内存占用为未优化Bitmap大小的一半 ,长度和宽度没发生变化。RGB_565对不要求透明度的图来说视觉影像不大。

e51a0d65ca2b41b0b2628c05a65c824d.png

 

3.易错:压缩compress不能改变bitamp占用内存的大小

Bitmap的compress(Bitmap.CompressFormat format, int quality, OutputStream stream)方法是将位图的压缩版本写入指定的输出流,该方法可能需要几秒钟才能完成,因此最好在子线程中调用。(注:并非所有格式都直接支持所有位图配置,因此从BitmapFactory返回的位图可能具有不同的位深,并且可能丢失了每个像素的alpha值(例如JPEG仅支持不透明的像素))。

compress方法的参数:format是压缩图像格式,quality是压缩质量(根据format不同,quality压缩效果也不同),stream是写入压缩数据的输出流。

format为Bitmap.CompressFormat.JPEG,根据quality 0-100压缩;

format为Bitmap.CompressFormat.PNG,则quality参数就会失效,因为PNG图片是无损的,无法压缩;

format为Bitmap.CompressFormat.WEBP,它会比JPEG更加省空间,根据quality 0-100压缩。

compress压缩损失的是颜色精度,所需的存储空间变小了,但使用压缩后的流重新生成Bitmap并不会改变bitmap占用内存的大小,因为bitmap的宽高未改变,而且Bitmap.Config未改变,即一个像素所占用的字节数也未改变,所以最终bitmap所占的内存并没有改变。

private fun bitmapCompress(bitmap: Bitmap){

    val out = ByteArrayOutputStream()

    Log.d(TAG, "———— JPEG ————")

    bitmap.compress( Bitmap.CompressFormat.JPEG, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val jpegArray = out.toByteArray()

    val jpeg = BitmapFactory.decodeByteArray( jpegArray, 0, jpegArray.size)

    logBmInfo(jpeg)

 

    out.reset()

    Log.d(TAG, "———— PNG ————")

    bitmap.compress( Bitmap.CompressFormat.PNG, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val pngArray = out.toByteArray()

    val png = BitmapFactory.decodeByteArray( pngArray, 0, pngArray.size)

    logBmInfo(png)

    

    out.reset()

    Log.d(TAG, "———— WEBP ————")

    bitmap.compress( Bitmap.CompressFormat.WEBP, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val webpArray = out.toByteArray()

    val webp = BitmapFactory.decodeByteArray( webpArray, 0, webpArray.size)

    logBmInfo(webp)

}

从执行日志结果如下,quality为30进行压缩时,JPEG格式和WEBP所占存储空间变小了(不一定WEBP格式所占存储空间小于JPEG格式),而PNG格式并未压缩。三者的流重新解码成bitmap,可见bitmap所占内存大小并未发生变化。

af79ffe172e546329ee4b1a257c25303.png

压缩后的流重新解码生成bitmap,展示出来会发现PNG格式无影响,JPEG格式和WEBP格式图片质量明显变差了。

 

4.Bitmap复用

除了减少bitmap对内存的占用,还有方案来优化,即重用已经占用内存的bitmap空间或使用现有的bitmap。

①重用BitmapFactory.Options.inBitmap

inBitmap是BitmapFactory.Options的一个属性,可以通过设置该属性来重用已经占用内存的bitmap空间

但是Bitmap重用有一定限制:

1)在Android4.4之前,只能重用相同大小的Bitmap内存区域;

2)在4.4之后可以重用任何Bitmap的区域,只要这块内存比将要分配内存的Bitmap大就可以;

3)重用的bitmap是要可变的。

以Android4.4之后为例,先通过设置 options.inJustDecodeBounds为true来查询需加载的bitmap宽高,然后判断reuseBitmap是否符合重用,若符合则将其赋值给options.inBitmap属性,最终得到想要的bitmap,即重用了reuseBitmap的内存空间。

private fun getBitmap(): Bitmap {

    val options = BitmapFactory.Options()

    options.inJustDecodeBounds = true

    BitmapFactory.decodeResource(resources, R.mipmap.timg, options)

    // 判断是否满足重用条件,这里就假设Bitmap.Config为ARGB_8888来计算内存大小

    if (reuseBitmap.allocationByteCount >= options.outWidth * options.outHeight * 4) {

        // reuseBitmap为可变的重用bitmap

        options.inBitmap = reuseBitmap

    }

    options.inJustDecodeBounds = false

    return BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

}

②LruCache

在使用RecyclerView时,如果itemView中含有图片,滑动时会导致bitmap不断重新创建,从而浪费内存空间。此时,可使用LruCache来缓存bitmap,再次需要时从缓存取出即可,无需重新创建。

private val memoryCache = object : LruCache<String, Bitmap>(4*1024*1024) { // 缓存4M图片

    override fun sizeOf(key: String, value: Bitmap): Int {

        // 告知lruCache bitmap所占内存大小

        return value.allocationByteCount

    }

}

fun putBitmap(key: String, bitmap: Bitmap) {

    memoryCache.put(key, bitmap)

}

fun getBitmap(key: String): Bitmap? {

    return memoryCache.get(key)

}

 

5.加载巨图

加载图片时,一般为了尽可能避免OOM都会按照如下做法:

1)对于图片显示:根据需要显示图片控件的大小对图片进行压缩显示。

2)如果图片数量非常多:则会使用LruCache等缓存机制,将所有图片占据的内容维持在一个范围内。

其实对于图片加载还有一种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等。

对于这种需求,首先不允许压缩,要按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性将整图加载到内存中,所以肯定是局部加载,那么就需要用到一个类:BitmapRegionDecoder。其次,既然屏幕显示不完,那么就要添加一个上下左右拖动的手势,让用户可以拖动查看。

①BitmapRegionDecoder

BitmapRegionDecoder主要用于显示图片的某一块矩形区域。

BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,支持传入文件路径,文件描述符,文件的inputstrem等。比如:

BitmapRegionDecoder bitmapRegionDecoder  =BitmapRegionDecoder.newInstance(inputStream, false);

这里传入了需要处理的图片,接下来就要指定显示的区域了:

Bitmap Bitmap = bitmapRegionDecoder.decodeRegion(rect, options);

第一个参数很明显是一个rect,第二个参数是BitmapFactory.Options,通过它可以控制图片的inSampleSize,inPreferredConfig等。返回值就是加载的局部图片。

BitmapRegionDecoder使用举例:

InputStream inputStream = getAssets().open( "world.jpg");

//获得图片的宽、高

BitmapFactory.Options tmpOptions = new BitmapFactory.Options();

tmpOptions.inJustDecodeBounds = true;

BitmapFactory.decodeStream(inputStream, null, tmpOptions);

int width = tmpOptions.outWidth;

int height = tmpOptions.outHeight;

//设置显示图片的中心区域

BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance( inputStream, false);

BitmapFactory.Options options = new BitmapFactory.Options();

options.inPreferredConfig = Bitmap.Config.RGB_565;

Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);

mImageView.setImageBitmap(bitmap);

这样就实现了使用BitmapRegionDecoder去加载assets中的图片,调用bitmapRegionDecoder.decodeRegion解析图片的中间矩形区域,返回bitmap,最终显示在ImageView上。

②自定义显示大图控件

为了滑动查看整个图,可以自定义一个控件去显示巨图,首先Rect的范围就是自定义View的大小,然后根据用户的移动手势,不断去更新Rect的参数即可。

参考鸿洋大神的https://blog.csdn.net/lmj623565791/article/details/49300989/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android Bitmap防止内存溢出 的相关文章

  • Android 中多个蓝牙连接的自定义 UUID

    我有一个 Android 设备作为服务器连接到多个蓝牙 Android 客户端 我了解 UUID 的概念以及它的独特之处 我的问题是 我可以为连接到我的服务器的所有客户端使用相同的 UUID 吗 如果没有 我如何以编程方式为我的客户端生成
  • 检查 WebView 元素时的 UiAutomator 错误

    I have recently started automation testing in android and was using UiAutomator tool for inspecting UI elements Surprisi
  • Android Q:file.mkdirs() 返回 false

    我们有一个应用程序 使用外部存储来存储一些临时文件 图像 二进制数据 该代码已经运行了几年 直到最近才发生重大变化 在 Android Q 上它不起作用 File f new File Environment getExternalStor
  • Android WebView里面的ScrollView只滚动scrollview

    在我的应用程序中 我有一个 ScrollView 其中包含一些线性视图 一些文本视图和一个 Webview 然后是其他线性布局等 问题是 WebView 不滚动 Scroll 仅侦听 ScrollView 有什么建议么
  • 如何重定向到 instagram://user?username={username}

    我的 html 页面上有这个链接 可以在特定用户上打开 Instagram 应用程序 a href Link to Instagram Profile a 我一直在寻找自动运行 url instagram user username USE
  • 如何更改终端的默认目录?

    我想更改 Android Studio v2 2 2 终端的默认目录 当我打开终端时 它基于项目的目录 C 项目路径 我经常需要使用adb shell 所以我必须导航到 SDK 路径 平台工具 才能使用 adb 命令 是否可以更改终端的默认
  • 在 Cordova 应用程序中获取额外功能

    我们有两个 Android 应用程序 一个使用本机 Java 实现 另一个使用 Ionic 编写 Ionic 应用程序启动我的应用程序 这是使用灯插件 https github com lampaa com lampa startapp 我
  • 如何在谷歌地图android上显示多个标记

    我想在谷歌地图android上显示带有多个标记的位置 问题是当我运行我的应用程序时 它只显示一个位置 标记 这是我的代码 public class koordinatTask extends AsyncTask
  • 对于一个单元格,RecyclerView onBindViewHolder 调用次数过多

    我正在将 RecyclerView 与 GridLayoutManager 一起使用 对于网格中的每个项目 我需要调用 REST api 来检索数据 然后 从远程异步获取数据后 我使用 UIL 加载 显示图像 一切似乎都很好 但我发现 on
  • logcat 中 mSecurityInputMethodService 为 null

    我写了一点android应显示智能手机当前位置 最后已知位置 的应用程序 尽管我复制了示例代码 并尝试了其他几种解决方案 但似乎每次都有相同的错误 我的应用程序由一个按钮组成 按下按钮应该log经度和纬度 但仅对数 mSecurityInp
  • Android 2.3 模拟器在更新位置时崩溃

    我正在使用 Eclipse 编写和调试 Android 应用程序 我需要做的事情之一是更新设备的位置 因此我尝试使用模拟器控制窗口中的位置控制面板 在 手动 选项卡上 我选择 十进制 输入有效的纬度和经度 然后单击 发送 不幸的是 接下来发
  • OnLongClickListener 不工作

    我有一个ImageView 我需要使用onLongClickListener对于图像视图 当我使用这段代码时 什么也没有发生 Code gallery Gallery findViewById R id gall1 gallery setA
  • Android Studio 缓慢的增量构建

    我已经完成了许多步骤来完善我们的构建系统 those https stackoverflow com questions 16775197 building and running app via gradle and android st
  • Android 如何将总天数准确更改为年、月、日?

    我正在做一个应用程序 该应用程序与根据给定的生日日期输入获取一个人的年龄有关 为此 我从下面的代码中获取从该日期到当前日期的总天数 String strThatDay 1991 05 10 SimpleDateFormat formatte
  • 在 Samsung Galaxy S5 Android 5.0 上使用 MediaPlayer 加载音频流需要超过 10 秒

    由于更新至 Android 5 0 MediaPlayer 在 Samsung Galaxy S5 上无法正常工作 启动音频流后加载时间超过 10 秒 示例代码 MediaPlayer mPlayer new MediaPlayer Str
  • Android - 以编程方式选择菜单选项

    有没有办法以编程方式选择菜单选项 基本上 我希望视图中的按钮能够执行与按特定菜单选项相同的操作 我正在考虑尝试调用 onOptionsItemSelected MenuItem item 但我不知道要为菜单项添加什么 是的 有一种方法可以选
  • 插件“Android Bundle Support”不兼容

    大家好 自从上次更新以来 当我启动 android studio 时 我遇到了一个非常奇怪的错误 我有这个错误 插件错误 插件 Android Bundle Support 不兼容 直到构建 AI 195 SNAPSHOT 我在网上找不到任
  • 通过电子邮件发送文本文件附件

    我正在尝试附加一个文本文件以便通过电子邮件发送 但每当我打开电子邮件应用程序时 它都会说该文件不存在 请帮助 Intent i new Intent Intent ACTION SEND i setType text plain i put
  • 在 Android 应用程序资源中使用 JSON 文件

    假设我的应用程序的原始资源文件夹中有一个包含 JSON 内容的文件 我如何将其读入应用程序 以便我可以解析 JSON See 开放原始资源 http developer android com reference android conte
  • 如何使用 AccessibilityService 在 Android 中模拟按键

    我正在编写一个辅助服务 我一直在尝试在应用程序上进行一些自动搜索 我使用accessibilityservice action paste来填充EditText 然后我需要模拟软键盘上的按键 但我不知道如何做 你们能帮我一下吗 你可以尝试A

随机推荐

  • Pycharm配置Git教程

    1 使用场景 平时习惯在windows下开发 xff0c 但是我们又需要实时将远方仓库的代码clone到本地 xff0c 也许要将自己修改的代码push到远端服务器 xff0c 有很多方法可以实现这个需求 xff0c 但是所用的编辑软件不一
  • Ubuntu18.04安装NVIDIA驱动后,循环登录,登录界面进不去,输完密码又回到登录界面

    我安装的是Ubuntu18 04 5 xff0c 3090公版显卡 xff0c 在安装好驱动之后 xff0c 一直循环在登录界面 xff0c 输入密码之后一闪又回到登录界面 xff0c 重装了多次驱动还是不行 解决方法 xff1a 后来发现
  • 安装达梦数据库选择安装路径时提示“无写入权限”

    在使用中标麒麟的Linux系统虚拟机安装达梦数据库时 xff0c 遇到了选择安装路径时 xff0c 数据库安装程序报 无写入权限 问题 经过一番折腾后发现 xff0c 问题原因时系统的临时目录空间太小所导致的 解决方法 xff1a 1 重新
  • Docker部署MySQL单机版

    简单版 一 查看本机是否有MySQL及MySQL端口 防止端口占用 xff09 ps ef grep mysql 二 拉取MySQL镜像 docker pull mysql 5 7 三 运行MySQL镜像 docker run d p 33
  • win10无法关机解决方法

    win10无法关机怎么办 下面阐述一下处理的过程 1 左键双击控制面板 控制面板已放到桌面 xff0c 再单击电源选项 2 在打开的电源选项窗口 xff0c 左键点击 xff1a 选择电源按纽的功能 xff0c 打开系统设置 3 在系统设置
  • 小米电视访问电脑共享文件夹

    输入win 43 R打开运行窗口 输入control进入控制面板 点击 网络和internet 网络共享中心 更改高级共享设置 a 专用 网络设置如图 xff1a b 来宾或公用 网络设置如图 xff1a c 所有网络 设置如图 xff1a
  • 让Everything搜索结果更清爽

    Everything的文件搜索功能很强大 xff0c 但是默认设置下搜索出的结果过于丰富 xff0c 总是会有一些乱七八糟的后缀名文件 xff08 如下图 xff09 xff0c 或许我们并不想搜索出那些文件 这时我们需要对它设置里的排除列
  • 上机 Qt5.14.2 编程应用

    上机 Qt5 14 2 编程应用 关于QT Qt是一个1991年由Qt Company开发的跨平台C 43 43 图形用户界面应用程序开发框架 它既可以开发GUI程序 xff0c 也可用于开发非GUI程序 xff0c 比如控制台工具和服务器
  • Android Studio报错:Error:Could not find com.android.tools.build:gradle:4.1 记一次不长记性的坑

    本文地址 xff1a https blog csdn net zengsidou article details 79797417 看字面意思 xff0c 这个问题是Gradle没有对应版本 在搜索引擎没有找到方法之后 xff0c 尝试自己
  • VBox关闭dhcp

    VBox关闭dhcp C Program Files Oracle VirtualBox gt VBoxManage exe list dhcpservers NetworkName HostInterfaceNetworking Virt
  • Android 使用LottieAnimationView 做启动动画

    lt xml version 61 34 1 0 34 encoding 61 34 utf 8 34 gt lt RelativeLayout xmlns android 61 34 http schemas android com ap
  • Android OkHttp★

    1 OkHttp OkHttp是Square公司开发的一个处理网络请求的开源项目 是目前Android使用最广泛的网络框架 OkHttp的特点 支持HTTP 2并允许对同一主机的所有请求共享一个socket连接 如果非HTTP 2 则通过连
  • Android GestureDetector★★★

    1 GestureDetecor 用户触摸屏幕时会产生许多手势 xff0c 一般通过重写View类的onTouch 方法可以处理一些触摸事件 xff0c 但是这个方法太过简单 xff0c 如果需要处理一些复杂的手势 xff0c 用这个接口就
  • Android canvas

    1 Canvas Canvas指画布 xff0c 表现在屏幕上就是一块区域 xff0c 可以在上面使用各种API绘制想要的东西 canvas内部维持了一个mutable Bitmap xff0c 所以它可以使用颜色值去填充整个Bitmap
  • Android apk打包流程★

    1 apk打包 Android开发中打包apk主要有两种方式 使用Android Studio集成直接生成apk 使用ant工具在命令行方式下打包apk 不管哪种方式 打包apk的本质过程都是一样的 Android的apk包文件包括两部分
  • Android ViewPager用法

    1 适配器PagerAdapter ViewPager使用适配器类将数据和view的处理分离 xff0c ViewPager的适配器叫PagerAdapter xff0c 这是一个抽象类 xff0c 不能实例化 xff0c 所以它有两个子类
  • Android Fragment★★

    1 Fragment fragment译为 碎片 xff0c 是Android 3 0 xff08 API 11 xff09 提出的 xff0c 最开始是为了适配大屏的平板 Fragment看起来和Activity一样 xff0c 是一个用
  • Android设计模式—适配器模式★★★

    1 适配器模式 适配器模式是指把一个类的接口变换成客户端所期待的另一种接口 xff0c 从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作 适配器模式是为了解决接口不兼容问题的 比如厂商给你的接口和你现有的接口对接不起来 旧的数据
  • Android 类加载机制

    nbsp 1 类加载机制 java文件不是可执行的文件 需要先编译成 class文件才可以被虚拟机执行 而类加载就是指通过类加载器把 class文件加载到虚拟机的内存空间 具体来说是方法区 类通常是按需加载 即第一次使用该类时才加载 Jav
  • Android Bitmap防止内存溢出

    1 Bitmap 在Android开发中经常会使用到Bitmap xff0c 而Bitmap使用不当很容易引发OOM Bitmap占用内存大小的计算公式为 xff1a 图片宽度 图片高度 一个像素点所占字节数 xff0c 因此减小这三个参数