前言
在講這次踩坑的問(wèn)題之前首先先介紹下AndroidAutoSize,ResourceImpl以及Density和ResourceImpl的關(guān)系
AndroidAutoSize
目前市面上比較主流的適配框架AndroidAutoSize.
這個(gè)方案主要的原理是修改Density來(lái)進(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è)值來(lái)設(shè)置hash值。通常情況下這幾個(gè)值都是一致的。那么每次創(chuàng)建的ResourcesKey的hash值也是一致,因此在正常情況下所有Activity的ResourcesImpl都是一致的。


Density與ResourceImpl的關(guān)系
ResourceImpl會(huì)持有一個(gè)DisplayMetrics對(duì)象,Density是DisplayMetrics的一個(gè)屬性。因此ResourceImpl與Density是一對(duì)一的關(guān)系。也就是說(shuō)整個(gè)應(yīng)用內(nèi)所有的Activity所使用的的Density正常情況下應(yīng)該都是一致的。
背景
前段時(shí)間QA偶然發(fā)現(xiàn)了一個(gè)問(wèn)題,那就是在第二個(gè)頁(yè)面有顯示過(guò)WebView以后,在返回第一個(gè)頁(yè)面時(shí),頁(yè)面UI出現(xiàn)了異常的情況。下面兩個(gè)動(dòng)圖是做了個(gè)demo模擬了一下項(xiàng)目里的情況
第一個(gè)頁(yè)面為MainActivity, 第二個(gè)頁(yè)面為SecondActivity。
MainActivity實(shí)現(xiàn)了cancelAdapt接口表示不用AndroidAutoSize的庫(kù)進(jìn)行適配。
SecondActivity實(shí)現(xiàn)了CustomAdapt, 以寬為適配,設(shè)計(jì)稿寬度為360。為了保證SecondActivity的頁(yè)面顯示始終正確,所在重寫了getResource方法,在getResource方法里面進(jìn)行了一次Autosize的調(diào)用。
不顯示W(wǎng)ebView
- MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁(yè)面」按鈕,跳轉(zhuǎn)到SecondActivity
- SecondActivity點(diǎn)擊返回鍵,返回到MainActivity。
- MainActivity點(diǎn)擊「顯示Fragment 」按鈕,顯示一個(gè)新的Fragment頁(yè)面


顯示W(wǎng)ebView
- MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁(yè)面」按鈕,跳轉(zhuǎn)到SecondActivity
- SecondActivity點(diǎn)擊切換成WebView頁(yè)面
- SecondActivity點(diǎn)擊返回鍵,返回到MainActivity
- MainActivity點(diǎn)擊「顯示Fragment 」按鈕,顯示一個(gè)新的Fragment頁(yè)面


可以看到兩個(gè)動(dòng)圖只有一個(gè)步驟的差異,但是從最終UI顯示效果圖來(lái)看,第二種情況的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è)方法就是用來(lái)設(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ì)頁(yè)面是實(shí)現(xiàn)的CancelAdapt,所以回到配對(duì)頁(yè)的時(shí)候會(huì)將density重新設(shè)置回來(lái)。
所以這里有三個(gè)問(wèn)題:
- 根據(jù)AutoSize的設(shè)置時(shí)機(jī)可知配對(duì)頁(yè)UI按照正常的生命周期流程執(zhí)行下來(lái)應(yīng)該是不會(huì)放大的,說(shuō)明這中間出現(xiàn)了預(yù)期以外的流程
- 如果是中間出現(xiàn)異常流程導(dǎo)致的問(wèn)題,那么進(jìn)入SecondActivity退出的時(shí)候就會(huì)出現(xiàn)
- 為什么進(jìn)入WebView頁(yè)面以后,再返回到MainActivity就會(huì)出現(xiàn)這個(gè)問(wèn)題
第一個(gè)問(wèn)題(預(yù)期以外的流程)
通過(guò)調(diào)試發(fā)現(xiàn)SecondActivity返回到MainActivity的時(shí)候,生命周期流程是下面這樣的

SecondActivity在MainActivity執(zhí)行完onResume以后會(huì)執(zhí)行一次onWindowFocusChanged方法,而onWindowFocusChanged方法會(huì)調(diào)用getResource,這導(dǎo)致density又變成了SecondActivity的縮放比例值。
第二個(gè)問(wèn)題(進(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èn)題(為什么顯示W(wǎng)ebView頁(yè)面以后,會(huì)影響MainActivity的density的值)

- 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);
}
}
-
由于Android的WebView依賴于Android內(nèi)置的WebViewGoogle的apk因此assetPath會(huì)至少增加一個(gè)
image.png 會(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);
}
}
- 當(dāng)前應(yīng)用內(nèi)所有的ResourceImpl都會(huì)被更新成同一個(gè)
- 這也就是為什么只有當(dāng)顯示W(wǎng)ebView頁(yè)面的時(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
}
