一、Bitmap的内存占用检测

Bitmap 一直以来都是 Android App 的内存消耗大户,很多 Java 甚至 native 内存问题的背后都是不当持有了大量大小很大的 Bitmap,我们可以使用Android Studio自带的Profile进行检测,由于Bitmap不会持有Context,所以,Profile无法检测出Bitmap导致的内存泄漏问题,但是重复创建Bitmap而没有及时回收,则会导致大量的内存占用,需要我们注意。对于Bitmap的内存监控,具体步骤如下:

1、进行内存Dump

在怀疑应用内存占用不正常的时间点内,点击Profile左侧的Capture heap dump,Android Studio会记录当前状态下应用中正在存活在内存中的实例对象

2、过滤Bitmap

在搜索框中过滤Bitmap,结果中会显示当前进程中所有存活的Bitmap对象,我们可以按照大小进行排序,然后一次点击其中的某一个Bitmap,从右侧的References中定位到此Bitmap的创建位置,需要注意的是,由我们调用系统API创建的Bitmap也会显示在这里,这里我们可以优先排查检测我们自己创建的Bitmap

  • Shallow size就是对象本身占用内存的大小,不包含其引用的对象;
  • Retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和

二、Bitmap的内存占用分析

通过上面的分析定位,可以找到内存占用较大的Bitmap,我们通常采用的方案,是对图片进行压缩,减少其内存占用情况,常用的压缩方案有以下几种,我们逐一来进行分析

我们这里采用的是一张分辨率为 4400 * 2475 的图片

1、默认大小

放到Assets文件夹下,读取后加载到ImageView中:

打印得到的内存大小为:41.54MB

private fun loadAssets() {val bytes = assets.open("big_picture.jpg").readBytes()val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)list.add(bitmap)Log.d(TAG, "原始图片大小:${bitmap.allocationByteCount / 1024.0 / 1024.0}")
}

2、Drawable大小

把文件放到drawable文件夹下,通过R.drawable.xxx的形式来读取:

打印得到的内存大小为:280.82MB

private fun loadDefault() {val decodeResource = BitmapFactory.decodeResource(resources, R.drawable.big_picture_default)val byteCount = decodeResource.allocationByteCount / 1024.0 / 1024.0Log.d(TAG, "Drawable大小:$byteCount")
}

3、Dwawable-XX大小

把文件放在drawable-xxhidp文件夹下,通过R.drawable.xxx的形式来读取:

打印得到的内存大小为:31.19MB

private fun loadXX() {val decodeResource = BitmapFactory.decodeResource(resources, R.drawable.big_picture_xx)val byteCount = decodeResource.allocationByteCount / 1024.0 / 1024.0Log.d(TAG, "DrawableXX大小:$byteCount")
}

4、采样率压缩

图片宽高分别压缩为原来的一半:

打印得到的内存大小为:10.38MB

/*** 尺寸压缩*/
private fun sizeCompress() {val bytes = assets.open("big_picture.jpg").readBytes()val options = BitmapFactory.Options()options.inSampleSize = 2val outBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)Log.d(TAG, "尺寸压缩1/2:${outBitmap.allocationByteCount / 1024.0 / 1024.0}")
}

5、质量压缩为原来的一半:

打印得到的内存大小为:41.54MB

/*** 质量压缩* 它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。* 因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素*/
private fun qualityCompress() {val bytes = assets.open("big_picture.jpg").readBytes()val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)val stream = ByteArrayOutputStream()bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream)val byteArray = stream.toByteArray()val outBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)Log.d(TAG, "质量压缩50%:${outBitmap.allocationByteCount / 1024.0 / 1024.0}")
}

6、像素压缩

RGB通道设置为RGB_565后:

打印得到的内存大小为:20.77MB

/*** RGB通道压缩*/
private fun rgbCompress() {val bytes = assets.open("big_picture.jpg").readBytes()val options = BitmapFactory.Options()options.inPreferredConfig = Bitmap.Config.RGB_565val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)Log.d(TAG, "RGB通道压缩RGB_565:${bitmap.allocationByteCount / 1024.0 / 1024.0}")
}

7、使用Glide设置图片:

打印得到的内存大小为:1.25MB

private fun glideCompress(){val s = object :RequestListener<Drawable>{override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>, isFirstResource: Boolean): Boolean {return false}override fun onResourceReady(resource: Drawable, model: Any?, target: Target<Drawable>, dataSource: DataSource?, isFirstResource: Boolean): Boolean {val width = resource.intrinsicWidthval height = resource.intrinsicHeightresource.setBounds(0,0,width,height)val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)Log.d(TAG, "Glide加载:${bitmap.allocationByteCount / 1024.0 / 1024.0}")return false}}val bytes = assets.open("big_picture.jpg").readBytes()Glide.with(this).load(bytes).listener(s).into(img_view)}

三、分析与总结:

1、Bitmap的计算方式:

Bitmap文件在Android设备中的计算方式为:
Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小

2、BitmapFactory.Options中几个关于分辨率的属性参数:

  • inDensity:Bitmap位图自身的密度、分辨率
  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率
  • inScreenDensity: 设备屏幕分辨率

其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值:

density

0.75

1

1.5

2

3

3.5

4

densityDpi

120

160

240

320

480

560

640

DpiFolder

ldpi

mdpi

hdpi

xhdpi

xxhdpi

xxxhdpi

xxxxhdpi

3、不同分辨率文件夹的缩放:

基于以上分析,可以得到以下几个结论:

  • 同一张图片,放在不同资源目录下,其分辨率会有变化,
  • Bitmap存放文件夹分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少
  • 图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160
  • 资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放

4、结论验证

根据第三点的结论,结合上面的实验代码进行验证处理,文件放到drawable文件夹下,系统会默认当做mdpi来进行处理,而放到xx-hdpi下,则系统会当做二倍图来处理,根据这个计算规则,UI通常给的都是二倍图,我们把图片放到drawable-xxhdpi文件夹中,则会降低图片在系统中的内存占用,而如果把二倍图片放到drawable-xxxhdpi下,则会由于缩放原因图片变得模糊不清

5、质量压缩分析

质量压缩后图片在系统中的内存大小没有变化,这是因为质量压缩只能实现对文件存储的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素

6、Glide加载分析

使用Glide加载图片占用的内存最小,这是因为Glide在加载图片时,会根据ImageView来实际计算控件真实需要的图片大小,并对原始图片做大小缩放,保证清晰度的情况下尽可能的降低图片占用的内存大小

四、Bitmap的内存优化方案

1、采用第三方加载库:

当需要匹配多种分辨率设备时,尽量采用Gilde等第三方图片加载框架进行图片加载,而不是直接使用img_view.setImageResource(R.drawable.big_picture_xx)的形式

2、图片检测分析库:

在开发阶段可以使用一些第三方的图片监控库,用来检测我们是否使用了超过实际使用宽高的图片,比如:BitmapCanary,在引用后,可以直接在图片控件中显示控件中加载的图片的宽高比例,如下图所示,上面的文字大小分别表明当前控件加载的图片宽高超过控件宽高的倍数

3、Bitmap的复用

采用Bitmap的缓存池,避免重复创建,由于Bitmap的创建不依赖于Context,因此Bitmap不会持有Context的引用,也就不会导致页面内存泄漏的问题出现,我们可以把需要经常用到的Bitmap添加到缓存池中,避免重复创建Bitmap带来的内存消耗,
注意:复用 Bitmap 之前,需要手动判断Bitmap是否可以被复用。这是因为 Bitmap 的复用有一定的限制,在Android 4.4版本之前,只能复用相同大小Bitmap的内存区域,在Android4.4以后的版本,可以重用任何 Bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以(参考:Android Develop),示例代码如下:

/*** candidate:旧的图片,targetOptions:新的图片的Options*/
private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// From Android 4.4 (KitKat) onward we can re-use if the byte size of// the new bitmap is smaller than the reusable bitmap candidate// allocation byte count.val width: Int = targetOptions.outWidth / targetOptions.inSampleSizeval height: Int = targetOptions.outHeight / targetOptions.inSampleSizeval byteCount: Int = width * height * getBytesPerPixel(candidate.config)byteCount <= candidate.allocationByteCount} else {// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1candidate.width == targetOptions.outWidth&& candidate.height == targetOptions.outHeight&& targetOptions.inSampleSize == 1}
}

4. 合理管理图片的内存缓存

为了提高图片的加载速度,我们通常会对图片做适当的缓存,但是缓存占用的内存过大时,会导致系统频繁GC从而引发卡顿,这里,系统提供了API告诉我们可以在合理的时机释放内存,当系统回调onLowMemory()时,我们可以尝试释放缓存中的图片资源,用来释放内存

class MainApplication : Application() {override fun onLowMemory() {super.onLowMemory()}
}

5、采用设备分级策略:

比如某些设备的RAM为2G,某些设备的RAM为4G,
当我们检测到设备的内存为2GB时,一般为低端机型,我们可以采用一些手段降低应用的内存占用,比如设置图片的色彩通道为RGB_565,图片内存上限为10MB等,
当我们检测到设备的内存为4G甚至6G以上时,这类设备通常为高端机型,为了用户体验,提高图片的加载速度,我们可以在此类设备中设置色彩通道为RGB_888,内存上限设置为20MB或者更大
这里为判断设备RAM的代码:

public static String getRAMInfo(Context context) {ActivityManager activityManager = (ActivityManager) context.getSystemService(context.ACTIVITY_SERVICE);ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();activityManager.getMemoryInfo(memoryInfo);long totalSize = memoryInfo.totalMem;return Formatter.formatFileSize(context, totalSize);
}

6、合理选择像素解析:

Bitmap.Config参数:

  • ALPHA_8:只有透明度,没有任何RGB值,一个像素占用1个字节
  • ARGB_4444:每一个像素点由4个,A(Alpha)、R(Red)、G(Green)、B(Blue)值表示。每一个占用4位(bit),所以总共占用16bit=16/8(byte)=2byte。也即每一个像素占用2字节
  • ARGB_8888:Android手机上默认的格式,同时也是电脑上通用的格式。也是由4个值表示的,但每一个值占用8bit,所以总共是32bit=4byte。也即每一个像素占用4个字节
  • RGB_565:没有透明度(alpha),所以,没法支持半透明或是全透明。只有R(占用5bit),G(占用6bit),B(占用5bit),总共16bit=2byte。也即,每一个像素占用2字节

Android默认是按照ARGB_8888来解析的,当我们采用RGB_565解析时,每个像素的占用字节从4个降低为两个,可以降低内存占用,但是需要注意的是RGB_565不包含透明度,因此如果你的图片包含透明度时,优先采用ARGB_8888来解析,如果设备比较低端而内存吃紧的话,也可以采用ARGB_4444来解析,但是显示效果就会受到影响

7、分片加载图片:

有时候我们想要加载显示的图片很大或者很长,比如手机滚动截图功能生成的图片。针对这种情况,在不压缩图片的前提下,不建议一次性将整张图加载到内存,而是采用分片加载的方式来显示图片部分内容,然后根据手势操作,放大缩小或者移动图片显示区域。然后根据用户的滑动方向来继续渲染剩余的图片部分
Android SDK 中的 BitmapRegionDecoder 来实现分片加载的策略,示例代码如下:

/**
* 只渲染前200*200像素的内容
*/
private fun showRegionImage(){val stream = assets.open("big_picture.jpg")val decoder = BitmapRegionDecoder.newInstance(stream, false)val options = BitmapFactory.Options()val bitmap = decoder.decodeRegion(Rect(0, 0, 200, 200), options)img_view.setImageBitmap(bitmap)
}

8. 合理存放资源文件的位置

当UI图片的实际分辨率与展示分辨率相同时,图片不会进行缩放处理,此时的图片大小近似的相当于Android设备实际渲染大小

9. 及时释放Bitmap内存

下表是不同SDK版本中Bitmap对象和像素存放位置的区别(来源于Android Develop),

API

API 10+

API 11~API 25

API 26+

Bitmap对象存放

Java Heap

Java Heap

Java Heap

像素(pixel data)数据存放

native heap

Java Heap

native heap

不同Android版本中的Java堆内存个Native内存存放地址也不一样,其回收方式也不一样,下表中为不同存放位置中释放内存的时机(来源于:Android中Bitmap内存优化)

Native Heap 存放

退出Activity

退出App

手动调用recycler()

不释放

释放

无调用

不释放

不释放

Java Heap 存放

退出Activity

退出App

手动调用recycler()

释放

释放

无调用

释放

释放

五、线上Bitmap内存监控上报

这里根据微信Matrix的内存监控策略,我们可以设计一个简单的内存统计工具,封装全局图片加载框架,通过WeakHashMap在图片加载前,记录下每个Bitmap的内存大小和唯一id,唯一id用来追溯Bitmap的创建来源,当系统调用onLowMemory()或者OOM时(OOM时,因为主进程已经崩溃,因此需要在新的进程中上报结果),把内存统计结果上报到服务器,开发人员就可以根据这个上报结果定位占用内存较大的图片资源,从而分析解决,示例代码如下:

class BitmapMemoryMonitor {/*** 用来记录所有的Bitmap内存占用情况* 弱引用的形式,避免引用Bitmap导致无法释放*/private val bitmapHashMap = WeakHashMap<Bitmap, String>()fun recordBitmap(bitmap: Bitmap, id: String) {bitmapHashMap[bitmap] = id}/*** 上报当前bitmap的内存占用情况*/fun reportBitmapMemory() {val map = mutableMapOf<String, Int>()bitmapHashMap.filter { it.key != null }.forEach {val bitmap = it.keyval id = it.valuemap[id] = bitmap.allocationByteCount}}
}

探究Bitmap在Android中的内存占用相关推荐

  1. Android中一张图片占用的内存大小

    最近面试过程中发现对Android中一些知识有些模棱两可,之前总是看别人的总结,自己没去实践过,这两天对个别问题进行专门研究 探讨:如何计算Android中一张图片占据内存的大小 解释:此处说的占据内 ...

  2. Android中的内存泄漏和内存溢出

    一.内存泄漏 1.内存泄漏的现象和本质 内存泄漏(Memory Leak)是指某些对象已经不再使用了,但却无法被垃圾回收器回收内存,还一直占用着内存空间的现象,这就导致这一块内存泄露了. 而垃圾回收器 ...

  3. Android中的内存泄漏

    ** Android中的内存泄漏 ** Android中的内存泄漏: 概念:程序在申请内存后,当该内存不需再使用但却无法被释放 & 归还给程序的现象,对应用程序的影响,容易使得应用程序发生内存 ...

  4. 谈谈android中的内存泄漏

    写在前面 内存泄漏实际上很多时候,对于开发者来说不容易引起重视.因为相对于crash来说,android中一两个地方发生内存泄漏的时候,对于整体没有特别严重的影响.但是我想说的是,当内存泄漏多的时候, ...

  5. LeakCanary——消除Android中的内存泄露

    2019独角兽企业重金招聘Python工程师标准>>> ##LeakCanary ####简介 LeakCanary是Square公司最近公布的开源项目,旨在消除Android中的内 ...

  6. android中内存泄露,Android中的内存泄露

    编辑推荐: 本文来自于csdn,本文主要从java的内存模型讲起,最终举出几个内存泄露的例子和解决方案. java运行时内存模型 具体信息:http://gityuan.com/2016/01/09/ ...

  7. Android中HashMap内存优化之ArrayMap和SparseArray

    ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的.用于在一定情况下取代HashMap而达到节省内存的目的. 在Android开发中HashMap使用频率相 ...

  8. Linux中Cache内存占用过高解决办法

    在Linux系统中,我们经常用free命令来查看系统内存的使用状态.在一个RHEL6的系统上,free命令的显示内容大概是这样一个状态: 这里的默认显示单位是kb,我的服务器是128G内存,所以数字显 ...

  9. java 减少内存_java中减少内存占用小技巧

    Java做的系统给人的印象是什么?占内存!说道这句话就会有N多人站出来为java辩护,并举出一堆的性能测试报告来证明这一点. 其实从理论上来讲java做的系统并不比其他语言开发出来的系统更占用内存,那 ...

最新文章

  1. sqlserver sa
  2. 在线流程图绘制网站draw.io支持的三种存储介质
  3. MyBatis传入参数为list、数组、map写法
  4. 长春工业大学计算机科学与技术录取分数,2021年长春工业大学各省各专业最低投档录取分数线统计(文科 理科)...
  5. 暨南大学计算机复试线2019,暨南大学2019年考研复试分数线
  6. usb长包数据结束判断_如何判断南桥好坏 判断南桥好坏方法介绍【详解】
  7. 贵气烫金剪纸牛年新年春节海报PSD分层素材模板
  8. L3-021 神坛 (30 分)-PAT 团体程序设计天梯赛 GPLT
  9. ckeditor简单使用心得
  10. 云计算三种架构(IaaS, PaaS, SaaS)及部署模型
  11. java遍历文件夹下所有图片_遍历指定文件夹下的所有图片,并复制到指定目录下...
  12. 跑跑卡丁车rush服务器维护,跑跑卡丁车RUSH
  13. jQuery全选全删动态表格
  14. CenterFusion代码复现
  15. 莫比乌斯进阶:bzoj 3994 约数个数和(Mobius)
  16. Deep Photo的TensorFlow版本
  17. 梅姨眼中最爱读英国书籍的人竟然是TA?
  18. 举个栗子~Tableau 技巧(216):服务器视图中的文字乱码怎么办?
  19. OSSIM开源安全信息管理系统(十七)
  20. mysql表的导入和导出

热门文章

  1. JAVA--阿尔法平台编程练习---篮球弹跳
  2. 守护网络安全 呵护精神家园 --常见网络风险(二)
  3. 计算机应用基础试题模拟题,网络教育统考《计算机应用基础》模拟试题答案
  4. JavaScript解决异步的前世今生
  5. RC ORC Parquet之大数据文件存储格式的一哥之争
  6. Nginx+Tomcat实现负载均衡与动静分离
  7. OpenGauss数据库的详细安装过程
  8. Scikit-Learn&More,用于机器学习的综合数据集生成
  9. 量化感知训练_《量化健身 动作精讲》:专业解读健身动作的秘密
  10. 数学分析 曲面积分与场论初步(第22章)