Android LayoutInflater Factory 原始碼解析

頭條祁同偉發表於2019-02-25
本文概述

在上一篇文章《Android LayoutInflater 原始碼解析》中我們說到 View 的 inflate 中有一個方法 createViewFromTag,會首先嚐試通過 Factory 來 CreateView。

    View view;
    if (mFactory2 != null) {
        // ① 有mFactory2,則呼叫mFactory2的onCreateView方法
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        // ② 有mFactory,則呼叫mFactory的onCreateView方法
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
複製程式碼

正常情況下這個Factory是空的,那什麼時候不為空,以及 LayoutInflater Factory 的具體用法,我們今天就帶著這兩個問題來詳細學習下

備註:本文基於 Android 8.1.0。

1、LayoutInflater.Factory 簡介

LayoutInflater.Factory 中沒有說明,那我們來看下它唯一方法的說明:

Hook you can supply that is called when inflating from a LayoutInflater. You can use this to customize the tag names available in your XML layout files.

翻譯過來就是:通過 LayoutInflater 建立View時候的一個回撥,可以通過LayoutInflater.Factory來改造 XML 中存在的 tag。

我們來看下這個唯一的方法:

public abstract View onCreateView (String name, Context context, AttributeSet attrs)
複製程式碼

那麼我們就明白了,如果我們設定了LayoutInflater Factory ,在LayoutInflater 的 createViewFromTag 方法中就會通過這個 Factory 的 onCreateView 方法來建立 View。

2、LayoutInflater.Factory 作用

那怎麼理解上述引用的這個改造呢?舉個簡單的例子:比如你在 XML中 寫了一個 TextView標籤,然後在 onCreateView 這個回撥裡 判斷如果 name 是 TextView 的話可以變成一個Button,這樣的功能可以實現例如批量更換某一個控制元件等的用途。例子如下:

佈局檔案
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.liuzhaofutrue.teststart.MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
複製程式碼

接下來我們在 Java 程式碼中做修改:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                if(TextUtils.equals(name,"TextView")){
                    Button button = new Button(MainActivity.this);
                    button.setText("我替換了TextView");
                    button.setAllCaps(false);
                    return button;
                }
                return getDelegate().createView(parent, name, context, attrs);
            }

            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {
                return null;
            }
        });

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
複製程式碼

可以看到,本來在佈局檔案中需要展示的是一個 TextView,但是現在卻被改造成了一個 Button。

成功改造為Button

備註:其實還有一個關係密切的類 LayoutInflater.Factory2 ,與 LayoutInflater.Factory 的區別是

  • LayoutInflater.Factory2 是API 11 被加進來的
  • LayoutInflater.Factory2 繼承自 LayoutInflater.Factory
  • 可以對建立 View 的 Parent 進行控制

3、LayoutInflaterCompat

剛剛我們說到,LayoutInflater.Factory2 是API 11 被加進來的,那麼 LayoutInflaterCompat 就是拿來做相容的類。我們來看下它最重要的兩個方法:

    @Deprecated
    public static void setFactory(
            @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
        IMPL.setFactory(inflater, factory);
    }

    public static void setFactory2(
            @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
        IMPL.setFactory2(inflater, factory);
    }
複製程式碼

可以看到 setFactory 已經被標記為過時,更建議使用 setFactory2 方法。

    static final LayoutInflaterCompatBaseImpl IMPL;
    static {
        if (Build.VERSION.SDK_INT >= 21) {
            IMPL = new LayoutInflaterCompatApi21Impl();
        } else {
            IMPL = new LayoutInflaterCompatBaseImpl();
        }
    }
    
    @RequiresApi(21)
    static class LayoutInflaterCompatApi21Impl extends LayoutInflaterCompatBaseImpl {
        @SuppressWarnings("deprecation")
        @Override
        public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
            inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
        }

        @Override
        public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
            inflater.setFactory2(factory);
        }
    }
複製程式碼

這裡呼叫 setFactory 實際上還是呼叫的 setFactory2 方法,同時將 LayoutInflaterFactory 包裹為 Factory2Wrapper。

4、LayoutInflater.setFactory 使用注意

如果我們將LayoutInflater.setFactory 挪到 super.onCreate 的後面可以嗎? 程式竟然報錯了,我們看下Log:

    Process: com.example.teststart, PID: 24132
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.teststart/com.example.teststart.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2876)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2941)
     Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
        at android.view.LayoutInflater.setFactory2(LayoutInflater.java:317)
        at com.example.teststart.MainActivity.onCreate(MainActivity.java:18)
        at android.app.Activity.performCreate(Activity.java:6765)
複製程式碼

說明是 LayoutInflater 已經被設定了一個 Factory,而我們再設定的時候就會報錯。我們跟蹤下 LayoutInflater.from(this).setFactory2 方法

    private boolean mFactorySet;
    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }
複製程式碼

可以通過這個 mFactorySet 變數看出 setFactory2 方法只能被呼叫一次,重複設定則會丟擲異常。那Factory2是被誰設定了呢?

我們來看下 AppCompatActivity 的 onCreate 方法,

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }
複製程式碼

其中會呼叫 delegate.installViewFactory(); 最終會呼叫到 AppCompatDelegateImplV9 的 installViewFactory方法;

    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity`s LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat`s");
            }
        }
    }
複製程式碼

可以看到:

  • 如果 layoutInflater.getFactory() 為空,則 AppCompatActivity 會自動設定一個 Factory2,難怪我們在 super.onCreate 之後呼叫會報錯
  • 細心的小夥伴肯定也明白了,為什麼我們在 super.onCreate 之前設定 Factory之後,系統再次設定 Factory 的時候不會丟擲異常

備註:聰明的小夥伴肯定能想到使用反射來改變修改 LayoutInflater 中的 mFactorySet 為false就可以在 super.onCreate 之後再次設定 Factory了。

5、AppCompatActivity 為什麼 setFactory

那麼為什麼 AppCompatActivity 會自動設定一個 Factory呢?順著 AppCompatDelegateImplV9 的 installViewFactory方法繼續跟蹤,走到了 onCreateView 方法,它最終會呼叫到 AppCompatViewInflater 的 createView 方法

    public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        View view = null;
        // We need to `inject` our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            ......
        }
        return view;
    }
複製程式碼

原來 AppCompatActivity 設定 Factory 是為了將一些 widget 自動變成 相容widget (例如將 TextView 變成 AppCompatTextView)以便於向下相容新版本中的效果,在高版本中的一些 widget 新特性就是這樣在老版本中也能展示的

那如果我們設定了自己的 Factory 豈不是就避開了系統的相容?其實系統的相容我們仍然可以儲存下來,因為系統是通過 AppCompatDelegate.onCreateView 方法來實現 widget 相容的,那我們就可以在設定 Factory 的時候先呼叫 AppCompatDelegate.onCreateView 方法,再來做我們的處理。

    LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    
            // 呼叫 AppCompatDelegate 的createView方法
            getDelegate().createView(parent, name, context, attrs);
            // 再來執行我們的定製化操作
            return null;
        }
    
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    });
複製程式碼

6、總結

  1. LayoutInflater.Factory的意義:通過 LayoutInflater 建立 View 時候的一個回撥,可以通過 LayoutInflater.Factory 來改造或定製建立 View 的過程。
  2. LayoutInflater.setFactory 使用注意:不能在 super.onCreate 之後設定。
  3. AppCompatActivity 為什麼 setFactory ?向下相容新版本中的效果。

廣告時間

今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都需要,業務增長快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、非頻繁跳槽(如兩年兩跳),歡迎加我的微信詳聊:KOBE8242011

歡迎關注

相關文章