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)。

相关推荐
lw向北.7 分钟前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao15 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee43 分钟前
PHP伪协议总结
android·开发语言·php
mmsx7 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人10 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌11 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley12 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei14 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng14 小时前
安卓多渠道apk配置不同签名
android
枫_feng15 小时前
AOSP开发环境配置
android·安卓