我们本文要回答的问题:
- setContentView原理是什么?
- Activity在onResume之后才会显示的原因是什么?
- ViewRoot是干嘛的,是ViewTree的rootview吗?
一、整体流程图
先上份地图:
大概说一下流程(本文最后也会再重复一次):
- 创建
PhoneWindow
对象,往PhoneWindow
对象里面添加了一个DecorView
- 为
DecorView
绑定一个ViewRootImpl
,由这个ViewRootImpl
负责View
的绘制和刷新 ViewRootImpl
通过IWindowSession
向WMS
发起Binder调用,而WMS
也会通过IWindow
向应用端发起调用ViewRootImpl
会在WMS
里面注册一个窗口,然后由WMS
统一的管理所有窗口的大小,位置和层级- 在第一次绘制时,
ViewRootImpl
还会向WMS申请一块Surface
(WMS
向Surface
申请),有了Surface
之后,应用端就可以进行绘制了 - 应用端绘制完之后,
SurfaceFlinger
就会按照WMS
里面提供的层级等信息进行合成,最终显示
为了让大家在跟踪源码的时候不会迷路,先跟大家科普路下路上的这几个“门卫大哥”:
PhoneWindow
:Window
在手机端的唯一实现类,WMS
管理的就是一个个Window
,而不是View
DecorView
:顶层的View,我们平时setContentView
设置的View
对应的就是蓝色的ContentView
,ViewRootImpl
:这个哥们是View
和WMS
通信的桥梁,每次View
想跟WMS
通信要通过它;每次WMS
想让View
更新也要通过它;一个DecorView
对应一个ViewRootImpl
SurfaceFlinger
:负责Surface
的合成,一块Surface
就是一块画布,应用端其实都是在Surface
上绘制图形WindowManagerService
:我们口中经常说的WMS
,主要负责管理窗口,,并不负责view的绘制。以下是WMS
的主要作用:
对了,我采用的源码是Android 28的。
下面正式开始~ Action, go ~
二、源码分析
2.1 setContentView
// 类:---> View.java
public void setContentView(int layoutResId){
getWindow().setContentView(layoutResId)
;
...
}
//类:---->PhoneWindow.java
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
...
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}
我们可以看到,通过mLayoutInflater.inflate(layoutResID, mContentParent)
进行加载布局,这里有两个参数,一个是layoutId
,另外一个是mContentParent
,这个mContentParent
又是什么?
可以看到mContentParent
是通过installDecor()
初始化的,继续跟吧。
2.2 installDecor
---> 类:PhoneWindow.java
private void installDecor() {
if (mDecor == null) {
//1、new 一个 DecorView
mDecor = generateDecor(-1);
....
}
if (mContentParent == null) {
//2、inflate 布局
mContentParent = generateLayout(mDecor);
....
}
}
//1、new 一个 DecorView
protected DecorView generateDecor(int featureId) {
....
return new DecorView(context, featureId, this, getAttributes());
}
//2、inflate 布局
protected ViewGroup generateLayout(DecorView decor) {
....
// Inflate the window decor.
//3、inflate 布局,并添加到decorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//4、找到contentView,返回
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
///----->DecorView.java
void onResourcesLoaded(LayoutInflater inflater, int layoutResource){
//3、
final View root = inflater.inflate(layoutResource, null);
...
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
我们理一下流程,以上代码主要做了:
- 创建一个DecorView,这个DecorView本质是一个
FrameLayout
- inflate一个布局root
- 把root 添加到decorView里面
- 通过
findViewById
找到contentParent,其实就是id为R.id.content的View
提个小问题提提神,大家知道为什么在实现沉浸式布局的时候,通过
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
等配置为什么要在setContentView()
之前吗?看看installDecor()
方法你就知道了~
到这一步,整个界面就加载完成了吗?too naive,这一步只是初始化了View Tree,整个界面还没有显示出来,界面真正要显示出来还要看onResume()
这个生命周期里做了啥。继续跟吧~
2.3 handleResumeActivity(IBinder token)
---->类:ActivityThread.java
@Override
public void handleResumeActivity(IBinder token,..){
// TODO Push resumeArgs into the activity for consideration
//1、
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//2、
wm.addView(decor, l);
} else {
...
}
}
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
...
if (r.activity.mVisibleFromClient) {
//3、
r.activity.makeVisible();
}
}
}
整个流程分为三步:
- 回调
Activity
的onResume()
方法 - 调用
WindowManager
的addView()
方法 - 设置
Activity
为visible
我们接下来要重点看的是addView()
这个方法。为什么呢?因为第一步就是回调,第三步是让它可见,那就说明触发界面绘制的流程是在第二步。所以,我们接下来要跟踪第二步。
2.4 wm.addView(decor, l)
---->WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
----> WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
....
ViewRootImpl root;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
...
// do this last because it fires off messages to start doing things
//注释大意:最后才用调用这个方法,因为它会发送消息开始干活
try {
root.setView(view, wparams, panelParentView);
}
....
}
}
这个方法就做了两个操作:
- 创建了
ViewRootImpl
对象(所以,ViewRootImpl
和Window
的关系是一对一,第二次说明了) - 调用
ViewRootImpl
的setView方法
2.5 viewRootImpl.setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
try{
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
...
}
...
}
}
setContentView
这个方法,重点就做了这么几件事:
- 给
mView
赋值 - 调用
requestlayout
- 调用
windowSession
的addToDisplay
第一个分叉路口来了,我们要分开看requestlayout()
方法和addToDisplay()
方法。
分岔路 2.5.1 requestlayout()
----> ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查是否是主线程。下次面试官问你ui不能在子线程更新
//的异常在哪里抛出的,勇敢的告诉面试官,在ViewRootImpl
//的requestLayout()
checkThread();
mLayoutRequested = true;
//1
scheduleTraversals();
}
}
//1
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
....
}
}
在这一步里面会post
一个callback
,这个callback在下一次Vsync
信号到来的时候会被调用(关于下一次Vsync
信号来了怎么回调到Java层,请看这篇文章)。
Anyway,当Vsync信号到来后,最终会执行mTraversalRunnable
的run
方法,我们具体来看一下:
----> ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
...
performTraversals();
...
}
private void performTraversals() {
...
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
...
//绘制三部曲:measure、layout、draw
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
measure
、layout
、draw
这几个方法大家都很熟悉了,所以我们重点看relayoutWindow
这个方法做了什么事情?为什么它是放在measure
、layout
、draw
之前的?
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
//看到RemoteException,掐指一算,估摸有ipc通信
...
int relayoutResult = mWindowSession.relayout(..., mSurface);
...
}
其实这一步的作用就一个————向WMS
申请一块Surface
。
这里不是有一个Surface
了吗?不就是那个mSurface
??(黑人问号脸❓)
其实,此时这个mSurface
是一块空的Surface
,它只是被创建了出来,但是还没有分配内存空间,还是空白的。
public final Surface mSurface = new Surface();
应用端需要调用WMS
的relayoutWindow
给Surface
进行赋值,关于Surface
的传输和赋值过程我就不分析了,大家可以看文章。
Surface
赋值初始化成功后,客户端就可以进行绘制了。当绘制完成后,会调用unlockAndPostCanvas()
来通知SurfaceFlinger
进行合成。
现在,requestLayout()
这个方法就算看完了,接下来去另一个分岔路口。
分岔路 2.5.2 windowSession.addToDisplay()
这个WindowSession
是一个AIDL
,它的作用是用来给应用和WMS通信的
,它真正的实现类是Session
,具体找寻的流程如下:
---> Session.java
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,...) {
//这里要注意这个IWindow,IWindow是WMS向应用端发起Binder调用的
//Session把它传给WMS是为了以后WMS可以主动向应用端发起调用
return mService.addWindow(this, window, seq, attrs, ...);
}
我们可以看到,最终调用的是WMS
的addView
方法,调用这个方法的作用是向WMS
注册窗口对象,然后就会WMS
统一去协调这些Window
的层级、大小和位置。
对WMS
来说,它并不关心我们应用本身的Window
对象或者View
对象,对它来说,它一个重要的功能就是给应用端的Window
分配Surface
,并且,控制这些Surface
的显示顺序,位置和尺寸。
当应用端在Surface
绘制完了之后,SurfaceFlinger
就会按照这些图像数据,按照WMS
提供的尺寸、层级和位置等等进行合成,最后写到屏幕缓冲区里面,绘制出来。
2.6 小结
handleResumeActivity()
这个方法整体流程大概可以概括为以上几步。 整个绘制流程比较长,等源码看的差不多了,再考虑写一个系列吧~
三、总结
到这里其实整个流程就算完成了,来总结一下:
- 创建了
PhoneWindow
对象,往PhoneWindow
对象里面添加了一个DecorView
- 为
DecorView
绑定一个ViewRootImpl
,由这个ViewRootImpl
负责View
的绘制和刷新 ViewRootImpl
通过IWindowSession
向WMS
发起Binder调用,而WMS
也会通过IWindow
向应用端发起调用ViewRootImpl
会在WMS
里面注册一个窗口,然后由WMS
统一的管理所有窗口的大小,位置和层级- 在第一次绘制时,
ViewRootImpl
还会向WMS申请一块Surface
,有了Surface
之后,应用端就可以进行绘制了 - 绘制完之后,
SurfaceFlinger
就会按照WMS
里面提供的层级等信息进行合成,最终显示
四、问题解答
setContentView
的原理是什么?
setContentView()的原理是主要是:(1)创建
DecorView
和ViewRootImpl
,并将它们两者进行绑定(2)通过inflate
方法创建ViewTree,此时还没有显示
Activity
在onResume
之后才会显示的原因是什么?
在onResume方法中才会调用
ViewRootImpl
的performTraversal()
方法进行界面绘制以及makeVisible
方法进行界面显示;
ViewRoot
是干嘛的,是ViewTree的RootView吗?
ViewRoot
,或者说它的实现类ViewRootImpl
,跟View没有任何关系。ViewRoot
只是ViewTree
的管理者,而不是ViewTree
的根节点。真正的根节点是DecorView
。