换肤原理(android-skin-support)
插件换肤需要设置layoutInflater的Factory2。调用LayoutInflater::setFactory2(每个LayoutInflater都需要设置一次)。因此在Activity onCreate的时候统一设置 setFactory2。
备注:下面所说的Factory,都为 LayoutInflater.Factory 接口的实现类。
换肤冲突(calligraphy)
calligraphy 为 app 中已有的一套全局设置字体的框架。此框架会拦截 LayoutInflate (通过调用Activity的 attachBaseContext)。并对LayoutInflater 设置默认的 mFactory。
Fragment 换肤问题:
1.因为同时设置Factory所以Factory不能同时生效。
2.出现如下异常。
java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.view.LayoutInflater.setFactory2(LayoutInflater.java:369)
at android.support.v4.view.LayoutInflaterCompat.setFactory2(LayoutInflaterCompat.java:139)
at android.support.v4.app.Fragment.getLayoutInflater(Fragment.java:1332)
at android.support.v4.app.Fragment.onGetLayoutInflater(Fragment.java:1277)
at android.support.v4.app.Fragment.performGetLayoutInflater(Fragment.java:1308)
问题分析
-
正常情况:onStart的时候,activity 会分发 Fragment 的 create 事件,view 创建之前会调用
Fragment::performGetLayoutInflater()方法来获取 LayoutInflater。performGetLayoutInflater会调用调用 Fragment::onGetLayoutInflater() -> FragmentHostCallback::onGetLayoutInflater() -> FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this); 来得到一个新的LayoutInflater.
FragmentActivity::getLayoutInflater()里面调用 getWindow().getLayoutInflater() 。window为PhoneWindow,所以这里的Inflater的实现类为 PhoneLayoutInflater。
cloneInContext(Context newContext)中,通过原来的PhoneLayoutInflater 又创建一个新的 PhoneLayoutInflater ,并继承原来的Factory等属性。(因为LayoutInflater为新创建的,虽然保留了Activity中Inflater的Factory,但调用setFactory不会抛异常,会重新进行赋值)。
-
实际情况:
calligraphy字体库使用时会调用
override fun attachBaseContext(newBase: Context?) { // super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)) super.attachBaseContext(CalligraphyContextWrapper(newBase)) }
attachBaseContext()方法可以拦截 ContextImpl 中的实现方法。此字体库对 getSystemService() 方法进行了拦截,并对其中的Inflater服务进行拦截处理
public Object getSystemService(String name) { if (LAYOUT_INFLATER_SERVICE.equals(name)) { if (mInflater == null) { mInflater = new CalligraphyLayoutInflater(LayoutInflater.from(getBaseContext()), this, mAttributeId); } return mInflater; } return super.getSystemService(name); }
并在构造方法中对Factory进行了设置
protected CalligraphyLayoutInflater(LayoutInflater original, Context newContext, int attributeId) { super(original, newContext); mAttributeId = attributeId; setUpLayoutFactory(); } private void setUpLayoutFactory() { if (!(getFactory() instanceof CalligraphyFactory)) { setFactory(new CalligraphyFactory(getFactory(), mAttributeId)); } }
如果字体皮肤框架的Factory没有和其他Factory冲突的话,这样设置是没问题的,但是Factory已经被皮肤框架给换了,所以这里 !(getFactory() instanceof CalligraphyFactory) = true 。因此就会对Fragment的LayoutInflater设置了Factory ,但是Fragment的Inflater默认自己会设置工厂,这里就会有冲突,导致出现上面的异常。
问题解决
-
将此字体框架升到最新版本(2.3.0),最新版本中对cloneInContext 创建的Inflater(Fragment之类使用的LayoutInflater)不进行设置Factory。并且在CalligraphyLayoutInflater中对setFactory2进行了重写
@Override @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setFactory2(Factory2 factory2) { // Only set our factory and wrap calls to the Factory2 trying to be set! if (!(factory2 instanceof WrapperFactory2)) { // LayoutInflaterCompat.setFactory(this, new WrapperFactory2(factory2, mCalligraphyFactory)); super.setFactory2(new WrapperFactory2(factory2, mCalligraphyFactory)); } else { super.setFactory2(factory2); } }
android-skin-support和calligraphy结合的调用流程:
-
因为字体和换肤都要设置Factory,而字体库不仅拦截了 Factory 还对 LayoutInflater 进行了拦截,所以优先被 CalligraphyLayoutInflater 进行拦截。
-
当换肤拦截器调用 setFactory2(),会执行到 CalligraphyLayoutInflater 的 setFactory2 中。
-
当View创建的时候则会调用 WrapperFactory2.onCreateView() ==>这个factory包含两个信息,一个是原factory2(SkinCompatDelegate),一个是字体的 mCalligraphyFactory (CalligraphyFactory)。
-
WrapperFactory2.onCreateView()中先使用原来的factory2(SkinCompatDelegate)去创建View (创建换完皮肤的View)。将创建好的View拿过来调用 mCalligraphyFactory.onViewCreated 来对View进行字体的设置。
-
复现问题Demo地址:https://github.com/zhonghaohu/SkinTest.git