android-----View工作原理系列(二)

来源:互联网 发布:linux wine 运行 exe 编辑:程序博客网 时间:2024/05/21 07:04

        看过《Android开发艺术探索》View的绘制源码之后,里面在讲解绘制最开始执行的方法是ViewRootImpl里面的performTraversals,觉得有点费解,为什么直接就执行到这个方法呢?这中间一定也存在着执行到performTraversals的过程,本着想要了解清楚的想法,看了看源码,在此分享一下:

        上一篇我介绍了invalidate、postInvalidate以及requestLayout的源码,这篇后面部分会用到里面的知识,如果你对这三个方法不太了解,可以看看上一篇博客;

        我们平常会在Activity的onCreate里面调用setContentView设置Activity的布局文件,那么很自然应该从这个方法开始分析,因为他才是我们主动调用的,调用之后经过一系列过程把我们的布局文件中的内容显示到界面上;

        首先查看Activity的setContentView:

public void setContentView(int layoutResID) {        getWindow().setContentView(layoutResID);        initActionBar();    }
        可以发现他调用的是getWindow()的setContentView方法,那么这里getWindow返回值是什么呢?

        Activity#getWindow

public Window getWindow() {        return mWindow;    }
        可以看到返回值是mWindow,他的类型是:

private Window mWindow;
        而Window是一个抽象类,我们需要找到mWindow的赋值语句,如下:

mWindow = PolicyManager.makeNewWindow(this);
        可以看到他的值是PolicyManager的静态方法makeNewWindow的返回值,查看这个方法:

 public static Window makeNewWindow(Context context) {        return sPolicy.makeNewWindow(context);    }
        该方法直接调用的是sPolicy的makeNewWindow方法,这里的sPolicy是IPolicy类型对象,而IPolicy是接口,所以我们还需要找到他的具体实现,在PolicyManager类最开始,我们找到如下代码:

private static final String POLICY_IMPL_CLASS_NAME =        "com.android.internal.policy.impl.Policy";    private static final IPolicy sPolicy;    static {        // Pull in the actual implementation of the policy at run-time        try {            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);            sPolicy = (IPolicy)policyClass.newInstance();        } catch (ClassNotFoundException ex) {            throw new RuntimeException(                    POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);        } catch (InstantiationException ex) {            throw new RuntimeException(                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);        } catch (IllegalAccessException ex) {            throw new RuntimeException(                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);        }    }
        可以发现sPolicy实际上是通过反射的方式创建的Policy对象,也就是sPolicy.makeNewWindow实际上调用的是Policy.makeNewWindow,进入Policy里面找到makeNewWindow方法:

  public Window makeNewWindow(Context context) {        return new PhoneWindow(context);    }
        终于找到了,其实就是返回了一个PhoneWindow对象而已了,回到我们的Activity#setContentView,getWindow().setContentView(layoutResID);实际上执行的是PhoneWindow的setContentView方法啦,那么我们就去PhoneWindow的setContentView里面看看吧:

@Override    public void setContentView(int layoutResID) {        if (mContentParent == null) {            installDecor();        } else {            mContentParent.removeAllViews();        }        mLayoutInflater.inflate(layoutResID, mContentParent);        final Callback cb = getCallback();        if (cb != null && !isDestroyed()) {            cb.onContentChanged();        }    }
        代码不长,但是内容其实挺多的,首先查看mContentParent是否为空,mContentParent是ViewGroup类型对象,如果你是第一次执行setContentView的话,mContentParent的值当然为null了,为null执行installDecor来初始化一个DecorView对象,不为null的话将mContentParent里面的View全部移除掉,从这里也可以看出来setContentView是可以多次调用的,不过第二次调用会移掉第一次已经添加进去的View而已了,因为我们是第一次执行setContentView,那么就该执行installDecor方法了:

    private void installDecor() {        if (mDecor == null) {            mDecor = generateDecor();            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);            mDecor.setIsRootNamespace(true);            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);            }        }        if (mContentParent == null) {            mContentParent = generateLayout(mDecor);            ...............            ...............            ...............        }    }
        这部分代码比较长,我省略了非关键部分,首先判断mDecor是否为null,mDecor是DecorView类型对象,这里我们第一次调用setContentView,那么mDecor为null,执行第3行代码,通过generateDecor生成一个DecorView对象并且返回:

  protected DecorView generateDecor() {        return new DecorView(getContext(), -1);    }
        可以看到这个方法就只是创建一个DecorView对象返回而已;

        接着判断mContentParent为null,执行第11行通过generateLayout生成一个ViewGroup对象返回,这里会将刚刚创建的DecorView对象传递进去,很自然该看看generateLayout的源码了:

        鉴于generateLayout的源码比较长,我截取了对我们有用的部分用伪代码的方式来进行分析:

 protected ViewGroup generateLayout(DecorView decor) {        // Apply data from current theme.        //1.获得窗口style属性        TypedArray a = getWindowStyle();                //根据这些style属性来设置窗口是否显示标题、是否浮动、是都有动画等等        .............        .............        //2.获取feature(风格)属性,来选择不同的窗口修饰布局文件        int layoutResource;        int features = getLocalFeatures();        //接下来就是根据不同的features值来给layoutResource赋不同的布局文件id        .............        .............        //3.加载选定好的布局文件,将其添加至DecorView上面,并且指定contentParent的值        View in = mLayoutInflater.inflate(layoutResource, null);        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);        .............        .............        //4.设置一些background、title之类的属性 }
        generateLayout具体的执行过程在伪代码里面已经解释的比较清楚了,这里我们补充点知识,在generateLayout里面第2步中,我们通过getLocalFeatures获取到了风格属性,一般来说我们设置窗体风格属性的方式有两种:(1):通过requestFeature()指定,相应的获取方法就是getLocalFeatures了;(2):通过requestWindowFeature或者在Activity的xml配置中设置属性:android:theme=" ",相应的获取方法是getWindowStyle;我们来看下其中一个Activity窗口修饰布局文件是R.layout.screen_title,也就是下面这样子的布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:orientation="vertical"      android:fitsSystemWindows="true">      <FrameLayout          android:layout_width="match_parent"           android:layout_height="?android:attr/windowTitleSize"          style="?android:attr/windowTitleBackgroundStyle">          <TextView android:id="@android:id/title"               style="?android:attr/windowTitleStyle"              android:background="@null"              android:fadingEdge="horizontal"              android:gravity="center_vertical"              android:layout_width="match_parent"              android:layout_height="match_parent" />      </FrameLayout>      <FrameLayout android:id="@android:id/content"          android:layout_width="match_parent"           android:layout_height="0dip"          android:layout_weight="1"          android:foregroundGravity="fill_horizontal|top"          android:foreground="?android:attr/windowContentOverlay" />  </LinearLayout>  
        该布局文件是一个LinearLayout里面嵌套有两个FrameLayout,第一个FrameLayout就是我们的标题,第2个FrameLayout是内容存放地方,如果你仔细看的话,会发现generateLayout伪代码里面的第19行ID_ANDROID_CONTENT的值其实就是第二个FrameLayout的id值,这样的话,我们的generateLayout代码分析结束了;

        那么我们回到PhoneWindow里面的setContentView方法里面,发现此时mContentParent的值将等于我们上面布局中的第二个FrameLayout,继续分析PhoneWindow里面的setContentView方法,接下来执行的将是mLayoutInflater.inflate(layoutResID, mContentParent);也就是把我们当前的布局添加到了mContentParent里面,这里也说明了一点我们平常添加的布局文件其实只是Activity总体布局中第2个FrameLayout部分的子布局而已,这点我将会在这篇博客最后面通过实例来进行验证,很自然,这里是通过LayoutInflater的inflate方法将当前布局添加进去的,这也解释了为什么说setContentView其实也是通过inflate来进行布局解析并且添加到界面中了;那么我们就该看看LayoutInflater的inflate的源码了:

        这个方法的具体源码分析大家可以看我的另外一篇博客android------LayoutInflater的inflate方法详解,在这里我只是讲解我们用到的部分:

        LayoutInflater#inflate

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {        synchronized (mConstructorArgs) {            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");            final AttributeSet attrs = Xml.asAttributeSet(parser);            Context lastContext = (Context)mConstructorArgs[0];            mConstructorArgs[0] = mContext;            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)) {                    if (root == null || !attachToRoot) {                        throw new InflateException("<merge /> can be used only with a valid "                                + "ViewGroup root and attachToRoot=true");                    }                    rInflate(parser, root, attrs, false);                } else {                    // Temp is the root view that was found in the xml                    View temp;                    if (TAG_1995.equals(name)) {                        temp = new BlinkLayout(mContext, attrs);                    } else {                        temp = createViewFromTag(root, name, 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                    rInflate(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) {                InflateException ex = new InflateException(e.getMessage());                ex.initCause(e);                throw ex;            } catch (IOException e) {                InflateException ex = new InflateException(                        parser.getPositionDescription()                        + ": " + e.getMessage());                ex.initCause(e);                throw ex;            } finally {                // Don't retain static reference on context.                mConstructorArgs[0] = lastContext;                mConstructorArgs[1] = null;            }            Trace.traceEnd(Trace.TRACE_TAG_VIEW);            return result;        }    }

        这里首先解析根布局,在第45行通过createViewFromTag创建出根布局view,具体里面是采用反射实现的,将根布局view赋值给temp,接着在第68行调用rInflate,将根布局View作为参数传递进去,这个方法会循环解析根布局下面的所有子元素,并且将他们添加到根布局中,我们可以看看rInflate这个方法:

        LayoutInflater#rInflate

 void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,            boolean finishInflate) throws XmlPullParserException, IOException {        final int depth = parser.getDepth();        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_INCLUDE.equals(name)) {                if (parser.getDepth() == 0) {                    throw new InflateException("<include /> cannot be the root element");                }                parseInclude(parser, parent, attrs);            } else if (TAG_MERGE.equals(name)) {                throw new InflateException("<merge /> must be the root element");            } else if (TAG_1995.equals(name)) {                final View view = new BlinkLayout(mContext, attrs);                final ViewGroup viewGroup = (ViewGroup) parent;                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);                rInflate(parser, view, attrs, true);                viewGroup.addView(view, params);                            } else {                final View view = createViewFromTag(parent, name, attrs);                final ViewGroup viewGroup = (ViewGroup) parent;                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);                rInflate(parser, view, attrs, true);                viewGroup.addView(view, params);            }        }        if (finishInflate) parent.onFinishInflate();    }
        可以看到第7行是一个while循环,就是用来遍历根布局view下面子元素的,除了在解析的过程中抛出一些异常之外,我们会发现第30行以及第36行都执行了viewGroup.addView(view, params);这句话,这句话的主要作用就是将当前遍历到的子元素view添加到vireGroup中,这里的viewGroup就是我们传入的参数---根布局view,那么我们就该看看addView中做了些什么操作了;

        这个方法位于ViewGroup里面:

        ViewGroup#addView

public void addView(View child, int index, LayoutParams params) {        if (DBG) {            System.out.println(this + " addView");        }        // addViewInner() will call child.requestLayout() when setting the new LayoutParams        // therefore, we call requestLayout() on ourselves before, so that the child's request        // will be blocked at our level        requestLayout();        invalidate(true);        addViewInner(child, index, params, false);    }
        看到了我们熟悉的requestLayout以及invalidate,在上一篇博客我已经分析了这两个方法是怎么执行到performTraversals的过程,大家可以去上一篇看看;

        到这里的话,我们分析了通过Activity的setContentView执行到了ViewRootImpl里面的performTraversals的过程,接下来从performTraversals开始就是我们View的绘制过程了,也就是measure、layout、draw过程了,这个部分网上源码分析很多的,我只会在下篇从伪代码的角度进行总结;

         接下来我通过实例来验证我在上面说到的平常我们添加的View其实只是添加到DecorView布局里面第2个Fragment部分:

        首先来看下布局文件:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/linearLayout"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:id="@+id/button"        android:layout_width="match_parent"        android:layout_height="100dp"        android:text="验证按钮"/></LinearLayout>
        接着就是MainActivity了:

public class MainActivity extends Activity {public Button mButton;public LinearLayout mLinearLayout;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mLinearLayout = (LinearLayout) findViewById(R.id.linearLayout);ViewParent parent = mLinearLayout.getParent();System.out.println(parent);System.out.println(parent.getParent());}}

         点击按钮查看Logcat输出:

07-05 05:21:26.618: I/System.out(2586): android.widget.FrameLayout{4175cf98 V.E..... ......I. 0,0-0,0 #1020002 android:id/content}
07-05 05:21:26.648: I/System.out(2586): android.widget.LinearLayout{41756830 V.E..... ......I. 0,0-0,0}

        我们获取到了LinearLayout,随后通过getParent获取到他的父布局,同样调用getParent获得父布局的父布局,可以看到第一行输出是FrameLayout,验证了我们上面说的setContentView只是将参数中的布局添加到FrameLayout里面,而第二行输出LinearLayout的原因是我们在Manifest里面设置Activity的theme主题是:

        android:theme="@android:style/Theme"

        接下来以一张图来说明Activity布局格局:






0 0
原创粉丝点击