前言
在讲这次踩坑的问题之前首先先介绍下AndroidAutoSize,ResourceImpl以及Density和ResourceImpl的关系
AndroidAutoSize
目前市面上比较主流的适配框架AndroidAutoSize.
这个方案主要的原理是修改Density来进行UI的缩放, 假设app以宽为基准,设计稿为360,因此所有宽度为1080的设备的density都为3,即1dp=3px。下面是Android各种单位转化为px的计算公式:
public static float applyDimension(@ComplexDimensionUnit int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
//将dp值转化成px的处理,density相当于一个缩放比例
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是真正实现Resource功能的类,这个类是根据ResourcesKey创建,Resources会根据下图这几个参数进行创建,而且还会根据这几个值来设置hash值。通常情况下这几个值都是一致的。那么每次创建的ResourcesKey的hash值也是一致,因此在正常情况下所有Activity的ResourcesImpl都是一致的。
Density与ResourceImpl的关系
ResourceImpl会持有一个DisplayMetrics对象,Density是DisplayMetrics的一个属性。因此ResourceImpl与Density是一对一的关系。也就是说整个应用内所有的Activity所使用的的Density正常情况下应该都是一致的。
背景
前段时间QA偶然发现了一个问题,那就是在第二个页面有显示过WebView以后,在返回第一个页面时,页面UI出现了异常的情况。下面两个动图是做了个demo模拟了一下项目里的情况
第一个页面为MainActivity, 第二个页面为SecondActivity。
MainActivity实现了cancelAdapt接口表示不用AndroidAutoSize的库进行适配。
SecondActivity实现了CustomAdapt, 以宽为适配,设计稿宽度为360。为了保证SecondActivity的页面显示始终正确,所在重写了getResource方法,在getResource方法里面进行了一次Autosize的调用。
不显示WebView
- MainActivity点击「跳转第二个页面」按钮,跳转到SecondActivity
- SecondActivity点击返回键,返回到MainActivity。
- MainActivity点击「显示Fragment 」按钮,显示一个新的Fragment页面
显示WebView
- MainActivity点击「跳转第二个页面」按钮,跳转到SecondActivity
- SecondActivity点击切换成WebView页面
- SecondActivity点击返回键,返回到MainActivity
- MainActivity点击「显示Fragment 」按钮,显示一个新的Fragment页面
可以看到两个动图只有一个步骤的差异,但是从最终UI显示效果图来看,第二种情况的UI明显有放大的现象,那么这是为什么呢?
流程分析
@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); 之后执行
if (mAutoAdaptStrategy != null) {
///这个方法就是用来设置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 实现 CancelAdapt 接口表示放弃适配, 所有的适配效果都将失效
if (target instanceof CancelAdapt) {
AutoSizeLog.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
}
//如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果
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会在Activity创建的时候设置一次density,然后会在onStart的时候再次设置一次density,为什么?因为Resourceimpl是应用内唯一的,所以修改了density会导致所有的Activity都会生效。如果有些Activity不想要进行相同的适配方案,那么返回到上一个Activity的时候就必须再做一次applyAdapt的操作。因为配对页面是实现的CancelAdapt,所以回到配对页的时候会将density重新设置回来。
所以这里有三个问题:
- 根据AutoSize的设置时机可知配对页UI按照正常的生命周期流程执行下来应该是不会放大的,说明这中间出现了预期以外的流程
- 如果是中间出现异常流程导致的问题,那么进入SecondActivity退出的时候就会出现
- 为什么进入WebView页面以后,再返回到MainActivity就会出现这个问题
第一个问题(预期以外的流程)
通过调试发现SecondActivity返回到MainActivity的时候,生命周期流程是下面这样的
SecondActivity在MainActivity执行完onResume以后会执行一次onWindowFocusChanged方法,而onWindowFocusChanged方法会调用getResource,这导致density又变成了SecondActivity的缩放比例值。
第二个问题(进入SecondActivity当不显示WebView时,然后点击退出为什么不会出现UI异常的情况)
后面发现SecondActivity继承的是AppCompatActivity
AppCompatActivity在初始化PhoneWindow的时候会调用getResources,然后去调用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调用的是内部持有的mBase对象的getResource方法,mBase则是AppCompatActivity在attachBaseContext的时候创建的Context对象。这个ContextThemeWrapper是在appcompat兼容包内的,并不是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,所以这个地方会创建一个新的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会自己创建一个resource以及resourceImpl对象,因此修改这个值里面的density并不会影响其他Activity的density值。这也就是为什么进入SecondActivity退出的时候,MainActivity并不会出现UI异常的情况
第三个问题(为什么显示WebView页面以后,会影响MainActivity的density的值)
- WebView在初始化的时候会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.
// 会更新一遍所有的resourceimpl
ResourcesManager.getInstance().appendLibAssetsForMainAssetPath(
appInfo.getBaseResourcePath(), newAssetPaths);
}
}
-
由于Android的WebView依赖于Android内置的WebViewGoogle的apk因此assetPath会至少增加一个
会将这个assetsPath更新到所有的ResourcesImpl当中。
/**
* 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);
}
}
- 当前应用内所有的ResourceImpl都会被更新成同一个
- 这也就是为什么只有当显示WebView页面的时候才会影响MainActivity的density的值
解决方案
https://github.com/JessYanCoding/AndroidAutoSize/issues/13
根据AutoSize作者提供的参考,我们可以重写MainActivity的getResource,然后取消AutoSize的适配。
override fun getResources(): Resources {
val resource = super.getResources()
AutoSizeCompat.cancelAdapt(resource)
return resource
}