Android系统源码分析--View绘制流程之-setContentView

上一篇分析了四大组件之ContentProvider,这也是四大组件最后一个。因此,从这篇开始我们分析新的篇章–View绘制流程,View绘制流程在Android开发中占有非常重要的位置,只要有视图的显示,都离不开View的绘制,所以了解View绘制原理对于应用开发以及系统的学习至关重要。由于View绘制流程比较复杂,并且涉及的知识非常多,所以后面我会按照下面几方面来介绍View的绘制流程。每篇不是很长,但是尽量的详细,让每个人都看懂。

  • Android系统源码分析–View绘制流程之-setContentView
  • Android系统源码分析–View绘制流程之-inflate
  • Android系统源码分析–View绘制流程之-onMeasure
  • Android系统源码分析–View绘制流程之-onLayout
  • Android系统源码分析–View绘制流程之-onDraw
  • Android系统源码分析–View绘制流程之-硬件加速
  • Android系统源码分析–View绘制流程之-addView
  • Android系统源码分析–View绘制流程之-弹性效果

所以这篇我们先分析View绘制流程的setContentView方法,按照惯例,先贴一下流程图:

setContentView

1.PhoneWindow.setContentView

调用setContentView最开始的地方是在我们继承Activity的子类中的onCreate方法中,这个方法其实是调用的Activity中的setContentView方法:

1
2
3
4
5
public void setContentView(@LayoutRes int layoutResID) {
// getWindow获取的是PhoneWindow,所以这里是调用的PhoneWindow的setContentView方法
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

其实这个getWindow获取的是继承Window的PhoneWindow,所以这里getWindow.setContentView是调用的PhoneWindow.setContentView方法,具体的自己可以看看代码哪里赋值的就知道了。另外这个方法还有两个类似的方法:

1
2
3
4
5
6
7
8
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}

这三个方法差不多,只不过下面的两个直接传递了view对象,而第一个是传递了view的id。我们接着看PhoneWindow.setContentView方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void setContentView(int layoutResID) {
// 根据layout的id加载一个布局,然后通过findViewById(R.id.content)加载出布局中id为content
// 的FrameLayout赋值给mContentParent,并且将该view添加到mDecor(DecorView)中
if (mContentParent == null) {// 第一次是空
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 没有过度效果,并且不是第一次setContentView,那么要先移除盛放setContentView传递进来
// 的View的父容器中的所有子view
mContentParent.removeAllViews();
}
// 窗口是否需要过度显示
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
} else {// 不需要过度,加载id为layoutResID的视图并且添加到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// 绘制视图
mContentParent.requestApplyInsets();
...
mContentParentExplicitlySet = true;
}

上面注释很详细,但是还是需要解释一下mContentParent,这个mContentParent是一个FrameLayout,这里的Content是指你setContentView传递进来的id指向的视图,所以mContentParent也就是指放置传递进来的视图的父视图。看下面的图:

ContentView

上面的ActionBarContextView是标题,不过有些设置是不会显示整个标题的,所以这里只是一种情况,下面的id为content的FrameLayout就是这个mContentParent,你通过setContentView方法传递的视图会放到这个id为content的FrameLayout上面,这样你的Activity就显示了你写的布局视图了,这里先解释一下,我们下面看看是不是真的这样。由于第一次创建Activity时mContentParent是空的,所以会走PhoneWindow.installDecor方法。

2.PhoneWindow.installDecor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void installDecor() {
mForceDecorInstall = false;
// 继承FrameLayout,是窗口顶级视图,也就是Activity显示View的根View,包含一个TitleView和一个ContentView
if (mDecor == null) {// 首次为空
// 创建DecorView(FrameLayout)
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {// 第一次setContentView时为空
// 这个mContentParent就是后面从系统的frameworks\base\core\res\res\layout\目录下加载出来
// 的layout布局(这个Layout布局加载完成后会添加到mDecor(DecorView)中)中的一个id为content的
// FrameLayout控件,这个FrameLayout控件用来盛放setContentView传递进来的View
mContentParent = generateLayout(mDecor);
...
// 判断是否存在id为decor_content_parent的view(我只看到screen_action_bar.xml这个里面有这个id)
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
...
if (mDecorContentParent.getTitle() == null) {
// 设置标题
mDecorContentParent.setWindowTitle(mTitle);
}
...
} else {
// 标题视图
mTitleView = (TextView) findViewById(R.id.title);
// 有的布局中是没有id为title的控件的,也就是不显示标题
if (mTitleView != null) {
// 判断是否有不显示标题的特性
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {// 显示标题
mTitleView.setText(mTitle);
}
}
}
// 背景
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
// 过度效果
...
}
}

这里出现了一个mDecor,这个mDecor是DecorView,继承FrameLayout,是窗口顶级视图,也就是Activity显示View的根View,包含一个TitleView和一个ContentView,也就是上面图形中的最外层蓝色的边框所指代的视图,当然,这里第一次加载时也是空的,那么会调用generateDecor函数来创建mDecor,然后通过generateLayout方法创建mContentParent视图,创建完成后会设置标题,设置标题的就不分析了,比较简单,下面先看创建mDecor的方法。

3.PhoneWindow.generateDecor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected DecorView generateDecor(int featureId) {
...
// activity.
Context context;
if (mUseDecorContext) {// 从Activity的setContentView方法调用则为true
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {// 系统进程时没有Application的context,所以就用现有的context
context = getContext();
} else {// 应用会有application的Context
...
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

这里判断了一个applicationContext是否存在,主要是区分这个是系统调用还是应用,系统是没有applicationContext的,最后通过new关键字创建对象DecorView,这里就获取到了DecorView。

5.PhoneWindow.generateLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
protected ViewGroup generateLayout(DecorView decor) {
...
// 根据Window的属性调用相应的requestFeature
...
// 获取Window的各种属性来设置flag和参数
...
// 根据之前的flag和feature来加载一个layout资源到DecorView中,并把可以作为容器的View返回
// 这个layout布局文件在frameworks\base\core\res\res\layout\目录下
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
...
} else {
layoutResource = R.layout.screen_title_icons;
}
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
...
} else {
layoutResource = R.layout.screen_custom_title;
}
...
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
...
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
// 根据layoutResource(布局id)加载系统中布局文件(Layout)并添加到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// contentParent是用来添加Activity中布局的父布局(FrameLayout),并带有相关主题样式,就是上面
// 提到的id为content的FrameLayout,返回后会赋值给PhoneWindow中的mContentParent
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
// 设置mDecor背景之类
...
mDecor.finishChanging();
return contentParent;
}

前面一大段if-else语句是根据属性值获取系统中的layout的id,主要有下面几种:

* R.layout.screen_swipe_dismiss
* R.layout.screen_title_icons
* R.layout.screen_progress
* R.layout.screen_custom_title
* R.layout.screen_title
* R.layout.screen_simple_overlay_action_mode
* R.layout.screen_simple

这些布局文件都在系统frameworks\base\core\res\res\layout\目录下,我们看其中一个screen_simple.xml布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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>

其中我们需要获取的contentParent就是xml布局中id为content的FrameLayout,为什么是这个,我们通过上面代码分析,上面我们看到了mDecor.onResourcesLoaded方法,这里的第二个参数layoutResource就是上面的xml布局,所以这里就是加载这个布局的我们看看是不是

6.DecorView.onResourcesLoaded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
// DecorView中的标题视图,可能是空,也就是没有标题
mDecorCaptionView = createDecorCaptionView(inflater);
// 加载Layout作为根布局(frameworks\base\core\res\res\layout\目录下layout布局文件)
// 这里获取到的root是没有宽高的
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {// 有标题
// 这里可以看到mDecorCaptionView不为空时,将mDecorCaptionView添加到DecorView,然后再将
// Layout添加到mDecorCaptionView
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {// 没有标题
// 如果mDecorCaptionView为空,则直接将跟布局Layout添加到DecorView
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}

这里首先创建了标题视图,然后通过LayoutInflater.inflate加载了id为layoutResource的布局文件并赋值给root引用,最终返回的也是这个root,所以上面方法5中加载了这个布局文件,加载完成后,如果标题视图文件存在,则将root添加到标题视图中,再将标题视图添加到DecorView上,如果没有标题视图,则直接将root布局添加到DecorView上面,宽高是MATCH_PARENT。再回到5中,在调用完mDecor.onResourcesLoaded方法后通过id为ID_ANDROID_CONTENT获取了一个ViewGroup,那么这个ID_ANDROID_CONTENT是什么,通过查找我们发现是:

1
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

这里就可以知道获取的contentParent就是上面xml布局中的id为content的FrameLayout布局,所以到这里整体结构基本明白了。

另外上面调用了一个createDecorCaptionView方法并且传入了LayoutInflater,那么看看这个方法做了哪些操作。

7.DecorView.createDecorCaptionView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
...
if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) {
if (decorCaptionView == null) {
decorCaptionView = inflateDecorCaptionView(inflater);
}
decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
} else {
decorCaptionView = null;
}
...
return decorCaptionView;
}

这里其实就一个方法需要再看看那就是inflateDecorCaptionView方法。

8.DecorView.inflateDecorCaptionView

1
2
3
4
5
6
7
8
9
private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {
final Context context = getContext();
inflater = inflater.from(context);
// 从frameworks\base\core\res\res\layout\中加载decor_caption.xml布局
final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,
null);
...
return view;
}

这里其实就是通过LayoutInflater.inflate方法加载frameworks\base\core\res\res\layout\下的decor_caption.xml布局,这个LayoutInflater.inflate由于比较重要,所以我们放到下一章单独讲解。

10.LayoutInflater.inflate

我们在第二步初始化完DecorView和mContentParent视图后开始调用mLayoutInflater.inflate(layoutResID, mContentParent)方法,加载我们setContentView方法传递进来的视图,也就是我们自己写的Activity布局,之前的都是系统的布局。我们知道mContentParent是放置我们自己写的Activity视图的容器,所以后面就简单了。

1
2
3
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

上面我们说了这个方法具体分析我们下一章单独分析。所以我们接着前面分析。

11.View.requestApplyInsets

1
2
3
public void requestApplyInsets() {
requestFitSystemWindows();
}

12.View.requestFitSystemWindows

1
2
3
4
5
public void requestFitSystemWindows() {
if (mParent != null) {
mParent.requestFitSystemWindows();
}
}

这里的mParent是ViewParent的具体实现ViewRootImpl,所以调用的是ViewRootImpl里的requestFitSystemWindows方法。

13.ViewRootImpl.requestFitSystemWindows

1
2
3
4
5
public void requestFitSystemWindows() {
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();// 绘制视图
}

checkThread这个是检测线程的方法,也就是检测当前线程是不是主线程,也就是setContentView方法要在UI线程调用。然后调用scheduleTraversals方法开始绘制视图。

15.ViewRootImpl.scheduleTraversals

1
2
3
4
5
6
7
8
9
10
11
12
13
void scheduleTraversals() {
// 当mTraversalScheduled为false,也就是没有重绘请求或者没有未执行完的重绘时才开始重绘
if (!mTraversalScheduled) {
// 一旦开始重绘此处设置为True,当执行完毕后调用unscheduleTraversals函数,
// 重新设置为false,避免同时存在多次绘制
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 将消息放入消息处理器中,最终调用doTraversal方法
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}

mTraversalScheduled只有调用这个方法后才设置为true,所以在开始调用这个方法的时候是false,后面会将mTraversalRunnable放到消息处理器中,这个mTraversalRunnable是一个实现了Runnable接口的对象,所以从这里调用了TraversalRunnable中的run方法。

16.TraversalRunnable.run

1
2
3
public void run() {
doTraversal();
}

这里很简单就是调用了doTraversal方法。

17.ViewRootImpl.doTraversal

1
2
3
4
5
6
7
8
9
10
11
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
...
// 执行View绘制流程
performTraversals();
...
}
}

这里主要是调用performTraversals方法,开始View的真正绘制。

18.ViewRootImpl.performTraversals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private void performTraversals() {
...
// 这里是需要测量的条件:第一次加载View,需要调整窗口大小,需要适应系统窗口,视图显示状态改变,
// 视图布局参数不为空,强制窗口重新布局。首先要满足这个几个条件才可能执行测量
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
// 窗口没有停止,或者通知需要绘制
if (!mStopped || mReportNextDraw) {
...
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
...
// 1.第一步:测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
} else {
...
}
...
if (didLayout) {// 执行布局
// 2.第二步:布局
performLayout(lp, mWidth, mHeight);
...
}
...
// 如果没有取消绘制,并且不是新的Surface,那么执行绘制
if (!cancelDraw && !newSurface) {
...
// 3.第三步:绘制
performDraw();
} else {// 如果取消了绘制或者是新的Surface,那么要重新测量、布局和绘制
...
}
mIsInTraversal = false;
}

这里开始进入测量,布局,绘制的过程,里面通过各个条件来判断需要执行哪一步或者哪几部,因为这一段主要是设计测量、布局、绘制,所以这章就不分析了,这个方法放到《Android系统源码分析–View绘制流程之-onMeasure》一章讲解。

我们下一章开始分析《Android系统源码分析–View绘制流程之-inflate》。

参考文章:

代码地址:

直接拉取导入开发工具(Intellij idea或者Android studio)

由于coding与腾讯云合作,改变很多,所以后续代码切换到Gitlab。

https://gitlab.com/yuchuangu85/android-25

Android开发群:192508518

微信公众账号:Code-MX

注:本文原创,转载请注明出处,多谢。

×

纯属好玩

扫码支持
扫码打赏,多少你说算

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 1.PhoneWindow.setContentView
  2. 2. 2.PhoneWindow.installDecor
  3. 3. 3.PhoneWindow.generateDecor
  4. 4. 5.PhoneWindow.generateLayout
  5. 5. 6.DecorView.onResourcesLoaded
  6. 6. 7.DecorView.createDecorCaptionView
  7. 7. 8.DecorView.inflateDecorCaptionView
  8. 8. 10.LayoutInflater.inflate
  9. 9. 11.View.requestApplyInsets
  10. 10. 12.View.requestFitSystemWindows
  11. 11. 13.ViewRootImpl.requestFitSystemWindows
  12. 12. 15.ViewRootImpl.scheduleTraversals
  13. 13. 16.TraversalRunnable.run
  14. 14. 17.ViewRootImpl.doTraversal
  15. 15. 18.ViewRootImpl.performTraversals
  • 参考文章:
  • 代码地址:
  • ,