解决mFactorySet在Android Q中被非SDK接口限制的问题

mFactorySet问题由来

mFactorySet这个值若是熟悉的同窗必定知道,一般咱们在使用换肤框架的时候,须要使用咱们自定义的LayoutInflater.Factory类,这时候就须要调用LayoutInflater的setFactory方法。而我以前编写的一个基于Factory去给原生控件增长shapre xml属性的框架也是一样的原理(无需自定义View,完全解放shape,selector吧)。 咱们来看一下setFactory方法的源码:java

image

经过源码得知,咱们在调用setFactory方法的时候,首先会判断mFactorySet的值,若是mFactorySet为true,则表明该LayoutInflater已经设置了factory,而系统通常会在Activity的onCreate方法中设置本身的factory类。
若是这时候咱们想要经过替换本身的factory类来实现换肤功能的话,咱们会经过反射去修改mFactorySet的值为false,这样就能够调用setFactory方法。 下面就是在android q以前经常使用的方法:android

try {
        Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
        field.setAccessible(true);
        field.setBoolean(inflater, false);
        BackgroundFactory factory = new BackgroundFactory();
        inflater.setFactory2(factory);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
复制代码

然而android Q更新以后,将mFactorySet加入来非SDK接口限制的黑名单,若是在q上咱们再经过这种方法去setFactroy,会报以下错误:api

java.lang.NoSuchFieldException: No field mFactorySet in class Landroid/view/LayoutInflater; (declaration of 'android.view.LayoutInflater' appears in /system/framework/framework.jar!classes3.dex)
复制代码

这就是这个问题的由来,接下来就来探讨一下如何解决这个问题。app

解决android Q上没法二次setFactroy的问题。

google官方提供的方法

虽然google已经将这个字段放在sdk限制名单里,可是它也给咱们提供来解决方案:框架

# mFactorySet is being modified by app developers to reset the factory
# on an existing LayoutInflater. Instead, a developer should use the
# existing LayoutInflater#cloneInContext() to create a new LayoutInflater
# and set the factory on it instead.
#
# This is often desired at the Activity level, so that any part of
# the application getting a LayoutInflater using the Activity as
# a Context will get the LayoutInflater with a custom factory. To
# do this, the Activity has to replace the returned LayoutInflater.
# Something like this should work:
#
#  private LayoutInflater mLayoutInflater;
#
#  @Override
#  public Object getSystemService(String name) {
#    if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
#      if (mLayoutInflater == null) {
#        mLayoutInflater =
#          ((LayoutInflater)super.getSystemService(name)).cloneInContext(this);
#        mLayoutInflater.setFactory(new CustomLayoutFactory());
#      }
#      return mLayoutInflater;
#    }
#    return super.getSystemService(name);
#  }
复制代码

谷歌官方建议方法为,先调用LayoutInflater的cloneInContext的方法,而后setFactory,这样就能够从新设置factroy,所以咱们按照谷歌的方法能够这么用:ide

LayoutInflater inflater = LayoutInflater.from(context).cloneInContext(context);
inflater.setFactory(factory);
复制代码

可是经过调试发现,这种方法对咱们来讲是无效的,缘由以下:post

  • cloneInContext返回的LayoutInflater是一个新的LayoutInflater对象,和LayoutInflater.from(context)并非同一个对象
  • 新返回的inflater进行setFactory是没有问题的,可是Activity以及Fragment建立View的时候仍是经过LayoutInflater.from(context)获取的,因此cloneInContext并不能直接对原inflater进行修改。

经过反射LayoutInflaterCompat去修改factory

之因此想到LayoutInflaterCompat,是由于LayoutInflaterCompat也提供来setFacttory方法,它属于supportv4包,google应该不会随随便便让本身是support包被加入sdk限制接口,咱们来看一下LayoutInflaterCompat的源码: this

image

LayoutInflaterCompat提供了setFactory2方法,那咱们可否直接经过这个方法来设置factory呢,显然是不能的,由于该方法内有api的限制:

if (VERSION.SDK_INT < 21) {
            Factory f = inflater.getFactory();
            if (f instanceof Factory2) {
                forceSetFactory2(inflater, (Factory2)f);
            } else {
                forceSetFactory2(inflater, factory);
            }
   }
复制代码

只有api小于21才会去调用forceSetFactory2,顾名思义,强制去设置factory。经过forceSetFactory2源码可得,它是经过反射去设置inflater的factory。
可是咱们开发app不可能只是适配api21如下版本,因此这个方法并不可用。可是它给了咱们思路,咱们能够直接经过反射去设置factory的值。所以咱们能够经过以下代码去强制设置factory的值:google

private static void forceSetFactory2(LayoutInflater inflater) {
        Class<LayoutInflaterCompat> compatClass = LayoutInflaterCompat.class;
        Class<LayoutInflater> inflaterClass = LayoutInflater.class;
        try {
            Field sCheckedField = compatClass.getDeclaredField("sCheckedField");
            sCheckedField.setAccessible(true);
            sCheckedField.setBoolean(inflater, false);
            Field mFactory = inflaterClass.getDeclaredField("mFactory");
            mFactory.setAccessible(true);
            Field mFactory2 = inflaterClass.getDeclaredField("mFactory2");
            mFactory2.setAccessible(true);
            BackgroundFactory factory = new BackgroundFactory();
            if (inflater.getFactory2() != null) {
                factory.setInterceptFactory2(inflater.getFactory2());
            } else if (inflater.getFactory() != null) {
                factory.setInterceptFactory(inflater.getFactory());
            }
            mFactory2.set(inflater, factory);
            mFactory.set(inflater, factory);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
复制代码

总结

如何绕过mFactorySet的限制去设置factory的方法已经给出,可是有一点要注意:尽可能同时设置factory和factory2,这样才能尽量保证view建立的时候使用咱们自定义的factory类。spa

相关文章
相关标签/搜索