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

来源:互联网 发布:薛之谦淘宝店铺号 编辑:程序博客网 时间:2024/06/13 06:25

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

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

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    @Override423    public void setContentView(View view) {424        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));425    }
 @Override428    public void setContentView(View view, ViewGroup.LayoutParams params) {429        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window430        // decor, when theme attributes and the like are crystalized. Do not check the feature431        // before this happens.432        if (mContentParent == null) {433            installDecor();434        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {435            mContentParent.removeAllViews();436        }437438        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赋值给mContentParent2627            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.24922493        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 {                //......25522553        mDecor.startChanging(); //通知 开始改变2554        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);25552556        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);            //......2604        mDecor.finishChanging();//通知 改变完成26052606        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 that1254     * was processed in {@link android.app.Activity#onCreate}.  This will1255     * implicitly call {@link #getDecorView} for you, with all of the1256     * associated side-effects.1257     *1258     * @return The view if found or null otherwise.1259     */1260    @Nullable1261    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. 在线源码地址
1. Android应用setContentView与LayoutInflater加载解析机制源码分析
2. Android 源码解析 之 setContentView
3. Android UI 窗口体系 —— 源码阅读

阅读全文
1 3