Android - 修改屏幕的density豆村,竟然會導(dǎo)致獲取的dimension是錯誤的液兽?

??在很久之前,業(yè)界推出了一種屏幕適配的方案--動態(tài)修改屏幕的density掌动。這個(gè)方案四啰,當(dāng)時(shí)轟動一時(shí),各種分析文章層出不窮粗恢,大家似乎找到了屏幕適配的仙丹靈藥柑晒。

??時(shí)間過去了這么久,不可否認(rèn)眷射,此方案確實(shí)非常的優(yōu)秀匙赞,它能幫助我們快速且準(zhǔn)確的還原視覺。正如此妖碉,在我們的項(xiàng)目中涌庭,也是使用了此方案來進(jìn)行屏幕適配。

??但是欧宜,最近我們陸續(xù)收到一些bug坐榆,正如標(biāo)題所言,在我們代碼中鱼鸠,正常的獲取dimension猛拴,得到的值卻是意料之外的,進(jìn)而導(dǎo)致我們頁面上蚀狰,很多UI顯示不正確愉昆。因此,我們對此問題進(jìn)行了深入分析麻蹋,找到了其中原因跛溉,給出了解決方案。

1. 背景

??在幾個(gè)月前扮授,我在負(fù)責(zé)中大屏(包括平板芳室,折疊屏)的適配工作。在很多頁面里面刹勃,中大屏相比于普通手機(jī)堪侯,UI的差別可能在于尺寸的不同±笕剩基于此伍宦,我就采用了如下的方案:

同一個(gè)dimension芽死,在不同的values文件定義不同的值。

例如:有一個(gè)button_width次洼,在values文件下定義為10dp关贵,在values-w500dp文件下就定義為30dp。 如此卖毁,同一行代碼揖曾,如下:

resources.getdimensionionPixelOffset(R.dimen.button_width)

在寬度小于500dp的手機(jī)上,獲取的就是10dp; 反之亥啦,獲取的就是30dp炭剪。

??如上方案,在一定程度上能夠簡化中大屏適配的工作禁悠。因?yàn)闊o論是什么屏幕尺寸念祭,對應(yīng)的代碼都是一樣的,唯一不同的就是values里面定義的值不同的碍侦。

??適配工作按照上面的方案粱坤,快樂的進(jìn)行中,最后也是準(zhǔn)時(shí)上線瓷产,似乎一切都萬事大吉站玄?

??事實(shí)證明,我還是想的過于天真濒旦。直到有一天株旷,有同事反饋說:dimension獲取的不對,我的設(shè)備屏幕寬度沒有達(dá)到xxdp尔邓,但是還是獲取了values-wxxdp文件下的值晾剖。

??我當(dāng)時(shí)第一反應(yīng)是:

??我看到這個(gè)問題,難以置信梯嗽,因?yàn)檫@個(gè)方案的可行性是不容置疑的齿尽,網(wǎng)上有很多使用的例子,包括Google也要求這樣使用的灯节。但是固定的復(fù)現(xiàn)路徑直接貼我臉上循头,讓我不得不懷疑自身。

??于是炎疆,我自己先搞了一個(gè)Demo卡骂,試了下,沒有問題靶稳搿全跨?后來,同事告訴我亿遂,咱們的項(xiàng)目會修改屏幕的density,于是我把修改density的代碼CV到Demo工程中浓若,一運(yùn)行盒使,哦吼!完美復(fù)現(xiàn)七嫌。

??總結(jié)下,我們這個(gè)問題的復(fù)現(xiàn)場景:

  1. Activity是橫屏的苞慢,screenOrientation聲明為sensorLandscape, 保證能夠隨傳感器變換方向诵原。
  2. 在Activity的onCreate方法里面,會動態(tài)修改屏幕的density挽放。
  3. 此時(shí)再去通過resource獲取dimension绍赛,屏幕經(jīng)過修改density之后,寬度沒有達(dá)到xxdp,但是還是取了values-wxxdp文件下的dimension值辑畦。

??復(fù)現(xiàn)代碼可參考:DensityDemo

2. 解決方案

??為了不浪費(fèi)各位的寶貴時(shí)間吗蚌,我先貼出解決方案,然后分析此問題的原因纯出。

??解決方案非常簡單蚯妇,如下是我們修改density的代碼:

fun Resources.setDensity() {
    val metrics = displayMetrics ?: return

    val width = metrics.widthPixels
    val height = metrics.heightPixels + getNavigationBarHeight()
    //獲取以設(shè)計(jì)圖總寬度360dp下的density值
    val targetDensity =
        (min(width, height) / 360f).toInt()
    //獲取以設(shè)計(jì)圖總寬度360dp下的dpi值
    val targetDensityDpi = (160f * targetDensity).toInt()

    // 更新displayMetrics的信息
    metrics.density = targetDensity.toFloat()
    metrics.scaledDensity = targetDensity.toFloat()
    metrics.densityDpi = targetDensityDpi

    // 更新configuration的信息
    val configuration = configuration ?: return
    configuration.screenWidthDp = (width / targetDensity)
    configuration.screenHeightDp = (height / targetDensity)
    configuration.densityDpi = targetDensityDpi

    // 修復(fù)dimen獲取不對的問題,7.0及其以下調(diào)用此方法暂筝。
//    updateConfiguration(configuration, metrics)
}

??代碼中箩言,最為關(guān)鍵的兩步分別是:

  1. 更新configuration的信息,將configurationscreenWidthDp焕襟、screenHeightDpdensityDpi更新為預(yù)期值
  2. 將更新完的configurationmetrics陨收,傳遞給ResourceupdateConfiguration方法,去刷新內(nèi)部的信息鸵赖。這一步非常重要务漩,也是解決此問題的關(guān)鍵所在。

??這里還需要關(guān)注的一點(diǎn)就是它褪,updateConfiguration方法在Android 7.0 以上饵骨,被Google標(biāo)記為過時(shí)。所以我們還需要去找代替的方法列赎。代替的代碼大致如下:

override fun getResources(): Resources {
    val resources = super.getResources()
    resources.setDensity()
    return createConfigurationContext(resources.configuration).resources
}

??在通過上面的setDensity方法宏悦,更新完對應(yīng)的信息之后,我們需要重寫Activity的getResource方法包吝,通過createConfigurationContext方法去更新Resource里面的configuration饼煞。

??總結(jié)如下:

  1. 在Android 7.0 及其以下,我們可以通過updateConfiguration方法去更新Resource內(nèi)部的信息诗越,進(jìn)而解決dimension不對的問題砖瞧。
  2. 在Android 7.0以上版本,需要通過createConfigurationContext去更新Resource內(nèi)部的信息嚷狞,同時(shí)setDensity方法也是需要調(diào)用的块促,進(jìn)而解決dimension不對的問題荣堰。

??額外說一句,其實(shí)在高版本上調(diào)用updateConfiguration也是可行的竭翠,就是需要看下兼容性問題振坚;完全沒必要重寫getResources方法,畢竟createConfigurationContext方法成本也挺大的斋扰。

3. 問題分析

??在分析源碼渡八,我們先對這個(gè)問題進(jìn)行一些分析。項(xiàng)目中聲明多個(gè)values-wxxdp文件传货,在獲取值的時(shí)候屎鳍,系統(tǒng)會根據(jù)當(dāng)前的屏幕寬度,自動匹配符合要求的values文件下问裕,然后再從對應(yīng)的values文件下獲取定義好的值逮壁。

??現(xiàn)在出現(xiàn)的問題,就是匹配錯了values文件夾粮宛。為啥會匹配錯呢窥淆?是不是屏幕寬度不對呢?所以巍杈,當(dāng)時(shí)排查方向就是祖乳,反復(fù)校驗(yàn)屏幕的寬度是否正確。

??屏幕的絕對像素值是不會改變的秉氧,但是屏幕寬度的dp值會跟隨修改的density而變換眷昆。所以,我們當(dāng)時(shí)第一排查方向是汁咏,是不是哪里沒有改到亚斋,導(dǎo)致系統(tǒng)自己在獲取屏幕寬度有問題?

??獲取dimension基本都是通過Resource的getdimensionionXXX方法攘滩。于是帅刊,我們從這個(gè)方法開始入手,查看方法內(nèi)部的代碼實(shí)現(xiàn)漂问,但是從Java層并沒有發(fā)現(xiàn)可疑的點(diǎn)赖瞒。

??getdimensionionXXX方法,最終會調(diào)用到AssetManagergetResourceValue方法蚤假。

boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    Objects.requireNonNull(outValue, "outValue");
    synchronized (this) {
        ensureValidLocked();
        final int cookie = nativeGetResourceValue(
                mObject, resId, (short) densityDpi, outValue, resolveRefs);
        if (cookie <= 0) {
            return false;
        }

        // Convert the changing configurations flags populated by native code.
        outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                outValue.changingConfigurations);

        if (outValue.type == TypedValue.TYPE_STRING) {
            if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
                return false;
            }
        }
        return true;
    }
}

??getResourceValue方法有一個(gè)TypedValue參數(shù)栏饮,最終獲取的dimension值這個(gè)參數(shù)的data字段。data字段雖然是一個(gè)int類型磷仰,但是它自身是一個(gè)復(fù)合數(shù)據(jù)袍嬉,內(nèi)部只有部分bit位才表示最終的dimension值。

??經(jīng)過上面的分析,我們并沒有找到匹配屏幕寬度的地方伺通,且TypedValue內(nèi)部字段的賦值操作箍土,也是通過nativeGetResourceValue這個(gè)native方法進(jìn)行的。到這里罐监,看來必須深入到C++層吴藻,去查看對應(yīng)的實(shí)現(xiàn)。

??友情提示弓柱,下面將進(jìn)入枯燥繁雜的C++代碼分析環(huán)節(jié)调缨。如下C++代碼均參考于Android 14.0版本的aosp。

??AssetManager.java對應(yīng)的C++文件是android_util_AssetManager.cpp吆你。這個(gè)文件內(nèi)部的nativeGetResourceValue方法實(shí)現(xiàn)如下:

static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
                                   jshort density, jobject typed_value,
                                   jboolean resolve_references) {
  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
  ResourceTimer _timer(ResourceTimer::Counter::GetResourceValue);
  // 1. 獲取value。
  auto value = assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
                                         static_cast<uint16_t>(density));
  if (!value.has_value()) {
    return ApkAssetsCookieToJavaCookie(kInvalidCookie);
  }

  if (resolve_references) {
    auto result = assetmanager->ResolveReference(value.value());
    if (!result.has_value()) {
      return ApkAssetsCookieToJavaCookie(kInvalidCookie);
    }
  }
  // 2. 將value里面的值賦值給typed_value
  return CopyValue(env, *value, typed_value);
}

??這個(gè)方法里面俊犯,我們重點(diǎn)關(guān)注兩個(gè)點(diǎn)妇多。

  1. 通過assetmanagerGetResource方法,獲取了一個(gè)value值燕侠。這個(gè)value字段里面就包含我們想要的dimension者祖。
  2. 將value字段里面的值賦值給typed_value。這個(gè)typed_value里面绢彤,就是在Java層看到TypedValue七问。

??所以,我們重點(diǎn)就要放到GetResource方法里面去茫舶。這個(gè)assetmanager是一個(gè)AssetManager2的對象械巡,對應(yīng)代碼文件是AssetManager2.cpp。我們來看下GetResource方法的實(shí)現(xiàn):

base::expected<AssetManager2::SelectedValue, NullOrIOError> AssetManager2::GetResource(
    uint32_t resid, bool may_be_bag, uint16_t density_override) const {
  // 1. 找到對應(yīng)的結(jié)果饶氏。
  auto result = FindEntry(resid, density_override, false /* stop_at_first_match */,
                          false /* ignore_configuration */);
  if (!result.has_value()) {
    return base::unexpected(result.error());
  }

  auto result_map_entry = std::get_if<incfs::verified_map_ptr<ResTable_map_entry>>(&result->entry);
  if (result_map_entry != nullptr) {
    if (!may_be_bag) {
      LOG(ERROR) << base::StringPrintf("Resource %08x is a complex map type.", resid);
      return base::unexpected(std::nullopt);
    }

    // Create a reference since we can't represent this complex type as a Res_value.
    return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->type_flags,
                         resid, result->config);
  }

  // Convert the package ID to the runtime assigned package ID.
  // 2. 從result里面獲取entry字段
  Res_value value = std::get<Res_value>(result->entry);
  result->dynamic_ref_table->lookupResourceValue(&value);

  // 3. 取entry中的data字段讥耗,為dimension的結(jié)果
  return SelectedValue(value.dataType, value.data, result->cookie, result->type_flags,
                       resid, result->config);
}

??通過上述代碼,我們知道了疹启,dimension的值最終取自于result->entry.data字段古程。看來喊崖,關(guān)鍵在于result字段挣磨,也就是要去看FindEntry方法FindEntry方法的代碼較長荤懂,我僅截取重要部分進(jìn)行分析茁裙,代碼如下:

base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
    uint32_t resid, uint16_t density_override, bool stop_at_first_match,
    bool ignore_configuration) const {
  // ······省略部分代碼········
  // Might use this if density_override != 0.
  ResTable_config density_override_config;
  // Select our configuration or generate a density override configuration.
  // 1. 記錄AssetManager2自帶的賦值給desired_config,用于下面的匹配工作节仿。
  const ResTable_config* desired_config = &configuration_;
  if (density_override != 0 && density_override != configuration_.density) {
    density_override_config = configuration_;
    density_override_config.density = density_override;
    desired_config = &density_override_config;
  }
  // ······省略部分代碼········

  if (!stop_at_first_match && !ignore_configuration && !apk_assets_[result->cookie]->IsLoader()) {
    for (const auto& id_map : package_group.overlays_) {
      auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
      if (!overlay_entry) {
        // No id map entry exists for this target resource.
        continue;
      }
      if (overlay_entry.IsInlineValue()) {
        // The target resource is overlaid by an inline value not represented by a resource.
        ConfigDescription best_frro_config;
        Res_value best_frro_value;
        bool frro_found = false;
        for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
           // 2.尋找最優(yōu)匹配的values呜达。注意,這里match方法傳入的desired_config是屏幕的粟耻,不是資源的查近。
          if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
              && config.match(*desired_config)) {
            frro_found = true;
            best_frro_config = config;
            best_frro_value = value;
          }
        }
        if (!frro_found) {
          continue;
        }
        // 3. 找到最優(yōu)結(jié)果之后眉踱,賦值給result->entry。最后返回霜威。
        result->entry = best_frro_value;
        result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
        result->cookie = id_map.cookie;
        // ······省略部分代碼········
      }
      // ······省略部分代碼········
    }
  }
  // ······省略部分代碼········

  return result;
}

??這個(gè)方法重點(diǎn)代碼谈喳,我將其分為3個(gè)部分。

  1. configuration_賦值給desired_config,用于下述的匹配工作戈泼。這個(gè)configuration_非常重要婿禽,它是我們解決問題的關(guān)鍵所在,所以這里單獨(dú)拎出來大猛。
  2. 去尋找最優(yōu)匹配的value扭倾。這里的isBetterThan方法,目的是為了尋找最優(yōu)的values挽绩。怎么理解這個(gè)最優(yōu)呢膛壹?比如說,當(dāng)前定義了兩個(gè)values文件夾:values-w100dp,values-w200dp唉堪, 此時(shí)屏幕寬度是600dp模聋。按照規(guī)則,其實(shí)兩個(gè)values都符合要求唠亚,此時(shí)就要選擇最優(yōu)的values链方,即差值最小的,也就是最終會取values-w200dp內(nèi)部的值灶搜; 這里還有一個(gè)match方法祟蚀,就是去校驗(yàn)values的config跟屏幕的config(即AssetManager2的config)是否匹配。
  3. 最后一步割卖,就是將找到的結(jié)果暂题,賦值給result->entry,然后返回究珊。

??通過上面的分析薪者,我們基本了解,要找的答案剿涮,就在match方法里面言津。isBetterThan方法是尋找最優(yōu)的結(jié)果,內(nèi)部是通過差值來計(jì)算最優(yōu)值取试,這里就不過多介紹悬槽。來看下match方法的實(shí)現(xiàn),代碼實(shí)現(xiàn)在ResourceTypes.cpp文件中瞬浓,如下:

bool ResTable_config::match(const ResTable_config& settings) const {
    //····省略代碼·······
    if (screenSizeDp != 0) {
        // 這里的screenWidthDp就是values文件后面聲明的后綴初婆,類似:w500dp。
        // settings.screenWidthDp表示系統(tǒng)自身的屏幕寬度,這個(gè)是從AssetManager.cpp自帶的磅叛。
        if (screenWidthDp != 0 && screenWidthDp > settings.screenWidthDp) {
            if (kDebugTableSuperNoisy) {
                ALOGI("Filtering out width %d in requested %d", screenWidthDp,
                        settings.screenWidthDp);
            }
            return false;
        }
        if (screenHeightDp != 0 && screenHeightDp > settings.screenHeightDp) {
            if (kDebugTableSuperNoisy) {
                ALOGI("Filtering out height %d in requested %d", screenHeightDp,
                        settings.screenHeightDp);
            }
            return false;
        }
    }
    //····省略代碼·······
    return true;
}

??在上面的方法中屑咳,我們終于看到values的聲明的寬度跟屏幕寬度的比較代碼了。那為什么這個(gè)判斷就錯了呢弊琴?

??可以這么來理解兆龙,screenWidthDp != 0 && screenWidthDp > settings.screenWidthDp本來這個(gè)要判斷為true的,最終match方法也是要返回為false敲董。但是由于這個(gè)判斷失效了紫皇,導(dǎo)致最終return true,取錯了dimension腋寨。

??那么聪铺,什么情況下,如上的判斷會失效呢萄窜?首先铃剔,screenWidthDp是values文件靜態(tài)聲明的,所以不會有錯脂倦;唯一可能有問題的就是settings.screenHeightDp。那這個(gè)值又是從哪里來的呢元莫?

??settings是從AssetManager2.cpp里面?zhèn)鬟f過來赖阻,而這個(gè)參數(shù)是它的成員變量。所以踱蠢,只要settings.screenHeightDp是一個(gè)錯誤的值火欧,那么這個(gè)判斷就可能不生效了。

??看上去茎截,我們基本分析到原因所在苇侵。

屏幕寬度(dp單位)其實(shí)在系統(tǒng)的代碼中,有兩個(gè)地方保存了企锌,一個(gè)是Java層榆浓,一個(gè)C++層。而我們在更新屏幕density時(shí)撕攒,僅更新了Java層陡鹃,C++層并沒有更新。

所以抖坪,我們在獲取dimension時(shí)萍鲸,就會看到屏幕寬度(dp單位,這里指的是Java層的)明明沒有達(dá)到xxdp擦俐,但還是取了values-xxdp下的值脊阴,因?yàn)镃++層的屏幕寬度(dp單位)達(dá)到了甚至超過了xxdp。

??既然,我們知道了原因嘿期,那么就可以對癥下藥品擎。只要我們把修改density之后的屏幕寬度,更新到C++層秽五,再去獲取對應(yīng)的dimension就沒有問題了吧孽查?答案正是如此,那么怎么更新C++層的屏幕寬度呢坦喘?這個(gè)就要回去看AssetManager2.cpp的代碼了盲再。

4. 解決方案的分析

??上面有介紹到,AssetManager2.cpp內(nèi)部有一個(gè)configuration_的成員變量瓣铣,match方法就是從這個(gè)變量去獲取當(dāng)前屏幕的寬度答朋。所以,只要我們能找到這個(gè)變量在哪里設(shè)置棠笑,就可以知道怎么去更新它了梦碗。通過搜索AssetManager2的代碼,我發(fā)現(xiàn)了如下方法:

void AssetManager2::SetConfiguration(const ResTable_config& configuration) {
  const int diff = configuration_.diff(configuration);
  configuration_ = configuration;

  if (diff) {
    RebuildFilterList();
    InvalidateCaches(static_cast<uint32_t>(diff));
  }
}

??AssetManager2有一個(gè)SetConfiguration方法蓖救,在這個(gè)方法里面洪规,在更新configuration_。那么循捺,哪里在調(diào)用SetConfiguration這個(gè)方法呢斩例?我們就要回到android_util_AssetManager.cpp中,如下:

static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc,
                                   jstring locale, jint orientation, jint touchscreen, jint density,
                                   jint keyboard, jint keyboard_hidden, jint navigation,
                                   jint screen_width, jint screen_height,
                                   jint smallest_screen_width_dp, jint screen_width_dp,
                                   jint screen_height_dp, jint screen_layout, jint ui_mode,
                                   jint color_mode, jint grammatical_gender, jint major_version) {
  ATRACE_NAME("AssetManager::SetConfiguration");

  ResTable_config configuration;
  memset(&configuration, 0, sizeof(configuration));
  // 1. 傳遞過來的參數(shù)从橘,更新到configuration念赶。
  configuration.mcc = static_cast<uint16_t>(mcc);
  configuration.mnc = static_cast<uint16_t>(mnc);
  configuration.orientation = static_cast<uint8_t>(orientation);
  configuration.touchscreen = static_cast<uint8_t>(touchscreen);
  configuration.density = static_cast<uint16_t>(density);
  configuration.keyboard = static_cast<uint8_t>(keyboard);
  configuration.inputFlags = static_cast<uint8_t>(keyboard_hidden);
  configuration.navigation = static_cast<uint8_t>(navigation);
  configuration.screenWidth = static_cast<uint16_t>(screen_width);
  configuration.screenHeight = static_cast<uint16_t>(screen_height);
  configuration.smallestScreenWidthDp = static_cast<uint16_t>(smallest_screen_width_dp);
  configuration.screenWidthDp = static_cast<uint16_t>(screen_width_dp);
  configuration.screenHeightDp = static_cast<uint16_t>(screen_height_dp);
  configuration.screenLayout = static_cast<uint8_t>(screen_layout);
  configuration.uiMode = static_cast<uint8_t>(ui_mode);
  configuration.colorMode = static_cast<uint8_t>(color_mode);
  configuration.grammaticalInflection = static_cast<uint8_t>(grammatical_gender);
  configuration.sdkVersion = static_cast<uint16_t>(major_version);

  if (locale != nullptr) {
    ScopedUtfChars locale_utf8(env, locale);
    CHECK(locale_utf8.c_str() != nullptr);
    configuration.setBcp47Locale(locale_utf8.c_str());
  }

  // Constants duplicated from Java class android.content.res.Configuration.
  static const jint kScreenLayoutRoundMask = 0x300;
  static const jint kScreenLayoutRoundShift = 8;

  // In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer
  // in C++. We must extract the round qualifier out of the Java screenLayout and put it
  // into screenLayout2.
  configuration.screenLayout2 =
      static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift);

  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
  // 2. 刷新AssetManager2的configuration。
  assetmanager->SetConfiguration(configuration);
}

??NativeSetConfiguration方法內(nèi)部主要做了兩件事:

  1. 將傳遞過來最新值恰力,更新到configuration上去叉谜。
  2. 再將configuration傳遞給AssetManager2,用以刷新它內(nèi)部的值踩萎。

??NativeSetConfiguration是一個(gè)native 方法停局,它對應(yīng)的Java方法是哪一個(gè)呢?當(dāng)然是AssetManagernativeSetConfiguration方法香府。

??然后翻具,我們再去尋找,哪里在調(diào)用nativeSetConfiguration方法呢回还?最終就找到了ResourceupdateConfiguration方法裆泳,這也是為什么上面的解決方案中,我們調(diào)用下這個(gè)方法柠硕,就能解決這個(gè)問題工禾。

??同理运提,調(diào)用createConfigurationContext方法,因?yàn)橹匦聞?chuàng)建Resource闻葵,所以也會去調(diào)用nativeSetConfiguration方法民泵,間接的刷新了C++層的屏幕寬度。

5. 一個(gè)彩蛋

??我們排查的問題過程中槽畔,發(fā)現(xiàn)了另外一個(gè)問題栈妆。將Activity的screenOrientation刷新聲明為sensorLandscape之后,切換屏幕方向時(shí)(正向橫屏和反向橫屏之間的切換)厢钧,并不會回調(diào)onConfigurationChanged方法鳞尔。同時(shí),切換方向之后早直,屏幕的density還被重置了寥假。

??所以,我們還需要在這種情況下重新設(shè)置屏幕的density霞扬。那么去處理這種問題呢糕韧?

??目前,我想到了兩種方案喻圃。

(1).切換方向變換萤彩,重新設(shè)置density

??實(shí)現(xiàn)代碼如下:

        findViewById<View>(R.id.container).apply {
            viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
                private var mLastRotation = -1
                override fun onGlobalLayout() {
                    if (display.rotation != mLastRotation) {
                        mLastRotation = display.rotation
                        // 更新屏幕的density亲桦。
                        setupDensity()
                    }
                }

            })
        }

??但是這種方案有一個(gè)弊端裹驰,由于再layout階段再去設(shè)置density,可能會導(dǎo)致measure階段獲取的dimension有些問題史汗。

(2). 重寫getResource方法饮焦,去更新density

??代碼實(shí)現(xiàn)如下:

override fun getResources(): Resources {
    val resources = super.getResources()
    resources.setDensity()
    return createConfigurationContext(resources.configuration).resources
}

??但是由于getResources方法會頻繁調(diào)用怕吴,所以最好給setDensity方法加一個(gè)判斷窍侧,我這里僅是為了簡單县踢,所以沒做處理。

??實(shí)現(xiàn)代碼可參考:DensityDemo

6. 總結(jié)

??最后伟件,我們來做個(gè)總結(jié)硼啤。

  1. 在我們修改屏幕的density之后,僅更新Java層的值斧账,并沒有更新C++層谴返。所以導(dǎo)致在獲取dimension時(shí),C++層用的是舊值去判斷咧织,所以導(dǎo)致dimension獲取的不對嗓袱。
  2. 在我們更新完density之后,需要調(diào)用ResourceupdateConfiguration方法习绢,去更新C++層的屏幕寬度(dp單位)

??額外補(bǔ)充兩句渠抹,可能大家在實(shí)際開發(fā)過程中很少遇到這種問題蝙昙,原因應(yīng)該是,系統(tǒng)默認(rèn)的屏幕寬度和我們修改density之后的屏幕寬度都比指定的values-wxxdp要大梧却,或者要小奇颠,所以難以發(fā)現(xiàn)這個(gè)問題。

??為什么將Activity聲明為sensorLandscape就會復(fù)現(xiàn)這個(gè)問題呢放航?其實(shí)跟screenOrientation沒有關(guān)系烈拒,只要values設(shè)置的dp值在默認(rèn)的屏幕寬度和我們修改density之后的屏幕寬度中間,就會有問題广鳍。只不過荆几,在我們的場景,剛好是橫屏才會遇到這種情況搜锰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伴郁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蛋叼,更是在濱河造成了極大的恐慌焊傅,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狈涮,死亡現(xiàn)場離奇詭異狐胎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)歌馍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門握巢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人松却,你說我怎么就攤上這事暴浦。” “怎么了晓锻?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵歌焦,是天一觀的道長。 經(jīng)常有香客問我砚哆,道長独撇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任躁锁,我火速辦了婚禮纷铣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘战转。我一直安慰自己搜立,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布槐秧。 她就那樣靜靜地躺著啄踊,像睡著了一般寸潦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上社痛,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天见转,我揣著相機(jī)與錄音,去河邊找鬼蒜哀。 笑死斩箫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的撵儿。 我是一名探鬼主播乘客,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淀歇!你這毒婦竟也來了易核?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤浪默,失蹤者是張志新(化名)和其女友劉穎牡直,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纳决,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碰逸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阔加。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饵史。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖胜榔,靈堂內(nèi)的尸體忽然破棺而出胳喷,到底是詐尸還是另有隱情,我是刑警寧澤夭织,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布吭露,位于F島的核電站,受9級特大地震影響摔癣,放射性物質(zhì)發(fā)生泄漏奴饮。R本人自食惡果不足惜纬向,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一择浊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逾条,春花似錦琢岩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽江锨。三九已至,卻和暖如春糕篇,著一層夾襖步出監(jiān)牢的瞬間啄育,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工拌消, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挑豌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓墩崩,卻偏偏與公主長得像氓英,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鹦筹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容