首先明确两个问题:
- 图片大小和占用内存大小没有关系,图片大小之关系到apk的大小
- webp虽然图片小,占用内存方面和其他图片没有性能上的优势
几个基本概念
- px:像素(pixel),指的是屏幕上的物理点,最小的独立显示单位
- ppi:每英寸像素点(Pixels Per Inch)
- dpi:每英寸点(Dots Per Inch)
- dp:像素无关点(Density-Independent pixel),这个Android定义的虚拟值,和px关系式是px = dp * (dpi / 160)
ppi和dpi经常都会出现混用现象。它们是用来描述屏幕的属性,或者说是性能。从技术角度说,“像素”(P)只存在于计算机显示领域,而“点”(d)只出现于打印或印刷领域。
为什么规定160dpi规格的屏幕上,1dp = 1px?
这个在Google的官方文档中有给出了解释,因为第一款Android设备(HTC的T-Mobile G1)是属于160dpi的。
聊一个问题
设备一:Sony Z2 屏幕尺寸:5.2in 屏幕分辨率:1080*1920 DPI:424
设备二:华为 Mate 7 屏幕尺寸:6.0in 屏幕分辨率:1080*1920 DPI:367
比如一个要32dp的高度的控件,按照公式
Z2 32dp = 32 * (424/160)= 84.8px
Mate 7 32dp = 32*(367/160) = 73.4px
明显大小不一样啊,why?
内存的计算
理论上的内存大小:
图片占用内存 = 宽度像素 * 高度像素 * 单个像素占的字节数
这单个像素占的字节数是和安卓色彩模式有关系的,如下:
Android中的四种色彩模式:
ALPHA_8: 每个像素占用1byte内存
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存
RGB_565: 每个像素占用2byte内存
注:ARGB指的是一种色彩模式,里面A代表Alpha,R表示red,G表示green,B表示blue,其实所有的可见色都是红绿蓝组成的,所以红绿蓝又称为三原色。Android默认的bitmap色彩模式是ARGB_8888。通过bitmap源码可以追踪看到单个像素占的字节数和色彩模式是有关系的,通过Bitmap源码查看getRowBytes方法,这里源码暂时不放了,意义不大,知道就好。
BitmapFactory的decodeResource方法
/**
* @param res 包含图片资源的Resources对象,一般通过getResources()即可获取
* @param id 资源文件id, 如R.mipmap.ic_laucher
* @param opts 可为空,控制采样或图片是否需要完全解码还是只需要获取图片大小
* @return 解码的bitmap
*/
public static Bitmap decodeResource(Resources res, int id, Options opts) {
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
//1.读取资源id,返回流格式
is = res.openRawResource(id, value);
//2. 直接加载数据流格式进行解码,一般opts为空
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
可以看到在拿到资源图片后,转换成Inputstream,交由decodeResourceStream处理,见下面:
/**
* 根据输入的数据流确码成一个新的bitmap, 数据流是从资源处获取,在这里可以根据规则对图片进行一些缩放操作
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {//如果没有设置Options,系统会新创建一个Options对象
opts = new Options();
}
//若没有设置opts,inDensity就是初始值0,它代表图片资源密度
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//如果density等于0,则采用默认值160
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
//如果没有设置资源密度,则图片不会被缩放
//这里density的值对应的就是资源密度值,即图片文件夹所代表的的密度
opts.inDensity = density;
}
}
//此时inTargetDensity默认也为0
if (opts.inTargetDensity == 0 && res != null) {
//将手机的屏幕密度值赋值给最终图片显示的密度
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
这里调用了native层的decodeStream方法,下面是该方法源码;
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
//非重要代码忽略
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//资源本身的密度
const int density = env->GetIntField(options, gOptions_densityFieldID);
//最终加载的图片的密度
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
//手机的屏幕密度
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
//如果资源密度不为0,手机屏幕密度也不为0, 资源的密度与屏幕密度不相等时,图片缩放比例=屏幕密度/资源密度
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
const bool willScale = scale != 1.0f;//判断是否需要缩放
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);//这里+0.5是保证在图片缩小时,可能会出小数,这里加0.5是为了让除后的数向上取整
scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// 设置解码图片的colorType
SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
//设置图片的宽高
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
if (outputAllocator != &javaAllocator) {
outputBitmap->eraseColor(0);
}
SkPaint paint;
paint.setFilterLevel(SkPaint::kLow_FilterLevel);
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);//根据缩放比画出图像
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);//将图片画到画布上
}
主要注意两个地方:
这是获取缩放比例
scale = (float) targetDensity / density;
这里明确的看到放大系数的算法是 屏幕密度 / 文件夹密度
targetDensity 没有赋值的话,就是屏幕密度,
density 资源文件夹代表的密度
另:如果想更改调节缩放比例,这两个参数必须同时设置才有效
计算缩放后的图片大小
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
图片占用内存不仅和图片本身大小有关系,还和屏幕密度和图片所在的资源文件夹都有关系。
公式实际是:
图片占用内存 = 宽度 * 高度 * (屏幕密度 / 资源文件夹密度)^2 * 单个像素占的字节数
即
MemorySize ≈ (width * scale) * (height * scale) * 每个像素需要的字节数 ≈ width * height * scale ^ 2 * 每个像素需要的字节数
总结:
- 开发过程中只切一套大图放在高分辨率文件夹下(3x或者4x),是可行的,从公式可看出,使用同一个设备时,drawable表示的分辨率越高,则图片占用的内存越小,反之越大。所以,在做图片的兼容性时,如果只想使用一张图片,则应使用3倍甚至4倍的图片(3倍是主流机型,但在4倍手机上会被放大,图片可能失真),这样在低分辨率的手机上,不仅显示清晰,而且系统会自动进行缩放,从而确保占用较小的内存。
- 图片占用内存和设备的分辨率和资源所在的文件夹是有关系的
附
Android手机屏幕标准 | 对应图标尺寸标准 | 屏幕密度 (densityDpi) | scale |
xxxhdpi 3840*2160 | 192*192 | 640 | 4 |
xxhdpi 1920*1080 | 144*144 | 480 | 3 |
xhdpi 1280*720 | 96*96 | 320 | 2 |
hdpi 480*800 | 72*72 | 240 | 1.5 |
mdpi 480*320 | 48*48 | 160 | 1 |
ldpi 320*240 | 36*36 | 120 | 0.75 |