本文经作者许可发表。
最近正好整理了图片的理论基础,所以这次就来说说Android中如何计算一张图片在内存中的大小,想要优化应该从何入手。
1个
问问题
在看这篇文章之前,我们先思考几个问题:
Q1:一张png格式的图片,图片文件大小为55.8KB,那么加载到内存中占用的空间是多少?
Q2:为什么有时候,同一个app,app里面的界面是一样的图片像素和尺寸的区别,界面上是一样的图片,但是在不同的设备上,内存消耗是不一样的?
Q3:同一张图片在界面上显示的控件大小不同时,其内存大小是否也会随之变化?
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操作中,在某些场景下,加载到内存中的图片分辨率会经过一层转换。 因此,虽然最终图片大小的计算公式仍然是分辨率*像素大小,但此时的分辨率已经不再是图片本身的分辨率了。
我们来做一个实验,从以下考虑因素相互结合的以下场景中加载同一张图片,看看它占用了多少内存空间:
测试代码模板如下:
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之后还有另外一种方法,位图复用时得到的size的含义也发生了变化。 这些特殊场景我就不细说了,有兴趣的可以自行查看。 Anyway,这里我们知道,在大多数情况下,我们可以通过打印getByteCount()图片占用的内存大小来验证我们的实验。
图片就是上图:一张png格式的图片,分辨率为1080*452,图片文件本身大小为56KB
序号前提位图内存大小
1个
图片位于res/drawable,device dpi=240,device 1dp=1.5px,控件宽高=50dp
4393440B (4.19MB)
2个
图片位于res/drawable, device dpi=240, device 1dp=1.5px, control width and height=500dp
4393440B (4.19MB)
3个
图片位于res/drawable-hdpi,device dpi=240,device 1dp=1.5px
1952640B (1.86MB)
4个
图片位于res/drawable-xhdpi,device dpi=240,device 1dp=1.5px
1098360B (1.05MB)
5个
图片位于res/drawable-xhdpi,device dpi=160,device 1dp=1px
488160B (476.7KB)
6个
图片位于res/drawable-hdpi,device dpi=160,device 1dp=1px
866880 (846.5KB)
7
图片位于res/drawable,device dpi=160,device 1dp=1px
1952640B (1.86MB)
8个
图片位于磁盘上,设备dpi=160,设备1dp=1px
1952640B (1.86MB)
9
图片位于磁盘上,设备dpi=240,设备1dp=1.5px
1952640B (1.86MB)
你看见了吗? 明明是同一张图,但是在不同的场景下,占用的内存大小可能不同,后面会具体分析。
以上场景列出了不同的图片来源、不同的Android设备、不同大小的显示控件。 这些是正在考虑的情景。 我们继续看一个场景:同一张图片,保存为不同格式的文件(不重命名,可以用ps);
图片:jpg格式的图片,分辨率为1080*452,图片文件本身大小为85.2KB
ps:还是上面那张图,不过是通过PhotoShop保存成jpg格式的
序列号前提Bitmap内存大小比较对象
10
图片位于res/drawable,device dpi=240,device 1dp=1.5px
4393440B (4.19MB)
序列号 1
11
图片位于res/drawable-hdpi,device dpi=240,device 1dp=1.5px
1952640B (1.86MB)
序列号 3
12
图片位于res/drawable-xhdpi,device dpi=240,device 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,其他格式如下:
在上面的实验中,应该是这个尺寸的,那为什么会出现一些其他尺寸的数据呢? 那么,让我们详细分析每一个:
分析要点1
我们先来看序号1和2的实验。 两者的区别仅在于图片中显示的控件大小。 之所以做这个测试是因为有人认为图片占用内存空间的大小与界面显示的图片大小有关,显示控件越大占用内存越多。
显然,这种理解是错误的。
想一想,图片在控件上绘制之前必须加载到内存中,所以当图片需要申请内存空间时,它不知道此时要显示的控件的大小,怎么可能大小控件的影响图片占用 至于内存空间,除非事先通知,否则手动参与图片加载过程。
分析要点2
我们来看一下序号为2、3、4的实验,这三者的区别在于res中图片在不同的资源目录下。 res中图片放在不同目录下,为什么最后图片加载到内存的大小不一样?
查看Bitmap.decodeResource()的源码会发现,系统在加载res目录下的资源图片时,会根据图片存放目录的不同进行分辨率转换,转换规则是:
新图片的高度=原图片的高度*(设备的dpi/目录对应的dpi)
新图片的宽度=原图片的宽度*(设备的dpi/目录对应的dpi)
目录名与dpi的对应关系如下,不带后缀的drawable对应160dpi:
那么,我们来看一下实验2,根据上面的理论,我们来计算这张图片的内存大小:
转换后的分辨率:1080 * (240/160) * 452 * (240/160) = 1620 * 678
显然,此时的分辨率并不是原始图像的分辨率。 经过一层转换,最终计算出图像大小:
1620 * 678 * 4B = 4393440B ≈ 4.19MB
现在你知道序号2的实验结果是怎么来的了吧。 同理,序号3的资源目录是对应240的hdpi,而设备的dpi正好是240,所以转换后的分辨率还是原图本身,结果也是1.86MB。
总结:
res中不同资源目录下的图片,在加载到内存中时图片像素和尺寸的区别,会先进行分辨率转换,然后再计算大小。 影响转换的因素是设备的dpi和不同的资源目录。
分析点3
基于分析点2的理论,再看5、6、7号的实验,这三个实验其实就是用来和2、3、4号的实验进行对比的,也就是我们可以从这些6个实验。 结论是:
因此,可能会出现同一个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格式,但是后者不支持透明,所以这个方案不通用。 这取决于您应用中图像的透明度要求。 当然也可以换成ARGB_4444,但是画质会大打折扣。 Google 官方不推荐。
由于基本都是用图片开源库,下面列出一些修改像素格式的图片开源库:
//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
<meta-data android:name="com.inthecheesefactory.lab.glidepicasso.GlideConfiguration" android:value="GlideModule"/>
//Picasso,默认 ARGB_8888
Picasso.with(imageView.getContext()).load(url).config(Bitmap.Config.RGB_565).into(imageView);
以上代码摘自网络。 正确性应该是可信的。 它尚未得到验证。 有兴趣的可以去相关的源码中确认一下。
降低分辨率
如果能让系统在加载图片的时候不以原图的分辨率为标准,而是降低一定的比例,那么自然就可以达到减少图片内存的效果。
同样,系统提供了相关的API:
BitmapFactory.Options.inSampleSize
设置inSampleSize后,Bitmap的宽高会缩小inSampleSize倍。 例如:一张宽高为2048×1536的图片,设置inSampleSize为4后,实际加载到内存中的图片宽高为512×384,占用内存为0.75M而不是12M,节省了15倍
以上段落摘自文末链接。 网上也有很多关于如何操作的讲解文章,这里就不赘述了。 那些开源图片库的内部处理我没看过,不过我猜想他们对图片的优化处理应该也是通过这个API来操作的。
事实上,无论是哪个图片开源库,在加载图片的时候,内部都必须对图片进行优化,即使我们没有手动指定图片压缩过程。 这就是我上面说的,为什么大家在使用开源图片库的时候,不能再按照图片内存大小部分说的理论来计算图片的内存大小了。
我们可以做一个实验,先看fresco的实验:
开源库前提位图内存大小
壁画
图片位于res/drawable,device dpi=240,device 1dp=1.5px
1952640B (1.86MB)
壁画
图片位于res/drawable-hdpi,device dpi=240,device 1dp=1.5px
1952640B (1.86MB)
壁画
图片位于res/drawable-xhdpi,device dpi=240,device 1dp=1.5px
1952640B (1.86MB)
壁画
图片位于磁盘上,设备dpi=240,设备1dp=1.5px
1952640B (1.86MB)
如果使用fresco,无论图片来源在哪里,都是根据原图的分辨率计算分辨率。 从得到的数据也可以确认,fresco默认对ARGB_8888格式的像素大小进行处理。
我猜想,在fresco里面加载res图片的时候,应该是先通过自己的方式获取图片文件对象,最后可能会通过BitmapFactory的decodeFile()或者decodeByteArray()等方式加载图片,反正不是加载图片通过decodeResource(),从而解释为什么不管放在哪个res目录下,图片的大小都是根据原图的分辨率来计算的。
有时间的话可以去看看源码。
我们来看看Glide的实验:
开源库前提位图内存大小
滑行
图片位于res/drawable,device dpi=240,device 1dp=1.5px,展示给一个宽高为500dp的控件
94200B (91.99KB)
滑行
图片位于res/drawable-hdpi,device dpi=240,device 1dp=1.5px,显示给一个宽高为500dp的控件
94200B (91.99KB)
滑行
图片位于res/drawable-hdpi,device dpi=240,device 1dp=1.5px,不显示给控件,只获取Bitmap对象
1952640B (1.86MB)
滑行
图片位于磁盘,设备dpi=240,设备1dp=1.5px,不显示给控件,只获取Bitmap对象
1952640B (1.86MB)
滑行
图片位于磁盘,设备dpi=240,设备1dp=1.5px,显示为全屏控制(1920*984)
7557120B (7.21MB)
可以看到,Glide的处理和fresco有很大的不同:
如果只获取位图对象,则根据原图的分辨率计算图片占用的内存大小。 但是如果通过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目录进行分辨率转换。
所以在image memory size那一章,我会说,如果你用的是开源库image,那么那些理论都不适用,因为系统开启了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() 方法。
4个
总结
最后,一个小总结:
这篇文章整理出来的理论,基本都是在总结别人博客内容的基础上,通过相关实验验证的。 得出结论的正确性自然弱于阅读源码本身。 所以,如果有错误的地方,欢迎指点。 有时间的话也可以看看相关的源码来确认一下。