ps:源码是基于 android api 27 来分析的,demo 是用 kotlin 语言写的。
在Android 中,如果在Activity 的onCreate 方法直接获取View 的宽高是获取不到的,但如果是调用 View的post方法通过它(post方法)参数Runnable 接口的回调却能够获取到 View 的宽高,View 的 post 方法是马上执行的吗?它的执行时机又是什么时候呢?下面我们举个例子验证一下以下几点:
-
post方法中的Runnable接口的回调能否直接获取View的宽; -
post方法中Runnable接口的回调和Activity的onResume方法的先后顺序; -
Activity的onResume方法能否直接获取View的宽; -
View没有被添加到Window里的时候,执行post方法,Runnable接口能否被回调。
PostDemo
(1)新建一个 Kotlin 类型的类MyView 并继承于View:
class MyView: View {
constructor(context: Context): super(context) {
}
constructor(context: Context,@Nullable attrs: AttributeSet): super(context,attrs) {}
constructor(context: Context,@Nullable attrs: AttributeSet,defStyleAttr: Int): super(context,attrs,defStyleAttr) { }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
Log.d(MainActivity.TAG,"------onMeasure--")
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
Log.d(MainActivity.TAG,"------onLayout--")
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
Log.d(MainActivity.TAG,"------onDraw--")
}
}
(2)新建一个 Kotlin 语言类型的Activity,名叫MainActivity:
class MainActivity: AppCompatActivity() {
companion object {
var TAG: String = "MainActivity"
}
var mView: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mView = findViewById(R.id.my_view);
mView?.post(Runnable {
Log.d(TAG,"在 post 方法中获取 View 的宽--" + mView?.getWidth())
})
}
override fun onResume() {
super.onResume()
Log.d(TAG,"----onResume---")
Log.d(TAG,"在 onResume 方法中获取 View 的宽--" + mView?.getWidth())
}
}
(3)MainActivity的布局界面activity_main.xml如下所示:
<?xml version="1.0" encoding="utf-8"?>
<com.xe.postdemo.MyView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0000"
tools:context="com.xe.postdemo.MainActivity">
</com.xe.postdemo.MyView>
把程序运行一下,日志打印如下所示:
08-28 08:54:24.364 10133-10133/com.xe.postdemo D/MainActivity: ----onResume---
08-28 08:54:24.364 10133-10133/com.xe.postdemo D/MainActivity: 在 onResume 方法中获取 View 的宽--0
08-28 08:54:24.435 10133-10133/com.xe.postdemo D/MainActivity: ------onMeasure--
08-28 08:54:24.487 10133-10133/com.xe.postdemo D/MainActivity: ------onMeasure--
08-28 08:54:24.490 10133-10133/com.xe.postdemo D/MainActivity: ------onLayout--
08-28 08:54:24.501 2285-2738/? D/Launcher.AllAppsList: updatePackage, find appInfo=AppInfo, id=-1, itemType=0, user=UserHandle{0}, mIconType=0, pkgName=com.xe.postdemo, className=com.xe.postdemo.MainActivity, screenId=-1, container=-1, cellX=-1, cellY=-1, spanX=1, spanY=1, isLandscapePos=false from ComponentInfo{com.xe.postdemo/com.xe.postdemo.MainActivity}
08-28 08:54:24.505 2285-2738/? D/Launcher.Model: onReceiveBackground, mAllAppsList=add=[], remove=[], modified=[(0, AppInfo, id=-1, itemType=0, user=UserHandle{0}, mIconType=0, pkgName=com.xe.postdemo, className=com.xe.postdemo.MainActivity, screenId=-1, container=-1, cellX=-1, cellY=-1, spanX=1, spanY=1, isLandscapePos=false)]
08-28 08:54:24.542 10133-10133/com.xe.postdemo D/MainActivity: ------onDraw--
08-28 08:54:24.714 1472-1555/? I/Timeline: Timeline: Activity_windows_visible id: ActivityRecord{afbc1c9 u0 com.xe.postdemo/.MainActivity t7087} time:4018603
08-28 08:54:24.715 1472-1530/? I/ActivityManager: Displayed com.xe.postdemo/.MainActivity: +4s904ms
08-28 08:54:24.762 10133-10133/com.xe.postdemo D/MainActivity: 在 post 方法中获取 View 的宽--720
从日志可以看出:
post方法中Runnable 接口的回调中是可以直接获取到View 的;
Activity的onResume 方法比post 方法中Runnable 接口的回调先执行,post方法中Runnable 接口是在View 的绘制(主要是View 的onMeasure、onLayout和onDraw 方法)之后才会被回调;
在Activity的onResume 方法不能直接获取View 的宽,因为Activity 的onResume 方法比View 的onMeasure 和onLayout 方法先执行。
好,我们把MainActivity 的onCreate 方法做一下修改,其他不变:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
// mView = findViewById(R.id.my_view);
mView = MyView(this)
mView?.post(Runnable {
Log.d(TAG,"在 post 方法中获取 View 的宽--" + mView?.getWidth())
})
}
运行程序,日志打印如下所示:
08-31 13:24:02.481 10794-10794/com.xe.postdemo D/MainActivity: ----onResume---
08-31 13:24:02.481 10794-10794/com.xe.postdemo D/MainActivity: 在 onResume 方法中获取 View 的宽--0
从日志可以看出:
View没有被添加到Window里的时候,执行post方法,Runnable接口不会被回调。
好了,以上验证的4点以及View的post方法执行时机可以从源码中找到原因,我们先从View的post方法看起;
public boolean post(Runnable action) {
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.
getRunQueue().post(action);
return true;
}
这里attachInfo 是为空的,所以不会执行 attachInfo.mHandler.post(action)这行代码,我们看 getRunQueue().post(action)这行代码,getRunQueue() 方法得到的是一个HandlerActionQueue 类型的对象,我们点击HandlerActionQueue 的post方法查看;
public void post(Runnable action) {
postDelayed(action, 0);
}
HandlerActionQueue的post方法又调用了自己的postDelayed方法,这里的参数0表示延时所有的事件,我们往下看HandlerActionQueue的postDelayed方法;
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
从这个方法看出:HandlerAction表示要执行的任务,要执行的任务HandlerAction保存在数组长度为4的mActions数组中,mCount表示数组mActions的下标,每次都加1;这个postDelayd方法并没有马上执行任务,而是保存了任务,那么执行任务的语句在哪里呢?
有时候我们会说看到Activity的界面后 onResume方法就会被回调,所以我们从调用Activity的onResume方法的AcitivityThread.handleResumeActivity方法说起;
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
......
//1、
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
......
if (!r.activity.mFinished && willBeVisible
......
if (r.activity.mVisibleFromClient) {
//2、
r.activity.makeVisible();
}
}
......
} else {
......
}
}
这里的注释1 最终会调用Activity的onResume方法,
我们往下看注释2 的代码,它是Activity的makeVisible方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
//3、
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
注释3 表示将View视图添加到ViewManager中,我们点击注释3 的代码进去看看,ViewManager的实现类是WindowManagerImpl,所以我们看的是WindowManagerImpl的addView方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//4、
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
注释4 处调用了WindowManagerGlobal的addView方法,我们往下看;
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
synchronized (mLock) {
......
//5、
root = new ViewRootImpl(view.getContext(), display);
......
try {
//6、
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
......
}
}
}
我们点击注释5 的代码,看看ViewRootImpl的构造方法;
public ViewRootImpl(Context context, Display display) {
......
//7、
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
......
}
一刚开始的时候,我们在Activity的onCreate方法中先执行View.post方法,但是ViewRootImpl在Activity的onResume方法之后才会被初始化,还顺便在ViewRootImpl的构造方法中初始化AttachInfo,所以说一开始View.post方法中的attachInfo就为null,从而执行getRunQueue().post(action)语句;
我们往下看注释6 的代码,也就是ViewRootImpl的setView方法;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//8、
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.
//9、
requestLayout();
......
}
}
}
这里注释8 的View是底部容器DecorView;
我们继续往下看注释9 的方法,也就是ViewRootImpl的requestLayout方法;
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
requestLayout方法调用了ViewRootImpl的scheduleTraversals方法,我们且看scheduleTraversals方法;
void scheduleTraversals() {
if (!mTraversalScheduled) {
......
//10、
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//11、
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
注释10 这里先不说,后面再说,
注释11 的代码会回调mTraversalRunnable对象,而mTraversalRunnable对象是一个Runnable接口的实现类TraversalRunnable具体的对象,mTraversalRunnable对象又调用了ViewRootImpl的doTraversal方法,doTraversal方法又调用了ViewRootImpl的performTraversals方法,我们来看看performTraversals方法:
private void performTraversals() {
//12
// cache mView since it is used so much below...
final View host = mView;
......
if (mFirst) {
......
//13、
host.dispatchAttachedToWindow(mAttachInfo, 0);
......
} else {
......
}
......
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
mForceNextWindowRelayout = false;
......
if (!mStopped || mReportNextDraw) {
......
//14、
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
} else {
......
}
......
if (didLayout) {
//15、
performLayout(lp, mWidth, mHeight);
......
}
......
if (!cancelDraw && !newSurface) {
......
//16、
performDraw();
} else {
......
}
......
}
注释12 就是上面注释8 中说的底部容器DecorView,
注释13 表示DecorView关联AttachInfo,dispatchAttachedToWindow方法是在ViewGroup 里实现,该方法会遍历DecorView 的子元素进行 关联AttachInfo,我们看一下ViewGroup 的dispatchAttachedToWindow 方法:
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
......
for (int i = 0; i < count; i++) {
final View child = children[i];
//17、
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
......
}
这个方法是很重要的:
子元素关联了AttachInfo,然后将之前 View.post 保存的任务添加到AttachInfo 内部的Handler,所以View 没有被添加到Window 里的时候,执行post 方法,Runnable接口没有被回调;
我们看注释17 的代码,它是View 的dispatchAttachedToWindow 方法;
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
......
// Transfer all pending runnables.
if (mRunQueue != null) {
//18、
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
......
}
注释18 的代码表示调用了HandlerActionQueue 的executeActions 方法,我们来看看executeActions 方法;
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
//19、
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
//20、
mActions = null;
mCount = 0;
}
}
注释19 行的代码我们先留下悬念;
注释20 是将mActions 置空,从第二次调用 View.post 开始,Runnable会被添加到AttachInfo 内部的Handler,而不是HandlerAction,View的onMeasure、onLayout和onDraw 方法也不会被调用;
我们回过头来看注释14 的代码,也就是ViewRootImpl 的performMeasure 方法;
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
......
try {
//21、
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
注释21 的mView 表示DecorView,它的measure 方法是在View 里,我们来看一下View 的measure 方法;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
if (forceLayout || needsLayout) {
......
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
//22、
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
......
}
......
}
......
}
注释22 的代码是调用的是DecorView 的onMeasure 方法,而不是View 的onMeasure 方法,我们往下看DecorView 的onMeasure 方法;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
DecorView 的onMeasure 方法调用了 父类的onMeasure 方法,最终的实现是在FrameLayout 中measure 方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//23、
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
......
}
}
......
}
注释23 的代码会对所有子View 进行了一遍测量,并计算出所有子 View 的最大宽度和最大高度,我们往下看FrameLayout 的measureChildWithMargins 方法;
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
......
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我们假设child 是具体的View,而不是ViewGroup,View的measure 方法最终调用了View 的onMeasure 方法,onMeasure方法最终是测量View 的宽高;
我们回过头来看:注释15 的代码最终会调用 View.onLayout 方法,它的调用过程是:
ViewRootImpl.performLayout-> host.layout(ViewGroup.layout)->super.layout(View.layout)-> View.onLayout,
最终完成View 位置的确定;
注释16 的代码最终会调用 View.onDraw 方法,它的调用过程是 :ViewRootImpl.performDraw -> ViewRootImpl.draw->ViewRootImpl.drawSoftware-> mView.draw(DecorView.draw)->super.draw(View.draw)-> View.onDraw,
最终完成 View 的绘画出来;
好了,现在我们可以解答上面剩下的疑问了,AcitivityThread.handleResumeActivity方法 先调用自己的performResumeActivity 方法,而该方法最终调用Activity 的onResume 方法,而后再调用Activity 的makeVisible 方法,Activity的makeVisible 方法最终完成View 的测量宽高、位置确定和绘画,所以Activity 的onResume 方法不能直接获取View 的宽。
我们在回过头来看,注释19 的代码,它的延时时间为0啊,而且比 View 的onMeasure、onLayout和onDraw 方法先被调用啊,为什么从上面的demo 日志看出最终Runnable 接口等View 的onMeasure、onLayout和onDraw 方法调用完之后再调用?
是因为注释10 的代码先比注释19 的代码先被调用,注释10 表示开启了同步消息屏障,Android中它有一个异步消息优先级比较高的权利,保障 View 绘制完后再给其他消息执行,所以在 View.post方法中的Runnable 接口的回调能直接获取View 的宽。
Activity 的onResume 方法是在Activity 的makeVisible 方法先被调用的,而View 的post 中Runnable 接口是在View 绘制完才会被回调的,所以Activity的onResume 方法先比 View.post 方法中Runnable 接口被调用。