banner
jzman

jzman

Coding、思考、自觉。
github

LayoutInflater.Factoryの使用とソースコードの解析

PS:事実が証明するように、恐れているものから逃げることになります。自分を信じることが大切です。

LayoutInflater.Factory は、レイアウトを読み込むためのコールバックインターフェイス(フック)であり、LayoutInflater.Factory を使用してレイアウトファイルをカスタマイズできます。実際には、LayoutInflater.Factory のコールバック内で対応するタグに基づいて特定のビューを変更し、そのビューを返すことができます。LayoutInflater.Factory のソースコードは以下の通りです:

// LayoutInflater.java
public interface Factory {
    /**
     * @param name タグ名、例えば TextView
     * @param context コンテキスト
     * @param attrs Xml属性
     *
     * @return View 新しく作成されたビュー。null を返すとフックは無効になります。
     */
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

public interface Factory2 extends Factory {
    // API 11 以降、親を追加しました
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

レイアウトファイルが読み込まれる際に createViewFromTag メソッドが呼び出され、ビューを作成します。このメソッドは、mFactory、mFactory2、mPrivateFactory が null でないかを順に確認し、作成されたビューが null でない場合はそのビューを直接返します。ソースコードは以下の通りです:

// LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
    //...
  
    View view;
    // mFactory、mFactory2、mPrivateFactory が null でないかを順に確認し、作成されたビューが null でない場合はそのビューを直接返します
    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);
    }
    
    // レイアウトファイルからビューを解析します
    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.Factory はレイアウトファイルを変更するために使用されます。この場合、どこかで 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) {
            // 統合されたファクトリは現在 getFactory() に設定されていますが、getFactory2() には設定されていません(v21以前)。
            // 統合されたファクトリを mFactory2 に強制的に設定しようとします
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // それ以外の場合は、元のラップされた 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) {
            // 統合されたファクトリは現在 getFactory() に設定されていますが、getFactory2() には設定されていません(v21以前)。
            // 統合されたファクトリを mFactory2 に強制的に設定しようとします
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // それ以外の場合は、元のラップされた 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 'mFactory2' フィールドが見つかりませんでした "
                    + LayoutInflater.class.getName()
                    + "; インフレーションは予期しない結果になる可能性があります。", e);
        }
        sCheckedField = true;
    }
    if (sLayoutInflaterFactory2Field != null) {
        try {
            sLayoutInflaterFactory2Field.set(inflater, factory);
        } catch (IllegalAccessException e) {
            Log.e(TAG, "forceSetFactory2 LayoutInflater "
                    + inflater + " に Factory2 を設定できませんでした; インフレーションは予期しない結果になる可能性があります。", 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, "Activity の LayoutInflater にはすでにファクトリがインストールされているため、AppCompat のインストールはできません。");
        }
    }
}

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 などの判断を行う理由です。デフォルトのファクトリが実装する onCreateView メソッドからビューを取得し、LayoutInflater.Factory が設定されていない場合は、レイアウトファイルから直接ビューを解析します。

一般的に作成されるアクティビティは AppCompatActivity を継承しているため、デフォルトで LayoutInflater.Factory が設定されます。LayoutInflater を使用してレイアウトファイルを読み込むと、その onCreateView メソッドが呼び出されます。ソースコードを確認すると、AppCompatDelegateImpl と Activity の両方が LayoutInflater.Factory2 インターフェイスを実装していることがわかります。ここでは AppCompatDelegateImpl の具体的な実装を確認します。ソースコードは以下の通りです:

/**
 * {@link LayoutInflater.Factory2} からの実装。
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

/**
 * {@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)) {
            // デフォルトのクラス名または明示的に null に設定された場合、どちらの場合も
            // 基本の inflater を作成します(リフレクションなし)
            mAppCompatViewInflater = new AppCompatViewInflater();
        } else {
            try {
                Class viewInflaterClass = Class.forName(viewInflaterClassName);
                mAppCompatViewInflater =
                        (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                .newInstance();
            } catch (Throwable t) {
                Log.i(TAG, "カスタムビューインフレーターのインスタンス化に失敗しました "
                        + viewInflaterClassName + "。デフォルトに戻ります。", t);
                mAppCompatViewInflater = new AppCompatViewInflater();
            }
        }
    }

    boolean inheritContext = false;
    if (IS_PRE_LOLLIPOP) {
        inheritContext = (attrs instanceof XmlPullParser)
                // XmlPullParser がある場合、レイアウト内の位置を検出できます
                ? ((XmlPullParser) attrs).getDepth() > 1
                // それ以外の場合は古いヒューリスティックを使用する必要があります
                : shouldInheritContext((ViewParent) parent);
    }
    // 重要な位置
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* L より前の android:theme を読み取るだけ */
            true, /* レガシー理由から常にアプリのテーマをフォールバックとして読み取る */
            VectorEnabledTintResources.shouldBeUsed() /* 有効な場合のみコンテキストをラップします */
    );
}

上記のコードでは、まず定義された属性集合を取得し、カスタムのインフレーターがあるかどうかを確認します。カスタムがあれば、その完全なクラス名を使用してリフレクションで AppCompatViewInflater オブジェクトを作成し、そうでなければデフォルトの AppCompatViewInflater を作成します。最後に、対応するインフレーターの createView メソッドを呼び出します。

ここまでの内容から、AppCompatViewInflater を直接使用することができるはずですが、なぜこれほど複雑にする必要があるのでしょうか。ここから、非常に強い拡張性があることがわかります。カスタムインフレーターを使用して、公式の 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;

    // 親のコンテキストを使用して、Lollipop の android:theme 属性をビュー階層に伝播させることができます
    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;

    // 標準のフレームワークバージョンの代わりに、tint 対応のビューを「注入」する必要があります
    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 に基づいてビューを作成できない場合は、name を使用してリフレクションでビューを作成します
        view = createViewFromTag(context, name, attrs);
    }

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

上記のコードでは、コンポーネントの名前に基づいてアプリ内のコンポーネントを対応する互換バージョンのコンポーネントに置き換えています。たとえば、TextView は AppCompatTextView に置き換えられます。name に基づいてビューを作成できない場合は、createViewFromTag を呼び出してビューを作成します。ソースコードは以下の通りです:

// 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 {
        // コンテキストへの参照を保持しないでください。
        mConstructorArgs[0] = null;
        mConstructorArgs[1] = null;
    }
}

ここでの createViewFromTag の発明は、name にシステムコンポーネントのプレフィックスを追加しようとすることです。最終的には createViewByPrefix を呼び出してビューを作成します。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;
    }
}

ここでは、リフレクションを使用してビューを作成するだけでなく、パフォーマンスの消費を減らすために HashMap を使用してビューのコンストラクタメソッドをキャッシュしています。まずキャッシュから対応するコンストラクタメソッドを取得し、キャッシュに存在しない場合のみリフレクションでコンストラクタメソッドのインスタンスを取得します。最終的にリフレクションを通じてビューの作成を完了します。これにより、LayoutInflater.Factory が設定されている場合、そのビューの作成プロセスは基本的に上記のようになります。

前述の分析から、レイアウトファイルを読み込む際に LayoutInflater.Factory が設定されているかどうかを確認し、すでに設定されている場合は具体的な LayoutInflater.Factory のルールに従ってビューを作成します。そうでない場合は、リフレクションを使用してビューを作成します。したがって、独自のルールに従ってビューを作成するために LayoutInflater.Factory をカスタマイズできます。以下はその例です:

// 第一の方法
@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);
}

上記のコードにより、レイアウトファイルを解析する際に、すべての TextView が Button に置き換えられます。他のビューは自動的に AppCompatXxx シリーズのビューに変換されます。これにより、LayoutInflater.Factory を独自に設定した場合、他のビューは新しい機能のサポートを失います。対応するログ内容は以下の通りです:

I/AppCompatDelegate: Activity の LayoutInflater にはすでにファクトリがインストールされているため、AppCompat のインストールはできません。

新機能を引き続き使用しながら、ビューの置き換えを実現するには、前述の分析から、システムコンポーネントの置き換えは AppCompatDelegateImpl の createView メソッドによって行われることがわかります。このメソッドは public であるため、カスタム後に AppCompatDelegateImpl の createView メソッドを呼び出すことで、他のビューに影響を与えずに新機能を保証できます。以下はその例です:

// 第一の方法
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);
    }
});

以下は、デフォルトのレイアウトと上記の 2 つの異なる置き換え方法によるレイアウトビュー構造の図です:

さらに、LayoutInflater.Factory を使用してフォントのグローバルな置き換えやスキン変更などの機能を拡張することができます。LayoutInflater.Factory のソースコード解析はここまでです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。