Android 从setContentView谈Activity界面的加载过程

一、前言

作为一个Android开发人员,setContentView方法肯定相当不陌生,因为在我们每一个需要呈现页面的Activity的onCreate方法中都会调用setContentView方法来加载我们事先写好的布局文件。然而或许大部分人也和我一样一直都是用用就好,也没有深入思考该方法具体是怎样将我们的布局文件呈现给用户的。接下来我们来好好研究研究这个方法的作用原理吧!

二、Android窗口

既然我们想要知道Android的页面加载过程,那么我们就得先了解Android系统中的窗口布局。一般来说,当我们设置窗口的Theme为常见的样式时,Android的窗口如下图所示: Android的窗口主要是图中PhoneWindow所包含的部分:


Android窗口模型

Android窗口模型解析

Android常用的窗口布局文件为R.layout.screen_title,位于frameworks/base/core/res/layout/:

<!--
    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中。

三、setContentView加载view的流程

  • 1、Activity中的setContentView方法
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的相应方法。

  • 2、 PhoneWindow中的setContentView方法
    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);   
    }

综上所述,该方法的主要工作为;

  • 第一步:
    如果mContentParent 为空(即这是第一次调用setContentView方法),则installDecor()
    如果不是第一次调用该方法,且无需使用 TransitionManager进行过渡,则直接将窗口中的所有子View均移除

  • 第二步:
    如果需要使用 TransitionManager进行过渡,使用 TransitionManager进行过渡
    否则采用恰当的方式将view添加到窗口中

四、部分方法详解

1、installDecor()方法:

该方法位于PhoneWindow类中

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.
    //源码未贴出
   }
  }
 }

2、ViewGroup generateLayout(DecorView decor)方法

//返回当前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;
}

五、总结

1、setContentView方法工作流程

setContentView方法的具体实现是在PhoneWindow类中,主要通过如下几个步骤完成xml布局资源文件或View的加载。

注意:
使用setContentView(View view)方法设置Activity的布局时,系统会默认将该view的width和height值均设为MATCH_PARENT,而不是使用view自己的属性值,所以如果想通过一个View对象设置布局,又想使用自己设置的参数值时,需要使用setContentView(View view, LayoutParams params)方法

  • 第一步:若是首次使用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对象了;

2、浅谈布局文件优化技巧

  • 从上面的分析可知,在加载xml布局文件时,系统是通过递归的方式从根节点到叶子节点一步一步对控件的属性进行解析的,所以xml文件的层次越深,效率越低,如果嵌套过多,还有可能导致栈溢出,所以在书写布局文件时,应尽量对布局文件进行优化,通过使用相对布局等方式减少不必要的嵌套层次

  • 在源码中,可以看到对merge标签进行处理的过程。在某些场合下,merge标签的使用也可以有效减少布局文件的嵌套层次。如某些比较复杂的布局文件,需要将布局文件拆分开来,分为一个根布局文件和若干个子布局文件,这时可能子布局文件的根节点在添加到根布局文件中时并没有太多意义,只会增加根布局文件的嵌套层次,这种情况下,在子布局文件处使用merge标签就可以去掉无谓的嵌套层次。不过merge标签的使用也是有限制的,首先merge标签只能用于一个xml文件的根节点;其次,使用inflate方法来加载一个根节点为merge标签的布局文件时,需要为该文件指定一个ViewGroup对象作为其父元素,同时需要设置attachToRoot属性为true,否则会抛出异常;

  • 利用include标签增加布局文件的重用性和可读性;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容