看过《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; @Override protected 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布局格局: