在onResume中是否可以测量宽高?
这道题想考察什么?
这道题想考察同学对 View的绘制流程 的理解。
考生应该如何回答
这个问题答案是:不一定能够正确的获取view的宽高,然后我们要分析原因,然后我们还要讲解解决办法。
如果是activity 启动后第一次进入onResume 生命周期,那么获取到的View的宽高是错误的;如果是从其他activity回到当前activity而执行的onResume方法,那么就能够获取到View的宽高。究其原因如下:
首先我们要找到系统调用 onResume 的地方,大家可以看到 ActivityThread 类,在这个类中有一个函数handleResumeActivity,这个函数在下面我们只保留了核心代码。
handleResumeActivity() {
//...
r = performResumeActivity(token, clearHide, reason);//调用 onResume() 方法
//...
wm.addView(decor, l);// WindowManager添加Decor(decor是DecorView)
//...
}
可以看出 onResume() 方法在 addView() 方法前调用。
重点关注:onResume() 方法所处的位置,前后都发生了什么?
从上面总结的流程看出,onResume() 方法是由 handleResumeActivity 触发的,而界面绘制被触发是因为 handleResumeActivity() 中调用了wm.addView(decor, l),大家看下面的分析;
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//...
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);
}
// do this last because it fires off messages to start doing things
try {
//触发开发绘制,参考 1
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
//...
throw e;
}
}
参考 1: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.
//触发界面刷新,参考 2
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
//...
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
//...
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
//...
//这里的 view 是 DecorView
view.assignParent(this);
//...
}
}
}
参考 2:requestLayout()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//准备刷新,参考 3
scheduleTraversals();
}
}
参考 3:scheduleTraversals(),这个函数的主要作用是告诉安卓系统app端需要界面的刷新,并且设置一个回调,用于接收系统发送的同步app到系统的信号。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步障碍 Message
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//屏幕刷新信号 VSYNC 监听回调把 mTraversalRunnable(执行doTraversal())push 到主线程了,异步 Message 会优先得到执行 ,具体看下 Choreographer 的实现
//mTraversalRunnable,参考 4
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
参考 4:mTraversalRunnable,当系统审核app端刷新界面的申请后,在合适的时机会调用之前设定的TraversalRunnable回调。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//参考 5
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
参考 5:doTraversal()
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步障碍 Message
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//参考 6,在上面移除同步障碍后,开始对控件树进行测量、布局、绘制
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
参考 6:performTraversals()
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
//...
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
//...
// We used to use the following condition to choose 32 bits drawing caches:
// PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
// However, windows are now always 32 bits by default, so choose 32 bits
mAttachInfo.mUse32BitDrawingCache = true;
mAttachInfo.mHasWindowFocus = false;
mAttachInfo.mWindowVisibility = viewVisibility;
mAttachInfo.mRecomputeGlobalAttributes = false;
mLastConfiguration.setTo(host.getResources().getConfiguration());
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
// Set the layout direction if it has not been set before (inherit is the default)
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(mLastConfiguration.getLayoutDirection());
}
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
//Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
//...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);// code 7
//...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//触发viewTree onMeasure
...
performLayout(lp, mWidth, mHeight);//触发viewTree onLayout
...
performDraw();//触发ViewTree onDraw
//...
}
小结:
在上面的代码中,performTraversals 函数是整个viewTree 的onMeasure 、onLayout、onDraw的发起点。所以,基于上面的代码流程分析大家不难发现activity 的onResume 函数是在 viewTree 的onMeasure 函数执行之前先执行,也就是在执行onResume函数的时候,viewTree并未完成度量。那么,当我们去onResume函数里面获取view 的宽高自然是获取不到的。
那么如何在onResume中获取view的宽高呢?
在 onResume() 中 handler.post(Runnable) 是无法正确的获取不到 View 的真实宽高
原因:能够获取到view的宽高的前提条件是view要完成onMeasure,那么我们的handler.post 的方式是不能够保证 runnable一定是在view进行了onMeasure后执行的,因此,这个方式不行。同样的道理,运用handler.postDelay也不是最佳的方案,因为,不同型号的手机代码的执行时间不同,导致postDelay到底需要delay多久对于用户来说仍旧是未知数。那么正确的做法是怎样的呢?答案就是调用View.post(Runnable)。
View.post(Runnable) 为什么可以获取到 View 的宽高?
public boolean post(Runnable action) {
//mAttachInfo 是在 ViewRootImpl 的构造函数中初始化的
//而 ViewRootmpl 的初始化是在 addView() 中调用
//所以此处的 mAttachInfo 为空,所以不会执行该 if 语句
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
//保存消息到 RunQueue 中,等到在 performTraversals() 方法中被执行
getRunQueue().post(action);
return true;
}
由参考 6 中的code 7 可知,通过 View.post(Runnable) 的 Message 会在 performMeaure() 之前被调用,那为什么还可以正确的获取到 View 的宽高呢?其实我们的 Message 并没有立即被执行,因为此时主线程的 Handler 正在执行的 Message 是 TraversalRunnable,而 performMeaure() 方法也是在该 Message 中被执行,所以排队等到主线程的 Handler 执行到我们 post 的 Message 时,View 的宽高已经测量完毕,因此我们也就很自然的能够获取到 View 的宽高。
总结
在 onResume() 中直接获取或者调用创建的 Handler 的 post 方法都是获取不到 View 宽高的,需要通过 View.post 才能获取。
最后
有需要以上面试题的朋友可以关注一下哇哇,以上都可以分享!!!
网友评论