前言
不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure()
、onLayout()
、onDraw()
这3个方法来完成的,实际上这只是系统暴露给我们使用的最基本的方法,背后的流程要比这个更加复杂,今天就和大家一起扒一下背后还做了什么事情。
我们知道Activity执行了onCreate()
、onStart()
、 onResume()
3个方法后,用户就能看见视图了。这背后实际上经历了2个过程,其一,通过ActivityThread
调度生命周期相关的方法;其二,通过setContentView()
把XML解析成View对象。这里兵分2路,先来看一下setContentView()
。
setContentView()方法
这里借用这篇文章的图来表示setContentView
整个流程
https://blog.csdn.net/Rayht/article/details/80782697
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
从这里进去会进去到Activity.setContentView()
,最后会调用PhoneWindow.setContentView()
##PhoneWindow
@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) {
//1.初始化DecorView
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 {
//2.解析传进来的xml布局,生成一个ContentView
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这里分为2条线,我们先来看一下初始化mDecor。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//1.生成DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//2.生成mContentParent
mContentParent = generateLayout(mDecor);
}
.......
}
protected DecorView generateDecor(int featureId) {
Context context;
......
//DecorView extends FrameLayout
return new DecorView(context, featureId, this, getAttributes());
}
mDecor就是DecorView对象,它是PhoneWindow的顶层View,查看DecorView源码可以看出DecorView 是一个FrameLayout,但是源码中并没有展示出它是一个怎样的布局,因为它是在注释2mContentParent = generateLayout(mDecor)
中添加的,下面就来一起看一下。
##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
// Inflate the window decor.
int layoutResource; //是一个布局文件id
int features = getLocalFeatures();
// 根据不同的主题将对应的布局文件id赋值给layoutResource
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
....
}
....
else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//通过LayoutInflater加载解析layoutResource布局文件
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
.......
}
这里展示的是generateLayout
的上半部分,先根据不同的主题生成不同的布局文件,然后解析layoutResource布局文件。
onResourcesLoaded()
##DecorView. onResourcesLoaded()
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
.....
mDecorCaptionView = createDecorCaptionView(inflater);
//1.通过XmlResourceParser解析XML布局文件,得到一个View对象
//这里的root就是DecorView中添加的layoutResource
final View root = inflater.inflate(layoutResource, null);
//2.把layoutResource布局文件添加到DecorView中
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
现在我们知道了,generateLayout
其实是给DecorView添加一个布局文件,下面就来看一下DecorView到底是怎样的布局?
上面说过会根据不同的主题选择不同的layoutResource
,这里我们看一下最常用的layoutResource = R.layout.screen_simple
。
可以看出这是一个垂直的LinearLayout,上面是一个ActionBar,下面是一个id为content
的FrameLayout,先给大家透漏一下,这个FrameLayout就是用于装载平时我们写的Activity中的xml布局。
generateLayout
下半部分
##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource; //是一个布局文件id
......
//往DecorView中添加layoutResource布局文件
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//查找DecorView中id为ID_ANDROID_CONTENT的View
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
.......
return contentParent;
}
这里就更简单了,通过findViewById
找到之前布局中id为content
的view,最后返回到这个contentParent
就是刚才的FrameLayout。
我们再回到最开始的setContentView()
方法
##PhoneWindow
@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) {
//1.初始化DecorView
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 {
//2.解析传进来的xml布局,生成一个ContentView
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
上面我们讲了installDecor()
实际上就是创建并初始化DecorView对象,也就是完成了mContentParent
的初始化,接下来看一下注释2,这里是解析Activity中传进来的布局layoutResID
,其中parent是mContentParent
,也就是之前说过的那个FrameLayout,这样就把Activity中的布局添加到DecorView中了。
小结一下:
setContentView()流程如下:
1、在PhoneWindow中创建顶层的DecorView;
2、在DecorView中会根据主题的不同加载一个不同的布局;
3、把Activity中的布局解析并添加到DecorView的FrameLayout中
这是我画的一幅图,按照这个去看源码会比较清晰。
到这里算是把setContentView流程分析完了,但把我们仅仅是把自己的Layout添加到DecorView中,但如何显示到屏幕上还没看呢。
布局文件中的UI是如何显示的呢?
其实在文章开头说过了,在ActivityThread的生命周期调度中完成的。刚才已经看过了onCreate(),后面还会调用onStart()、onResume(),而最终UI的显示就是在onResume()中完成的,也就是说我们平常接触最多的measure
、layout
、draw
3个方法都在onResume中完成的。而onResume()是在ActivityThread的handleResumeActivity()
中调用的。
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// 1.会调用Activity的onResume
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
final int forwardBit = isForward
? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//获取WindowManager对象
ViewManager wm = a.getWindowManager();
//初始化窗口布局属性
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//2.WindowManager添加DecorView
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
......
}
这里最关键的地方是注释1和注释2,这里先看一下注释2,wm是一个接口ViewManager
对象,而wm是通过Activity的getWindowManager()
获取的,最后你会发现wm是在WindowManagerImpl
中初始化的,
##WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
##WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
....
ViewRootImpl root;
View panelParentView = null;
//创建ViewRootImpl对象,挺重要的一个类,后面会解释
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
//1.关键调用
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
这里最关键的是调用了setView()
方法,这个方法内又调用了requestLayout()
##ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查是否在UI线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//callback与Choreographer交互,会在下一帧被渲染时触发
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里的 mChoreographer
实际上是Choreographer对象,Choreographer是在屏幕刷新机制中接收显示系统的VSync信号,postFrameCallback设置自己的callback与Choreographer交互,你设置的callCack会在下一个frame被渲染时触发。这里简单了解下即可。这里重点关注下mTraversalRunnable
对象。
##ViewRootImpl
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
traversal是遍历的意思,也就是说后面会做遍历操作,至于为什么,接着往下看。
##ViewRootImpl
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//关键代码
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()
这段代码非常长,但是核心的地方就是下面注释的3处。
##ViewRootImpl
private void performTraversals() {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
if (mFirst) {
.....
// host为DecorView
// 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view ,view.post就是利用了这个原理
host.dispatchAttachedToWindow(mAttachInfo, 0);
.....
}
// Ask host how big it wants to be
//1.执行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
//2.执行布局
performLayout(lp, mWidth, mHeight);
......
//3.执行绘制
performDraw();
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//调用View的measure()
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
//调用View的layout()
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
private void performDraw() {
......
draw(fullRedrawNeeded);
......
}
public void draw(Canvas canvas) {
......
//调用View的onDraw()
onDraw(canvas);
......
}
performMeasure()
:从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;performLayout()
:从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;performDraw()
:从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。
现在明白了为什么前面那个方法的名字是遍历了吧,因为最后是要完成以DecorView为根节点的view树的遍历。
大家对照我画的这幅图去看源码,会比较好理解。
关于View绘制流程真的很长,代码量也大,但是我们只需要关注核心流程就可以了。最后做一个总结:
- 1、在
onCreate
方法中通过setContentView
将XML布局解析成java对象,并添加到PhoneWindow的DecorView中; - 2、在
onResume
中将DecorView添加到WindowManagerImpl
中,然后通过ViewRootImpl
来执行View的绘制流程; - 3、在
ViewRootImpl
的performTraversals()
方法中分别调用performMeasure
、performLayout
、performDraw
来完成测量、布局、绘制; - 4、
performMeasure
、performLayout
、performDraw
这几个方法最终会调用measure()
、layout()
、draw()
来完成最终的绘制。
参考
Android 自定义View之View的绘制流程(一)
【朝花夕拾】Android自定义View篇之(一)View绘制流程