Q1:一张 png 格式的图片,分辨率是 1080*452,图片文件大小为 55.8KB,那么它加载进内存时所占的大小是多少?
- 电脑上看到的 png/jpg 格式的图片,只是图片的容器,
经过相对应压缩算法,将原图每个像素点信息转换成另一种格式表示
达到压缩的目的,减少图片文件大小。 - 图片加载进内存时,会先解析图片文件本身的数据格式,还原为位图,也就是Bitmap对象
Bitmap大小 取决于 像素点的数据格式、以及 分辨率 二者。
所以 png/jpg图片文件格式的 图片文件大小、 图片占用内存大小 是两回事,不相等
3) 图片内存大小
Bitmap.getByteCount() 可以获取当前图片占用的内存大小。
图片格式:png 或者 jpg 对于图片所占用的内存大小其实并没有影响
图片所占内存大小与图片在界面上显示的控件大小无关。
像素点格式:
ALPHA_8 (1Byte)
RGB_565 (2Byte)
ARGB_4444 (2Byte)
ARGB_8888 (4Byte)
RGBA_F16 (8Byte)
系统默认 ARGB_8888 作为像素点的数据格式,4个字节大小。
图片大小计算公式:分辨率 * 像素点大小
按公式,这张图片占用内存大小:10804524B = 1952640B ≈ 1.86 MB
图片放在 res 内的不同目录中,加载进内存所占据的大小不一样:
1) 根据图片所在res目录,转换图片分辨率
public class BitmapFactory {
public static Bitmap decodeResource(Resources res, int id, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
// value中保存有图片所在目录的 分辨率信息
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
} finally {
...
}
...
return bm;
}
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
/* decode图片时BitmapFactory.Options中的inDensity和inTargetDensity
* https://www.jianshu.com/p/0d28a97193d0
*/
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
//获取到的 文件夹分辨率为 0,使用默认值
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
/* 正常情况下,会把drawable 的 density 赋值给 Options
* 对应关系:
* dpi (dot per inch):每英寸的像素数
*
* 默认drawable(文件夹名后不跟分辨率)----->160
* ldpi -----> 120 px
* mdpi -----> 160 px
*
* hdpi -----> 240 px
* xhdpi -----> 320 px
*
* xxhdpi -----> 480 px
* xxxhdpi-----> 640 px
*/
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
/* 根据设备屏幕的像素密度赋值
* res.getDisplayMetrics().densityDpi 获取设备屏幕的像素密度
*/
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
/* 输出图片的宽高 = 原图宽高 / inSampleSize * (inTargetDensity / inDensity)
* inTargetDensity:设备密度(每英寸的像素数)
* inDensity:res目录密度
*
* (注意 inSampleSize>=1 并且 inSampleSize 只能是2的幂,不是2的幂下转到,最大的2的幂)
*
* 上面公式可知, inDensity越大,输出的图片尺寸越小。
* 也就是 res 分辨率越高的目录,输出的图片宽高/分辨率越小,所占内存自然也越小。
*/
return decodeStream(is, pad, opts);
}
}
i.设备密度的计算
设备英寸是指,设备屏幕对角线英寸数。
设备密度 = 设备长(宽)分辨率 / 设备长(宽)英寸 = 每英寸的像素数
根据设备分辨率,可以计算出设备【宽高比】,然后根据 设备英寸,算出设备【宽度英寸】数。
然后设备 【宽度分辨率 / 设备宽度英寸 = 每英寸像素数】 也就是设备密度。
ii.res目录的密度 (固定值)
默认drawable(文件夹名后不跟分辨率)----->160
ldpi -----> 120 px
mdpi -----> 160 px
hdpi -----> 240 px
xhdpi -----> 320 px
xxhdpi -----> 480 px
xxxhdpi-----> 640 px
iii. 1dp = 多少px
public class DisplayMetrics {
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
}
设备屏幕密度/res资源默认密度(160 dpi)
比如:
设备密度 = 240dpi 那么 1dp = 240/160 = 1.5px
根据公式计算出图片占用内存
占用内存 = 分辨率宽 * 分辨率高 * 像素点大小
Q2:为什么有时候,同一个 app,app 内的同个界面,界面上同张图片,但在不同设备上所耗内存却不一样?
不同设备的dpi(设备密度:每英寸像素数)不同,
res目录下图片加载时进行分辨率转换,得到的结果也就不同
转换后的分辨率不同,根据公式【分辨率*像素点大小】得到的内存占用大小也就不同。
分辨率转换公式:
输出宽高 = 原图宽高 / inSampleSize * (inTargetDensity / inDensity)
inTargetDensity:设备密度(每英寸的像素数)
inDensity:res目录密度
设备密度越大,占用内存越高
res目录密度越大,占用内存越低
Q3:同一张图片,在界面上显示的控件大小不同时,它的内存大小也会跟随着改变吗?
不会,与控件大小无关。
Q4:图片占用的内存大小公式:图片分辨率 * 每个像素点大小,这种说法正确吗,或者严谨吗?
位于res 内的不同资源目录中的图片,当加载进内存时,会先经过一次分辨率的转换
然后再计算大小,转换的影响因素是设备的 dpi 和不同的资源目录。
虽然最终图片大小的计算公式为 【分辨率*像素点大小】,但是此时的分辨率已经不是图片本身的分辨率了。
分辨率转换公式:
输出图片的宽高 = 原图宽高 / inSampleSize * (inTargetDensity / inDensity)
inTargetDensity:设备密度(每英寸的像素数)
inDensity:res目录密度
Q5:优化图片的内存大小有哪些方向可以着手?
图片占用内存 = 分辨率 * 像素点大小
1)降低图片分辨率
输出图片的宽高 = 原图宽高 / inSampleSize * (inTargetDensity / inDensity)
通过设置 inSampleSize,降低转换后输出的图片分辨率
设置 inSampleSize 后,图片的宽/高都会缩小 inSampleSize 倍。
比如:
设置inSampleSize 为 4,那么宽度分辨率缩小4倍。高度分辨率也缩小4倍。
图片占用内存 = 宽度分辨率 * 高度分辨率 * 像素大小
总共就节省了 4*4 16倍。
2)减少每个像素点大小 (不需要有透明度处理的图片,去除alpha通道)
采用其他像素格式
RGB_565 每个像素占用2B,但是不支持透明度
ARGB_4444 每个像素占用2B,但是会大大降低图片质量,Google官方并不推荐
3)内存预警,手动清理缓存,图片弱引用等。
特别说明:
1)哈夫曼算法,只是改变图片保存为文件时的存储结构,【减少的是图片文件的大小】。
当图片加载到内存时,会解析出来,【占用的内存并不会减少】。
2)使用第三方图片库,加载图片时,由于图片库本身对图片进行优化处理。
不能再按照图片以上规则来计算图片了。
比如:
res 目录下的图片。
如果 不使用 BitmapFactory.decodeResource() 来加载,就不会有像素转换的过程。
图片的大小就是以原图分辨率来计算了。
总结:
- 内存大小 = 分辨率 * 像素点大小
分辨率不一定是原图的分辨率,需要结合一些场景来讨论。
像素点大小就几种情况:ARGB_8888(4B)、RGB_565(2B) 等等。 - 如果不对图片进行优化处理,如压缩、裁剪之类的操作,
系统会根据图片的不同来源决定是否需要对原图的分辨率进行转换后再加载进内存。 - res 内的不同资源目录图片时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换
规则如下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )。 - 其他图片的来源,如磁盘,文件,流等,均按照原图的分辨率来进行计算图片的内存大小。
- jpg、png 只是图片的容器,图片文件本身的大小与它所占用的内存大小没有什么关系,
当然它们的压缩算法并不一样,在解码时所耗的内存与效率此时就会有些区别。
基于以上理论,以下场景的出现是合理的:
i.同个 app,在不同 dpi 设备中,同个界面的相同图片占用内存大小不一样
ii.同个 app,同一张图片,放于不同的 res 内的资源目录里时,所占的内存大小有可能不一样。
iii.以上场景之所说有可能,是因为,一旦使用某个热门的图片开源库,那么,以上理论基本就不适用了。
iv.因为系统支持对图片进行优化处理,允许先将图片压缩,降低分辨率后再加载进内存,以达到降低占用内存大小的目的
而热门的开源图片库,内部基本都会有一些图片的优化处理操作:
v.当使用 fresco 时,不管图片来源是哪里,即使是 res,图片占用的内存大小仍旧以原图的分辨率计算。
vi.当使用 Glide 时,如果有设置图片显示的控件,那么会自动按照控件的大小,降低图片的分辨率加载。
图片来源是 res 的分辨率转换规则对它也无效。
二、常用压缩图片方式
鲁班:https://github.com/Curzibn/Luban
implementation ‘top.zibin:Luban:1.1.8’