解決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

相關文章
相關標籤/搜索