LayoutInflater Factory使用

背景

通常我们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核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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大致逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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,如果不是则打印日志。这里没有什么约束条件。

1
2
3
4
5
6
7
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
1
2
3
4
5
6
7
8
9
10
11
12
@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");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的具体逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。具体逻辑参照下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的具体做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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已经设置过了,我们只能通过反射的方式来实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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. http://dandanlove.com/2017/11/15/layoutinflater-factory/
  2. https://www.jianshu.com/p/9a0cc0a5649c