android 深入理解LayoutInflater
@SystemService(Context.LAYOUT_INFLATER_SERVICE)public abstract class LayoutInflater {... ...}static { ... ...registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class, new CachedServiceFetcher() { @Override public LayoutInflater createService(ContextImpl ctx) {//创建LayoutInlater,具体类是PhoneLayoutInflater return new PhoneLayoutInflater(ctx.getOuterContext()); }}); ... ...}public class PhoneLayoutInflater extends LayoutInflater { //内置View类型的前缀,如TextView的完整路径是android.widget.TextView private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; ... ...}/** Override onCreateView to instantiate names that correspond to the widgets known to the Widget factory. If we don't find a match, call through to our super class.重写onCreateView以 实例化 与之对应的名称 (Widget 工厂所了解的)。 如果我们找不到匹配,请通过父类。*/@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { //在View名字的前面添加前缀来构造View的完整路径,例如,类名为TextView,那么TextVuiew完整的路径是android.widget.TextView for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs);}
代码不多,核心是覆写了LayoutInflater的onCreateView方法,该方法就是在传递进来的View的名字上加上“android.widget.”或者"android.webkit."前缀用以得到该内置View类(如TextView、Button等都在android.widget包下)的完整路径。最后根据类的完整路径来构造对应的View对象。
具体是一个怎样的流程?以Activity 的setContentView为例:
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2, Window.OnWindowDismissedCallback, WindowControllerCallback, AutofillManager.AutofillClient { ... ... }/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar();}/** * Set the activity content to an explicit view. This view is placed * directly into the activity's view hierarchy. It can itself be a complex * view hierarchy. When calling this method, the layout parameters of the * specified view are ignored. Both the width and the height of the view are * set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use * your own layout parameters, invoke * {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)} * instead. * * @param view The desired content to display. * * @see #setContentView(int) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar();}
Activity的setContentView方法实际调用的是Window的setContentView,而Window是一个抽象类,上文提到的Window的具体实现类是PhoneWindow。
@Overridepublic 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) { //1.当mContentparent为空时先构建DecorView // 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 { //解析layoutResID,通过inflate函数将指定的布局视图添加到mContentarent中 mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true;}
从上图发现:
我们发现mDecor中会加载一个系统定义好的布局,这个布局中又包裹了mContentParent,而这个mContentParent就是我们设置的布局,并添加到parent区域。
在PhoneWindow的setContentView中验证了这一点,首先会构建mContentParent对象,然后通过LayoutInflater的inflate函数将指定布局的视图添加到mContentParent中。
/** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. * * @param resource ID for an XML layout resource to load (e.g., * R.layout.main_page
) * @param root Optional view to be the parent of the generated hierarchy. * @return The root View of the inflated hierarchy. If root was supplied, * this is the root View; otherwise it is the root of the inflated * XML file. */ public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } /** * Inflate a new view hierarchy from the specified xml node. Throws * {@link InflateException} if there is an error. * * * ImportantFor performance * reasons, view inflation relies heavily on pre-processing of XML files * that is done at build time. Therefore, it is not currently possible to * use LayoutInflater with an XmlPullParser over a plain XML file at runtime. * * @param parser XML dom node containing the description of the view * hierarchy. * @param root Optional view to be the parent of the generated hierarchy. * @return The root View of the inflated hierarchy. If root was supplied, * this is the root View; otherwise it is the root of the inflated * XML file. */ public View inflate(XmlPullParser parser, @Nullable ViewGroup root) { return inflate(parser, root, root != null); } /** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. * * @param resource ID for an XML layout resource to load (e.g., * R.layout.main_page
) * @param root Optional view to be the parent of the generated hierarchy (if * attachToRoot is true), or else simply an object that * provides a set of LayoutParams values for root of the returned * hierarchy (if attachToRoot is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to * the root parameter? If false, root is only used to create the * correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ 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(); } } /** * Inflate a new view hierarchy from the specified XML node. Throws * {@link InflateException} if there is an error. *
* ImportantFor performance * reasons, view inflation relies heavily on pre-processing of XML files * that is done at build time. Therefore, it is not currently possible to * use LayoutInflater with an XmlPullParser over a plain XML file at runtime. * * @param parser XML dom node containing the description of the view * hierarchy. * @param root Optional view to be the parent of the generated hierarchy (if * attachToRoot is true), or else simply an object that * provides a set of LayoutParams values for root of the returned * hierarchy (if attachToRoot is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to * the root parameter? If false, root is only used to create the * correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ #LayoutInflater //参数1 为xml解析器 参数2 为要解析布局的父视图 参数3为是否将要解析的视图添加到父视图中//这里使用的是android的XmlPullParser解析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]; //Context对象 mConstructorArgs[0] = inflaterContext; //存储父视图 View result = root; try { // Look for the root node. int type; //找到root元素 while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } ... ... final String name = parser.getName(); ... ... //1. 解析merge标签 if (TAG_MERGE.equals(name)) { ... ... rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root view that was found in the xml //2.不是merge标签直接解析布局中的视图 //3.这里通过xml的tag来解析layout根视图 //name就是要解析的视图的类名,如RelativeLayout final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { ... ... // Create layout params that match root, if supplied // 生成布局参数 params = root.generateLayoutParams(attrs); //如果attachToRoot为false,那么给temp设置布局参数 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. //解析temp视图下所有的子View rInflateChildren(parser, temp, attrs, true); // We are supposed to attach all the views we found (int temp) // to root. Do that now. //如果Root不为空,且attachToRoot为true,那么将temp添加到父视图中 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. //如果root为空或者attachToRoot为false,那么返回结果就是temp if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { ... ... } return result; }}
上述的inflate方法中,主要有以下几步:
1.解析xml中的根标签(第一个元素)
2.如果根标签是merge,那么调用rInflate进行解析,rInflate会将merge标签下的所有子VIew直接添加到根标签中。
3.如果标签是普通元素,那么运行到代码3,调用createViewFromTag对该元素进行解析。
4.调用rInflate解析temp根元素下的所有的子View,并且将这些子View都添加到temp下;
5.返回解析到的根视图。
解析单个元素的createViewFromTag方法/** * Convenience method for calling through to the five-arg createViewFromTag * method. This method passes {@code false} for the {@code ignoreThemeAttr} * argument and should be used for everything except {@code >include>} * tag parsing. */private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) { return createViewFromTag(parent, name, context, attrs, false);}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; //1.用户可以通过设置LayoutInflater的factory来自行解析View,默认这些Factory都为空,可以忽略这段 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); }//2.没有Factory的情况下通过onCreateView或者createView创建View if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try {//3.内置View控件的解析 if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { //4.自定义控件的解析 view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { ... ... } }
本程序的重点在于代码2,以及以后的代码,createViewFromTag将该元素的parent及名字传递过来。
区分内置View和自定义View的方式:
当这个tag的名字中没有包含“.”(在名字中查找“.”返回-1)时,LayoutInflater会认为这是一个内置的View。
例:
这里的FrameLayout就是xml元素的名字,因此在执行inflate时就会调用3处的onCreateView来解析这个FrameLayout标签。当我们使用自定义View时,在xml中必须写View的完整路径。
此时调用代码注释的4的createView来解析该View。
在上文的PhoneLayoutInflater中,PhoneLayoutInflater覆写了onCreateView方法,也就是代码3处的onCreateView方法,该方法就是在View的标签名的前面设置一个"android.widget."前缀,然后传递给createView进行解析。
也就是内置View 和 自定义 View最终都调用了createView进行解析,只是Google为了让开发者在xml中更方便定义View,只写View名称而不需要写完整的路径。
在LayoutInflater解析时,如果遇到只写类名的View,那么认为是内置的View控件,在onCreateView中将"android.widget."前缀传递给craeteView方法。
最后在crateView中构造View 的完整路径来解析。
如果是自定义控件,那么必须写完整路径,此时调用createView且前缀为null进行解析。
//createView相关代码//根据完整路径的类名通过反射机制构造View对象public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { //1.从缓存中获取构造函数 Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { //2.没有缓存构造函数 if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it //如果prefix不为空,那么构造完整的View路径,并且加载该类 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); } } //3.从Class对象获取构造函数 constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); //4.将构造函数存入缓存中 sConstructorMap.put(name, constructor); } else { ... ... } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = mContext; } Object[] args = mConstructorArgs; args[1] = attrs; //5.通过反射构造View final View view = constructor.newInstance(args); if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } mConstructorArgs[0] = lastContext; return view; } catch (NoSuchMethodException e) { ... ... }}
createView相对比较简单,如果有前缀,那么构造View的完整路径,并且将该类加载到虚拟机中,然后获取该类的构造函数并缓存起来,再通过构造函数来创建View的对象,最后将View对象返回,这就是解析单个View的过程。
而我们的窗口是一棵视图树,LayoutInflater需要解析这棵树,这个功能就交给了rInflate方法。
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). * * Note: Default visibility so the BridgeInflater can * override it. */void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { //1.获取树的深度,深度优先遍历 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(" cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) {// 解析merge标签,抛出异常,因为merge标签必须为根视图。 throw new InflateException(" must be the root element"); } else { //3.根据元素名进行解析 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); //将解析到的View添加到ViewGroup中,也就是其parent viewGroup.addView(view, params); } } if (pendingRequestFocus) { parent.restoreDefaultFocus(); } if (finishInflate) { parent.onFinishInflate(); }}
rInflate 通过深度优先遍历,每解析一个View元素就会递归调用rInflate,直到这条路径下的最后一个元素,然后再回溯过来将每个View元素添加到它们的parent中。通过rInflate的解析之后,整棵视图树就构建完毕。当调用了activity的onResume()之后,我们通过steContentView设置的内容就会出现在视野中。
使用深度优先搜索来遍历这个图的具体过程是:
首先从一个未走到过的顶点作为起始顶点,比如1号顶点作为起点。
沿1号顶点的边去尝试访问其它未走到过的顶点,首先发现2号顶点还没有走到过,于是来到了2号顶点。
再以2号顶点作为出发点继续尝试访问其它未走到过的顶点,这样又来到了4号顶点。
再以4号顶点作为出发点继续尝试访问其它未走到过的顶点。
但是,此时沿4号顶点的边,已经不能访问到其它未走到过的顶点了,所以需要返回到2号顶点。
返回到2号顶点后,发现沿2号顶点的边也不能再访问到其它未走到过的顶点。此时又会来到3号顶点(2->1->3),再以3号顶点作为出发点继续访问其它未走到过的顶点,于是又来到了5号顶点。
至此,所有顶点我们都走到过了,遍历结束。
参考《Android源码设计模式》
深度优先遍历的主要思想是:
1.首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点;
2.当没有未访问过的顶点时,则回到上一个顶点,继续试探别的顶点,直到所有的顶点都被访问过。
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- android笔记4-xml解析
- No 93 · android xml的生成和解析
- Android日历周视图 可添加事件标记
- AsyncTask 源码解析
- 三、ANDROID SDK下文件解析
- 探究Android界面的显示机制
- 【Android工具】被忽略的UI检视利器:Hierarchy Viewer
- Android学习系列(19)-App数据格式之解析Xml