×

【Android View源码分析(一)】setContentView加载视图机制深度分析

96
大圣代
2017.08.14 23:37* 字数 2125

【本文出自大圣代的技术专栏 http://blog.csdn.net/qq_23191031
【转载烦请注明出处,尊重他人劳动成果就是对您自己的尊重】


不喜欢看文字的可以直接到文字尾,看图说话。

1, 前言

  1. 在前面《【Android 控件架构】详解Android控件架构与常用坐标系》的文章中我们提到了setContentView()方法,当时只是匆匆带过,并没有阐明具体流程。而这篇文章就是从Activity中的setContentView()方法出发结合上篇的视图框架,详细分析setContentView()的工作原理。还是贴一张图复习一下吧。
  1. 从上面的文章中我们知道setContentView()方法是用来设置ContentView布局地,当系统调用了setContentView()方法所有的控件就得到了显示,但是你有想过Android系统是如何让xml文件加载到界面并显示出来的呢?setContentView()中具体是如何实现的呢?就让我们在这些疑问来进入下面的探讨吧。

2 从setContentView说起(基于Api 25 Android 7.1.1)

本来是想基于Api 26来看的,可是后来才想起来 Android 8.0的源码还没发布。。。

2-1 Activity源码中的setContentView

经过阅读Android的源码发现,系统为我们提供了三个setContentView()的重载方法,他们都调用了getWindow()中的setContentView()方法。


    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

那么 getWindow()方法有事做什么的呢,咱们继续往下看。

2-2 关于窗口Window类的一些关系

getWindow()的作用

    /**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
   // 如果返回为null表示,则表示当前Activity不在窗口上
    public Window getWindow() {
        return mWindow;
    }
 
     ...

     mWindow = new PhoneWindow(this, window);

通过源码我们可以看到getWindow()方法返回的就是PhoneWindow的实例对象(PhoneWindow是抽象类Window的唯一实现类 PhoneWindow在线源码地址

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    private final static String TAG = "PhoneWindow";

    ...

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    // This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    private ViewGroup mContentParent;

    private ViewGroup mContentRoot;
    ...
}

而在PhoneWindow中我们看到了作为成员变量的 mDecor,(在Android 7.1.1中DecorView已经不再是PhoneWindow的内部类了,而且包都换了,有图有真相)。

Android 5.1.1
Android 7.1.1

查看DecorView之后发现public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks,看见没有,DecorView才是Activity的根布局(root view),他继承了 FrameLayout负责Activity视图的加载,而DecorView本身则是由PhoneWindow加载的。PhoneWindow是如何加载DecorView的呢,咱们带着问题继续往下看

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    private static final String TAG = "DecorView";

    private static final boolean DEBUG_MEASURE = false;

    private static final boolean SWEEP_OPEN_MENU = false;

    // The height of a window which has focus in DIP.
    private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
    // The height of a window which has not in DIP.
    private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
        ....
 }

一言不可就上图:

Android 5.xx时期PhoneWindow与DecorView的关系
Android 7.1.1时期PhoneWindow与DecorView的关系

2-3 PhoneWindow中的setContentView方法

Window类中setContentView方法是抽象的,所以我们直接去看PhonWindow类中关于 setContentView方法的实现过程

@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) {
            //创建DecorView,并添加到mContentParent上
            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 {
            //将要加载的资源添加到mContentParent上
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            //回调通知表示完成界面加载
            cb.onContentChanged();
        }
    }

源码中的第一步就是验证mContentParent是否为 null,如果为null则表示程序是第一次运行,执行installDecor。如果不为null则会判断当前是否设置了FEATURE_CONTENT_TRANSITIONS(这个属性表示内容加载时需不需要过场动画,默认为false)。如果没有使用过场动画则移除mContentParent中的所有view(所以说 setContentView方法可以多次调用,因为他会移除掉所有的控件);

如果在初始化mContentParent之后,用户设置了启用转场动画则使用Scene开启过度,否则mLayoutInflater.inflate(layoutResID, mContentParent);将我们的资源文件通过LayoutInflater对象转化为控件树添加到mContentParent中。

再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:

422    @Override
423    public void setContentView(View view) {
424        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
425    }
 @Override
428    public void setContentView(View view, ViewGroup.LayoutParams params) {
429        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
430        // decor, when theme attributes and the like are crystalized. Do not check the feature
431        // before this happens.
432        if (mContentParent == null) {
433            installDecor();
434        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
435            mContentParent.removeAllViews();
436        }
437
438        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
439            view.setLayoutParams(params);
440            final Scene newScene = new Scene(mContentParent, view);
441            transitionTo(newScene);
442        } else {
443            mContentParent.addView(view, params);
444        }
445        mContentParent.requestApplyInsets();
446        final Callback cb = getCallback();
447        if (cb != null && !isDestroyed()) {
448            cb.onContentChanged();
449        }
450        mContentParentExplicitlySet = true;
451    }

看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。

所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。

2-4 installDecor()方法 源码分析

2614    private void installDecor() {
2615        mForceDecorInstall = false;
2616        if (mDecor == null) {
2617            mDecor = generateDecor(-1);
2618            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
2619            mDecor.setIsRootNamespace(true);
2620            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
2621                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
2622            }
2623        } else {
2624            mDecor.setWindow(this);
2625        }
2626        if (mContentParent == null) {
                     //根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
2627            mContentParent = generateLayout(mDecor);

                      //......

2674            } else {
2675                mTitleView = (TextView) findViewById(R.id.title);
2676                if (mTitleView != null) {
                           //根据FEATURE_NO_TITLE隐藏,或者设置mTitleView的值  
2677                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
2678                        final View titleContainer = findViewById(R.id.title_container);
2679                        if (titleContainer != null) {
2680                            titleContainer.setVisibility(View.GONE);
2681                        } else {
2682                            mTitleView.setVisibility(View.GONE);
2683                        }
2684                        mContentParent.setForeground(null);
2685                    } else {
2686                        mTitleView.setText(mTitle);
2687                    }
2688                }
2689            }

我在源码中发现了一个很重要的东西,请看第2677行!!!,这就在最根本上解释了:为什么要在setContentView()方法之前设置requestWindowFeature(Window.FEATURE_NO_TITLE)才能不显示TitleActionBar部分,达到全屏的效果。

言归正传,installDecor()方法一进来就判断mDcor是否为空,为空怎么办创建一个喽,咦generateDecor(-1)传一个 -1 是什么鬼???代码规范呢!Google也可以这么写代码么??......咳咳。

2263    protected DecorView generateDecor(int featureId) {
      //......
2281        return new DecorView(context, featureId, this, getAttributes());
2282    }

ps:怎么又一大堆,看来7.1.1的源码和5.1.1的差异真是不小啊。啥,Androdi5.1.1里面的长啥样?

protected DecorView generateDecor() {  
        return new DecorView(getContext(), -1);  
    }  

不看不知道,一看吓一跳。看见没有,一共两行。这里就不展开讨论了.....

2-5 generateLayout()方法 源码分析

在源码 2626行,我们看到当 mContentParent == null的时候使用generateLayout(mDecor)方法创建一个mContentParent出来。generateLayout(mDecor)看名字好像倒是像用来设置layout的。

2284  protected ViewGroup generateLayout(DecorView decor) {
2285        // Apply data from current theme.
             //首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags  
2287        TypedArray a = getWindowStyle();
2288       
            //...
2299        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
2300        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
2301                & (~getForcedWindowFlags());
2302        if (mIsFloating) {
2303            setLayout(WRAP_CONTENT, WRAP_CONTENT);
2304            setFlags(0, flagsToUpdate);
2305        } else {
2306            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
2307        }
2309        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
2310            requestFeature(FEATURE_NO_TITLE);
2311        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
2312            // Don't allow an action bar if there is no title.
2313            requestFeature(FEATURE_ACTION_BAR);
2314        }
            
            //....

            //...根据当前sdk的版本确定是否需要menukey  
2413        WindowManager.LayoutParams params = getAttributes();

2491        // Inflate the window decor.
2492
2493        int layoutResource;
2494        int features = getLocalFeatures();

            //......
            //根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值

            //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值 
2495        // System.out.println("Features: 0x" + Integer.toHexString(features));
2496        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
2497            layoutResource = R.layout.screen_swipe_dismiss;
2498        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
2499            if (mIsFloating) {
2500                TypedValue res = new TypedValue();
2501                getContext().getTheme().resolveAttribute(
2502                        R.attr.dialogTitleIconsDecorLayout, res, true);
2503                layoutResource = res.resourceId;
2504            } else {
2505                layoutResource = R.layout.screen_title_icons;
2506            }
2507            // XXX Remove this once action bar supports these features.
2508            removeFeature(FEATURE_ACTION_BAR);
2509            // System.out.println("Title Icons!");
2510        } else if {
                //......
2552
2553        mDecor.startChanging(); //通知 开始改变
2554        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
2555
2556        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
           
            //......
 
2604        mDecor.finishChanging();//通知 改变完成
2605
2606        return contentParent;
2607    }
    }

从整体角度来讲这个方法就是根据用户设置的风格、标签为窗口选择不同的主布局文件,DecorView做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。 哎!我怎么没看见DecorView添加布局的代码呢?别急下边就告诉你怎么回事。

在进入这个方法时,系统就会调用getWindowStyle() 在当前的Window的theme中获取我们的Window属性,对我们的Window设置各种requestFeature,setFlags等等。

getWindowStyle()为抽象类Window提供的方法,具体源码如下:

665    public final TypedArray getWindowStyle() {
666        synchronized (this) {
667            if (mWindowStyle == null) {
668                mWindowStyle = mContext.obtainStyledAttributes(
669                        com.android.internal.R.styleable.Window);
670            }
671            return mWindowStyle;
672        }
673    }

我们顺藤摸瓜找到属性位置 源码地址

<!-- The set of attributes that describe a Windows's theme. -->  
   <declare-styleable name="Window">  
       <attr name="windowBackground" />  
       <attr name="windowContentOverlay" />  
       <attr name="windowFrame" />  
       <attr name="windowNoTitle" />  
       <attr name="windowFullscreen" />  
       <attr name="windowOverscan" />  
       <attr name="windowIsFloating" />  
       <attr name="windowIsTranslucent" />  
       <attr name="windowShowWallpaper" />  
       <attr name="windowAnimationStyle" />  
       <attr name="windowSoftInputMode" />  
       <attr name="windowDisablePreview" />  
       <attr name="windowNoDisplay" />  
       <attr name="textColor" />  
       <attr name="backgroundDimEnabled" />  
       <attr name="backgroundDimAmount" />  

所以这里就是解析我们为Activit设置theme的地方,至于theme一般可以在AndroidManifest.xml文件中设置。

设置theme的位置
而AppTheme则在 res/value/style.xml文件里

接下来就到关键的部分了,2494-2510行:通过对features和mIsFloating的判断,获取不同的主布局文件为layoutResource进行赋值,值可以为R.layout.screen_custom_title;R.layout.screen_action_bar;等等。

经过上面的源码我们可以看到设置features,除了theme中设置的,我们还可以在代码中进行:

//通过java文件设置:

requestWindowFeature(Window.FEATURE_NO_TITLE);

//通过xml文件设置:

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

其实我们平时requestWindowFeature()设置的features值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。两方式具体流程不同,但是效果是一样的。

所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。

我靠,我还是没看见DecorView添加布局的代码啊 ,这就来:

源码 2554行,进行了如下操作:

2554        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

看名字是在进行资源文件的加载,具体是怎么操作的呢:

1801    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
         //......
1813        final View root = inflater.inflate(layoutResource, null);
        //......
1824            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        //......
1826        mContentRoot = (ViewGroup) root;
        //......
1828    }

在源码1824行,系统将 layoutResource 所代表的主布局文件。添加到 DecorView 中,而在源码中第 2556行我们可以看到,系统又在DecorView中需找一个ID_ANDROID_CONTENT布局赋值给contentParent

 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

ID_ANDROID_CONTENT又是个什么东西呢?我在Windows抽象类中找到了它的源码。注释说的很明确,每一个主布局都拥有id为content的控件。通过mContentRoot = (ViewGroup) root;我们可以清楚的知道,layoutResource既为整个窗口的根布局。

/**
 * The ID that the main layout in the XML layout file should have.
 */
  public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

随手贴几个布局文件加以证明:
R.layout.screen_simple:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />

     <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

R.layout.screen_simple_overlay_action_mode

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />

    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
</FrameLayout>

同样在Windows抽象类中找到了findViewByID方法的源码,findViewByID的作用就是将在DecoreView中需找 idcontentFragmentLayout赋值给 contentParent

1252    /**
1253     * Finds a view that was identified by the id attribute from the XML that
1254     * was processed in {@link android.app.Activity#onCreate}.  This will
1255     * implicitly call {@link #getDecorView} for you, with all of the
1256     * associated side-effects.
1257     *
1258     * @return The view if found or null otherwise.
1259     */
1260    @Nullable
1261    public View findViewById(@IdRes int id) {
1262        return getDecorView().findViewById(id);
1263    }

最后generateLayout()的最后系统还会调用Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。至此Android setContentView()方法分析完成。

3,总结

图片被缩小了不清楚,不要紧。请右键 - 在新标签中打开图片。

一张图片解决问题

由此就组成了我们在《【Android 控件架构】详解Android控件架构与常用坐标系》一篇中提到的视图框架(图中contentView就是源码中的contentParent)

4,参考:

如果说我比别人看得更远些,那是因为我站在了巨人的肩上

  1. 在线源码地址
  2. Android应用setContentView与LayoutInflater加载解析机制源码分析
  3. Android 源码解析 之 setContentView
  4. Android UI 窗口体系 —— 源码阅读
Android控件体系
Web note ad 1