问题
在讲解图片占用内存前,我们先问自己几个问题:
- 我们在对手机进行屏幕适时,常想可不可以只切一套图适配所有的手机呢?
- 一张图片加载到手机中,占用内存到底有多少?
- 图片占用内存跟哪些东西有关?跟手机有关系么?同一张图片放在不同的dpi文件夹下内存占用会变化么?
- 如果是网络图片,加载到手机中,占用内存跟手机屏幕有关系么?
带着这些问题我们来一层层解析。我们先看看加载本地资源,不同手机所占内存情况:
一、加载本地资源,不同手机占内存情况
我们如果加载app内图片,想知道它占用多少内存,可先将此资源转成bitmap进行查看。
1. 从资源中获取bitmap
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.mipmap.testxh); |
获取到bitmap,我们还需要知道此bitmap在内存占多少空间,具体方法如下。
2. 获取图片大小
public int getBitmapSize(Bitmap bitmap){ |
接下来就来测试,不同的手机、同一张图片放在不同的密度文件夹下,占用内存情况。
3. 同一图片在不同屏幕的手机、不同的屏幕密度文件夹下占用内存大小
经测试同一张图片分别放在不同的mipmap文件夹(mipmap-hdpi, mipmap-xhdpi, mipmap-xxhdpi)下或是drawable文件夹(drawable-hdpi, drawable-xhdpi, drawable-xxhdpi)下,相同的dpi下的文件夹下加载出来的图片,bitmap占用内存大小一样;
对于同一张图片,放在不同手机、不同的屏幕密度文件夹下占用内存情况又是如何呢,这里我们以一张大小为1024*731 = 748544B, 大小为485.11K 的图片为例,下面是测试手机占用的内存情况。
对于不同的手机屏幕密度的手机占用内存大小
从上表可以看出不同屏幕密度的手机加载图片,如果图片放在与自己屏幕密度相同的文件夹下,占用的内存都是2994176B,与图片本身大小748544B存在一个4倍关系,因为图片采用的ARGB-888色彩格式,每个像素点占用4个字节。
从上述测试可以得出,bitmap占用内存大小,与手机的屏幕密度、图片所放文件夹密度、图片的色彩格式有关。
这里总结一下获取Bitmap图片大小的代码:手机在加载图片时,会先查找自己本密度的文夹下是否存在资源,不存在则会向上查找,再向下查找,并对图片进行相应倍数的缩放:
如果在与自己屏幕密度相同的文件夹下存在此资源,会原样显示出来,占用内存正好是: 图片的分辨率*色彩格式占用字节数;
若自己屏幕密度相同的文件夹下不存在此文件,而在大于自己屏幕密度的文件夹下存在此资源,会进行缩小相应的倍数的平方;
若在大于自己屏幕密度的文件夹下没找到此资源,则会向小于自己屏幕密度的文件夹下查找,如果存在,则会进行放大相应的倍数的平方,这两种情况图片占用内存为:
占用内存=图片宽度 X 图片高度/((资源文件夹密度/手机屏幕密度)^2) * 色彩格式每一个像素占用字节数
4. 图片占用内存与图片的色彩格式的关系
我们在计算bitmap大小时,是通过计算getRowBytes * bitmap.getHeight()得来的,后面的乘数就是图片的高度,而第一个乘数getRowBytes是什么呢?我们根进Bitmap代码查看getRowBytes函数:
/** |
该方法最终调用的是Bitmap中的native方法:
private static native int nativeRowBytes(long nativeBitmap); |
我们再查看对应的Bitmap.cpp里的nativeRowBytes方法
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) { |
我们可以看到这里的bitmap形式是以SkBitmap对象展现的,这个Bitmap就和图片展示的色彩格式有关,我们再看看SkBitmap里是怎么计算rowBytes的:
size_t SkBitmap::ComputeRowBytes(Config c, int width) { |
可以看到,图片的宽乘以了一个SkColorTypeBytesPerPixel(ct)变量,对于不同色彩格式,每个像素占用的字节数就是在SkColorTypeBytesPerPixel中定义的。这就是为什么上面得出的bitmap大小,在自己屏幕密度的文件夹下图片占用的内存大小都被乘以了4,因为bitmap加载默认采用的是RGBA_8888编码格式。
5. 图片占用内存与手机屏幕密度、图片所在文件夹密度的关系
那么手机怎么加载图片时,为什么同样的图片在不同的屏幕分辨率的手机上、不同的屏幕密度文件夹下占用内存会相差这么大呢?
在加载资源图片时,我们一般会借助于BitmapFactory的decodeResource方法,此方法的源代码如下:
/** |
我们再来看看BitmapFactory的decodeResourceStream方法
/** |
可以看到这里调用了native decodeStream方法:
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) { |
inDensity,inTargetDensity,inScreenDensity, inScaled三者关系
通过追查代码,我们可以看到图片资源通过数据流解码时,会根据inDensity,inTargetDensity,inScreenDensity三个值和是否被缩放标识inScaled
- inDensity:图片本身的像素密度(其实就是图片资源所在的哪个密度文件夹下,如在xxhdpi下就是480,如果在asstes、手机内存/sd卡下,默认是160);
- inTargetDensity:图片最终在bitmap里的像素密度,如果没有赋值,会将inTargetDensity设置成inScreenDensity;
- inScreenDensity:手机本身的屏幕密度,如我们测试的三星手机dpi=640, 如果inDensity与inTargetDensity不相等时,就需要对图片进行缩放,inScaled = inTargetDensity/inDensity。
我们上面研究了加载应用程序的图片占用内存大小与手机屏幕密码和图片所放的密度文件夹、图片的编码格式有关,那如果加载的是网络图片或是本地图片,在不同的手机上占用内存又是否一样呢?
二、加载sd卡下的资源或是网络图片解析
手机无论是加载sd卡图片,assets路径下还是网络图片,都需要先把图片读成数据流格式,再调用相应的decodeStream方法,将数据流转成bitmap形式,在调用decodeStream如果不设置Options的话,通过以上三款手机打印出图片所占内存大小均为:2994176B,也就是跟手机的屏幕密度没有关系。
那如果设置Options中的参数,图片占用的内存会不会与手机的屏幕密度有关系呢?我在测试中发现单独手动设置图片密度inDensity或是inTargetDensity,并不起作用,图片占用内存一直都是图片本身大小。
为什么没起作用呢,这需要我们从资源加载的源头看起。
1. 根据手机本地图片路径获取Bitmap
我们先来看一下BitmapFactory的decodeFile函数:
//读取手机本地的图片资源 |
2. 根据网络地址获取图片Bitmap
/** |
可以看到通过路径加载图片,最终还是会调用BitmapFactory里的decodeStream方法,我们再来看看decodeStream方法。
3. 将数据流转成Bitmap
/** |
如果数据流来自于资源,则调用BitmapFactory的nativeDecodeAsset,
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts); |
否则调用decodeStreamInternal方法:
/** |
此方法会调用native的nativeDecodeStream方法:
4. native层的数据流解析
- nativeDecodeStream/nativeDecodeAsset
通过追踪上述两种nativeDecodeStream方法和nativeDecodeAsset方法,它们最终都会调用nativeDecodeStreamScaled或是nativeDecodeAssetScaled方法,它们会添加两个参数,一个是false,一个是1.0f,这两个参数具体代表什么呢?
//解码Asset资源的数据流 |
- nativeDecodeStreamScaled/nativeDecodeAssetScaled
nativeDecodeAssetScaled或是nativeDecodeStreamScaled方法中最后两个参数,分别是applyScale,sclae,一个是是否申请缩放,一个是缩放比例,也就是从这种数据流加载的图片,默认都不会进缩放。我们注意到,这两个函数最终都会走到doDecode方法里,我们直接看nativeDecodeStreamScaled方法,发现此方法只是对输入流进行了转换,转成SkStream类型。
static jobject nativeDecodeStreamScaled(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, |
- doDecode
我们来看最终的doDecode函数:
static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding, |
通过上面分析直至native中的decode函数,我们发现options里的参数只提取了sampleSize、optionsJustBounds,但是没有见到inDensity,inTargetDensity,inScreenDensity等参数的提取。如果我在加载流前,设置ops.inDensity和ops.inTargetDensity参数如下,图片占用内存大小会缩小到原来的1/4
BitmapFactory.Options ops = new BitmapFactory.Options(); |
但是如果只设置inDensity或是inTargetDensity参数,是完全不起作用,感觉是因为只设置了一个参数,另一个参数默认为0, 前面咱们判断过,只要有一个参数为0, 就不会计算缩放比。所以默认还是显示原来图片尺寸大小,只有两个参数均设置,都不为0, 才会去计算缩放比。
通过上面的分析,我们可以回答最开始的问题了。
结论:
1. 在对手机进行屏幕适时,可以只切一套图适配所有的手机。
但是如果只切一套小图,那在高屏幕密度手机上,会对图片进行放大,这样图片占用的内存往往比切相应图片放在高密度文件夹下,占用的内存还要大。
那如果只切一套大图放在高幕文件夹下,在小屏幕密度手机上,会缩小显示,按道理是行得通的。但系统在对图片进行缩放时,会进行大量计算,会对手机的性能有一定的影响。同时如果图片缩放比较狠,可能导致图片出现抖动或是毛边。
所以最好切出不同比便的图片放在不同幕度的文件夹下,对于性能要求不大高的图片,可以只切一套大图;
2. 一张图片占用内存=图片长 * 图片宽 / (资源图片文件密度/手机屏幕密度)^2 * 每一象素占用字节数,所以图片占用内存跟图片本身大小、手机屏幕密度、图片所在的文件夹密度,图片编码的色彩格式有关;
3. 对于网络图片,在不同屏幕密度的手机上加载出来,占用内存是一样的。
4. 对于网络或是assets/手机本地图片加载,如果想通过设置Options里的
inDensity或是inTargetDensity参数来调整图片的缩放比,必须两个参数均设置才能起作用,只设置一个,不会起作用。
5. drawable和mipmap文件夹存放图片的区别,首先图片放在drawable-xhdpi和mipmap-xhdpi下,两者占用的内存是一样的,
Mipmaps早在Android2.2+就可以用了,但是直到4.3 google才强烈建议使用。把图片放到mipmaps可以提高系统渲染图片的速度,提高图片质量,减少GPU压力。其他并没有什么区别。