美文网首页
解决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

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