您的位置:首页 > 移动开发 > Android开发

Android View绘制(一)- LayoutInflater.inflate() 流程简要解析

2018-03-03 15:33 417 查看

Android View绘制- LayoutInflater.inflate() 流程简要解析

布局,作为 Android 中展示 UI 的最主要的元素,其实它是怎么通过布局文件转化为实际的 UI 的?这篇文章,就是对这一过程进行简要的解析,并且提出一些实际开发过程需要注意的问题。

阅读该文章你可以了解到

LayoutInfalter 的工作过程

布局优化的一些建议

LayoutInflater 原理讲解

我们需要在 Activity 上显示一个布局文件,通常在 Activity 的 onCreate() 使用以下代码:

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_class_memory_leack);
}


实际上, setContentView() 的调用链如下

// Activity.setContentView(),这里的getWindow() 拿到的是一个 PhoneWindow 对象
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
// PhoneWinwod.setContent()
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}


最终会进入到 PhoneWindow 这里,然后最终调用的代码是第二十二行的 mLayoutInflater.inflate(layoutResID, mContentParent); 这一行代码,这里将 mContentParent 传入作为父容器。然后最终都会调用到 LayoutInflater 的 inflate() 方法,如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;//注意该方法的返回值

try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}

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

if (TAG_MERGE.equals(name)) {//如果开始标签是 Merge 标签,则不会创建父布局元素
// 直接通过 rInflate() 方法,遍历生成子 View
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 {
// Temp is the root view that was found in the xml
// 创建该 Layout 布局中的父布局
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.
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);
}

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

} 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.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

return result;
}
}


这里看一些关键代码

第三十三行,如果标签是 Merge 标签,则会跳过为当前标签创建 View 的流程,这也是布局文件里面 Merge 标签优化的原理所在。

第四十四行,通过 createViewFromTag() 方法,创建该 布局文件中的最高父节点 View

第五十五行到第七十二行,通过 判断形参 attachToRoot 和 root 的值,决定,是否将 Inflate 出来的父节点 View,加入到 root 父容器里面,同时影响该方法的返回值,规则如下:

如果 root,为null,则不会添加到 root 中,并且没有设置 LayoutParams,该方法最后返回父节点 View

如果 root 不为空,attachRoot 没有指定或者为 ture,则默认添加到 root 中,会将 LayoutParams 传递给 root.addView() 方法,同时返回值为 root View。

如果 root 不为空,attachRoot 为 false,则等同第一种情况,只不过会根据 root 计算 LayoutParmas,并且给当前 Inflate 的 View,也就是 temp 设置 LayoutParmas,当然只有宽和高属性(其实等于调用了 new LayoutParams() 构造方法)。

inflate() 也是个遍历调用的过程,会从父容器调用遍历到子 View,调用的代码是第四十行和第六十七行的代码,也就是最终还是会调用到 LayoutInflater.rInflate() 方法。

然后简单看一下 createViewFromTag() 方法

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");//文件属性
}

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

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

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


这里的代码比较简单,就是调用各种 LayoutFactory 的 onCreateView() 方法去创建 View,这里的 LayoutFactory 有其它用途,也就是可以通过自定义 Factory 实现换肤功能。

这里简单提一下吧,不过具体的换肤功能还是比较复杂的:

private void testChangeSkin(){
getLayoutInflater().setFactory(new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
//通过获取你所需要的换肤属性 或者换肤控件,对一些属性进行调整
return null;
}
});
}


就是通过为 Window 设置自定义的 Factory,然后控制每个 View 的创建,然后在创建的时候, 读取自定义属性,控制 View 的一些颜色,字体属性,达到自定义换肤的目的,类似实现的 github 上有开源的框架,感兴趣的可以去了解一下。

https://github.com/fengjundev/Android-Skin-Loader

再次回到代码,看一下 rInflate() 方法,这个方法也是比较重要的:

private static final String TAG_MERGE = "merge";// merge 标签,减少布局嵌套
private static final String TAG_INCLUDE = "include";//include 标签,增加布局文件复用
private static final String TAG_1995 = "blink";// 类似 FrameLayout
private static final String TAG_REQUEST_FOCUS = "requestFocus";//requestFocus 请求焦点

void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

final int depth = parser.getDepth();//depth ViewTree 的深度
int type;

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)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
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 {
//最后调用 createViewFromTag() 方法创建这个节点的 View
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
// 根据父布局,计算 LayoutParmas,得出当前 View 宽高测量数值
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//循环调用 rInflate()方法,直到 xml 文件末尾
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}

if (finishInflate) {
parent.onFinishInflate();//会回调这个方法,表示该父 View 的所有子 View inflate() 完成
}
}


关键的解释,已经在代码中添加了注释,比较有趣的是,这里 Inflate 过程也是递归调用的,并且每一个 父 View 的 子 View inflate() 完成,还有回调 onFinishInflate() 方法,那么我们是不是可以在父 View 的这个方法里面,拿到 子View 的宽高呢?

需要知道这个答案的,可以看后续的文章,不过答案自然是,不能的。

原理优化以及进阶使用

讲解了 Inflate 的流程,自然还需要做做一番拓展,看下这个知识点,在我们日常开发有哪些高逼格的使用技巧

实现自定义换肤,上面已经简单提到过了

优化布局,了解那些布局标签的真正原理

inflate() 是一个递归的过程,所以我们需要合理设计自己的布局文件,避免使用过于复杂的布局。

关于 inflate() 耗时的计算,可以在父布局的 onFinishInflate() 添加耗时完成的计算,查看哪些布局文件是耗时的, 然后做对应的优化。

最终 inflate() 完成之后,只是粗略的设置了把父布局和子 View 融合在一起,并且把 LayoutParams 保存在 子 View 中,并还没有完成子 View 真正的绘制和布局,需要了解的,看后序文章分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐