踩坑之路: AndroidAutoSize導(dǎo)致UI顯示異常

前言

在講這次踩坑的問題之前首先先介紹下AndroidAutoSize剖煌,ResourceImpl以及Density和ResourceImpl的關(guān)系

AndroidAutoSize

目前市面上比較主流的適配框架AndroidAutoSize.
這個(gè)方案主要的原理是修改Density來進(jìn)行UI的縮放萤捆, 假設(shè)app以寬為基準(zhǔn)济舆,設(shè)計(jì)稿為360屏鳍,因此所有寬度為1080的設(shè)備的density都為3锈锤,即1dp=3px闯割。下面是Android各種單位轉(zhuǎn)化為px的計(jì)算公式:

public static float applyDimension(@ComplexDimensionUnit int unit, float value,
                                   DisplayMetrics metrics)
{
    switch (unit) {
    case COMPLEX_UNIT_PX:
        return value;
    case COMPLEX_UNIT_DIP:
        //將dp值轉(zhuǎn)化成px的處理拉讯,density相當(dāng)于一個(gè)縮放比例
        return value * metrics.density;
    case COMPLEX_UNIT_SP:
        return value * metrics.scaledDensity;
    case COMPLEX_UNIT_PT:
        return value * metrics.xdpi * (1.0f/72);
    case COMPLEX_UNIT_IN:
        return value * metrics.xdpi;
    case COMPLEX_UNIT_MM:
        return value * metrics.xdpi * (1.0f/25.4f);
    }
    return 0;
}

ResourceImpl

ResourcesImpl是真正實(shí)現(xiàn)Resource功能的類,這個(gè)類是根據(jù)ResourcesKey創(chuàng)建挑辆,Resources會(huì)根據(jù)下圖這幾個(gè)參數(shù)進(jìn)行創(chuàng)建例朱,而且還會(huì)根據(jù)這幾個(gè)值來設(shè)置hash值。通常情況下這幾個(gè)值都是一致的鱼蝉。那么每次創(chuàng)建的ResourcesKey的hash值也是一致洒嗤,因此在正常情況下所有Activity的ResourcesImpl都是一致的。


image.png

image.png

Density與ResourceImpl的關(guān)系

ResourceImpl會(huì)持有一個(gè)DisplayMetrics對(duì)象魁亦,Density是DisplayMetrics的一個(gè)屬性渔隶。因此ResourceImpl與Density是一對(duì)一的關(guān)系。也就是說整個(gè)應(yīng)用內(nèi)所有的Activity所使用的的Density正常情況下應(yīng)該都是一致的。

背景

前段時(shí)間QA偶然發(fā)現(xiàn)了一個(gè)問題间唉,那就是在第二個(gè)頁面有顯示過WebView以后绞灼,在返回第一個(gè)頁面時(shí),頁面UI出現(xiàn)了異常的情況呈野。下面兩個(gè)動(dòng)圖是做了個(gè)demo模擬了一下項(xiàng)目里的情況
第一個(gè)頁面為MainActivity低矮, 第二個(gè)頁面為SecondActivity。
MainActivity實(shí)現(xiàn)了cancelAdapt接口表示不用AndroidAutoSize的庫進(jìn)行適配被冒。
SecondActivity實(shí)現(xiàn)了CustomAdapt, 以寬為適配军掂,設(shè)計(jì)稿寬度為360。為了保證SecondActivity的頁面顯示始終正確昨悼,所在重寫了getResource方法蝗锥,在getResource方法里面進(jìn)行了一次Autosize的調(diào)用。

不顯示W(wǎng)ebView

  1. MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁面」按鈕率触,跳轉(zhuǎn)到SecondActivity
  2. SecondActivity點(diǎn)擊返回鍵终议,返回到MainActivity。
  3. MainActivity點(diǎn)擊「顯示Fragment 」按鈕闲延,顯示一個(gè)新的Fragment頁面
不跳轉(zhuǎn)到WebView.gif

最終UI顯示效果.png

顯示W(wǎng)ebView

  1. MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁面」按鈕痊剖,跳轉(zhuǎn)到SecondActivity
  2. SecondActivity點(diǎn)擊切換成WebView頁面
  3. SecondActivity點(diǎn)擊返回鍵,返回到MainActivity
  4. MainActivity點(diǎn)擊「顯示Fragment 」按鈕垒玲,顯示一個(gè)新的Fragment頁面
跳轉(zhuǎn)到WebView.gif

最終UI顯示效果.png

可以看到兩個(gè)動(dòng)圖只有一個(gè)步驟的差異陆馁,但是從最終UI顯示效果圖來看,第二種情況的UI明顯有放大的現(xiàn)象合愈,那么這是為什么呢叮贩?


流程分析

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    if (AutoSizeConfig.getInstance().isCustomFragment()) {
        if (mFragmentLifecycleCallbacksToAndroidx != null && activity instanceof androidx.fragment.app.FragmentActivity) {
            ((androidx.fragment.app.FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFragmentLifecycleCallbacksToAndroidx, true);
        } else if (mFragmentLifecycleCallbacks != null && activity instanceof android.support.v4.app.FragmentActivity) {
            ((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFragmentLifecycleCallbacks, true);
        }
    }

    //Activity 中的 setContentView(View) 一定要在 super.onCreate(Bundle); 之后執(zhí)行
    if (mAutoAdaptStrategy != null) {
        ///這個(gè)方法就是用來設(shè)置density的,也可以取消適配
        mAutoAdaptStrategy.applyAdapt(activity, activity);
    }
}

@Override
public void onActivityStarted(Activity activity) {
    if (mAutoAdaptStrategy != null) {
        mAutoAdaptStrategy.applyAdapt(activity, activity);
    }
}
public void applyAdapt(Object target, Activity activity) {
    //如果 target 實(shí)現(xiàn) CancelAdapt 接口表示放棄適配, 所有的適配效果都將失效
    if (target instanceof CancelAdapt) {
        AutoSizeLog.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
        AutoSize.cancelAdapt(activity);
        return;
    }

    //如果 target 實(shí)現(xiàn) CustomAdapt 接口表示該 target 想自定義一些用于適配的參數(shù), 從而改變最終的適配效果
    if (target instanceof CustomAdapt) {
        AutoSizeLog.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
        AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
    } else {
        AutoSizeLog.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
        AutoSize.autoConvertDensityOfGlobal(activity);
    }
}

AutoSize會(huì)在Activity創(chuàng)建的時(shí)候設(shè)置一次density佛析,然后會(huì)在onStart的時(shí)候再次設(shè)置一次density益老,為什么?因?yàn)镽esourceimpl是應(yīng)用內(nèi)唯一的寸莫,所以修改了density會(huì)導(dǎo)致所有的Activity都會(huì)生效捺萌。如果有些Activity不想要進(jìn)行相同的適配方案,那么返回到上一個(gè)Activity的時(shí)候就必須再做一次applyAdapt的操作膘茎。因?yàn)榕鋵?duì)頁面是實(shí)現(xiàn)的CancelAdapt桃纯,所以回到配對(duì)頁的時(shí)候會(huì)將density重新設(shè)置回來。
所以這里有三個(gè)問題:

  1. 根據(jù)AutoSize的設(shè)置時(shí)機(jī)可知配對(duì)頁UI按照正常的生命周期流程執(zhí)行下來應(yīng)該是不會(huì)放大的披坏,說明這中間出現(xiàn)了預(yù)期以外的流程
  2. 如果是中間出現(xiàn)異常流程導(dǎo)致的問題态坦,那么進(jìn)入SecondActivity退出的時(shí)候就會(huì)出現(xiàn)
  3. 為什么進(jìn)入WebView頁面以后,再返回到MainActivity就會(huì)出現(xiàn)這個(gè)問題

第一個(gè)問題(預(yù)期以外的流程)

通過調(diào)試發(fā)現(xiàn)SecondActivity返回到MainActivity的時(shí)候棒拂,生命周期流程是下面這樣的


流程圖 (2).jpg

SecondActivity在MainActivity執(zhí)行完onResume以后會(huì)執(zhí)行一次onWindowFocusChanged方法伞梯,而onWindowFocusChanged方法會(huì)調(diào)用getResource,這導(dǎo)致density又變成了SecondActivity的縮放比例值。

第二個(gè)問題(進(jìn)入SecondActivity當(dāng)不顯示W(wǎng)ebView時(shí)谜诫,然后點(diǎn)擊退出為什么不會(huì)出現(xiàn)UI異常的情況)

后面發(fā)現(xiàn)SecondActivity繼承的是AppCompatActivity
AppCompatActivity在初始化PhoneWindow的時(shí)候會(huì)調(diào)用getResources漾峡,然后去調(diào)用super的gerResource方法

@Override
public Resources getResources() {
    return getResourcesInternal();
}

private Resources getResourcesInternal() {
    if (mResources == null) {
        if (mOverrideConfiguration == null) {
            mResources = super.getResources();
        } else {
            final Context resContext = createConfigurationContext(mOverrideConfiguration);
            mResources = resContext.getResources();
        }
    }
    return mResources;
}

而super調(diào)用的是內(nèi)部持有的mBase對(duì)象的getResource方法,mBase則是AppCompatActivity在attachBaseContext的時(shí)候創(chuàng)建的Context對(duì)象猜绣。這個(gè)ContextThemeWrapper是在appcompat兼容包內(nèi)的灰殴,并不是Android包下面的ContextThemeWrapper

public Context attachBaseContext2(@NonNull final Context baseContext) {
    // Next, we'll wrap the base context to ensure any method overrides or themes are left
    // intact. Since ThemeOverlay.AppCompat theme is empty, we'll get the base context's theme.
    final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(baseContext,
            R.style.Theme_AppCompat_Empty);
    wrappedContext.applyOverrideConfiguration(config);

    return super.attachBaseContext2(wrappedContext);
}
@Override
public Resources getResources() {
    return getResourcesInternal();
}

private Resources getResourcesInternal() {
    if (mResources == null) {
        if (mOverrideConfiguration == null) {
            mResources = super.getResources();
        } else if (Build.VERSION.SDK_INT >= 17) {
            //我們的app版本是30敬特,所以這個(gè)地方會(huì)創(chuàng)建一個(gè)新的context掰邢,并且生成新的resource
            final Context resContext = createConfigurationContext(mOverrideConfiguration);
            mResources = resContext.getResources();
        } else {
            Resources res = super.getResources();
            Configuration newConfig = new Configuration(res.getConfiguration());
            newConfig.updateFrom(mOverrideConfiguration);
            mResources = new Resources(res.getAssets(), res.getDisplayMetrics(), newConfig);
        }
    }
    return mResources;
}

所以AppCompatActivity會(huì)自己創(chuàng)建一個(gè)resource以及resourceImpl對(duì)象,因此修改這個(gè)值里面的density并不會(huì)影響其他Activity的density值伟阔。這也就是為什么進(jìn)入SecondActivity退出的時(shí)候辣之,MainActivity并不會(huì)出現(xiàn)UI異常的情況

第三個(gè)問題(為什么顯示W(wǎng)ebView頁面以后,會(huì)影響MainActivity的density的值)

image.png
  1. WebView在初始化的時(shí)候會(huì)addWebViewAssetPath方法
public void addWebViewAssetPath(Context context) {
    final String[] newAssetPaths =
            WebViewFactory.getLoadedPackageInfo().applicationInfo.getAllApkPaths();
    final ApplicationInfo appInfo = context.getApplicationInfo();

    // Build the new library asset path list.
    String[] newLibAssets = appInfo.sharedLibraryFiles;
    for (String newAssetPath : newAssetPaths) {
        newLibAssets = ArrayUtils.appendElement(String.class, newLibAssets, newAssetPath);
    }

    if (newLibAssets != appInfo.sharedLibraryFiles) {
        // Update the ApplicationInfo object with the new list.
        // We know this will persist and future Resources created via ResourcesManager
        // will include the shared library because this ApplicationInfo comes from the
        // underlying LoadedApk in ContextImpl, which does not change during the life of the
        // application.
        appInfo.sharedLibraryFiles = newLibAssets;

        // Update existing Resources with the WebView library.
        // 會(huì)更新一遍所有的resourceimpl
        ResourcesManager.getInstance().appendLibAssetsForMainAssetPath(
                appInfo.getBaseResourcePath(), newAssetPaths);
    }
}
  1. 由于Android的WebView依賴于Android內(nèi)置的WebViewGoogle的apk因此assetPath會(huì)至少增加一個(gè)


    image.png
  2. 會(huì)將這個(gè)assetsPath更新到所有的ResourcesImpl當(dāng)中皱炉。


/**
 * Appends the library asset paths to any ResourcesImpl object that contains the main
 * assetPath.
 * @param assetPath The main asset path for which to add the library asset path.
 * @param libAssets The library asset paths to add.
 */
public void appendLibAssetsForMainAssetPath(String assetPath, String[] libAssets) {
    synchronized (this) {
        ...代碼省略...
        redirectResourcesToNewImplLocked(updatedResourceKeys);
    }
}
  1. 當(dāng)前應(yīng)用內(nèi)所有的ResourceImpl都會(huì)被更新成同一個(gè)
  2. 這也就是為什么只有當(dāng)顯示W(wǎng)ebView頁面的時(shí)候才會(huì)影響MainActivity的density的值

解決方案

https://github.com/JessYanCoding/AndroidAutoSize/issues/13
根據(jù)AutoSize作者提供的參考怀估,我們可以重寫MainActivity的getResource,然后取消AutoSize的適配合搅。

override fun getResources(): Resources {
    val resource = super.getResources()
    AutoSizeCompat.cancelAdapt(resource)
    return resource
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末多搀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子灾部,更是在濱河造成了極大的恐慌康铭,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赌髓,死亡現(xiàn)場離奇詭異从藤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锁蠕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門夷野,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人荣倾,你說我怎么就攤上這事悯搔。” “怎么了舌仍?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵妒貌,是天一觀的道長。 經(jīng)常有香客問我抡笼,道長苏揣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任推姻,我火速辦了婚禮平匈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己增炭,他們只是感情好忍燥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著隙姿,像睡著了一般梅垄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上输玷,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天队丝,我揣著相機(jī)與錄音,去河邊找鬼欲鹏。 笑死机久,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赔嚎。 我是一名探鬼主播膘盖,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尤误!你這毒婦竟也來了侠畔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤损晤,失蹤者是張志新(化名)和其女友劉穎软棺,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉馆,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡码党,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斥黑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揖盘。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锌奴,靈堂內(nèi)的尸體忽然破棺而出兽狭,到底是詐尸還是另有隱情,我是刑警寧澤鹿蜀,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布箕慧,位于F島的核電站,受9級(jí)特大地震影響茴恰,放射性物質(zhì)發(fā)生泄漏颠焦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一往枣、第九天 我趴在偏房一處隱蔽的房頂上張望伐庭。 院中可真熱鬧粉渠,春花似錦、人聲如沸圾另。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽集乔。三九已至去件,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扰路,已是汗流浹背尤溜。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幼衰,地道東北人靴跛。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像渡嚣,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肥印,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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