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?
实际上们计算计算的dpi并不是公式里面的dpi,dpi只有120,160,240,320,480,640几种,可以通过系统api获取getResources().getDisplayMetrics().densityDpi,我们获取的dpi实际上只要处于任一个系统dpi范围内即可,比如计算得到的367和424都属于320~480dpi范围内,属于scale = 3x,所以计算公式实际是32 *(480/160) = 32*3 = 96px。两个手机的大小实际上是一致的。
理论上的内存大小:
图片占用内存 = 宽度像素 * 高度像素 * 单个像素占的字节数
/**
* @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 * 每个像素需要的字节数
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 |