做为一个Android开发人员,setContentView方法确定至关不陌生,由于在咱们每个须要呈现页面的Activity的onCreate方法中都会调用setContentView方法来加载咱们事先写好的布局文件。然而或许大部分人也和我同样一直都是用用就好,也没有深刻思考该方法具体是怎样将咱们的布局文件呈现给用户的。接下来咱们来好好研究研究这个方法的做用原理吧! java
既然咱们想要知道Android的页面加载过程,那么咱们就得先了解Android系统中的窗口布局。通常来讲,当咱们设置窗口的Theme为常见的样式时,Android的窗口以下图所示: Android的窗口主要是图中PhoneWindow所包含的部分: android
Android经常使用的窗口布局文件为R.layout.screen_title,位于frameworks/base/core/res/layout/: web
<!-- This is an optimized layout for a screen, with the minimum set of features enabled. --> <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>
能够看出,DecorView中包含一个Vertical的LinearLayout布局文件,文件中有两个FrameLayout,上面一个FrameLayout用于显示Activity的标题,下面一个FrameLayout用于显示Activity的具体内容,也就是说,咱们经过setContentView方法加载的布局文件/View将显示在该FrameLayout中。 app
public void setContentView(@LayoutRes int layoutResID) { //getWindow()方法将返回与该Activity相关联的Window对象 getWindow().setContentView(layoutResID); /* * 当该Activity是另外一个Activity的子Activity、该Activity不含属性值Window.FEATURE_ACTION_BAR * 或者该Activity目前已有一个ActionBar时,该方法不进行任何操做,直接返回 * 不然初始化窗口的ActionBar,并为其设置相应的属性值 */ initWindowDecorActionBar(); } public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar(); } public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); initWindowDecorActionBar(); } public void addContentView(View view, ViewGroup.LayoutParams params) { getWindow().addContentView(view, params); initWindowDecorActionBar(); }
能够看到,在Activity的四个setContentView方法中,都分别调用了Window的相应方法。 ide
Window中的setContentView方法均为抽象方法,因此跳过,直接看Window的实现类PhoneWindow中的setContentView方法 函数
@Override public void setContentView(int layoutResID) { /* * private ViewGroup mContentParent:该变量即为Activity的根布局文件,这是mDecor自身或mDecor的子类 * installDecor()方法用于加载mDecor,后面详说 * FEATURE_CONTENT_TRANSITIONS:窗口内容发生变化时是否须要使用TransitionManager进行过渡的标识 */ if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { // 须要使用TransitionManager进行过渡时的处理 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { //不须要过渡时,经过inflate方法将layoutResID中的View树添加到窗口中 mLayoutInflater.inflate(layoutResID, mContentParent); } //请求设置Window内容的属性值,将其写入一个WindowInsets类中 mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } } @Override public void setContentView(View view) { //注意:当使用该方法设置窗口布局文件时,系统将默认设置view的width和height均为MATCH_PARENT //这里即可以解释上一篇博客《Android LayoutInflater.inflater方法详解》中的Case1了 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { //该方法与上面setContentView(int layoutResID)惟一的不一样点在于第二个if语句的else语句块内容以下 //经过调用mContentParent的addView方法将view添加到窗口中 //也就是说这两个方法惟一的区别在于将view添加到窗口的方式不一样,其他并没有差异 mContentParent.addView(view, params); }
综上所述,该方法的主要工做为; 布局
第一步: post
若是mContentParent 为空(即这是第一次调用setContentView方法),则installDecor() 优化
若是不是第一次调用该方法,且无需使用 TransitionManager进行过渡,则直接将窗口中的全部子View均移除 spa
第二步:
若是须要使用 TransitionManager进行过渡,使用 TransitionManager进行过渡
不然采用恰当的方式将view添加到窗口中
该方法位于PhoneWindoe类中
private void installDecor() { // 若是mDecor为空,则生成一个Decor,并设置其属性 if (mDecor == null) { // 此句即mDecor = new DecorView(getContext(), -1) mDecor = generateDecor(); /* * setDescendantFocusability用于设置mDecor中的子View的聚焦性 * 该方法决定了mDecor与其中包含的子View之间关于焦点获取的关系 * FOCUS_AFTER_DESCENDANTS表示只有当mDecor的子View都不肯意获取焦点时 才让mDecor获取焦点 */ mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // 设置mDecor为整个Activity窗口的根节点,今后处能够看出窗口根节点为一个DecorView mDecor.setIsRootNamespace(true); /* * if条件知足时,在animation时执行mInvalidatePanelMenuRunnable这个Runnable动做 */ if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } // 若是mContentParent为空,则生成一个Decor,并设置其属性 // 后面会详说generateLayout(DecorView decor)方法 if (mContentParent == null) { mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if // appropriate. mDecor.makeOptionalFitsSystemWindows(); /* * DecorContentParent位于com.android.internal.widget中,是一个接口 * 由应用程序窗口的顶层Decor实现,该类主要为mDecor提供了许多title/window decor features */ final DecorContentParent decorContentParent = (DecorContentParent) mDecor .findViewById(R.id.decor_content_parent); if (decorContentParent != null) { /* * decorContentParent非空时 * 1. 将decorContentParent赋值给mDecorContentParent * 2. 设置窗口回调函数 * 3.设置窗口的title、icon、logo等属性值 * 为了增强博客的可读性,就未将这部分代码贴出来,只将主要功能进行了简单介绍 * 想要详细了解的能够直接参看源码 */ } else { /* * decorContentParent为空时根据窗口是否为一个包含Title的窗口决定是否显示title * 若是窗口包含特征FEATURE_NO_TITLE,则隐藏窗口的title view 不然设置窗口的title */ } if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { mDecor.setBackgroundFallback(mBackgroundFallbackResource); } if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { // Only inflate or create a new TransitionManager if the caller // hasn't already set a custom one. //源码未贴出 } } }
//返回当前Activity的内容区域视图,即咱们的布局文件显示区域mContentParent protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. //从当前Window的Theme中获取一组属性值,赋给a TypedArray a = getWindowStyle(); /* * 此处有段代码未贴出,功能为: * 1. 根据Activity的Theme特征,为当前窗口选择布局文件的修饰feature * 2. Inflate the window decor */ int layoutResource; int features = getLocalFeatures(); /* * 此处有段代码未贴出 * 1. getLocalFeatures()返回一个用于描述当前Window特征的整数值 * 2. layoutResource为根据features所指代的窗口特征值而为当前窗口选定的资源文件id * 3. 系统包含多个布局资源文件,位于frameworks/base/core/res/layout/ * 4. 主要有:R.layout.dialog_titile_icons、R.layout.screen_title_icons * R.layout.screen_progress、R.layout.dialog_custom_title * R.layout.dialog_title * R.layout.screen_title 最经常使用的Activity窗口修饰布局文件 * R.layout.screen_simple 全屏的Activity窗口布局文件 */ //startChanging()方法内容:mChanging = true; mDecor.startChanging(); //将layoutResource资源文件包含的View树添加到decor中 //width和height均为MATCH_PARENT //并为mContentRoot和contentParent赋值 View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { ProgressBar progress = getCircularProgressBar(false); if (progress != null) { progress.setIndeterminate(true); } } if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { registerSwipeCallbacks(); } //后面包含一段只能应用于顶层窗口的一些Remaining steps //主要用于设置一些title和background属性 return contentParent; }
setContentView方法的具体实现是在PhoneWindow类中,主要经过以下几个步骤完成xml布局资源文件或View的加载。
第一步:如果首次使用setContentView方法,则先建立一个DecorView对象mDecor,该对象是整个Activity窗口的根视图;而后根据程序中选择的Activity的Theme/Style等属性值为窗口添加布局属性和相应的修饰文件,并经过findViewById方法获取对应的根布局文件添加到mDecor中,也就是说,第一次使用该方法时会将Activity显示区域进行初始化;若不是第一次使用该方法,则以前已完成初始化过程并得到了mDecor和mContentParent对象,则只须要将以前添加到mContentParent区域的Views移除,空出该区域从新进行布局便可,简而言之,就是对mContentParent区域进行刷新;
第二步:经过inflate(加载xml文件)或addView(加载View)方法将Activity的布局文件添加到mContentParent区域;
当setContentView设置显示OK之后,回调Activity的onContentChanged方法,通知Activity布局文件已经成功加载完成,接下来咱们即可以使用findViewById方法获取布局文件中含有id属性的view对象了;
注意:使用setContentView(View view)方法设置Activity的布局时,系统会默认将该view的width和height值均设为MATCH_PARENT,而不是使用view本身的属性值,因此若是想经过一个View对象设置布局,又想使用本身设置的参数值时,须要使用setContentView(View view, LayoutParams params)方法
从上面的分析可知,在加载xml布局文件时,系统是经过递归的方式从根节点到叶子节点一步一步对控件的属性进行解析的,因此xml文件的层次越深,效率越低,若是嵌套过多,还有可能致使栈溢出,因此在书写布局文件时,应尽可能对布局文件进行优化,经过使用相对布局等方式减小没必要要的嵌套层次
在源码中,能够看到对merge标签进行处理的过程。在某些场合下,merge标签的使用也能够有效减小布局文件的嵌套层次。如某些比较复杂的布局文件,须要将布局文件拆分开来,分为一个根布局文件和若干个子布局文件,这时可能子布局文件的根节点在添加到根布局文件中时并无太多意义,只会增长根布局文件的嵌套层次,这种状况下,在子布局文件处使用merge标签就能够去掉无谓的嵌套层次。不过merge标签的使用也是有限制的,首先merge标签只能用于一个xml文件的根节点;其次,使用inflate方法来加载一个根节点为merge标签的布局文件时,须要为该文件指定一个ViewGroup对象做为其父元素,同时须要设置attachToRoot属性为true,不然会抛出异常;
利用include标签增长布局文件的重用性和可读性;