前言
在講這次踩坑的問題之前首先先介紹下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都是一致的。
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
- MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁面」按鈕率触,跳轉(zhuǎn)到SecondActivity
- SecondActivity點(diǎn)擊返回鍵终议,返回到MainActivity。
- MainActivity點(diǎn)擊「顯示Fragment 」按鈕闲延,顯示一個(gè)新的Fragment頁面
顯示W(wǎng)ebView
- MainActivity點(diǎn)擊「跳轉(zhuǎn)第二個(gè)頁面」按鈕痊剖,跳轉(zhuǎn)到SecondActivity
- SecondActivity點(diǎn)擊切換成WebView頁面
- SecondActivity點(diǎn)擊返回鍵,返回到MainActivity
- MainActivity點(diǎn)擊「顯示Fragment 」按鈕垒玲,顯示一個(gè)新的Fragment頁面
可以看到兩個(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è)問題:
- 根據(jù)AutoSize的設(shè)置時(shí)機(jī)可知配對(duì)頁UI按照正常的生命周期流程執(zhí)行下來應(yīng)該是不會(huì)放大的披坏,說明這中間出現(xiàn)了預(yù)期以外的流程
- 如果是中間出現(xiàn)異常流程導(dǎo)致的問題态坦,那么進(jìn)入SecondActivity退出的時(shí)候就會(huì)出現(xiàn)
- 為什么進(jìn)入WebView頁面以后,再返回到MainActivity就會(huì)出現(xiàn)這個(gè)問題
第一個(gè)問題(預(yù)期以外的流程)
通過調(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è)問題(進(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的值)
- 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è)
會(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頁面的時(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
}