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

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 轴方向的物理像素密度。
kotlin 复制代码
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像素之间的转换,如:

kotlin 复制代码
    /**
     * 将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。解析时加载的图片大小不同,内存占用也不同。

示例代码

kotlin 复制代码
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)的屏幕:

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

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

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

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

kotlin 复制代码
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")

执行结果:

arduino 复制代码
图片大小 -> width:132, height:132, allocationByteCount:69696 = 68 KB

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

less 复制代码
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层的实现: 可以看到这里还有个scale系数,scale = (float) targetDensity / density,生成bitmap时的宽高都会经过scale得到最终的宽高,所以最终公式如下:

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

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

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

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

相关推荐
Android 小码峰啊1 小时前
Android Dagger 2 框架的注解模块深入剖析 (一)
android·adb·android studio·android-studio·androidx·android runtime
Android 小码峰啊1 小时前
Android Fresco 框架缓存模块源码深度剖析(二)
android
大胃粥3 小时前
Android V app 冷启动(8) 动画结束
android
ufo00l4 小时前
Kotlin在Android中有哪些重要的应用和知识点是需要学习或者重点关注的
android
AJi4 小时前
Android音视频框架探索(二):Binder——系统服务的通信基础
android·ffmpeg·音视频开发
tjsoft4 小时前
Nginx配置伪静态,URL重写
android·运维·nginx
努力学习的小廉4 小时前
【C++11(中)】—— 我与C++的不解之缘(三十一)
android·java·c++
tangweiguo030519875 小时前
打破界限:Android XML与Jetpack Compose深度互操作指南
android·kotlin·compose
Watink Cpper6 小时前
[MySQL初阶]MySQL(8)索引机制:下
android·数据库·b树·mysql·b+树·myisam·innodedb
一起搞IT吧6 小时前
高通camx IOVA内存不足,导致10-15x持续拍照后,点击拍照键定屏无反应,过一会相机闪退
android·数码相机