Android源码分析LayoutInflater

源码基于安卓8.0分析结果

  • 在这篇文章Android源码分析(Activity.setContentView源码解析),分析得出,底层走的就是LayoutInflater.from(this),inflate(),如果inflate()传入的view的话,就调用两次LayoutInflater.from(this),inflate(),,如果是inflate()传入的resId(布局的id)的话,就调用三次LayoutInflater.from(this),inflate(),,具体请看上篇文章。
  • 怎么使用,一共有三种的方式,主要看下这种方式LayoutInflater inflater1 = getLayoutInflater();,其实调用的就是Activity的getLayoutInflater()方法。

    1
    2
    3
     LayoutInflater inflater1 = getLayoutInflater();//调用Activity的getLayoutInflater()
    LayoutInflater inflater2 = LayoutInflater.from(this);
    LayoutInflater inflater3 = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  • Activity的getLayoutInflater()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * Convenience for calling
    * {@link android.view.Window#getLayoutInflater}.
    */
    @NonNull
    public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
    }
  • 通过Android源码分析(事件传递)这边文章,我们知道,Window类只有唯一的子类PhoneWindow。所以主要的是去看PhoneWindow

    1
    2
    3
    4
    public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
    }

LayoutInflater inflater2 = LayoutInflater.from(this);的实现的方式

1
2
3
4
5
6
7
8
9
10
11
/**
* Obtains the LayoutInflater from the given context.
*/
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;
}

  • 以上三种方法,最终还是调用了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获取LayoutInflater实例对象。
  • inflate的方法

    1
    2
    3
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
    return inflate(parser, root, root != null);
    }
  • 到这里来inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot),个人的对这个方法的理解是:把布局文件填充成View,如果传入的root不为null,并且attachToRoottrue,就把view填充到root上,相反就不添加上来!

    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) + ")");
    }
    //这里通过底层的方法,得到一个XmlResourceParser对象
    final XmlResourceParser parser = res.getLayout(resource);
    try {
    return inflate(parser, root, attachToRoot);
    } finally {
    parser.close();
    }
    }
  • inflate(parser, root, attachToRoot),特别关心这个方法

    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
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
    //底层的方法,不知道原理
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    // TODO: 2018/6/5 我没有搞明白 为啥整个值在这里做什么的, 在这个方法里面都没有使用
    // from传入的Context
    final Context inflaterContext = mContext;
    // 判断parser是否是AttributeSet,如果不是则用XmlPullAttributes去包装一下。
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    // 保存之前的Context
    Context lastContext = (Context) mConstructorArgs[0];
    // 赋值为传入的Context
    mConstructorArgs[0] = inflaterContext;
    // 默认返回的是传入的Parent
    View result = root;
    try {
    // Look for the root node.
    int type;// 迭代xml中的所有元素,挨个解析
    while ((type = parser.next()) != XmlPullParser.START_TAG &&
    type != XmlPullParser.END_DOCUMENT) {
    // Empty
    }
    // //如果没找到有效的开始标签则抛出InflateException
    if (type != XmlPullParser.START_TAG) {
    throw new InflateException(parser.getPositionDescription()
    + ": No start tag found!");
    }

    final String name = parser.getName();

    if (DEBUG) {
    System.out.println("**************************");
    System.out.println("Creating root view: "
    + name);
    System.out.println("**************************");
    }
    //// 如果根节点是“merge”标签
    if (TAG_MERGE.equals(name)) {
    // 根节点为空或者不添加到根节点上,则抛出异常。
    // 因为“merge”标签必须是要被添加到父节点上的,不能独立存在。
    if (root == null || !attachToRoot) {
    throw new InflateException("<merge /> can be used only with a valid "
    + "ViewGroup root and attachToRoot=true");
    }
    // 递归实例化root(也就是传入Parent)下所有的View
    // 如果xml中的节点是merge节点,则调用rInflate()--方法 // 递归实例化根节点的子View
    rInflate(parser, root, inflaterContext, attrs, false);
    } else {
    // Temp is the root view that was found in the xml
    //通过View的父View,View的名称、attrs属性实例化View(内部调用onCreateView()和createView())。
    // TODO: 2018/6/6 // 实例化根节点的View
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

    ViewGroup.LayoutParams params = null;

    if (root != null) {
    if (DEBUG) {
    System.out.println("Creating params from root: " +
    root);
    }
    // Create layout params that match root, if supplied
    params = root.generateLayoutParams(attrs);
    if (!attachToRoot) {
    // Set the layout params for temp if we are not
    // attaching. (If we are, we use addView, below)
    temp.setLayoutParams(params);
    }
    }

    if (DEBUG) {
    System.out.println("-----> start inflating children");
    }

    // Inflate all children under temp against its context.
    // 递归实例化跟节点的子View
    rInflateChildren(parser, temp, attrs, true);

    if (DEBUG) {
    System.out.println("-----> done inflating children");
    }

    // 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); // TODO: 2018/6/6 返回父View
    }

    // Decide whether to return the root that was passed in or the
    // top view found in xml.
    // TODO: 2018/6/6 父View是空或者不把填充的View添加到父View)
    if (root == null || !attachToRoot) {
    result = temp; // TODO: 2018/6/6 返回根节点View
    }
    }

    } catch (XmlPullParserException e) {
    final InflateException ie = new InflateException(e.getMessage(), e);
    ie.setStackTrace(EMPTY_STACK_TRACE);
    throw ie;
    } catch (Exception e) {
    final InflateException ie = new InflateException(parser.getPositionDescription()
    + ": " + e.getMessage(), e);
    ie.setStackTrace(EMPTY_STACK_TRACE);
    throw ie;
    } finally {
    // Don't retain static reference on context.
    //不要在上下文中保留静态引用。
    // 把这之前保存的Context从新放回全局变量中。
    mConstructorArgs[0] = lastContext;
    mConstructorArgs[1] = null;

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    return result;
    }
    }
  • 注意其中有个判断TAG_MERGE.equals(name)) ,这里就是merge标签的开始,以下的问题,都带着这个疑问往下面看。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     if (TAG_MERGE.equals(name)) {
    // 根节点为空或者不添加到根节点上,则抛出异常。
    // 因为“merge”标签必须是要被添加到父节点上的,不能独立存在。
    if (root == null || !attachToRoot) {
    throw new InflateException("<merge /> can be used only with a valid "
    + "ViewGroup root and attachToRoot=true");
    }
    // 递归实例化root(也就是传入Parent)下所有的View
    // 如果xml中的节点是merge节点,则调用rInflate()--方法 // 递归实例化根节点的子View
    rInflate(parser, root, inflaterContext, attrs, false);
    }
  • 解析到merge标签的话,就会进入到这个方法rInflate(parser, root, inflaterContext, attrs, false);:递归实例化根节点的子View,在这里说明一点的是这个方法传入的root会不会为null.这篇文章说过Android源码分析(Activity.setContentView源码解析),肯定不会为null,一般是我们自己去使用的时候,有可能为null,具体的差别可以参考这篇文章Android LayoutInflater深度解析 给你带来全新的认识

  • 关于rInflate(parser, root, inflaterContext, attrs, false);方法:递归方法用于降序XML层次结构并实例化视图,实例化它们的子节点,然后调用onFinishInflate(),, 完成填充

    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
    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;
    // 迭代xml中的所有元素,挨个解析
    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();
    //1、解析请求焦点 ,这个控件一直有焦点 requestFocus
    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)) {// 如果xml中的节点是include节点,则调用parseInclude方法
    if (parser.getDepth() == 0) {
    throw new InflateException("<include /> cannot be the root element");
    }
    //解析include标签
    parseInclude(parser, context, parent, attrs);
    } else if (TAG_MERGE.equals(name)) {
    // TODO: 2018/5/23 merge 作为布局里面的元素 会报错的哦 ,注意哦
    //想象下 两个merge 标签重合在一起
    throw new InflateException("<merge /> must be the root element");
    } else {
    // TODO: 2018/5/23 其实就是如果是merge标签,
    // TODO: 2018/5/23 那么直接将其中的子元素添加到merge标签parent中,这样就保证了不会引入额外的层级。
    // 1、我们的例子会进入这里 // 通过View的名称实例化View
    final View view = createViewFromTag(parent, name, context, attrs);
    // 2、获取merge标签的parent
    final ViewGroup viewGroup = (ViewGroup) parent;
    // 3、获取布局参数
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    // 4、递归解析每个子元素
    rInflateChildren(parser, view, attrs, true);
    // 5、将子元素直接添加到merge标签的parent view中
    viewGroup.addView(view, params);
    }
    }

    if (pendingRequestFocus) {
    parent.restoreDefaultFocus();
    }
    /**
    * onFinishInflate() View 中的一个空实现,标记完全填充完成了
    * The method onFinishInflate() will be called after all children have been added.
    * 以后是所有的孩子调用完成了,之后就调用这个方法
    */
    if (finishInflate) {
    parent.onFinishInflate();
    }
    }
  • 通过对上面的方法分析,还打了断点,打断点注意一点,用模拟器,基本上手机厂商都改了frameWork层的代码,打出来是乱的。代码会走到这里来,看代码的分析,就把新的View,通过ViewGroup.addView()进去了,这也是merge便签的原理:merge标签可以自动消除当一个布局插入到另一个布局时产生的多余的View Group,也可用于替换FrameLayout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // TODO: 2018/5/23 其实就是如果是merge标签,
    // TODO: 2018/5/23 那么直接将其中的子元素添加到merge标签parent中,这样就保证了不会引入额外的层级。
    // 1、我们的例子会进入这里 // 通过View的名称实例化View
    final View view = createViewFromTag(parent, name, context, attrs);
    // 2、获取merge标签的parent
    final ViewGroup viewGroup = (ViewGroup) parent;
    // 3、获取布局参数
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    // 4、递归解析每个子元素
    rInflateChildren(parser, view, attrs, true);
    // 5、将子元素直接添加到merge标签的parent view中
    viewGroup.addView(view, params);
  • merge的原理我们清楚了,分析另外一条线,没有merge标签,就会走到else的方法来。

    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
    // 如果根节点是“merge”标签
    if (TAG_MERGE.equals(name)) {

    } else {
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ViewGroup.LayoutParams params = null;
    if (root != null) {
    params = root.generateLayoutParams(attrs);
    if (!attachToRoot) {
    temp.setLayoutParams(params);
    }
    }
    // 递归实例化跟节点的子View start inflating children
    rInflateChildren(parser, temp, attrs, true);

    if (DEBUG) {
    System.out.println("-----> done inflating children");
    }
    if (root != null && attachToRoot) {
    root.addView(temp, params); // TODO: 2018/6/6 返回父View
    }

    // Decide whether to return the root that was passed in or the
    // top view found in xml.
    // TODO: 2018/6/6 父View是空或者不把填充的View添加到父View)
    if (root == null || !attachToRoot) {
    result = temp; // TODO: 2018/6/6 返回根节点View
    }
  • 1、这个方法createViewFromTag(View parent, String name, Context context, AttributeSet attrs)

    1
    2
    3
    private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
    }
  • 最终调用的是这个方法createViewFromTag(parent, name, context, attrs, false);这个方法

  • ,什么时候会走到下面来,name.equals("view")成立的情况,我记得以前我为了在布局中加了一根线就是这样写的,写快了,写成小写的了view,应该写成大写View.
    1
    2
    3
    <view
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

小写的view:我自己打的断点发现,如果根标签是view的,注意是小写的,view,这个name会为null ,接着下面就会出现空指针的异常—> 我很聪明 很牛逼

1
2
3
4
5
6
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
//根据我的断点的话,只要进来的话,这个那么就会为null
name = attrs.getAttributeValue(null, "class");
}

1
2
3
4
5
6
7
8
9
10
// Apply a theme wrapper, if allowed and one is specified.
//如果允许的话,应用一个主题包装器,并指定一个。
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();
}

name ==null,就会抛出ava.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference,也就是这个原因,所以写代码还是要仔细,仔细!!

在这里有个很有意思的布局,开发中很少使用到,而且也不清楚,作者为啥写,姑且这样理解Let's party like it's 1995! 写这个代码的哥们,在1995年经历了一件很开心的事情

1
2
3
4
5
//如果有这个标签的话,就出现这个布局,闪动的布局
if (name.equals(TAG_1995)) {
// Let's party like it's 1995! 写这个代码的哥们,在1995年经历了一件很开心的事情
return new BlinkLayout(context, attrs);
}

BlinkLayout使用的方式,blink 翻译过来会一闪一闪的

1
2
3
4
5
6
7
8
9
<blink
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:text="blink单词也是这个意思.闪烁频率500毫秒" />
</blink>

BlinkLayout代码如下, BLINK_DELAY 这个是闪动的时间间隔,如果开发中需要,改动这个时间就行了,感觉这个布局也没啥用!

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
public  class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;
private boolean mBlink;
private boolean mBlinkState;
private final Handler mHandler;

public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO: 2018/5/22 对handler 源码比较熟悉的话,这里就是Handler另外的一种使用的方法
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
mBlinkState = !mBlinkState;
makeBlink();
}
//invalidate()是用来刷新View的,必须是在UI线程中进行工作。
// 比如在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面
invalidate();
return true;
}
return false;
}
});
}

private void makeBlink() {
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();

mBlink = true;
mBlinkState = true;

makeBlink();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();

mBlink = false;
mBlinkState = true;

mHandler.removeMessages(MESSAGE_BLINK);
}

@Override
protected void dispatchDraw(Canvas canvas) {
//根据这个状态是否去dispatchDraw
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}

}

下面是后续的代码,通过工厂创建了view。去定位过,在public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2,很多的地方,如果需要研究的,还需要更深层的源码理解,这里就先mark下,只需要知道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
    try {
View view;
if (mFactory2 != null) {
//各个工厂先onCreateView()
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;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}

  • 2、关于这个方法rInflateChildren(parser, temp, attrs, true);:递归实例化跟节点的子View.其实调用的还是这个方法rInflate(parser, root, inflaterContext, attrs, false);,文章的前部分贴出了这部分的代码
    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);
    }

主要是为了分析几个Tag

1
2
3
4
5
6
private static final String TAG_MERGE = "merge";
private static final String TAG_INCLUDE = "include";
private static final String TAG_1995 = "blink";
private static final String TAG_REQUEST_FOCUS = "requestFocus";
private static final String TAG_TAG = "tag";
private static final String ATTR_LAYOUT = "layout";

requestFocus:解析请求焦点 ,这个控件一直有焦点 requestFocus,一般作用于 EditText,需要长期获取焦点的控件,但是真正的实现的方法,我们都是代码去实现了,很少这样使用,这里就说明一下,有这样的使用的方法!

1
2
3
4
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
}

1
2
3
4
5
6
7
<EditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<!-- 当前控件处于焦点状态 -->
<requestFocus />
</EditText>

tag:在包含的视图上设置键标记,其实就是给view设置上了一个tag标签

1
2
3
4
5
6
7
if (TAG_TAG.equals(name)) {
//在包含的视图上设置键标记。
parseViewTag(parser, parent, attrs);
}
final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
final CharSequence value = ta.getText(R.styleable.ViewTag_value);
view.setTag(key, value);

merge:在这里如果解析到了merge标签的话,这里就直接抛出异常了,必须是根元素!两种情况下,一种是套用了两个merge标签,或者是一个ViewGruop标签嵌套了一个merge,都会抛出异常

1
2
3
4
5
if (TAG_MERGE.equals(name)) {
// TODO: 2018/5/23 merge 作为布局里面的元素 会报错的哦 ,注意哦
//想象下 两个merge 标签重合在一起
throw new InflateException("<merge /> must be the root element");
}

include:标签,在这里如果include作为了一个根元素,也会抛出异常,当然,如果在代码中见到别人那么用的话,建议自己去面壁思过会!

1
2
3
4
5
6
7
if (TAG_INCLUDE.equals(name)) {// 如果xml中的节点是include节点,则调用parseInclude方法
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
//解析include标签
parseInclude(parser, context, parent, attrs);
}

parseInclude(parser, context, parent, attrs);,这个方法也就是include必须走的方法,搞明白的话,就可以搞明白原理,而且在特殊的情况下,include标签会和 merge 标签一起使用。这个方法也可以解释include标签的原理!

  • 可以从这里可以看到,include只能在ViewGroup中使用,在 ‘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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    private void parseInclude(XmlPullParser parser, Context context, View parent,
    AttributeSet attrs) throws XmlPullParserException, IOException {
    int type;
    if (parent instanceof ViewGroup) {
    // Apply a theme wrapper, if requested. This is sort of a weird
    // edge case, since developers think the <include> overwrites
    // values in the AttributeSet of the included View. So, if the
    // included View has a theme attribute, we'll need to ignore it.
    //如果VIEW视图有一个主题属性,我们需要忽略它。
    final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
    final int themeResId = ta.getResourceId(0, 0);
    final boolean hasThemeOverride = themeResId != 0;
    if (hasThemeOverride) {
    context = new ContextThemeWrapper(context, themeResId);
    }
    ta.recycle();

    //如果布局指向主题属性,我们必须
    //按摩该值以从中获取资源标识符。
    int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
    if (layout == 0) {
    final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
    // TODO: 2018/6/6 include标签中没有设置layout属性,会抛出异常
    if (value == null || value.length() <= 0) {//
    throw new InflateException("You must specify a layout in the"
    + " include tag: <include layout=\"@layout/layoutID\" />");
    }

    // Attempt to resolve the "?attr/name" string to an attribute
    // within the default (e.g. application) package.
    layout = context.getResources().getIdentifier(
    value.substring(1), "attr", context.getPackageName());

    }

    // The layout might be referencing a theme attribute.
    if (mTempValue == null) {
    mTempValue = new TypedValue();
    }
    if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
    layout = mTempValue.resourceId;
    }

    if (layout == 0) {
    final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
    throw new InflateException("You must specify a valid layout "
    + "reference. The layout ID " + value + " is not valid.");
    } else {
    final XmlResourceParser childParser = context.getResources().getLayout(layout);

    try {// 获取属性集,即在include标签中设置的属性
    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
    type != XmlPullParser.END_DOCUMENT) {
    // Empty.
    }

    if (type != XmlPullParser.START_TAG) {
    throw new InflateException(childParser.getPositionDescription() +
    ": No start tag found!");
    }
    // 1、解析include中的第一个元素
    final String childName = childParser.getName();
    // 如果第一个元素是merge标签,那么调用rInflate函数解析
    if (TAG_MERGE.equals(childName)) {
    // The <merge> tag doesn't support android:theme, so
    // nothing special to do here.
    rInflate(childParser, parent, context, childAttrs, false);
    } else {
    //2、我们例子中的情况会走到这一步,首先根据include的属性集创建被include进来的xml布局的根view
    // 这里的根view对应为my_title_layout.xml中的RelativeLayout
    final View view = createViewFromTag(parent, childName,
    context, childAttrs, hasThemeOverride);
    final ViewGroup group = (ViewGroup) parent;

    final TypedArray a = context.obtainStyledAttributes(
    attrs, R.styleable.Include);
    //这里就是设置的<include>布局设置的id ,如果include设置了这个id ,那么这个id就不等于 View.No_ID=-1
    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
    a.recycle();

    // We try to load the layout params set in the <include /> tag.
    // If the parent can't generate layout params (ex. missing width
    // or height for the framework ViewGroups, though this is not
    // necessarily true of all ViewGroups) then we expect it to throw
    // a runtime exception.
    // We catch this exception and set localParams accordingly: true
    // means we successfully loaded layout params from the <include>
    // tag, false means we need to rely on the included layout params.
    ViewGroup.LayoutParams params = null;
    try {
    // 获3、取布局属性
    params = group.generateLayoutParams(attrs);
    } catch (RuntimeException e) {
    // Ignore, just fail over to child attrs.
    }
    // 被inlcude进来的根view设置布局参数
    if (params == null) {
    params = group.generateLayoutParams(childAttrs);
    }
    view.setLayoutParams(params);
    // 4、Inflate all children. 解析所有子控件
    // Inflate all children.
    rInflateChildren(childParser, view, childAttrs, true);

    if (id != View.NO_ID) {
    // 5、将include中设置的id设置给根view,因此实际上my_title_layout.xml中
    // 的RelativeLayout的id会变成include标签中的id,include不设置id,那么也可以通过relative的找到.
    view.setId(id);
    }

    switch (visibility) {
    case 0:
    view.setVisibility(View.VISIBLE);
    break;
    case 1:
    view.setVisibility(View.INVISIBLE);
    break;
    case 2:
    view.setVisibility(View.GONE);
    break;
    }
    // 6、将根view添加到父控件中
    group.addView(view);
    }
    } finally {
    childParser.close();
    }
    }
    } else {
    throw new InflateException("<include /> can only be used inside of a ViewGroup");
    }
    //默认的可见性使得LayoutInflater_Delegate代表可以调用它。
    LayoutInflater.consumeChildElements(parser);
  • 分段分析上面的那段代码,
    1、如果VIEW视图有一个主题属性,我们需要忽略它。

    1
    2
    3
    4
    5
    6
    7
    8
    //如果VIEW视图有一个主题属性,我们需要忽略它。
    final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
    final int themeResId = ta.getResourceId(0, 0);
    final boolean hasThemeOverride = themeResId != 0;
    if (hasThemeOverride) {
    context = new ContextThemeWrapper(context, themeResId);
    }
    ta.recycle();

2、如果布局指向主题属性,我们必须从该值以从中获取资源标识符!

1
2
3
//如果布局指向主题属性,我们必须
//按摩该值以从中获取资源标识符。
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);

3、在这里出现value == null || value.length() <= 0的话,就会抛出异常,也就是说include标签中没有设置layout属性,会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
// TODO: 2018/6/6 include标签中没有设置layout属性,会抛出异常
if (value == null || value.length() <= 0) {//
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
// Attempt to resolve the "?attr/name" string to an attribute
// within the default (e.g. application) package.
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());

}

3、解析include中的第一个元素,如果这里第一个childeNamemerge,那么就是 includemerge 一起使用的情况,调用的方法是rInflate(childParser, parent, context, childAttrs, false);和上面一样,不会生成多余的层级

1
2
3
4
5
6
7
8
// 1、解析include中的第一个元素
final String childName = childParser.getName();
// 如果第一个元素是merge标签,那么调用rInflate函数解析
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
}

4、首先根据include的属性集创建被include进来的xml布局的根view,这个view其实就include 进来的根ViewGroup,因为在前面已经把merge的情况过滤掉了,

1
2
3
4
else {
//2、我们例子中的情况会走到这一步,首先根据include的属性集创建被include进来的xml布局的根view
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);

5、特别关心这个final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);函数,其实就是给include标签设是否设置了id,就好像下面的布局一样,如果设置了这个android:id="@+id/my_title_ly"

1
2
3
4
5
<include
android:id="@+id/my_title_ly"
android:layout_width="match_parent"
android:layout_height="200dp"
layout="@layout/my_include_layout" />

这个是上面得include的布局my_include_layout

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/my_title_parent_id"
android:layout_height="wrap_content" >
....
</RelativeLayout>

下面的这段代码就会执行,这也就解释了:安卓include标签的找不到id的问题,因为你在include设置了这个id属性,那么你找这个my_title_parent_id这个id,findViewById(R.id.my_title_parent_id)肯定也会找出来为null!

1
2
3
4
5
6
7
8
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
if (id != View.NO_ID) {
// 5、将include中设置的id设置给根view,因此实际上my_title_layout.xml中
// 的RelativeLayout的id会变成include标签中的id,include不设置id,那么也可以通过relative的找到.
view.setId(id);
}

}

为什么findViewById(R.id.my_title_parent_id)会找出来为null,简单叙述下就是在Activity中调用方法其实是WindowfindViewById,getDecorView().findViewById(id),也就是PhoneWindow里面的DecorViewfindViewById,也就是ViewfindViewById;可以看这边文章Android源码分析(事件传递)涉及到这几个类了。如下面的方法,这个View 有id,找id的话就会走findViewTraversal这个方法,找的id和setId不一样的话,就会返回null,这就是找不到 id的原因。这个原因和ViewStub标签中设置了inflatedId一样的效果,下篇文章会讲到,非常有意思!已经写好Android源码分析(ViewStub源码解析)

1
2
3
4
5
6
7
8
9
10
11
12
 public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
return null;
}

View中设置id的方法

1
2
3
4
5
6
 public void setId(@IdRes int id) {
mID = id;
if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
mID = generateViewId();
}
}

6、关于final int visibility = a.getInt(R.styleable.Include_visibility, -1);这行代码,也就是说我们设置了include标签’visibility’其实也就是设置layoutvisibility的属性!

1
2
3
4
5
6
7
8
9
10
11
12
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}

7、将根view添加到父控件中,最后都会走到这里来,只不过使用了merge是不会产生多余的层级的,但是使用的时候有场景的要求!

1
group.addView(view);

  • 最后做了一张图,谢谢
    LayoutInflater.from(this).inflate(resId,null);代码解析.png

  • 说明几点

    • 布局转化为View,关键方法就是LayoutInflater.from(this),inflate()
    • PhoneWindow这个类很关键
    • 如果需要打断点,建议使用模拟器,手机的厂商基本上改动过framework的代码,打出来不准确
    • merge原理就是通过addView(view),从而减少了层级
      • BlinkLayout这个是LayoutInflater的内部类,感觉是谷歌工程师皮了一下
    • include只能在ViewGroup中使用,在 ‘View’结点中使用的话,会报错

 上一篇
Android源码分析:View的绘制流程 Android源码分析:View的绘制流程
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 源码基于安卓8.0分析结果 View是何时开始绘制的?Activity走了onCreate方法吗?这篇文章就是从程序的入口ActivityThread入口程序,去解释Vi
2018-06-08 Shiming_Li
下一篇 
Android源码分析ViewStub源码解析 Android源码分析ViewStub源码解析
源码基于安卓8.0分析结果 ViewStub是一种不可见的并且大小为0的试图,它可以延迟到运行时才填充inflate 布局资源,当Viewstub设为可见或者是inflate的时候,就会填充布局资源,这个布局和普通的试图就基本上没有任何区别
2018-06-07 Shiming_Li
  目录