LayoutInflater Factory使用

1,613 阅读6分钟

背景

通常我们hook布局解析有几种方式。

  1. 通过asm插桩的方式。代码中一般会写LayoutInflater.from(Context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)。通过asm hook该处,调用我们预定义的代码,然后就可以拿到view做一些想做的事情;
  2. 利用LayoutInflater中两个成员变量mFactory和mFactory2实例。其实AppCompatActivity就是这么做的。AppCompatActivity是为了实现TextView替换为AppCompatTextView实现一些TextView没有的效果,那么我们也可以利用LayoutInflater的factory实现我们想要的一些功能。

本文我们着重分析下LayoutInflater的两个Factory,以及AppCompatActivity的具体实现逻辑。

LayoutInflater inflate方法实现逻辑

在说Factory之前有一个细节我们需要注意下。 LayoutInflater.from(context) 这里获取是根据context获取的,不同的context是不同的实例,因此在setFactory的时候,需要针对一个基类的activity来处理,不可以设置全局。

LayoutInflater inflate核心代码如下

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    advanceToRootNode(parser);
    final String name = parser.getName();

    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);//@1

    ViewGroup.LayoutParams params = null;

    if (root != null) {
        // Create layout params that match root, if supplied
        params = root.generateLayoutParams(attrs);                          //@2
        if (!attachToRoot) {
            // Set the layout params for temp if we are not
            // attaching. (If we are, we use addView, below)
            temp.setLayoutParams(params);
        }
    }

    // Inflate all children under temp against its context.
    rInflateChildren(parser, temp, attrs, true);                            //@3

    // We are supposed to attach all the views we found (int temp)
    // to root. Do that now.
    if (root != null && attachToRoot) {
        root.addView(temp, params);                                         //@4
    }

    // Decide whether to return the root that was passed in or the
    // top view found in xml.
    if (root == null || !attachToRoot) {
        result = temp;
    }

    return result;
}

inflate过程大概经历4件事情, @1 createViewFromTag 创建view @2 设置LayoutParams @3 rInflateChildren解析子view @4 addView添加到布局中

createViewFromTag中创建view大致逻辑如下

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    View view = tryCreateView(parent, name, context, attrs);

    if (view == null) {
        if (-1 == name.indexOf('.')) {
            view = onCreateView(context, parent, name, attrs);
        } else {
            view = createView(context, name, null, attrs);
        }
    }

    return view;
}
public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {

    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    return view;
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    if (constructor == null) {
        // Class not found in the cache, see if it's real, and try to add it
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class); // @1

        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        
    }

    Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = viewContext;
    Object[] args = mConstructorArgs;
    args[1] = attrs;

    try {
        final View view = constructor.newInstance(args);            // @2
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        return view;
    } finally {
        mConstructorArgs[0] = lastContext;
    }
}

createViewFromTag逻辑如下:

  1. 尝试tryCreateView,依次尝试factory2和factory创建view;
  2. 如果tryCreateView为空,则调用onCreateView或者createView,两者区别为onCreateView是没有.,即系统View如TextView,onCreateView最终会对name之前拼接android.view.,最终还会调到createView中;
  3. createView逻辑为调用class.forName获取类,然后获取类的构造方法,创建view实例。

AppCompatActivity和LayoutInflater的两个Factory

两个Factory的区别

mFactory2和mFactory的区别是调用方法的区别,mFactory2多了一个parent参数,即为当前创建的view的父view。

installViewFactory设置Factory

AppCompatActivity中onCreate时会执行installViewFactory方法,内部先判断factory是否为空,如果为空则调用LayoutInflaterCompat.setFactory2方法,否则检查factory2是否是AppCompatDelegateImpl,如果不是则打印日志。这里没有什么约束条件。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    super.onCreate(savedInstanceState);
}
@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}
LayoutInflaterCompat.java
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);
        }
    }
}

LayoutInflaterCompat提供了对SDK小于21的支持,因此一般通过该方法进行设置factory,我们再看下inflater.setFactory2的具体逻辑。

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);
    }
}

这里会有强制限制的逻辑,也就是setFactory只能设置一次,那么继承AppCompatActivity的activity中只能通过反射的方式去设置factory了。 如果mFactory为空,则直接将两者都设置为需要替换的factory,否则会创建FactoryMerger,包裹之前的mFactory和mFactory2。使用优先级是先调用需要替换的factory,如果创建view为空,则调用origin的factory。具体逻辑参照下面代码。

private static class FactoryMerger implements Factory2 {
    private final Factory mF1, mF2;
    private final Factory2 mF12, mF22;

    FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
        mF1 = f1;
        mF2 = f2;
        mF12 = f12;
        mF22 = f22;
    }

    @Nullable
    public View onCreateView(@NonNull String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        View v = mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF2.onCreateView(name, context, attrs);
    }

    @Nullable
    public View onCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context, @NonNull AttributeSet attrs) {
        View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
                : mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
                : mF2.onCreateView(name, context, attrs);
    }
}

AppCompatActivity替换的Factory执行逻辑

设置完factory后,我们看下factory的具体做了什么事情。

AppCompatDelegateImpl.java
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

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

@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
    }

    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 */
    );
}

AppCompatDelegateImpl最终交给了AppCompatViewInflater来处理。

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) {
        // We then apply the theme on the context, if specified
        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;
        case "ToggleButton":
            view = createToggleButton(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }

    if (view == null && originalContext != context) {
        // If the original context does not equal our themed context, then we need to manually
        // inflate it using the name so that android:theme takes effect.
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}

上面代码是AppCompatViewInflater的核心逻辑了,即根据name来创建对应的CompatView。

demo

了解了LayoutInflater的两个Factory以及AppCompatActivity的实现逻辑,我们可以简单写个demo。如果activity继承了AppCompatActivity,因为AppCompatActivity已经设置过了,我们只能通过反射的方式来实现了。

public class LayoutInflaterActivity extends AppCompatActivity {
    private static final String TAG = "LayoutInflaterActivity";

    private static void setLayoutInflaterFac(Context context) {
        final LayoutInflater.Factory f1 = LayoutInflater.from(context).getFactory();
        final LayoutInflater.Factory2 f2 = LayoutInflater.from(context).getFactory2();

        LayoutInflater.Factory2 fNew = new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                View view = f2.onCreateView(parent, name, context, attrs);
                handleView(view);
                return view;
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                View view = f1.onCreateView(name, context, attrs);
                handleView(view);
                return view;
            }

            private void handleView(View view) {
                Log.i(TAG, "handleView: " + view);
            }
        };

        try {
            Field sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
            sLayoutInflaterFactory2Field.setAccessible(true);

            sLayoutInflaterFactory2Field.set(LayoutInflater.from(context), fNew);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setLayoutInflaterFac(this);

        setContentView(R.layout.activity_layout_inflater);

        inflateViewStub();
    }

    private void inflateViewStub() {
        ViewStub viewStub = findViewById(R.id.layout_inflater_view_stub);
        viewStub.inflate();
    }
}

参考文章

  1. dandanlove.com/2017/11/15/…
  2. www.jianshu.com/p/9a0cc0a56…