banner
jzman

jzman

Coding、思考、自觉。
github

LayoutInflater.Factory使用及源碼解析

PS:事實證明,恐懼什麼你就會逃避什麼,一定要相信自己。

LayoutInflater.Factory 是提供給你的一个加載佈局使用的回調介面 (Hook),可以使用 LayoutInflater.Factory 來自定義佈局文件,實際上就是可以在 LayoutInflater.Factory 的回調中可以根據對應的 Tag 來修改某個 View,然後返回出去,LayoutInflater.Factory 源碼如下:

// LayoutInflater.java
public interface Factory {
    /**
     * @param name Tag名稱,如TextView
     * @param context 上下文環境
     * @param attrs Xml屬性
     *
     * @return View 新創建的View,如果返回null,則Hook無效
     */
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

public interface Factory2 extends Factory {
    // since API 11,多添加了一個參數parent
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

已知在佈局文件加載時調用 createViewFromTag 方法創建 View 的,它會依次判斷 mFactory、mFactory2、mPrivateFactory 是否為 null,如果創建出的 View 不為 null,則直接返回該 View,源碼如下:

// LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
    //...
  
    View view;
    // 依次判斷mFactory、mFactory2、mPrivateFactory是否為null,如果創建出的View不為null,則直接返回該View
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    
    // 從佈局文件中解析出View
    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }

    return view;
   // ...
}

前面知道 LayoutInflater.Fractory 是用來對佈局文件進行修改的,此處這樣做肯定是某個地方已經設置 mFactory 或 mFactory2,查看源碼知 setFactory 和 setFactory2 在 LayoutInflaterCompat 被調用,LayoutInflaterCompat 為了達到兼容的目的,對 setFactory 和 setFactory2 進行了處理,其中 setFactory 已經被廢棄,源碼如下:

// LayoutInflaterCompat.java
@Deprecated
public static void setFactory(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
    if (Build.VERSION.SDK_INT >= 21) {
        inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
    } else {
        final LayoutInflater.Factory2 factory2 = factory != null
                ? new Factory2Wrapper(factory) : null;
        inflater.setFactory2(factory2);

        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory2);
        }
    }
}

public static void setFactory2(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
    inflater.setFactory2(factory);

    if (Build.VERSION.SDK_INT < 21) {
        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory);
        }
    }
}

從 setFactory2 使用了 forceSetFactory2 方法裡面通過反射強制設置了 LayoutInflater 的 mFactory2 屬性值,源碼如下:

// LayoutInflaterCompat.java
private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
    if (!sCheckedField) {
        try {
            sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
            sLayoutInflaterFactory2Field.setAccessible(true);
        } catch (NoSuchFieldException e) {
            Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class "
                    + LayoutInflater.class.getName()
                    + "; inflation may have unexpected results.", e);
        }
        sCheckedField = true;
    }
    if (sLayoutInflaterFactory2Field != null) {
        try {
            sLayoutInflaterFactory2Field.set(inflater, factory);
        } catch (IllegalAccessException e) {
            Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater "
                    + inflater + "; inflation may have unexpected results.", e);
        }
    }
}

那麼 LayoutInflater 的 setFactory2 方法在哪調用的呢,查看源碼知分別在 AppCompatDelegateImpl 和 Fragment 裡面調用過,這裡以 AppCompatDelegateImpl 為例來進行分析,查看源碼可知,AppCompatDelegateImpl 裡面有個方法 installViewFactory 裡面統一設置了 LayoutInflater.Factory,源碼如下:

// AppCompatDelegateImpl.java
@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        // 統一設置LayoutInflater.Factory
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
         // 如果自行設置了LayoutInflater.Factory則失去新特性的支持並打印日誌
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

繼續查看 installViewFactory 方法是在 AppCompatActivity 的 onCreate 方法中調用的,源碼如下:

// AppCompatActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    // AppCompatActivity統一設置LayoutInflater.Factory
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && mThemeId != 0) {
        if (Build.VERSION.SDK_INT >= 23) {
            onApplyThemeResource(getTheme(), mThemeId, false);
        } else {
            setTheme(mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
}

從 Android 5.0 開始 Google 添加了很多新特性,進而為了能夠向前兼容,又推出了 support.v7 包,裡面就有上文中提到的 AppcompatActivity,這也就是之前 createViewFromTag 中要進行 mFactory1、mFactory2 等的判斷,先從默認 Factory 實現的 onCreateView 方法獲取 View,如果沒有設置過 LayoutInflater.Factory 則直接從佈局文件中解析出 View。

因為一般創建的 Activity 都是繼承 AppCompatActivity,也就默認設置了 LayoutInflater.Factory,當使用 LayoutInflater 加載佈局文件時就會調用其 onCreateView 方法,查看源碼知 AppCompatDelegateImpl 和 Activity 都實現了 LayoutInflater.Factory2 介面,這裡查看 AppCompatDelegateImpl 中的具體實現,源碼如下:

/**
 * From {@link LayoutInflater.Factory2}.
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

/**
 * From {@link LayoutInflater.Factory2}.
 */
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return onCreateView(null, name, context, attrs);
}

可知最終調用的是 AppCompatDelegateImpl 中的 createView 方法,源碼如下:

// AppCompatDelegateImpl.java
@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        String viewInflaterClassName =
                a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
        if ((viewInflaterClassName == null)
                || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
            // Either default class name or set explicitly to null. In both cases
            // create the base inflater (no reflection)
            mAppCompatViewInflater = new AppCompatViewInflater();
        } else {
            try {
                Class viewInflaterClass = Class.forName(viewInflaterClassName);
                mAppCompatViewInflater =
                        (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                .newInstance();
            } catch (Throwable t) {
                Log.i(TAG, "Failed to instantiate custom view inflater "
                        + viewInflaterClassName + ". Falling back to default.", t);
                mAppCompatViewInflater = new AppCompatViewInflater();
            }
        }
    }

    boolean inheritContext = false;
    if (IS_PRE_LOLLIPOP) {
        inheritContext = (attrs instanceof XmlPullParser)
                // If we have a XmlPullParser, we can detect where we are in the layout
                ? ((XmlPullParser) attrs).getDepth() > 1
                // Otherwise we have to use the old heuristic
                : shouldInheritContext((ViewParent) parent);
    }
    // 關鍵位置
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

上面代碼中都是先獲取定義的屬性集合,然後看有沒有自定的 Inflater,如果自定義了就根據完整類名反射創建出 AppCompatViewInflater 對象,反之則創建默認的 AppCompatViewInflater,最後調用對應 Inflater 的 createView 方法。

看到這裡,明明可以直接使用 AppCompatViewInflater 就可以了,為什麼還要這麼繁瑣,從這裡就可以看出這裡由很強的擴展性,可以自定義 Inflater 來替換官方提供的 AppCompatViewInflater,繼續查看 AppCompatViewInflater 的 createView 方法,源碼如下:

// AppCompatViewInflater.java
final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;

    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }

    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Button":
            view = createButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "EditText":
            view = createEditText(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Spinner":
            view = createSpinner(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageButton":
            view = createImageButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckBox":
            view = createCheckBox(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RadioButton":
            view = createRadioButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckedTextView":
            view = createCheckedTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "AutoCompleteTextView":
            view = createAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "MultiAutoCompleteTextView":
            view = createMultiAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RatingBar":
            view = createRatingBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "SeekBar":
            view = createSeekBar(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            view = createView(context, name, attrs);
    }

    if (view == null && originalContext != context) {
        // 如果不能根據name創建出View,則根據name去用反射去創建View
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        checkOnClickListener(view, attrs);
    }
    return view;
}

上述代碼根據組件的名稱將 app 下面的組件替換成了對應兼容版本的組件,如 TextView 替換成了 AppCompatTextView,如果不能根據 name 創建出 View,則調用 createViewFromTag 去創建 View,源碼如下:

// AppCompatViewInflater.java
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    try {
        mConstructorArgs[0] = context;
        mConstructorArgs[1] = attrs;
        // 如果name中沒有出現點號,則嘗試添加系統組件前綴
        if (-1 == name.indexOf('.')) {
            for (int i = 0; i < sClassPrefixList.length; i++) {
                final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                if (view != null) {
                    return view;
                }
            }
            return null;
        } else {// 不添加前綴
            return createViewByPrefix(context, name, null);
        }
    } catch (Exception e) {
        return null;
    } finally {
        // Don't retain references on context.
        mConstructorArgs[0] = null;
        mConstructorArgs[1] = null;
    }
}

可知這裡的 createViewFromTag 發明就是嘗試在 name 上去添加系統組件的前綴,最後都要去調用 createViewByPrefix 去創建 View,createViewByPrefix 裡面使用過反射去創建對象,源碼如下:

// AppCompatViewInflater.java
private View createViewByPrefix(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
    // 借鑒緩存的使用
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    try {
        if (constructor == null) {
            Class<? extends View> clazz = context.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            constructor = clazz.getConstructor(sConstructorSignature);
            sConstructorMap.put(name, constructor);
        }
        constructor.setAccessible(true);
        return constructor.newInstance(mConstructorArgs);
    } catch (Exception e) {
        return null;
    }
}

這裡除了使用反射去創建 View 之外,為了降低性能的消耗還使用 HaskMap 做了 View 的構造方法的緩存,先從緩存中取對應的構造方法,緩存中不存在對應的構造方法實例才會去反射構造方法實例,最終通過反射完成 View 的創建,至此如果設置了 LayoutInflater.Factory,則其創建 View 的過程基本如上。

從上文的分析我們知道當加載佈局文件時會先看有沒有設置 LayoutInflater.Factory,如果已經設置了則會由具體的 LayoutInflater.Factory 的規則去創建 View,否則就直接去使用反射去創建 View,所以可以自定義 LayoutInflater.Factory 來按照自己的規則去創建 View,如下:

// 第一種方式
@Override
protected void onCreate(Bundle savedInstanceState) {
    LayoutInflater layoutInflater =  getLayoutInflater();
    //設置LayoutInflater.Factory,必須在super.onCreate之前設置
    layoutInflater.setFactory(new LayoutInflater.Factory() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            // 將TextView替換成Button返回
            if (name.equals("TextView")){
                Button button = new Button(MainActivity.this);
                button.setText("我被替換成了Button");
                return button;
            }
            return null;
        }
    });
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

通過上述代碼,當在加載佈局文件解析 View 的時候,所有的 TextView 都會被替換成 Button,其他的 View 不也會自動轉換為 AppCompatXxx 系列的 View,這樣就使得當自行設置 LayoutInflater.Factory 會之後其他 View 會失去新特性的支持,對應 Log 內容如下:

I/AppCompatDelegate: The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's

那麼如何保證能夠繼續使用新特性,又能夠實現 View 的替換呢,通過前面分析我們知道系統組件的替換調用是 AppCompatDelegateImpl 的 createView 方法,而這個方法又是 public 的,所以只要能夠在自定義之後繼續調用 AppCompatDelegateImpl 的 createView 方法就能夠保證其他 View 不被影響,如下:

// 第一種方式
LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

        if (name.equals("TextView")){
            Button button = new Button(MainActivity.this);
            button.setText("我被替換成了Button");
            return button;
        }

        AppCompatDelegate compatDelegate = getDelegate();
        View view = compatDelegate.createView(parent,name,context,attrs);
        return view;
    }

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

下面分別是默認佈局以及上述兩種不同替換方式的佈局視圖結構,如下圖所示:

此外,可以使用 LayoutInflater.Factory 來全局替換字體、換膚等功能進行延伸,關於 LayoutInflater.Factory 的源碼解析就到此為止。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。