美文网首页
解决Android6.0下class自己不能转型成自己的问题(X

解决Android6.0下class自己不能转型成自己的问题(X

作者: 东方景明 | 来源:发表于2020-03-05 20:27 被阅读0次

先上总结:

  • 在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就行。

相关文章

  • 解决Android6.0下class自己不能转型成自己的问题(X

    先上总结: 在LayoutInflater中存在view的构造器缓存 Android6.0以下view的构造器缓存...

  • java 类的向上向下转换

    通俗理解向上转型:就是子类转型成父类。 class A { } class B extends A { } A a...

  • iOS开发过程中的一些常见问题解决方案整理

    解决"OBJC_CLASS$_xxx", referenced from: objc-class-ref in x...

  • 易仁英雄团

    阅读自我的这个课题,可以帮助自己解决什么问题,带来什么价值 【解决】解决自我认知的问题,不再去苛求一下达到完美,成...

  • 《富爸爸财富大趋势》

    1.商人是解决财务问题的人,如果他们不能解决自己的财务问题,就会破产。而如果政府官僚不能解决自己的问题,他们就会将...

  • 目的论

    每个成年人的人生,都是自己营造的,只是这种营造,是一个轮回。轮回是为了给自己制造机会,解决此前不能解决的问题。但成...

  • 自己的问题自己解决

    今天女儿回来后,气愤的说:托管班的一个男同学龚××特别讨厌,不仅骂脏话,还拿手机不停的拍她,她制止了对方,...

  • 自己的问题自己解决

    整整一天都窝在家里,没有出门,除了看完了《天堂电影院》以外,还补习了之前落下的PPT课程,同时还想清楚了一些事情:...

  • Android .dex文件简介

    1.引言 上节我问过自己问题dvm,将Class转成成.dex文件,然后再将.dex文件转换成Class文件。那么...

  • 读书档案,持续更新:《零秒思考》

    一、阅读本书的目的是什么? 1、解决自己不能深度思考的问题;2、解决自己总是不能专注思考的问题; 二、阅读后的收获...

网友评论

      本文标题:解决Android6.0下class自己不能转型成自己的问题(X

      本文链接:https://www.haomeiwen.com/subject/ahlcrhtx.html