您的位置:首页 > 移动开发 > Android开发

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

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



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

1, 前言

在前面《【Android 控件架构】详解Android控件架构与常用坐标系》的文章中我们提到了
setContentView()
方法,当时只是匆匆带过,并没有阐明具体流程。而这篇文章就是从Activity中的
setContentView()
方法出发结合上篇的视图框架,详细分析
setContentView()
的工作原理。还是贴一张图复习一下吧。



从上面的文章中我们知道
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的内部类了,而且包都换了,有图有真相)。





查看
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;
....
}


一言不可就上图:





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文件中设置。





接下来就到关键的部分了,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
中需找
id
content
FragmentLayout
赋值给
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. 在线源码地址

1. Android应用setContentView与LayoutInflater加载解析机制源码分析

2. Android 源码解析 之 setContentView

3. Android UI 窗口体系 —— 源码阅读
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐