1、开篇
我们在使用Activity的时候,常常会有在onCreate或者onResume方法中获取View的宽高的需求,但是如果我们直接调用View.getWidth()和View.getHeight()获取到的值是0,很明显此时View的测量工作还没有开始。那么问题来了:
- View的测量、布局和绘制流程是在什么时候进行的?
- 怎么在onCreate或者onResume方法中获取View的正确宽高?
注意:本文分析的是android.app.Activity类的源码,不是androidx.appcompat.app.AppCompatActivity,两者会有一些差异,但总体的流程大致相同。
2、从setContentView开始
既然要开始View的测绘流程,那么首先必须得有View呀。我们使用Activity的时候通常会在onCreate方法中使用setContentView来设置我们的界面布局,那么就从它开始分析,看看我们的布局最终被添加到哪里去了。setContentView有三个重载方法,我们就看最常用的传layoutId的那个:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
这里很简单,就直接调用了Window的.setContentView方法。Window是一个抽象类,它的实现类是PhoneWindow,这个读者们可以记一下。 来看看PhoneWindow的setContentView
public void setContentView(int layoutResID) {
if (mContentParent == null) {
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();
}
mContentParentExplicitlySet = true;
}
可以看到,我们的布局被添加到mContentParent上面去了,那么mContentParent又是什么呢?
注意到方法一开始做了一个判断
if (mContentParent == null) {
installDecor();
}
那么installDecor方法中必定对mContentParent进行了赋值操作,我们来看看
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
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) {
mContentParent = generateLayout(mDecor);
...
}
}
这里先对mDecor赋值,然后再通过mDecor给mContentParent赋值。mDecor是DecorView的实例,而DecorView继承自FrameLayout,来看看它的赋值:
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
这里就是直接new了一个DecorView而已。我们再看mContentParent的赋值
mContentParent = generateLayout(mDecor);
PhoneWindow#generateLayout:
protected ViewGroup generateLayout(DecorView decor) {
//根据主题等进行各种设置
...
// 根据不同的条件获取不同的布局
int layoutResource;
int features = getLocalFeatures();
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
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) {
....
} else ...
mDecor.startChanging();
// 把最终获取的布局添加到mDecor
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 获取 id 为 android.R.id.content 的View赋值给contentParent,也就是最终赋值给mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
mDecor.finishChanging();
return contentParent;
}
Window#findViewById:
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
这个方法先是做了很多的设置,然后再根据不同的feature获取不同的布局文件添加到mDecor中,这些布局文件有一个共同的特点,就是必定有一个 id 为 android.R.id.content 的FrameLayout,以screen_simple.xml为例
<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>
至此,我们就明白了,Activity的布局最终设置到了Window中持有的mDecor上去了。而这个mDecor必定有一个id为android.R.id.content的Framelayout,这也是我们为什么可以在Activity中通过findViewById获取这个View来实现类似加水印等之类的功能了。
另外,还有一个地方值得注意的是:
public void setContentView(int layoutResID) {
...
mContentParentExplicitlySet = true;
}
这里设置了一个标记mContentParentExplicitlySet以标记setContentView已经被调用过了。而在requestFeature方法中有这么一段代码:
public boolean requestFeature(int featureId) {
if (mContentParentExplicitlySet) {
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
...
return super.requestFeature(featureId);
}
这也就是requestFeature必须在setContentView方法前调用的原因了。至于为什么要这么设计,结合前面对PhoneWindow#generateLayout方法的分析,很容易就明白了。
3、View的测绘流程
由于我们在onResume中仍然无法获取View的宽高,那么View的测绘流程应该在onResume之后。ActivityThread源码分析中说过,ActivityThread负责Activity等四大组件的生命周期调度。onResume生命周期的调度在ActivityThread#handleResumeActivity方法:
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
// 调度Activity的onResume生命周期
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
// 虽然这里使用ViewManager接收返回值,但是从Activity#getWindowManager()方法看,返回值类型是子类WindowManager
// WindowManager是一个接口,实现类是WindowManagerImpl
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 关键步骤,调用WindowManager的addView方法
wm.addView(decor, l);
} else ...
}
} else ...
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
...
ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
WindowManager.LayoutParams l = impl != null
? impl.mWindowAttributes : r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode = (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
// 这里调用了mDecor.setVisibility(View.VISIBLE)恢复它的可见
r.activity.makeVisible();
}
}
r.nextIdle = mNewActivities;
mNewActivities = r;
Looper.myQueue().addIdleHandler(new Idler());
}
前文提到,我们的View被添加到了DecorView中,所以这里我们只关心对DecorView的操作,排除其他代码的干扰。
可以看到,这里首先设置了DecorView的可见性为INVISIBLE,在最后在调用Activity#makeVisible()重新设置为VISIBLE。这是为了避免在这个过程中软键盘的弹起。
此外,处理DecorView有两个关键地方:
// 把DecorView交给WindowManager管理
wm.addView(decor, l);
// 这里应该是根据软键盘设置来更新布局参数
wm.updateViewLayout(decor, l);
先看WindowManager的addView方法,它的实现在WindowManagerImpl里
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 交给了WindowManagerGlobal#addView来处理
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
看WindowManagerGlobal#addView:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
....
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
...
}
}
}
这里做了一些异常判断,其中“Params must be WindowManager.LayoutParams”我们可能比较熟悉。
这个方法里创建了一个ViewRootImpl的实例对象,从名字可以看出来,ViewRootImpl应该是View树的根结点的实现类,不过它没有对应的抽象类ViewRoot。ViewRootImpl实现了ViewParent接口,可以作为View的Parent,但它不是ViewGroup或者View的子类。ViewRootImpl的类头注释如下:
The top of a view hierarchy, implementing the needed protocol between View and the WindowManager. This is for the most part an internal implementation detail of WindowManagerGlobal
既然这里的root是View树的根结点,那么root.setView(view, wparams, panelParentView, userId)应该就是把我们的页面布局挂在到View树上了。
/**
* We have one child
* ViewRootImpl作为ViewParent只有一个子节点
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
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();
// 注册到WMS,初始化事件接收
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}
mForceDecorViewVisibility = (mWindowAttributes.privateFlags
& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
// mWindowSession是与WMS通信的IBinder接口,接收
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
} catch (RemoteException e) {
...
} finally {
...
}
...
if (inputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
Looper.myLooper());
}
// 把view的parent设置为此ViewRootImpl对象
view.assignParent(this);
...
// 初始化输入时间处理流水线,责任链模式
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
...
}
}
}
就是这里了!requestLayout中开始View树的测绘流程。值得一提的是,向WMS注册和接收WMS事件分发的设置也在这里。
跟进一下requestLayout方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
这里的逻辑很简单,如果当前正在处理布局流程,直接返回;做一个线程检查,然后遍历View树进程测绘。
看一下线程检查:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
这里如果当前线程不是mThread,那么就会抛出一个著名的异常了。这就是为什么说“不能在子线程更新UI”的原因了。但是这个线程检查在ViewRootImpl,我们回想一下,ViewRootImpl对象是在WindowManagerGlobal#addView方法里面创建的,也就是在Activity的onResume之后,那么在此之前如果我们通过子线程更新UI是不是可以成功呢?是的,只要更新UI的代码先于onResume执行的话,子线程更新UI是可以成功的。不过这里我们不深入讨论,有兴趣的读者可以自行测试一下。
回到我们的主流程来,看一下scheduleTraversals方法
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里通过mChoreographer设置了一个回调,回调里执行了doTraversal方法。
Choreographer是接收系统垂直同步信号(vsync)的类,后续文章在深入分析它。
doTraversal方法:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
...
performTraversals();
...
}
}
performTraversals方法:
private void performTraversals() {
final View host = mView;
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
...
performDraw();
}
performTraversals方法十分庞大,不过我们只关注View的测绘流程的三大步骤和一个关键的View#dispatchAttachedToWindow就好了。至此,我们已经明白了三大流程的执行时机。
4、onCreate和onResume中正确获取View宽高
通过上面的分析我们已经明白了三大流程的执行时机,但是还不足以解决我们开篇提出的问题。问题的答案在View#dispatchAttachedToWindow方法中:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
if (mFloatingTreeObserver != null) {
// 关键!
info.mTreeObserver.merge(mFloatingTreeObserver);
mFloatingTreeObserver = null;
}
...
if (mRunQueue != null) {
// 执行消息队列中的操作
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
...
}
这个方法里有两个与我们的问题紧密相关的地方:
-
一是把mFloatingTreeObserver合并到了info.mTreeObserver,有经验的同学可能会知道,View类中有一个getViewTreeObserver方法
public ViewTreeObserver getViewTreeObserver() { if (mAttachInfo != null) { return mAttachInfo.mTreeObserver; } if (mFloatingTreeObserver == null) { mFloatingTreeObserver = new ViewTreeObserver(mContext); } return mFloatingTreeObserver; }也就是说,getViewTreeObserver方法在dispatchAttachedToWindow前返回的值是mFloatingTreeObserver,dispatchAttachedToWindow后返回的值mAttachInfo.mTreeObserver。而ViewRootImpl#performTraversals方法中调用了mAttachInfo.mTreeObserver.dispatchOnGlobalLayout()!第一个答案就出来了,可以通过以下代码获取View的宽高
view.viewTreeObserver.addOnDrawListener { val width = view.width val height = view.height } -
第二个关键的地方就是mRunQueue.executeActions(info.mHandler)了,看一下mRunQueue的注释:
/** * Queue of pending runnables. Used to postpone calls to post() until this * view is attached and has a handler. */ private HandlerActionQueue mRunQueue;很明显,它是用于在dispatchAttachedToWindow前缓存View#post方法传递的Runnable对象,以在dispatchAttachedToWindow后执行。所以获取View宽高的第二个方法也很容易得出了:
view.post { val width = view.width val height = view.height }或许有同学可能会有疑问,ViewRootImpl#performTraversals方法中dispatchAttachedToWindow比performMeasure先执行,
private void performTraversals() { final View host = mView; ... host.dispatchAttachedToWindow(mAttachInfo, 0); ... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... }这样还能在这些Runnabe中获取View的宽高吗?其实是可以的。由于Handler的消息循环机制,dispatchAttachedToWindow方法中只是把这些Runnable加入了执行队列,真正执行起码会在performTraversals方法执行之后。
本文详细解析了Android中Activity的视图绘制流程,包括视图的测量、布局和绘制过程,以及如何在onCreate和onResume方法中正确获取视图的尺寸。
1568





