Android图片占用内存探索

转自:请叫我大苏android

连接:算法

https://www.jianshu.com/p/3c5ac5fdb62aapi


最近封装了个高斯模糊组件,正好将图片相关的理论基础也梳理了下,因此,此次就来说讲,在 Android 中,怎么计算一张图片在内存中占据的大小,若是要优化,能够从哪些方向着手。缓存


1
提问



阅读本篇以前,先来想一些问题:bash


Q1:一张 png 格式的图片,图片文件大小为 55.8KB,那么它加载进内存时所占的大小是多少?网络


Q2:为何有时候,同一个 app,app 内的同个界面,界面上同张图片,但在不一样设备上所耗内存却不同?app


Q3:同一张图片,在界面上显示的控件大小不一样时,它的内存大小也会跟随着改变吗?ide


Q4:图片占用的内存大小公式:图片分辨率 * 每一个像素点大小,这种说法正确吗,或者严谨吗?测试


Q5:优化图片的内存大小有哪些方向能够着手?优化


2
正文


在 Android 开发中,常常须要对图片进行优化,由于图片很容易耗尽内存。那么,就须要知道,一张图片的大小是如何计算的,当加载进内存中时,占用的空间又是多少?


先来看张图片:



这是一张普通的 png 图片,来看看它的具体信息:




图片的分辨率是 1080*452,而咱们在电脑上看到的这张 png 图片大小仅有 55.8KB,那么问题来了:


咱们看到的一张大小为 55.8KB 的 png 图片,它在内存中占有的大小也是 55.8KB 吗?


理清这点蛮重要的,由于碰到过有人说,我一张图片就几 KB,虽然界面上显示了上百张,但为何内存占用却这么高?


因此,咱们须要搞清楚一个概念:咱们在电脑上看到的 png 格式或者 jpg 格式的图片,png(jpg) 只是这张图片的容器,它们是通过相对应的压缩算法将原图每一个像素点信息转换用另外一种数据格式表示,以此达到压缩目的,减小图片文件大小。


而当咱们经过代码,将这张图片加载进内存时,会先解析图片文件自己的数据格式,而后还原为位图,也就是 Bitmap 对象,Bitmap 的大小取决于像素点的数据格式以及分辨率二者了。


因此,一张 png 或者 jpg 格式的图片大小,跟这张图片加载进内存所占用的大小彻底是两回事。你不能说,我 jpg 图片也就 10KB,那它就只占用 10KB 的内存空间,这是不对的。


那么,一张图片占用的内存空间大小究竟该如何计算?


末尾附上的一篇大神文章里讲得特别详细,感兴趣能够看一看。这里不打算讲这么专业,仍是按照我粗坯的理解来给大伙讲讲。


图片内存大小


网上不少文章都会介绍说,计算一张图片占用的内存大小公式:分辨率 * 每一个像素点的大小。


这句话,说对也对,说不对也不对,我只是以为,不结合场景来讲的话,直接就这样表达有点不严谨。


在 Android 原生的 Bitmap 操做中,某些场景下,图片被加载进内存时的分辨率会通过一层转换,因此,虽然最终图片大小的计算公式仍旧是分辨率*像素点大小,但此时的分辨率已不是图片自己的分辨率了。


咱们来作个实验,分别从以下的几种考虑点相互组合的场景中,加载同一张图片,看一下占用的内存空间大小分别是多少:


  • 图片的不一样来源:磁盘、res 资源文件

  • 图片文件的不一样格式:png、jpg

  • 图片显示的不一样大小的控件

  • 不一样的 Android 系统设备


测试代码模板以下:


private void loadResImage(ImageView imageView) {    BitmapFactory.Options options = new BitmapFactory.Options();    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);    //Bitmap bitmap = BitmapFactory.decodeFile("mnt/sdcard/weixin.png", options);    imageView.setImageBitmap(bitmap);    Log.i("!!!!!!", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());    Log.i("!!!!!!", "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());    Log.i("!!!!!!", "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);    Log.i("!!!!!!", "imageview.width:" + imageView.getWidth() + ":::imageview.height:" +    imageView.getHeight());}复制代码


ps:这里提一下,使用 Bitmap 的 getByteCount() 方法能够获取当前图片占用的内存大小,固然在 api 19 以后有另一个方法,并且当 bitmap 是复用时获取的大小含义也有些变化,这些特殊场景就不细说,感兴趣自行查阅。反正这里知道,大部分场景能够经过 getByteCount() 打印图片占用的内存大小来验证咱们的实验便可。


图片就是上图那张:分辨率为 1080*452 的 png 格式的图片,图片文件自己大小 56KB


序号 前提 Bitmap内存大小
1 图片位于res/drawable,设备dpi=240,设备1dp=1.5px,控件宽高=50dp 4393440B(4.19MB)
2 图片位于res/drawable,设备dpi=240,设备1dp=1.5px,控件宽高=500dp 4393440B(4.19MB)
3 图片位于res/drawable-hdpi,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)
4 图片位于res/drawable-xhdpi,设备dpi=240,设备1dp=1.5px 1098360B(1.05MB)
5 图片位于res/drawable-xhdpi,设备dpi=160,设备1dp=1px 488160B(476.7KB)
6 图片位于res/drawable-hdpi,设备dpi=160,设备1dp=1px 866880(846.5KB)
7 图片位于res/drawable,设备dpi=160,设备1dp=1px 1952640B(1.86MB)
8 图片位于磁盘中,设备dpi=160,设备1dp=1px 1952640B(1.86MB)
9 图片位于磁盘中,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)


看见没有,明明都是同一张图片,但在不一样场景下,所占用的内存大小倒是有可能不同的,具体稍后分析。


以上场景中列出了图片的不一样来源,不一样 Android 设备,显示控件的不一样大小这几种考虑点下的场景。咱们继续来看一种场景:同一张图片,保存成不一样格式的文件(不是重命名,可借助ps);


图片:分辨率 1080*452 的 jpg 格式的图片,图片文件自己大小 85.2KB


ps:仍是一样上面那张图片,只是经过 PhotoShop 存储为 jpg 格式


序号 前提 Bitmap内存大小 比较对象
10 图片位于res/drawable,设备dpi=240,设备1dp=1.5px 4393440B(4.19MB) 序号1
11 图片位于res/drawable-hdpi,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB) 序号3
12 图片位于res/drawable-xhdpi,设备dpi=240,设备1dp=1.5px 1098360B(1.05MB) 序号4
13 图片位于磁盘中,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB) 序号9


这里列出的几种场景,每一个场景比较的实验对象序号也写在每行最后了,大伙能够本身比对确认一下,是否是发现,数据都是同样的,因此这里能够先获得一点结论:


图片的不一样格式:png 或者 jpg 对于图片所占用的内存大小其实并无影响


好了,咱们开始来分析这些实验数据:


首先,若是按照图片大小的计算公式:分辨率 * 像素点大小


那么,这张图片的大小按照这个公式应该是:1080 * 452 * 4B = 1952640B ≈ 1.86MB


ps: 这里像素点大小以 4B 来计算是由于,当没有特别指定时,系统默认为 ARGB_8888 做为像素点的数据格式,其余的格式以下:


  • ALPHA_8 -- (1B)

  • RGB_565 -- (2B)

  • ARGB_4444 -- (2B)

  • ARGB_8888 -- (4B)

  • RGBA_F16 -- (8B)


上述实验中,按理就应该都是这个大小,那,为何还会出现一些其余大小的数据呢?因此,具体咱们就一条条来分析下:


分析点1


先看序号 1,2 的实验,这二者的区别仅在于图片显示的控件的大小上面。作这个测试是由于,有些人会认为,图片占据内存空间大小与图片在界面上显示的大小会有关系,显示控件越大占用内存越多。


显然,这种理解是错误的。


想一想,图片确定是先加载进内存后,才绘制到控件上,那么当图片要申请内存空间时,它此时还不知道要显示的控件大小的,怎么可能控件的大小会影响到图片占用的内存空间呢,除非提早告知,手动参与图片加载过程。


分析点2


再来看看序号 2,3,4 的实验,这三个的区别,仅仅在于图片在 res 内的不一样资源目录中。当图片放在 res 内的不一样目录中时,为何最终图片加载进内存所占据的大小会不同呢?


若是大家去看下 Bitmap.decodeResource() 源码,大家会发现,系统在加载 res 目录下的资源图片时,会根据图片存放的不一样目录作一次分辨率的转换,而转换的规则是:


新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )

新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )


目录名称与 dpi 的对应关系以下,drawable 没带后缀对应 160 dpi:



因此,咱们来看下序号 2 的实验,按照上述理论的话,咱们来计算看看这张图片的内存大小:


转换后的分辨率:1080 * (240/160) * 452 * (240/160) = 1620 * 678


显然,此时的分辨率已不是原图的分辨率了,通过一层转换,最后计算图片大小:


1620 * 678 * 4B = 4393440B ≈ 4.19MB


这下知道序号 2 的实验结果怎么来的了吧,一样的道理,序号 3 资源目录是 hdpi 对应的是 240,而设备的 dpi 恰好也是 240,因此转换后的分辨率仍是原图自己,结果也才会是 1.86MB。


小结一下:


位于 res 内的不一样资源目录中的图片,当加载进内存时,会先通过一次分辨率的转换,而后再计算大小,转换的影响因素是设备的 dpi 和不一样的资源目录。


分析点3


基于分析点 2 的理论,看下序号 5,6,7 的实验,这三个实验实际上是用于跟序号 2,3,4 的实验进行对比的,也就是这 6 个实验咱们能够得出的结论是:


  • 同一图片,在同一台设备中,若是图片放在 res 内的不一样资源目录下,那么图片占用的内存空间是会不同的

  • 同一图片,放在 res 内相同的资源目录下,但在不一样 dpi 的设备中,图片占用的内存空间也是会不同的


因此,有可能出现这种状况,同一个 app,但跑在不一样 dpi 设备上,一样的界面,但所耗的内存有多是不同的。


为何这里还要说是有可能不同呢?按照上面的理论,同图片,同目录,但不一样 dpi 设备,那显然分辨率转换就不同,所耗内存应该是确定不同的啊,为何还要用有可能这种说辞?


emmm,继续看下面的分析点吧。


分析点4


序号 8,9 的实验,实际上是想验证是否是只有当图片的来源是 res 内才会存在分辨率的转换,结果也确实证实了,当图片在磁盘中,SD 卡也好,assert 目录也好,网络也好(网络上的图片其实最终也是下载到磁盘),只要不是在 res 目录内,那么图片占据内存大小的计算公式,就是按原图的分辨率 * 像素点大小来。


其实,有空去看看 BitmapFactory 的源码,确实也只有 decodeResource() 方法内部会根据 dpi 进行分辨率的转换,其余 decodeXXX() 就没有了。


那么,为何在上个小节中,要特别说明,即便同一个 app,但跑在不一样 dpi 设备上,一样的界面,但所耗的内存有多是不同的。这里为何要特别用有可能这个词呢?


是吧,大伙想一想。明明按照咱们梳理后的理论,图片的内存大小计算公式是:分辨率*像素点大小,而后若是图片的来源是在 res 的话,就须要注意,图片是放于哪一个资源目录下的,以及设备自己的 dpi 值,由于系统取 res 内的资源图片会根据这两点作一次分辨率转换,这样的话,图片的内存大小不是确定就不同了吗?


emmm,这就取决于你本人的因素了,若是你开发的 app,图片的相关操做都是经过 BitmapFactory 来操做,那么上述问题就能够换成确定的表述。但如今,哪还有人本身写原生,Github 上那么多强大的图片开源库,而不一样的图片开源库,内部对于图片的加载处理,缓存策略,复用策略都是不同的。


因此,若是使用了某个图片开源库,那么对于加载一张图片到内存中占据了多大的空间,就须要你深刻这个图片开源库中去分析它的处理了。


由于基本全部的图片开源库,都会对图片操做进行优化,那么下面就继续来说讲图片的优化处理吧。


3
图片优化



有了上述的理论基础,如今再来想一想若是图片占用内存空间太多,要进行优化,能够着手的一些方向,也比较有眉目了吧。


图片占据内存大小的公式也就是:分辨率*像素点大小,只是在某些场景下,好比图片的来源是 res 的话,可能最终图片的分辨率并非原图的分辨率而已,但归根结底,对于计算机来讲,确实是按照这个公式计算。


因此,若是单从图片自己考虑优化的话,也就只有两个方向:


  • 下降分辨率

  • 减小每一个像素点大小


除了从图片自己考虑外,其余方面能够像内存预警时,手动清理,图片弱引用等等之类的操做。


减小像素点大小


第二个方向很好操做,毕竟系统默认是以 ARGB_8888 格式进行处理,那么每一个像素点就要占据 4B 的大小,改变这个格式天然就能下降图片占据内存的大小。


常见的是,将 ARGB_8888 换成 RGB_565 格式,但后者不支持透明度,因此此方案并不通用,取决于你 app 中图片的透明度需求,固然也能够换成 ARGB_4444,但会大大下降图片质量,Google 官方并不推荐。


因为基本是使用图片开源库了,如下列举一些图片开源库修改像素点格式的处理:


//fresco,默认使用ARGB_8888Fresco.initialize(context, ImagePipelineConfig.newBuilder(context).setBitmapsConfig(Bitmap.Config.RGB_565).build());//Glide,不一样版本,像素点格式不同public class GlideConfiguration implements GlideModule {      @Override      public void applyOptions(Context context, GlideBuilder builder) {          builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);      }      @Override      public void registerComponents(Context context, Glide glide) {      }  }  //在AndroidManifest.xml中将GlideModule定义为meta-data<meta-data android:name="com.inthecheesefactory.lab.glidepicasso.GlideConfiguration" android:value="GlideModule"/>  //Picasso,默认 ARGB_8888Picasso.with(imageView.getContext()).load(url).config(Bitmap.Config.RGB_565).into(imageView);复制代码


以上代码摘抄自网络,正确性应该可信,没验证过,感兴趣自行去相关源码确认一下。

下降分辨率


若是可以让系统在加载图片时,不以原图分辨率为准,而是下降必定的比例,那么,天然也就可以达到减小图片内存的效果。


一样的,系统提供了相关的 API:


BitmapFactory.Options.inSampleSize复制代码


设置 inSampleSize 以后,Bitmap 的宽、高都会缩小 inSampleSize 倍。例如:一张宽高为 2048x1536 的图片,设置 inSampleSize 为 4 以后,实际加载到内存中的图片宽高是 512x384。占有的内存就是 0.75M而不是 12M,足足节省了 15 倍


上面这段话摘抄自末尾给的连接那篇文章中,网上也有不少关于如何操做的讲解文章,这里就不细说了。我还没去看那些开源图片库的内部处理,但我猜测,它们对于图片的优化处理,应该也都是经过这个 API 来操做。


其实,无论哪一个图片开源库,在加载图片时,内部确定就有对图片进行了优化处理,即便咱们没手动说明要进行图片压缩处理。这也就是我在上面讲的,为何当你使用了开源图片库后,就不能再按照图片内存大小一节中所讲的理论来计算图片占据内存大小的缘由。


咱们能够来作个实验,先看下 fresco 的实验:


开源库 前提 Bitmap内存大小
fresco 图片位于res/drawable,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)
fresco 图片位于res/drawable-hdpi,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)
fresco 图片位于res/drawable-xhdpi,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)
fresco 图片位于磁盘中,设备dpi=240,设备1dp=1.5px 1952640B(1.86MB)


若是使用 fresco,那么无论图片来源是哪里,分辨率都是以原图的分辨率进行计算的了,从获得的数据也可以证明,fresco 对于像素点的大小默认以 ARGB_8888 格式处理。

我猜测,fresco 内部对于加载 res 的图片时,应该先以它本身的方式获取图片文件对象,最后有多是经过 BitmapFactory 的 decodeFile() 或者 decodeByteArray() 等等之类的方式加载图片,反正就是不经过 decodeResource() 来加载图片,这样才能说明,为何无论放于哪一个 res 目录内,图片的大小都是以原图分辨率来进行计算。


有时间能够去看看源码验证一下。


再来看看 Glide 的实验:


开源库 前提 Bitmap内存大小
Glide 图片位于res/drawable,设备dpi=240,设备1dp=1.5px,显示到宽高500dp的控件 94200B(91.99KB)
Glide 图片位于res/drawable-hdpi,设备dpi=240,设备1dp=1.5px,显示到宽高500dp的控件 94200B(91.99KB)
Glide 图片位于res/drawable-hdpi,设备dpi=240,设备1dp=1.5px,不显示到控件,只获取 Bitmap 对象 1952640B(1.86MB)
Glide 图片位于磁盘中,设备dpi=240,设备1dp=1.5px,不显示到控件,只获取 Bitmap 对象 1952640B(1.86MB)
Glide 图片位于磁盘中,设备dpi=240,设备1dp=1.5px,显示到全屏控件(1920*984) 7557120B(7.21MB)


能够看到,Glide 的处理与 fresco 又有很大的不一样:


若是只获取 bitmap 对象,那么图片占据的内存大小就是按原图的分辨率进行计算。但若是有经过 into(imageView) 将图片加载到某个控件上,那么分辨率会按照控件的大小进行压缩。


好比第一个,显示的控件宽高均为 500dp = 750px,而原图分辨率 1080*452,最后转换后的分辨率为:750 * 314,因此图片内存大小:750 * 314 * 4B = 94200B;


好比最后一个,显示的控件宽高为 1920*984,原图分辨率转换后为:1920 * 984,因此图片内存大小:1920 * 984 * 4B = 7557120B;


至于这个转换的规则是什么,我不清楚,有时间能够去源码看一下,但就是说,Glide 会自动根据显示的控件的大小来先进行分辨率的转换,而后才加载进内存。


但无论是 Glide,fresco,都无论图片的来源是否在 res 内,也无论设备的 dpi 是多少,是否须要和来源的 res 目录进行一次分辨率转换。


因此,我在图片内存大小这一章节中,才会说到,若是你使用了某个开源库图片,那么,那些理论就不适用了,由于系统开放了 inSampleSize 接口设置,容许咱们对须要加载进内存的图片先进行必定比例的压缩,以减小内存占用。


而这些图片开源库,内部天然会利用系统的这些支持,作一些内存优化,可能还涉及其余图片裁剪等等之类的优化处理,但无论怎么说,此时,系统原生的计算图片内存大小的理论基础天然就不适用了。


下降分辨率这点,除了图片开源库内部默认的优化处理外,它们天然也会提供相关的接口来给咱们使用,好比:


//frescoImageRequestBuilder.newBuilderWithSource(uri)    .setResizeOptions(new ResizeOptions(500, 500)).build()复制代码


对于 fresco 来讲,能够经过这种方式,手动下降分辨率,这样图片占用的内存大小也会跟着减小,但具体这个接口内部对于传入的 (500, 500) 是如何处理,我也还不清楚,由于咱们知道,系统开放的 API 只支持分辨率按必定比例压缩,那么 fresco 内部确定会进行一层的处理转换了。


须要注意一点,我使用的 fresco 是 0.14.1 版本,高版本我不清楚,此版本的 setResizeOptions() 接口只支持对 jpg 格式的图片有效,若是须要对 png 图片的处理,网上不少,自行查阅。


Glide 的话,自己就已经根据控件大小作了一次处理,若是还要手动处理,可使用它的 override() 方法。


4
总结


最后,来稍微总结一下:


  • 一张图片占用的内存大小的计算公式:分辨率 * 像素点大小;但分辨率不必定是原图的分辨率,须要结合一些场景来讨论,像素点大小就几种状况:ARGB_8888(4B)、RGB_565(2B) 等等。

  • 若是不对图片进行优化处理,如压缩、裁剪之类的操做,那么 Android 系统会根据图片的不一样来源决定是否须要对原图的分辨率进行转换后再加载进内存。

  • 图片来源是 res 内的不一样资源目录时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,作一次分辨率转换,规则以下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )。

  • 其余图片的来源,如磁盘,文件,流等,均按照原图的分辨率来进行计算图片的内存大小。

  • jpg、png 只是图片的容器,图片文件自己的大小与它所占用的内存大小没有什么关系,固然它们的压缩算法并不同,在解码时所耗的内存与效率此时就会有些区别。

  • 基于以上理论,如下场景的出现是合理的:

  • 同个 app,在不一样 dpi 设备中,同个界面的相同图片所占的内存大小有可能不同。

  • 同个 app,同一张图片,但图片放于不一样的 res 内的资源目录里时,所占的内存大小有可能不同。

  • 以上场景之所说有可能,是由于,一旦使用某个热门的图片开源库,那么,以上理论基本就不适用了。

  • 由于系统支持对图片进行优化处理,容许先将图片压缩,下降分辨率后再加载进内存,以达到下降占用内存大小的目的

  • 而热门的开源图片库,内部基本都会有一些图片的优化处理操做:

  • 当使用 fresco 时,无论图片来源是哪里,即便是 res,图片占用的内存大小仍旧以原图的分辨率计算。

  • 当使用 Glide 时,若是有设置图片显示的控件,那么会自动按照控件的大小,下降图片的分辨率加载。图片来源是 res 的分辨率转换规则对它也无效。


本篇所梳理出的理论、基本都是经过总结别人的博客内容,以及本身作相关实验验证后,得出来的结论,正确性相比阅读源码自己梳理结论天然要弱一些,因此,若是有错误的地方,欢迎指点一下。有时间,也能够去看看相关源码,来确认一下看看。