踩坑之路: AndroidAutoSize导致UI显示异常

前言

在讲这次踩坑的问题之前首先先介绍下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都是一致的。


image.png

image.png

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

  1. MainActivity点击「跳转第二个页面」按钮,跳转到SecondActivity
  2. SecondActivity点击返回键,返回到MainActivity。
  3. MainActivity点击「显示Fragment 」按钮,显示一个新的Fragment页面
不跳转到WebView.gif

最终UI显示效果.png

显示WebView

  1. MainActivity点击「跳转第二个页面」按钮,跳转到SecondActivity
  2. SecondActivity点击切换成WebView页面
  3. SecondActivity点击返回键,返回到MainActivity
  4. MainActivity点击「显示Fragment 」按钮,显示一个新的Fragment页面
跳转到WebView.gif

最终UI显示效果.png

可以看到两个动图只有一个步骤的差异,但是从最终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重新设置回来。
所以这里有三个问题:

  1. 根据AutoSize的设置时机可知配对页UI按照正常的生命周期流程执行下来应该是不会放大的,说明这中间出现了预期以外的流程
  2. 如果是中间出现异常流程导致的问题,那么进入SecondActivity退出的时候就会出现
  3. 为什么进入WebView页面以后,再返回到MainActivity就会出现这个问题

第一个问题(预期以外的流程)

通过调试发现SecondActivity返回到MainActivity的时候,生命周期流程是下面这样的


流程图 (2).jpg

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的值)

image.png
  1. 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);
    }
}
  1. 由于Android的WebView依赖于Android内置的WebViewGoogle的apk因此assetPath会至少增加一个


    image.png
  2. 会将这个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);
    }
}
  1. 当前应用内所有的ResourceImpl都会被更新成同一个
  2. 这也就是为什么只有当显示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
}

你可能感兴趣的:(踩坑之路: AndroidAutoSize导致UI显示异常)