android 深入理解LayoutInflater

27 篇文章 1 订阅
23 篇文章 3 订阅
@SystemService(Context.LAYOUT_INFLATER_SERVICE)
public abstract class LayoutInflater {
... ...
}


static {
 	... ...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @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。

@Override
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) {
       //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.,
     *        <code>R.layout.main_page</code>)
     * @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. *
     * <p>
     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For 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.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> 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.
     * <p>
     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For 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
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> 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 &gt;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 
    android:id="@+id/play_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

 

 

这里的FrameLayout就是xml元素的名字,因此在执行inflate时就会调用3处的onCreateView来解析这个FrameLayout标签。当我们使用自定义View时,在xml中必须写View的完整路径。

<com.duan.musicoco.view.ColorPickerView
    android:id="@+id/theme_custom_color_primary"
    />

 

此时调用代码注释的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().
 * <p>
 * <strong>Note:</strong> 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("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {// 解析merge标签,抛出异常,因为merge标签必须为根视图。
            throw new InflateException("<merge /> 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.当没有未访问过的顶点时,则回到上一个顶点,继续试探别的顶点,直到所有的顶点都被访问过。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值