被误用的屏幕分辨率限定符

前言

最近要给项目换几个图片资源外加调整一下UI尺寸细节,发现项目里面使用了有很多类似这种限定屏幕分辨率的配置限定符:


被误用的屏幕分辨率限定符_第1张图片
屏幕分辨率配置限定符

乍一看以为是针对特定的屏幕弄的一些特殊资源,似乎用法是当且仅当屏幕像素点数量和限定符一致时就能匹配到,所以里面会放一些针对该分辨率的特殊资源。然而经过一番测试,实际情况并不是我想象的那样,甚至让我产生了更多疑问。

  1. AxB的写法,到底A和B,哪个代表宽度,哪个代表高度?
  2. A和B的单位是dp还是px?
  3. 为什么某些情况下匹配不到我指定的屏幕分辨率目录下的资源?

目前已公开的情报

文档

遇到这种问题,第一个当然是要查关于配置限定符的官方文档,然而文档里并没有提到AxB这种写法的配置限定符,官方文档里列出了19个配置限定符(截至API 27),按照顺序,它们分别是:MCC和MNC,语言和区域,布局方向,最小dp宽度,可用dp宽度,可用dp高度,屏幕尺寸(值为normal, xlarge等),屏幕纵横比,是否圆形屏幕,UI模式,是否夜间模式,屏幕像素密度(值为hdpi, xhdpi等),触摸屏类型,键盘可用性,主要文本输入法(感觉叫硬件键盘类型更准确),导航键可用性,主要非触摸导航方法,平台版本。

完全没有提到屏幕分辨率限定符这种写法,是否代表这种写法完全不存在呢?

Android Studio

在AS里新建Android资源目录的时候,IDE会提示我们补充配置限定符,奇怪的是里面一共有20种配置限定符可选,多了一个叫Dimension的配置限定符:

Dimension配置限定符

随便填两个值,发现写法和我在项目里看到的是一样的,都是AxB的形式:

被误用的屏幕分辨率限定符_第2张图片
Dimension配置限定符举例

这说明使用屏幕分辨率作为限定符是一个合法的写法,不然IDE也不会大费周章的把它做进去了。但这里有两点让我很疑惑:

  1. IDE并没有告诉我是长在前还是宽在前,values-1920x1080values-1080x1920是一样的吗?
  2. 为什么Dimension限定符的描述是Screen dimension in dp?如果单位是dp,那项目里面所有的相关配置不都写错了吗?

网络文章

网上有很多提到这种写法的文章,似乎从来没有对AxB这种形式的限定符有提出任何疑问,只知道肯定是有效的。

从这些文章的意思看来,这种屏幕分辨率限定符,可以限制Android只在遇到屏幕分辨率完全匹配的时候才取得相应的特殊资源,比如2560x1536的手机肯定不会使用1920x1080限定的资源。

源码里找答案

没办法,只能去翻源码找答案了,这是效率最低的一种方式,毕竟很多逻辑描述起来简单,实现起来代码不知道要写多复杂。在看源码之前,还是先了解一下Android是如何查找最佳匹配资源的。

查找最佳匹配资源

Android查找最佳匹配资源的过程如下


被误用的屏幕分辨率限定符_第3张图片
Android查找最佳匹配资源流程

Android反复执行这个图中的2~4步骤直到只剩下唯一一个资源目录

这个图描述的逻辑很清晰,不过还是要指出其中的关键点:

  1. 步骤1事先排除了不匹配的配置限定符,比如对于可用高度来说,如果系统的可用高度是400dp,而某个限定符组合限定可用高度为500dp,那么显然这个限定符肯定得不到满足,直接就被排除在外了。完全不会参与资源查找,除非系统的配置出现了变化,使得该限定被满足了。
  2. 步骤2中识别限定符,是按顺序来的。不是说限定符越多,优先级越高,比如一台语言为英语的手机上,drawable-en中的资源比drawable-port-notouch-12key中的资源优先级高。因为当识别到en的时候,drawable-port-notouch-12key会被步骤4排除掉。

知道了Android的查找资源策略,我需要从源码中找到以下问题的答案:

  1. 分辨率限定符AxB和BxA的写法有什么区别
  2. 分辨率限定符中尺寸的单位到底是dp还是px
  3. 排除与系统配置不匹配的分辨率限定符的策略是什么(即步骤1如何排除不合法的分辨率限定资源)
  4. 分辨率限定符处于限定符的处理顺序中的什么位置(即步骤2识别限定符时,何时识别分辨率限定符)
  5. 分辨率限定符的匹配策略是怎样的,是否精确匹配(即步骤3如何匹配最佳分辨率限定符资源)

AxB与BxA的区别,以及dp还是px

要回答这个问题,必须知道Android如何解析分辨率限定符,以及系统自身的配置如何存储。

Android如何解析分辨率限定符

frameworks/base/tools/aapt/AaptConfig.cpp

bool parseScreenSize(const char* name, ResTable_config* out) {
    if (strcmp(name, kWildcardName) == 0) {
        if (out) {
            out->screenWidth = out->SCREENWIDTH_ANY;
            out->screenHeight = out->SCREENHEIGHT_ANY;
        }
        return true;
    }

    const char* x = name;
    while (*x >= '0' && *x <= '9') x++;
    if (x == name || *x != 'x') return false;
    String8 xName(name, x-name);
    x++;

    const char* y = x;
    while (*y >= '0' && *y <= '9') y++;
    if (y == name || *y != 0) return false;
    String8 yName(x, y-x);

    uint16_t w = (uint16_t)atoi(xName.string());
    uint16_t h = (uint16_t)atoi(yName.string());
    if (w < h) {
        return false;
    }

    if (out) {
        out->screenWidth = w;
        out->screenHeight = h;
    }

    return true;
}

可以看到,Android在解析分辨率限定符时,默认第一个数字大于等于第二个数字,否则解析就失败了。Android将解析到的第一个数字保存为宽,将第二个数字保存为高。也就是说,1920x10801080x1920,只有1920x1080是合法的。

系统自身的配置

/frameworks/base/core/java/android/content/res/ResourcesImpl.java

public void updateConfiguration(Configuration config, DisplayMetrics metrics,
                                CompatibilityInfo compat) {
            ...

            final int width, height;
            if (mMetrics.widthPixels >= mMetrics.heightPixels) {
                width = mMetrics.widthPixels;
                height = mMetrics.heightPixels;
            } else {
                //noinspection SuspiciousNameCombination
                width = mMetrics.heightPixels;
                //noinspection SuspiciousNameCombination
                height = mMetrics.widthPixels;
            }

            ...

            mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
                    adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
                    mConfiguration.orientation,
                    mConfiguration.touchscreen,
                    mConfiguration.densityDpi, mConfiguration.keyboard,
                    keyboardHidden, mConfiguration.navigation, width, height,
                    mConfiguration.smallestScreenWidthDp,
                    mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
                    mConfiguration.screenLayout, mConfiguration.uiMode,
                    mConfiguration.colorMode, Build.VERSION.RESOURCES_SDK_INT);

            ...
}

这里发现系统配置里面,屏幕分辨率宽高使用heightPixels和widthPixels,而且还故意将值大的设为width,因此系统配置里,分辨率配置,也是宽大于高,同时这里也说明分辨率限定符的单位是px而不是dp,因为系统配置里这俩值直接从heightPixels和widthPixels里取得并保存。

结论

分辨率限定符,单位是px。

必须将长边写在前,短边写在后,反之则非法。

排除策略与是否精确匹配

/frameworks/base/libs/androidfw/ResourceTypes.cpp

bool ResTable_config::match(const ResTable_config& settings) const {
    ...
    if (screenSize != 0) {
        if (screenWidth != 0 && screenWidth > settings.screenWidth) {
            return false;
        }
        if (screenHeight != 0 && screenHeight > settings.screenHeight) {
            return false;
        }
    }
    ...
    return true;
}

该方法返回false表示需要在第一个步骤中予以排除,settings中存储的是系统配置,可以看出,当分辨率限定符中的任一维度大于系统配置的对应维度时,会被排除。这同时说明分辨率限定符不是精确匹配的。

比如我们提供了2020x1080、1080x740以及960x540限定的资源,一台1920x1080的手机,会排除掉2020x1080的资源,匹配1080x740或960x540中的一个资源。

结论

分辨率限定符的排除策略是任一维度大于系统对应维度则排除。

分辨率限定符的匹配不是精确地,尺寸小于等于系统配置时即可能匹配到。

识别顺序

/frameworks/base/libs/androidfw/ResourceTypes.cpp中的ResTable_config::isBetterThan方法中的代码顺序即识别顺序。这里的识别顺序和文档里介绍的限定符识别顺序是一致的,屏幕分辨率限定符在其中处于倒数第二个,排在平台版本之前。

结论

分辨率限定符的识别顺序处于倒数第二,排在平台版本之前。

匹配策略

/frameworks/base/libs/androidfw/ResourceTypes.cpp

bool ResTable_config::isBetterThan(const ResTable_config& o,
        const ResTable_config* requested) const {
    if (requested) {
        ...

        if (screenSize || o.screenSize) {
            // "Better" is based on the sum of the difference between both
            // width and height from the requested dimensions.  We are
            // assuming the invalid configs (with smaller sizes) have
            // already been filtered.  Note that if a particular dimension
            // is unspecified, we will end up with a large value (the
            // difference between 0 and the requested dimension), which is
            // good since we will prefer a config that has specified a
            // size value.
            int myDelta = 0, otherDelta = 0;
            if (requested->screenWidth) {
                myDelta += requested->screenWidth - screenWidth;
                otherDelta += requested->screenWidth - o.screenWidth;
            }
            if (requested->screenHeight) {
                myDelta += requested->screenHeight - screenHeight;
                otherDelta += requested->screenHeight - o.screenHeight;
            }
            if (myDelta != otherDelta) {
                return myDelta < otherDelta;
            }
        }

        ...

        return false;
    }
    return isMoreSpecificThan(o);
}

这里的匹配策略是两个维度的差值之和越小,则优先级越高,也就是说越接近屏幕的尺寸的限定资源,优先级越高,当然这里进行对比的时候,已经将大于屏幕尺寸的资源排除掉了。

结论

屏幕分辨率限定符的匹配策略是选取最接近屏幕分辨率的资源。

讨论

经过上面的分析,可以得知屏幕分辨率限定符,非精确匹配,且要求两个维度均小于等于屏幕的相应维度的时候才能被匹配,匹配策略选取最接近屏幕分辨率的资源,识别顺序排倒数第二,单位是px,长边值写在前面。

从这个限定符的用法来看,它的名字应该叫最小可用分辨率限定符,才能准确表达它的含义。

这里要注意一点,一台en-1920x1080的手机,如果我们提供了一个图片的如下两个版本:
drawable-en
drawable-1920x1080
最终匹配到的是drawable-en版本,因为分辨率限定符的识别顺序实在太靠后,导致直接在识别en时被排除了。

测试最好用原生ROM。我的小米4上,即便是分辨率限定符反过来写,如1080x1920,MIUI也能正常识别,一度让我以为我代码看错了,然后找了台Nexus 5X运行,则和源码一致,认定其不合法,不予匹配。

从结论来看,这个屏幕分辨率限定符并不是很多人想象中那样工作的,不是只适配特定分辨率,而是会影响到所有完全大于该分辨率的屏幕,用了得不偿失,建议大家不要用这个限定符。

你可能感兴趣的:(被误用的屏幕分辨率限定符)