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

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

那么,这张图片的大小按照这个公式应该是: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 上那么多强大的图片开源库,而不同的图片开源库,内部对于图片的加载处理,缓存策略,复用策略都是不一样的。

所以,如果使用了某个图片开源库,那么对于加载一张图片到内存中占据了多大的空间,就需要你深入这个图片开源库中去分析它的处理了。

因为基本所有的图片开源库,都会对图片操作进行优化,那么下面就继续来讲讲图片的优化处理吧。

图片优化

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

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

所以,如果单从图片本身考虑优化的话,也就只有两个方向:

  • 降低分辨率
  • 减少每个像素点大小

除了从图片本身考虑外,其他方面可以像内存预警时,手动清理,图片弱引用等等之类的操作。

减少像素点大小

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

常见的是,将 ARGB_8888 换成 RGB_565 格式,但后者不支持透明度,所以此方案并不通用,取决于你 app 中图片的透明度需求,当然也可以缓存 ARGB_4444,但会降低质量。

由于基本是使用图片开源库了,以下列举一些图片开源库的处理方式:

//fresco,默认使用ARGB_8888
 Fresco.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//Picasso,默认 ARGB_8888
 Picasso.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 接口设置,允许我们对需要加载进内存的图片先进行一定比例的压缩,以减少内存占用。

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

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

//fresco
 ImageRequestBuilder.newBuilderWithSource(uri)
 .setResizeOptions(new ResizeOptions(500, 500)).build()

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

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

Glide 的话,本身就已经根据控件大小做了一次处理,如果还要手动处理,可以使用它的 override() 方法。

总结

最后,来稍微总结一下:

  • 一张图片占用的内存大小的计算公式:分辨率 * 像素点大小;但分辨率不一定是原图的分辨率,需要结合一些场景来讨论,像素点大小就几种情况:ARGB_8888(4B)、RGB_565(2B) 等等。
  • 如果不对图片进行优化处理,如压缩、裁剪之类的操作,那么 Android 系统会根据图片的不同来源决定是否需要对原图的分辨率进行转换后再加载进内存。
  • 图片来源是 res 内的不同资源目录时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换,规则如下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )。
  • 其他图片的来源,如磁盘,文件,流等,均按照原图的分辨率来进行计算图片的内存大小。
  • jpg、png 只是图片的容器,图片文件本身的大小与它所占用的内存大小没有什么关系。
  • 基于以上理论,以下场景的出现是合理的:
  • 同个 app,在不同 dpi 设备中,同个界面的相同图片所占的内存大小有可能不一样。
  • 同个 app,同一张图片,但图片放于不同的 res 内的资源目录里时,所占的内存大小有可能不一样。
  • 以上场景之所说有可能,是因为,一旦使用某个热门的图片开源库,那么,以上理论基本就不适用了。
  • 因为系统支持对图片进行优化处理,允许先将图片压缩,降低分辨率后再加载进内存,以达到降低占用内存大小的目的
  • 而热门的开源图片库,内部基本都会有一些图片的优化处理操作:
  • 当使用 fresco 时,不管图片来源是哪里,即使是 res,图片占用的内存大小仍旧以原图的分辨率计算。
  • 当使用 Glide 时,如果有设置图片显示的控件,那么会自动按照控件的大小,降低图片的分辨率加载。图片来源是 res 的分辨率转换规则对它也无效。
    本篇所梳理出的理论、基本都是通过总结别人的博客内容,以及自己做相关实验验证后,得出来的结论,正确性相比阅读源码本身梳理结论自然要弱一些,所以,如果有错误的地方,欢迎指点一下。有时间,也可以去看看相关源码,来确认一下看看。