解决Android6.0下class自己不能转型成自己的问题(X
先上总结:
- 在LayoutInflater中存在view的构造器缓存
- Android6.0以下view的构造器缓存只进不出
- 一个dex中的LayoutInflater在创建view时,如果缓存中的view构造器是另一个dex的classloader创建的,则会引发该异常
最近在项目中使用动态加载的dex后,经常会出现如下类似的异常:
Caused by: java.lang.ClassCastException: androidx.appcompat.widget.ContentFrameLayout cannot be cast to androidx.appcompat.widget.ContentFrameLayout
at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:829)
at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:659)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:552)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
观察到这个问题有如下特征:
- 基本都是在加载了另外一个dex之后出现
- Android6及以下会出现,7开始不会出现
- 都是UI相关的类
根据第一条,推测问题是由于同一个类由不同的类加载器加载,并同时使用导致。
找了一条简单稳定复现的路径,开始追踪源码,追踪是从这里开始的:
androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:829)
这行的前后:
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
在第二行下断点,并使用studio的evaluate expression功能获取两者的classloader。发现通过ContentFrameLayout.class.getClassLoader()取得的classloader和subDecor.findViewById().getClass().getClassLoader()取得的classloader确实不同,猜测被证实。
那么,为什么二者会使用了不同的类加载器呢?
由于我这个项目存在两个dex,第二个dex是用我自定义的类加载器加载的。可能是这个原因导致两个ContentFrameLayout加载时使用的类加载器不同。
为了验证这个猜想。我做了一个demo,在两个dex的第一个页面都主动加载ContentFrameLayout这个类。如果我猜想正确,则打开第二个dex的第一个页面时必然出现这个崩溃。结果证明我的猜测正确。
那么,为什么我在第二个dex里面的页面中使用ContentFrameLayout,系统会给我第一个dex加载的类呢?
这里就要去调研第一个dex加载的类是如何传递到第二个dex的。
由于在我的demo中,ContentFrameLayout是放在XML中被加载的,并未手动创建。所以唯一加载ContentFrameLayout的地方应该就是Activity.setContentView()。于是便从这里开始找(以下代码主要来自Android9,即SDK28):
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
Activity.setContentView()调用的是Window.setContentView(),Window是个抽象类,它的实现一般是PhoneWindow:
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//demo中的activity只是一个普通的activity,所以应该是走的这里
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
看起来Window也是使用LayoutInflater去生成view,看LayoutInflater.inflate():
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
进去看看:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
//这一行应该只是获取XML的解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
去inflate(parser, root, attachToRoot)看看:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
//里面代码比较多,注意到有这么一段:
//显然我们的view不是TAG_MERGE,所以应该是走else
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//这里是生成view的地方
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//layout的话当然要生成LayoutParams
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
//layout还要生成自己的子view
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
}
贴的代码比较多,其实关键就在createViewFromTag()里:
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
进一个同名方法:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
//首先交给Factory创建view
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
//没有的话再用mPrivateFactory去试试
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//还是没有生成,那只好自己来了
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//下面两个分支,最终都会走到createView()里面
if (-1 == name.indexOf('.')) {
//如果view的名称没有前缀了,就会尝试以“android.view.类名”为全名创建view
view = onCreateView(parent, name, attrs);
} else {
//否则认为是全名,直接创建
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
...
}
createView是一个final方法,子类不能复写:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
//注意这里,验证classloader,如果不是同一个,则从sConstructorMap中移除
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
//构造器为空,则先用反射取得一个
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//获取到之后加入sConstructorMap
sConstructorMap.put(name, constructor);
} else {
//filter相关代码,忽略
...
}
//一些获取参数的代码,忽略
...
//用构造器创建实例
final View view = constructor.newInstance(args);
...
return view;
...
}
可以看到,在LayoutInflater中有一个view构造器的缓存。在创建view时,先判断缓存中是否存在这个view的构造器。如果存在并且验证通过,则直接使用它创建新对象。否则的话要从classloader中加载这个类,又要用反射取得对应的构造器,这两步比较费时。
那么怎样才算验证通过呢,看verifyClassLoader():
private final boolean verifyClassLoader(Constructor<? extends View> constructor) {
//取出constructor对应的classloader
final ClassLoader constructorLoader = constructor.getDeclaringClass().getClassLoader();
if (constructorLoader == BOOT_CLASS_LOADER) {
// fast path for boot class loader (most common case?) - always ok
return true;
}
// in all normal cases (no dynamic code loading), we will exit the following loop on the
// first iteration (i.e. when the declaring classloader is the contexts class loader).
//取出context的classloader
ClassLoader cl = mContext.getClassLoader();
do {
if (constructorLoader == cl) {
return true;
}
cl = cl.getParent();
} while (cl != null);
return false;
}
简单来说就是缓存的构造器的classloader和context的classloader是不是同一个。
以上便是正常的view生成逻辑。
那么为什么6.0及以下会出问题呢?
合理推测问题就出现在这个缓存中,来看下6.0的代码:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
//注意这里,没有了验证classloader的代码
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
...
//拿来就用,不管是谁哪个classloader加载的
final View view = constructor.newInstance(args);
...
return view;
...
}
所以问题得解,6.0及以下的代码,LayoutInflater会保存之前加载过的view的构造器,创建时会直接拿来用。如果先用一个classloader加载一个类,在第二个dex中想用的时候,LayoutInflater还是会返回前一个classloader加载的类,导致崩溃。
问题原因找到了,那么如何解决呢?
其实高版本的Android代码已经给了我们答案,就是当发现缓存的view构造器不是当前classloader加载的时候,就删去这个缓存,重新加载该类的构造器。
不过在系统代码中删去缓存并不是一件容易的事。首先,我们修改不了系统代码。其次sConstructorMap和createView()都是final的,无法通过子类去复写(override)它。并且在使用缓存之前,也没有任何回调可以让我们操作缓存。
既然我们无法直接修改LayoutInflater这个类本身,那可不可以在LayoutInflater.createView()被调用的代码段之前做文章呢?只要能保证每次调用LayoutInflater.createView()之前,都检查一遍sConstructorMap缓存,并清理掉不可用的缓存,不也能实现目的吗?
那如何才能保证在每次调用LayoutInflater.createView()之前都检查一遍缓存呢?首先观察LayoutInflater源码发现LayoutInflater.createView()除了在外部调用,LayoutInflater.inflate()最终也会调用到createView(),也就是说内外都会调用createView()这个方法。
对于外部调用,我们可以写一个gradle脚本(或插件),让它在编译时对我们APP包含的代码进行扫描,发现LayoutInflater.createView()调用的地方,就在LayoutInflater.createView()前插入一句检查代码,以达到删除不可用缓存的目的。
对于内部调用,我们发现LayoutInflater可以设置Factory和Factory2两个回调。当LayoutInflater尝试创建view时,会优先调用Factory2和Factory的onCreateView()去生成view,如果Factory们返回的是null,再去尝试自己生成view。那其实我们就可以利用这个Factory,在onCreateView()的时候去检查缓存了。
那问题又来了,这个方案其实是要求对于每一个LayoutInflater,我都去设置一个Factory去删除缓存的。如何保证每一个LayoutInflater都能给设置到一个Factory呢?这里我们同样可以用gradle脚本,在每个获取LayoutInflater的地方前(或后)插入一行设置Factory的代码。
这里总结一下获取LayoutInflater的方法都有哪些:
1 LayoutInflater.from()
2 Activity.getLayoutInflater()
3 View.inflate(Context, int, ViewGroup)
4 Context.getSystemService(“layout_inflater")
5 Activity.setContentView(int)
其中第3点比较特殊,它也是一个final方法,并且全程拿不到LayoutInflater对象,所以对于这行代码,我会用如下三行代码替换:
LayoutInflater inflater = LayoutInflater.from(Context)
inflater.setFactory(myFactory)
inflater.inflate(int, ViewGroup)
第5点也需要注意。因为setContentView()直接就拿activity里面的LayoutInflater使用了,所以需要在setContentView()之前设置好Factory。
另外需要注意的是,我们设置Factory之后,原始代码还可能会设置自己的Factory。由于Factory只能设置一次,这里可能会遇到一些冲突的情况,需要额外处理。
有同学可能会问,上面的方案只覆盖了自己写的和第三方的代码,有没有可能系统自己的某些代码拿着某个LayoutInflater去生成view,而这个过程全程你都无法进行干预,导致转型问题发生呢?
对于这个问题,我的判断是没有这种可能。
首先,系统不会自己去生成view,所有的view都是我们自己去生成的,对于我们自己生成的view,这个过程可控。对于系统其它的UI,像systemUI,launcher等,它本身就不属于你的APP,无需操心。
其次,对于一些系统提供的ViewGroup,它们一般不会自己去生成view。就算有,它们的LayoutInflater来源都是Context,只要我们能保证所有的context都能返回带有缓存删除功能的LayoutInflater就行。