LayoutInflater源码分析

不起眼的小角色

在上一篇博客中,说到 Android 使用 xml 编写布局,可以起到一定的解耦效果,那么问题来了,Android 是怎么解析 xml 那呢?答案很显然,就是使用 LayoutInflater 这个类,这个类看似很不起眼,但是其实它无处不在,比如我们 Activity 的 setContentView() 方法中,Activity 的 getLayoutInflater() 方法,所以我觉得 LayoutInflater 有点像一个默默付出却又不被大家知道的一个类,下面我们就看分析分析这个不起眼的小角色吧!

LayoutInflater 概览

先说说 LayoutInflater 是干嘛的吧,一般来说,在 Android 中如果我们想把一个布局文件 (Layout 文件) 转换成一个 View 对象,那么唯一的办法就是使用 LayoutInflater,通俗点说就是解析 xml 并转换成 Android 中的 View 对象,其实一般返回的都是 ViewGroup 对象。

Inflater 这个单词翻译过来意思为打气筒,布局打气筒?!听起来让人感觉非常怪异,按照一般的思路,如果一个类是用来解析 xml 的,那么应该叫做 XmlParser 或者相似的才对,具体原因我们会在后面解析。

LayoutInflater 有好几种使用姿势,但是归根到底其实都是一种姿势的扩展,具体如下:

1
2
3
4
5
6
7
8
View view = null;
//1. View.Inflater
view = View.inflate(context, layoutResId, parent)
//2. LayoutInflater.form().inflate()
view = LayoutInflater.form(context).inflate(layoutResId, parent, false)
//3. Activity 的 getLayoutInflater() 方法
view = getLayoutInflater().inflate(layoutResId, parent, false)
//...

SystemService

查看上面这些方法的源码,最终都会先调用 LayoutInflater.from(context),然后在调用 from() 方法返回的 LayoutInflater 对象的 inflate() 方法,我们先看看 LayoutInflater 的 from() 方法,源码如下:

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
//LayoutInflater.java
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

//ContextImpl.java
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}

//SystemServiceRegistry.java
static{
//...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
//...
}

public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}

通过对上面代码的大致分析,我们可以得出以下几个结论:

  1. LayoutInflater 居然是个 SystemService ?! 喵喵喵? 解析个 xml 居然还用到系统服务了,听上去感觉挺高端的!
  2. 通过名字判断 CachedServiceFetcher 这个类一定是有缓存机制的。
  3. 我们平时使用的 LayoutInflater 对象其实是 PhoneLayoutInfalter 类的实例。

为什么 LayoutInflater 是系统的服务,原因很简单,解析 xml 是一件很频繁的一件事,如果每次解析都创建一个解析器,明显是不好的,关键就在于,这个缓存机制 CachedServiceFetcher 究竟是怎么缓存的,在 ContextImpl 可以找到答案,具体代码如下:

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

//ContextImpl.java
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();

//SystemServiceRegistry.java
public static Object[] createServiceCache() {
return new Object[sServiceCacheSize];
}

static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex;

public CachedServiceFetcher() {
mCacheIndex = sServiceCacheSize++;
}

@Override
@SuppressWarnings("unchecked")
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
synchronized (cache) {
Object service = cache[mCacheIndex];
if (service == null) {
try {
service = createService(ctx);
cache[mCacheIndex] = service;
} catch (ServiceNotFoundException e) {
onServiceNotFound(e);
}
}
return (T)service;
}
}

public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
}

从上面的代码中,可以看到,mServiceCache 是一个 Object 类型的数组,而在 SystemServiceRegistry 的 CachedServiceFetcher 这个内部类中,做了很多多线程的判断,我们都知道 HashMap 不是线程安全的,所以在 CachedServiceFetcher 的 getService() 方法中没有直接返回一个对象,而是将对象存储在名为 mServiceCache 的数组中,然后加了锁,并且添加了一个名为 mCacheIndex 的变量,这个变量是用来处理在多个线程的情况下,每个线程锁获取到的 SystemService 对象都是相互独立的。

那么问题来了,LayoutInflater 的实例究竟在整个 APP 中存在多少个呢?其实通过对上面的代码的分析,这个问题的答案就很简单了,因为每个 Activity 本身也是一个 Context 对象就表示对应一个 ContextImpl 类的实例,所以一个 APP 中 LayoutInflater 的数量和 Context 是一样的(不考虑多线程导致的多实例问题,如上面 mCacheIndex),不信?我们下面做个简单的实验就知道了。代码如下:

MainActivity

Main2Activity

通过打断点调试,我们可以看到,不同的 Context 对象所获取的 LayoutInflater 对象的实例是不一样的,而如果我们使用 applicationContext 这个 Context 实例去获取的,怎么获取都是同一个 LayoutInflater。

我们来猜猜为什么 Android 为什么要这么设计,为什么不把 LayoutInflater 设计成一个单例的(不考虑多线程的情况)呢?,我的看法如下:

  1. 因为每个 Activity 是可以设置一些类似主题的属性的,这些属性是一可以改变 Layout 相关的方法所返回的对象,比如在 Activity 中我们可以调用 setTheme() 方法来实现切换主题的效果,但是 setTheme() 这个方法只能改变当前 Activity 的主题,不能改变所有 Activity 所有的主题,所以 LayoutIflater 对于同一个 Activity 是单例的。
  2. 如果不把 LayoutInflater 设置成单例的,可能会带来很多不必要的性能损耗,比如上面提到的主题相关的配置,如果反复的去创建 LayoutInflater 类的实例,每次创建又要去读取这些配置,这样就会造成了很多重复的工作,而且频繁的创建对象/销毁对象也是一个不好的习惯,会造成内存抖动,从而频繁的触发 GC 最终导致 UI 卡顿。

简而言之,在一个 Activity 中我们可以放心大胆的使用 LayoutInflater.from() 来创建 LayoutInflater 的实例,不用担心 from() 方法会造成什么性能问题。至于在拿不到 Activity 的地方,也可使用 Application 的实例来创建 LayoutInflater 的实例,和使用 Activity 实例创建 LayoutInflater 的效果是一样的。

inflate()

创建完 LayoutInflater 对象之后,我们通常是调用它的 inflate() 方法,这个方法一共有四个重载,如下:

1
2
3
4
inflate(XmlPullParser, ViewGroup) //1
inflate(XmlPullParser, ViewGroup, Boolean)//2
inflate(Int, ViewGroup)//3
inflate(Int, ViewGroup, Boolean)//4

通过刚才函数的参数,就能知道,第一个重载一定在内部调用了第二个重载,第三个重载一定在内部调用了第四个重载,源码如下:

1
2
3
4
5
6
7
public View inflate(XmlPullParser parser, @Nullable ViewGroup root){
inflate(parser, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

通过源码能够发现,第一个重载和第三个重载在内部直接调用了第一个重载和第四个重载,并且第三个 Boolean 类型的参数其实就是根据第二个 ViewGroup 推断出来的。在看看第三个重载,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}

final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

通过上面的源码可以发现,其实第三个重载也是调用了第一个重载,只是在内部通过 Resource 的 getLayout() 方法创建了一个 XmlResourceParser,我们先来看看这个方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

//Resources.java
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}

getLayout() 方法调用了 loadXmlResourceParser() 方法,并且传入的 type 为 layout,在该方法中,首先会通过 obtainTempTypedValue() 方法创建一个 TypeValue 对象,并且在方法的结尾处的 finally 代码块中调用了 releaseTempTypedValue() 方法,很显然这两个方法是搭配使用的,我们来看看它们的定义,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Resources.java
private TypedValue obtainTempTypedValue() {
TypedValue tmpValue = null;
synchronized (mTmpValueLock) {
if (mTmpValue != null) {
//mTmpValue 如果被一个线程使用了,那么就将 mTmpValue 置为 null,
//这样就保证下一个线程操作的 TypeValue 对象是一个新的。
tmpValue = mTmpValue;
mTmpValue = null;
}
}
if (tmpValue == null) {
return new TypedValue();
}
return tmpValue;
}
private void releaseTempTypedValue(TypedValue value) {
synchronized (mTmpValueLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
}

通过源码我们可以看出,这两个方法的作用就是线程安全,保证不会出现多个线程操作同一个 TypeValue 的情况,并且还为 TypeValue 提供了缓存的机制,在没有多线程的情况下,TypeValue 对象只会被创一个实例。

回到上面的 loadXmlResourceParser() 方法,现在我们创建 TypeValue 对象,但是对象内部的字段并没有值,所以后面会调用 ResourcesImpl 的 getValue() 方法来获取具体的值,这个方法最终调用了一个 native 方法,这里我们不做深究,只需要知道在这里 TypeValue 存储了我们声明的布局文件的一些信息。

紧接着调用了 ResourcesImpl 的 loadXmlResourceParser() 方法,源码如下:

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
//ResourcesImpl.java
private static final int XML_BLOCK_CACHE_SIZE = 4;
private int mLastCachedXmlBlockIndex = -1;
private final int[] mCachedXmlBlockCookies = new int[XML_BLOCK_CACHE_SIZE];
private final String[] mCachedXmlBlockFiles = new String[XML_BLOCK_CACHE_SIZE];
private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[XML_BLOCK_CACHE_SIZE];

XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
if (id != 0) {
try {
synchronized (mCachedXmlBlocks) {
final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
final int num = cachedXmlBlockFiles.length;
for (int i = 0; i < num; i++) {
if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
&& cachedXmlBlockFiles[i].equals(file)) {
//返回缓存对象的 newParser()
return cachedXmlBlocks[i].newParser();
}
}
//没有缓存,读取文件
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
if (block != null) {
//如果缓存满了,就顶掉余数索引位置的缓存。
final int pos = (mLastCachedXmlBlockIndex + 1) % num;
mLastCachedXmlBlockIndex = pos;
final XmlBlock oldBlock = cachedXmlBlocks[pos];
if (oldBlock != null) {
oldBlock.close();
}
//存入缓存
cachedXmlBlockCookies[pos] = assetCookie;
cachedXmlBlockFiles[pos] = file;
cachedXmlBlocks[pos] = block;
return block.newParser();
}
}
} catch (Exception e) {
final NotFoundException rnf = new NotFoundException("File " + file
+ " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
}

throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
+ Integer.toHexString(id));
}

上面源码看上去好像很多,其实逻辑很简单,首先从缓存中获取,如果获取到了直接返回,如果没有获取到,就创建新的并存入缓存。值得注意的是 mCachedXmlBlocks,mCachedXmlBlockFiles 和 mCachedXmlBlockCookies 这三个缓存数组的大小只有 4,也就是最多只有 4 个 XmlBlock 对象缓存,暂时还不知道这么设计有啥优点。

无论是有缓存还是没有缓存,都会调用 XmlBlock 类的 newParser() 方法,源码如下:

1
2
3
4
5
6
7
8
public XmlResourceParser newParser() {
synchronized (this) {
if (mNative != 0) {
return new Parser(nativeCreateParseState(mNative), this);
}
return null;
}
}

可以看到,返回了一个 Parer 对象,这个类是 XmlBlock 的内部类,现在我们回到 LayoutInflater 的 inflate() 方法,我们现在真的最终返回的 XmlResourceParser 对象是 XmlBlock 的内部类 Parser 的实例,接着我们看 inflate() 的另外一个重载。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 //LayoutInflater.java
//删除不相干代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;

try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//...
}
//省略异常处理...
return result;
}
}

这个 inflate() 方法比较长,我们一段一段的看,首先会调用 Xml 的 asAttributeSet(parser),这个方法主要的作用就是判断传入的 parser 是否实现了 AttributeSet 这个接口,如果没有使用,系统会在”帮”你实现。具体可以看 XmlPullAttributes 这个类。

接下来会把我们传入的 context 对象存在 mConstructorArgs 这个数组中,这里为什么要这么做呢?我猜测和多线程有关,可以看到 infalate() 方法的开头,会把 mConstructorArgs 对象当做锁,所以为了防止多个线程使用同一个 context 对象调用 infalte() 方法,这里加锁。

进入到 try 代码块中,首先会循环调用 parser() 的 next() 方法,解析过 xml 的同学应该知道,解析 xml 有两种方法,一种是直接把整个 xml 解析完,然后存储在内存中,另外一种方法就是一个单位一个单位(这里的单位可能是一行,也可能是空格作为分隔)的解析,每解析一个单位会把这个单位的信息存储在对应的变量中,很显然这里使用的是第二种方法,可以看到只有当解析到的值不是 START_TAG 和 END_DOCUMENT 这两种类型的时候,才会跳出循环。

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
 //LayoutInflater.java
//删除不相干代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//...
try {
//...

if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

final String name = parser.getName();

//判断根布局是否是 merge 标签
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

rInflate(parser, root, inflaterContext, attrs, false);
} else {
//...
}

}
//省略异常处理...
return result;
}
}

接着上面的,跳出循环后,会获取到 name 字段的值,这个 name 就是我们在 Layout 中定义的控件的名称,如 TextView,ImageView 等,紧接着会判断如果解析到的的标签的名称为 merge,且 root 参数为空或者 attachToRoot 为 false,就会抛出异常,这也表明了,如果把 merge 标签作为一个布局文件的更布局,必须要传 root 且 attachToRoot 要为 true。注意是更布局。

如果根布局为 merge,root 不为 null,且 attachToRoot 为 true,就会调用 rInflate() 这个方法。

rInflate()

rInflate() 方法起到了一个递归创建 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
40
41
42
43
44
45
46
47
48
//LayoutInflater.java
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//获取深度
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;

while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {
continue;
}

final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//include 不能作为一个布局文件的根布局
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//merge 不能作为一个布局文件的根布局
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}

if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}

if (finishInflate) {
parent.onFinishInflate();
}
}

首先我们看看循环的条件,如下:

  1. 当前被解析的标签的类型不是 END_TAG,或者当前深度不能有变化。
  2. 当前被解析的标签的类型不是 END_DOCUMENT。

只有当以上两个条件都满足,才会继续循环,rInflate() 中的 r 表示 Recursive(递归),所以上面这两个条件其实就是用来跳出递归的,
在 if 表达式的 else 中,首先是通过 createViewFromTag() 这个方法创建 View 对象,然后又调用了 rInflateChildern() 方法,在这个内部又调用 rInflate() 方法,因此我们可以很容易的推测出 LayoutInflate 用的前序遍历来创建的 View 树。举个例子:

例子

假设我们布局文件的解构如上图所示,那么当开发者调用 inflate() 方法的时候,系统的解析顺序为: RootLayout -> Layout -> Button3 -> Button4 -> Button1 -> Button2。

createViewFromTag()

回到上面的 createViewFromTag() 处,这个方法就是创建 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

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//view 标签,是一个比较特殊的标签
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

//是否忽略主题属性
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

//此处为彩蛋
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

//...
}

在 createViewFromTag() 这个方法中,首先会判断解析的标签是不是 <view> 这个标签,这个标签可以指定一个 class 的属性,如下:

1
2
3
4
5
6
7
8
9
10
<view
android:layout_width="200dp"
android:layout_height="200dp"
class="android.widget.TextView"
/>
//上面的代码和下的代码效果一样
<TextView
android:layout_width="200dp"
android:layout_height="200dp"
/>

暂时还不知道这样写有什么好处。会到 createViewFromTag() 方法,在这个方法中有一个彩蛋,那就是有一个隐藏的布局,名为 BlinkLayout,这个布局在布局文件中叫做 blink,千万不了在项目中使用这个布局,因为那样你有可能会被你的老板给打死!

看完彩蛋,紧接着会有一连窜的判断,源码如下:

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
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//...
try {
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;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//判断是否是自定义的 View
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
}
//省略异常处理
}

上面代码中,首先会去尝试使用 mFactory2 和 mFactory 这两个对象去创建 View,mFactory2 的优先级比 mFactory 要高,Factory 和 Factory2 的不同之处在于,Factory2 的 onCreateView() 方法比 Factory 的 onCreateView() 多了一个 parent 参数,并且 Factory2 是继承自 Factory的,可能是因为后期为了扩展一些功能,强行加了一个 Factory2。

这里先暂时不看 Factory,我们假设没有 Factory 那么系统就会调用 LayoutInflater 的 onCreateView() 方法来创建 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
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
//LayoutInflater.java
//创建非系统默认 View,如 RecyclerView 自定义 View 等
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
//创建系统默认 View 如 TextView ImageView 等
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
//创建 View 的地方
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//从缓存中获取构造器对象
Constructor<? extends View> constructor = sConstructorMap.get(name);
//verifyClassLoader() 方法会验证 ClassLoader 对象
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;

try {
//没缓存
if (constructor == null) {
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

if (mFilter != null && clazz != null) {
//判断是否㤇被拦截
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
//有缓存
if (mFilter != null) {
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}

Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;

final View view = constructor.newInstance(args);
//处理 ViewStub 标签
if (view instanceof ViewStub) {
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
//省略异常处理
}

分析上面代码,可以看到,在 LayoutInflater 的 createView() 方法中,使用了反射对 View 的对象进行创建,并且我们可以通过设置 mFilter 这个类来设置哪些类不允许被创建,换句话说就是哪些 View 不能再布局文件中使用,不知道这个 Filter 的具体作用是什么。

createView() 在使用反射的时候,对 View 类的构造器对象进行缓存,这样可以一定程度上缓解反射带来的性能损耗,在最后,会设置 ViewStub 的 LayoutInflater,需要注意的是,这里是 new 了一个新的 LayoutInflater 对象的实例。

回到 createViewFromTag() 方法,那么问题来了,什么情况下 LayoutInflate 的 mFactory 和 mFactory2 会不为空呢?答案要从我们的 Activity 说起,在 比较早的时候(Android 5.0 之前),我们在声明 Activity 类的时候一般都是直接继承 Activity 这个类的,这种情况下,mFactory 和 mFactor2 都是为 null 的(不排除系统的 ROM 会在这里注入 Factory),但是在最近几年,由于 Android 的 UI 有了大幅度的优化,主要还是由于 IOS 的强势竞争,所以 Google 不得不考虑把精力放在 Android 的 UI 设计上,比如在 Android 5.0 推出 Material design 设计语言来对 UI 进行规范的管理(国内其实很少有 APP 使用 Material design 设计),并且添加新的属性来让开发者能过做出很酷炫的界面,这个时候问题就来了,怎么在低版本上使用这些功能呢?

AppCompatActivity

这个时候,Google 就弄一个名为 support 的库,顾名思义就是用来做兼容处理的,比如 Google 推荐开发者声明的 Activity 最好要继承 AppCompatActivity(来自 support 库),回到上一个问题,这个里的 AppCompatActivity 和 LayoutInflater 有什么关联呢?

答案就在源码中,代码如下:

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
//前提是我们 Activity 继承自 AppCompatActivity
//AppCompatActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
//这里的 AppCompatDelegate 对象会根据当前的版本号来创建
final AppCompatDelegate delegate = getDelegate();
//重点在这里
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);
}

//AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//设置 LayoutInflater 的 Factory2
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");
}
}
}

通过上面的源码,我们可以得知,如果我们声明的 Activity 继承自 AppCompatActivity 的话,在 AppCompatActivity 中会为 LayoutInflater 设置 Factory2,其实不仅仅是 LayoutInflater,其他很多其他的”新特性”,都是通过 AppCompatDelegate 这个类来实现的,通过名字可以判断这个类一定使用了委托/代理模式,这个模式的作用就是在不修改原有类的情况下,扩展原有类的功能。

竟然我们知道了,如果继承 AppCompatActivity 就会改变 LayoutInflate 中 createViewFromTag 的逻辑,那么我们就来看看 AppCompatActivity 为什么要设置 LayoutInflater 的 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
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
//AppCompatDelegateImplV9.java
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 首先会尝试调用 Activity 的 OnCreateView
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}

// 如果 Activity 的 onCreateView 无法处理,就调用 createView() 方法
return createView(parent, name, context, attrs);
}
@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)) {
//如果没有自定义 AppCompatViewInflater,就使用系统的 AppCompatViewInflater
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
//通过反射创建我们自定义的 AppCompatViewInflater
Class viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
//自定义的 AppCompatInflater 不正确,就默认使用系统的 AppCompatInflater
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);
}

//调用 AppCompatViewInflater 的 createView()
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 */
);
}

首先要说的是,AppCompatDelegateImplV9 这个类实现了 Factory2 这个接口,所以在里面会用两个 onCreateView() 方法,这两个 onCreateView() 最终会调用 createView() 这个方法,在分析这个方法中,我们可以得知,我们可以在 styles.xml 文件中自定义我们的 AppCompatViewInflater,那为什么系统提供我们自定义 AppCompatViewInflater 的方法呢?原因就是在 AppCompatViewInflater 这个类中,替换了很多原的 View 比如我们在布局文件中定义一个 TextView 那么在通过 AppCompatViewInflater 转换后,就会变成 AppCompatTextView,ImageView 会被替换成 AppCompatImageView 等等…

AppCompatViewInflater

为什么系统要替换呢?因为我们上面说到了,为了兼容低版本 Android 推出了 Support 库,或者说,为了让开发者在不改变原有代码的情况下去兼容新特性,最好的方法就是使用这种替换的手段,基本上不用修改任何原本的代码,只需要让 Activity 继承 AppCompatActivity 就可以轻松兼容新特性,不得不说,Google 的这个设计非常巧妙。(这里指的是 View 的兼容)

其实在平时开发中,细心的同学应该会发现,我们 findViewById() 获取到的 View 的类型基本上都是 AppComapt 打头的(Activity 继承 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
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;

//省略非关键性代码...

View view = null;

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;
//省略部分代码...
default:
view = createView(context, name, attrs);
}

if (view == null && originalContext != context) {
view = createViewFromTag(context, name, attrs);
}

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

return view;
}

分析上面的代码,大致流程就是,首先判断标签的名称,如果符合替换规则,就会调用对应的 createXXX() 方法,如果不符合替换规则,就会调用 createView() 方法,这个 createView 方法默认是一个空的实现,我可以通过我上面提到的自定义 AppCompatInflater 的方式来添加自己的逻辑,最后如果通过上面的逻辑依然没有创建出 View 对象(不满足替换规则),那么这个时候就会调用 createViewFromTag() 是不是感觉这个方法很眼熟,没错,在 LayoutInflater 也有一个名为 createViewFromTag 的方法,不过这两个方法的逻辑却不一样,具体源码如下:

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
//所有系统 View 的前缀
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
//View 中构造函数参数的对应类型
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};

private View createViewFromTag(Context context, String name, AttributeSet attrs) {
//<view> 这个特殊的标签
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;

if (-1 == name.indexOf('.')) {
//系统 View,如 TextView Button 等
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
//自定义 View
return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
return null;
} finally {
//防止内存泄漏
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
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) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}

在 createViewFromTag() 方法中,首先会尝试用 sClassPrefixList 中的每个前缀加反射的方法是去创建 View 对象,具体创建过程在 createViewByPrefix() 方法中,在该方法中,会先从 sConstructorMap 这个缓存中获取构造函数对象,让后调用构造函数对象的 newInstance() 方法来创建 View 的实例,了解自定义 View 的同学应该知道,我们在自定义 View 时候,必须重写两个参数的构造函数才能让我们的自定义 View 在 XML 中使用,不然会报错,原因就在上面的 createViewByPrefix() 方法中,通过反射获取构造器对象需要构造器的参数和传入的参数类型匹配才能正确获取,上面定义个构造器类型为 sConstructorSignature 这个数组,所以如果我们想让我们自定义的 View 在 XML 中可以使用,那么必须要重写两个参数的构造函数。

这里有个小技巧,众所周知,反射的性能是比较低下的,所以 Google 在这里把每个 View 的构造器对象进行了缓存,这样能够尽可能的提升在使用反射创建 View 的时候的效率。

某种意义上来讲,如果我们的 Activity 继承 AppCompatActivity 是可以一定程度上优化性能的,毕竟使用 new 的方式创建对象的速度肯定比使用反射创建要快(网上说的),这里下先提前说下,在 LayoutInflater 中也是使用的反射创建的 View 对象。

自定义 AppCompatViewInflater。

在上面分析,我们提到可以通过自定义的 AppCompatViewInflater 的方式来替换系统的 AppCompatActiivty,这个具体在开发者有什么用处呢?可以参考鸿神的这篇博客: Android 探究 LayoutInflater setFactory,在鸿神的这篇博客中有提到使用 LayoutInflater 的 setFactory 可以实现如:替换所有 TextView 的字体,换肤等等功能。在博客里面鸿神也也给出了初步的实现,以前我也曾经使用过博客中的方法来实现设置整个 APP 中所有的 TextView 的字体,不过结合今天对 LayoutInflater 的了解,我们完全可以使用更好的姿势来实现。具体如下(不是说鸿神写的有问题!):

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
public class CustomViewInflater extends AppCompatViewInflater {

@NonNull
@Override
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
//实现自己的逻辑
//如替换所有 TextView 的字体样式
AppCompatTextView textView = super.createTextView(context, attrs);
textView.setTypeface(Typeface.DEFAULT_BOLD);
return textView;
}
}

//在 styles.xml 中设置我们自定义的 AppCompatViewInflater 的完全限定名
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<!-- 重点在这里 -->
<item name="viewInflaterClass">com.lz.demo1.CustomViewInflater</item>
</style>
</resources>

运行代码后,你就会发现所有的 TextView 的字体都变成了粗体。

很明显,Google 的程序员是有意将 createXXX() 方法设置成 protected 的,为的就是让我们好好继承来重写。

这种实现方式相较于鸿神博客中的方式明显更好,我们不用去修改原有的类,符合开闭原则

小结

通过一顿分析,这里小结一波:

  1. 如果在不考虑实际情况的情况下,尽量让我们的 Activity 来继承 AppCompatActivity。
  2. 在继承 AppCompatActivity 的的情况下,我们可以大胆的使用一些新特性,不用担心兼容的问题。
  3. 既然官方提供了类型替换全局 View 的方法,我们就要使用官方提供的,不要自己弄一些骚的操作,性能低下不说,还会破坏代码的结构。

填充

上面我们通过分析 AppCompatActivity 的源码,知道了 LayoutInflater 的 Factory 具体作用,现在我们回到 LayoutInflater 的 inflate 方法,继续分析 LayoutInflater 的源码,具体如下:

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
 //LayoutInflater.java
//删除不相干代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//...
try {
//...

if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

final String name = parser.getName();

//判断根布局是否是 merge 标签
if (TAG_MERGE.equals(name)) {
//...
} else {
//根布局不是 merge 标签
//temp 这里表示布局文件的根布局的 View 对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}

rInflateChildren(parser, temp, attrs, true);

if (root != null && attachToRoot) {
root.addView(temp, params);
}

if (root == null || !attachToRoot) {
result = temp;
}
}

}
//省略异常处理...
return result;
}
}

createViewFromTag() 这个方法我们在前面已经分析过了,其作用就是创建 View 对象,第一个被创建的可定是根布局,紧接着会判断 root 这个参数是否为 null,如果 root 不为 null 会调用 root 的 generateLayoutParam() 方法来生成 LayoutParams 对象并在 attachToRoot 为 false的情况下将 LayoutParams 对象设置给 temp 即根布局。

设置完 LayoutParams 对象,会调用 rInflateChildren() 方法,在 rInflateChildren() 方法中又调用了 rInflate() 方法,源码如下:

1
2
3
4
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

上面在处理 merge 标签的时候,也时调用 rInflate() 方法,和这里的调用区别在于这里的最后个参数变为了 true 且 使用的是 parent 即根布局的 context 对象,这里也说明了,如果我们使用 merge 标签来作为布局文件的根布局,传入的 root 对象的 onFinishInflate() 方法是不会被调用的。

通过 rInflateChildren() 递归的创建了整个 View 树后,会判断 如果 root 不为 null 且 attachToRoot 为 true 的时候,LayoutInflater 会把创建出来的 View 添加到 root 这个 View 对象中去,而且在这个时候,inflate 是不会把解析的对象返回的。

回到最开始的问题,为什么 Google 要为这个类取名为 Inflater(打气筒或者填充器),原因很简单,LayoutInflater 即扮演了解析布局文件的任务,也会在解析完成后将解析好的 View 对象添加(填充)到传入的 root 对象中,如果 root 为 null 就不存储填充,inflate() 方法就会把解析好的 View 对象进行返回,所以这一过程有点像我们使用打气筒给一个气球冲气,故起名为 LayoutInflater。

总结

哇,不知不觉写了这么多,感觉废话很多,不过通过分析 LayoutInflater 的源码,相信你一定对 Android 中 View 的创建过程有了进一步的了解。最后我们来总结总结。

  • 开始我们通过分析了 LayoutInflater 的 from() 方法知道了系统中有很多 SystemService,并且这些 SystemService 并不是全局单例的(一部分可能是全局单例的),这些 SystemService 的实例个数是和 context 一一对应的。
  • 减少布局的嵌套会有效的减少 rInflate() 的递归调用次数,并且 View 的 onFinishInflate() 方法回调的时候,View 剪辑只是被解析完成,只能获取到对象的引用,并不能获取到宽高,具体原因要参考之前我们分析的 Activity 的创建过程。
  • 继承 AppCompatActivity 能够优化一部分的性能,虽然微乎其微。并且不得不说,Google 在设计 AppCompatActivity 的时候很巧妙,不仅耦合度拿捏的到位,也提供了充足的扩展能力。