首页 Android 正文
  • 本文约5406字,阅读需27分钟
  • 44
  • 0

Android | 屏幕信息DisplayMetrics与不同DPI设备的资源加载

摘要

DisplayMetrics 在Android开发中,dpi(Dots Per Inch,每英寸的像素数)是一个重要的概念,用于描述屏幕的像素密度。mdpi(Medium Density Pixel Image)是指每英寸有160像素点的屏幕。这个定义基于一个标准,即认为160dpi为基准密度,这意味着在mdpi屏幕上,1dp(设备无关像素)等于1px(像素...

DisplayMetrics

在Android开发中,dpi(Dots Per Inch,每英寸的像素数)是一个重要的概念,用于描述屏幕的像素密度。mdpi(Medium Density Pixel Image)是指每英寸有160像素点的屏幕。这个定义基于一个标准,即认为160dpi为基准密度,这意味着在mdpi屏幕上,1dp(设备无关像素)等于1px(像素)。

DisplayMetrics 是 Android 中用于描述设备显示屏的通用信息,如其大小、密度和缩放因子等。这是 Android 提供的一种帮助开发者适配不同屏幕尺寸和密度的工具。DisplayMetrics 主要包括以下重要字段和方法,用来获取屏幕的分辨率、密度、物理尺寸等信息。

density: 屏幕的逻辑密度,基础密度值为 1.0(mdpi 的屏幕密度),该值是设备独立像素(dp)与实际像素之间的比例。
densityDpi: 每英寸像素点数(dpi),是设备的屏幕密度,常见的屏幕密度有 mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 等。
scaledDensity: 文字缩放密度,用来适配字体的缩放设置。
heightPixels: 可用显示尺寸的高度(以像素为单位)。
widthPixels: 可用显示尺寸的宽度(以像素为单位)。
xdpi 和 ydpi: 屏幕在 X 和 Y 轴方向的物理像素密度。

resources.displayMetrics.run {
   log("density: $density")
   log("densityDpi: $densityDpi")
   log("scaledDensity: $scaledDensity")
   log("widthPixels: $widthPixels")
   log("heightPixels: $heightPixels")
   log("xdpi: $xdpi")
   log("ydpi: $ydpi")
}

手头的两台设备:

型号 density densityDpi scaledDensity heightPixels*widthPixels ydpi*xdpi
小米11青春版 2.75 440 2.75 2400*1080 401.845*401.639
OPPO Reno Ace 3.0 480 3.0 2400*1080 401.052*403.411

density和scaledDensity用途

density 和 scaledDensity通常用于dp、sp与px像素之间的转换,如:

 /**
    * 将dp值转换为px值
    */
   fun dp2px(context: Context, dp: Float): Int {
       val scale = context.resources.displayMetrics.density
       return (dp * scale + 0.5f).toInt()
   }

   fun px2dp(context: Context, px: Float): Int {
       val scale = context.resources.displayMetrics.density
       return (px / scale + 0.5f).toInt()
   }

   /**
    * 将sp值转换为px值
    */
   fun sp2px(context: Context, spValue: Float): Int {
       val fontScale = context.resources.displayMetrics.scaledDensity
       return (spValue * fontScale + 0.5f).toInt()
   }

   /**
    * 将px值转换为sp值
    */
   fun px2sp(context: Context, pxValue: Float): Int {
       val fontScale = context.resources.displayMetrics.scaledDensity
       return (pxValue / fontScale + 0.5f).toInt()
   }

density和scaledDensity有何不同?

假设设备的屏幕密度为 xxhdpi(480 dpi),在这种情况下:density = 3.0 (这个值与设备的屏幕密度有关),默认情况下,scaledDensity = density = 3.0 (当用户没有调整字体大小时)。但是,当用户调整了字体大小为更大的级别时,比如设置字体大小为 1.5倍,则:density 仍然是 3.0,scaledDensity 变成 3.0 * 1.5 = 4.5

所以,density 用于 dp 转 px,只与屏幕密度相关;而scaledDensity 用于 sp 转 px,不仅与屏幕密度相关,还与用户设置的字体缩放相关,字体大小调整后,两者的取值可能会不同

Drawable目录不同分辨率下的资源加载

不同分辨率文件夹中的图片,如 drawable-xxhdpidrawable-hdpidrawable-xhdpi 等,会影响解析出来的图片大小。Android 会根据设备的屏幕密度自动从不同的 drawable 文件夹中选择合适的资源文件。由于图片在不同分辨率文件夹中会有不同的尺寸,同一张图片在不同设备上解析的内存占用大小可能会不同。

Android 会根据设备的 屏幕密度(dpi),从相应的 drawable 文件夹中加载最适合当前设备的资源文件。常见的屏幕密度有:

ldpi: 低分辨率 (~120 dpi)
mdpi: 中分辨率 (~160 dpi),基准
hdpi: 高分辨率 (~240 dpi)
xhdpi: 超高分辨率 (~320 dpi)
xxhdpi: 超超高分辨率 (~480 dpi)
xxxhdpi: 超超超高分辨率 (~640 dpi)
对于同一个资源(例如 icon_launcher),它在 drawable-xxhdpi 文件夹中可能是 144x144 的图像,而在 drawable-mdpi 中可能只有 48x48。解析时加载的图片大小不同,内存占用也不同。

示例代码

val options = BitmapFactory.Options().apply {
     inJustDecodeBounds = true //只解析图片元信息,不加载到内存
}
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
//图片占用内存大小 (bytes) = 图片宽度 (pixels) × 图片高度 (pixels) × 每个像素的字节数
val memorySize = options.outWidth * options.outHeight * getBytesPerPixel(options.inPreferredConfig)
log("图片宽度: ${options.outWidth}, 高度: ${options.outHeight}")
log("图片加载到内存时占用大小: ${memorySize / 1024} KB")

// 根据 Bitmap.Config 获取每个像素的字节数
private fun getBytesPerPixel(config: Bitmap.Config): Int {
   return when (config) {
      Bitmap.Config.ARGB_8888 -> 4 // 4字节(32位)
      Bitmap.Config.RGB_565 -> 2  // 2字节(16位)
      Bitmap.Config.ARGB_4444 -> 2 // 2字节(已被废弃)
      Bitmap.Config.ALPHA_8 -> 1  // 1字节(8位,仅有透明度)
      else -> 4 // 默认情况,按 ARGB_8888 计算
   }
}

上述代码中,会通过 resources.displayMetrics.densityDpi 获取设备的屏幕密度。而密度的不同,会从不同的 drawable 文件夹中加载不同分辨率的图片,最终解析出的图片尺寸不同,导致内存占用不同。

假设设备是 xxhdpi(~480 dpi)的屏幕:

设备的屏幕密度: 480 dpi
图片宽度: 144, 高度: 144
图片加载到内存时占用大小: (144 x 144 x 4)/ 1024 = 81 KB

假设设备是 mdpi(~160 dpi)的屏幕:

设备的屏幕密度: 160 dpi
图片宽度: 48, 高度: 48
图片加载到内存时占用大小: (48 x 48 x 4)/ 1024 = 9 KB

上面的结论在手机是标准屏幕密度时是没问题的。让我们来换个设备,用上面提到的小米11手机再试试,该手机的屏幕密度densityDpi是440,并不是标准的480dpi,但是默认加载的依然是xxhdpi中的 144x144 大小的图片:
Android | 屏幕信息DisplayMetrics与不同DPI设备的资源加载
加载本地图片如果按照上述公式计算依然是81KB,但是这个数据对吗?通过最终加载出来的bitmap来验证下:

val options = BitmapFactory.Options().apply {
     inJustDecodeBounds = false //注意这里必须是false,否则decodeResource不会返回bitmap
}
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
log("图片大小 -> width:${bitmap.width}, height:${bitmap.height}, allocationByteCount:${bitmap.allocationByteCount}=${bitmap.allocationByteCount / 1024} KB")

执行结果:

图片大小 -> width:132, height:132, allocationByteCount:69696 = 68 KB

可以看到生成的bitmap,无论是宽高,还是内存大小都不是我们计算出来的结果,问题出在哪里呢?只能通过BitmapFactory.decodeResource源码方法来找答案了:

public static Bitmap decodeResource(Resources res, int id, Options opts) {
       validate(opts);
       Bitmap bm = null;
       InputStream is = null;

       try {
           final TypedValue value = new TypedValue();
           is = res.openRawResource(id, value);
           //这里
           bm = decodeResourceStream(res, value, is, null, opts);
       } catch (Exception e) {
       }
       //......
       return bm;
   }

 public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
           @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
       //这个方法设置了opts.inDensity和opts.inTargetDensity
       if (opts.inDensity == 0 && value != null) {
           final int density = value.density;
           if (density == TypedValue.DENSITY_DEFAULT) {
               opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
           } else if (density != TypedValue.DENSITY_NONE) {
               opts.inDensity = density;
           }
       }
       if (opts.inTargetDensity == 0 && res != null) {
           opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
       }  
       return decodeStream(is, pad, opts);
   }

上述流程中设置了opts.inDensity和opts.inTargetDensity ,然后通过decodeStream调用到Native层。

BitmapFactory.Options中:inDensity 是指图片资源的像素密度。inTargetDensity 是指当前设备的目标像素密度。如果两者不同,系统会根据这些密度信息缩放图片以适应屏幕分辨率。

直接看Native层的实现:
Android | 屏幕信息DisplayMetrics与不同DPI设备的资源加载
Native层实现

可以看到这里还有个scale系数,scale = (float) targetDensity / density ,生成bitmap时的宽高都会经过scale得到最终的宽高,所以最终公式如下:

图片所占内存:
= (width x scale) x (height x scale) x 每个像素所占字节数
= (width x targetDensity / density) x (height x targetDensity / density) x 每个像素所占字节数

对于小米11这款手机来说,targetDensity是440,density是480,代入公式计算一下:

宽(width) = 高(height) = 144 x 440/480 = 132
所占内存 = 144 x 440/480 x144 x 440/480 x 4 / 1024 = 68KB

嗯 ,跟bitmap.allocationByteCount返回的大小一样了,所以加载本地不同分辨率下的图片资源所占内存时还需要注意scale系数(targetDensity / density)。


扫描二维码,在手机上阅读
    评论