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 真正的绘制和布局,需要了解的,看后序文章分析。
相关文章推荐
- android应用程序窗口框架学习(2)-view绘制流程源代码解析-setContentView与LayoutInflater加载解析机制源码分析
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android 进阶学习:Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android中setContentView、addContentView、inflate、LayoutInflater和findViewById()区别
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
- android应用程序窗口框架学习(1)-view绘制流程源代码解析
- Android视图的绘制流程(下)——View的Layout与Draw过程
- Android应用setContentView与LayoutInflater加载解析机制源码分析(转载)
- [Android]inflate方法与 findViewById 方法区别 | LayoutInflater的inflate函数用法详解
- Android视图绘制流程完全解析,带你一步步深入了解View(二)