前言
在开发过程中,我们一般会遇到在Activity或者Frament布局完成之后,获取某些View的高度或者位置,来通过代码灵活计算View之间的相对关系。
正文
View.post的用法如下:
public class MainActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
ImageView imageView = findViewById(R.id.girl);
// 注释1
imageView.post(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "width =" + imageView.getWidth());
Log.d("MainActivity", "height =" + imageView.getHeight());
}
});
}
}
布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent"
android:background="#f5f5f5"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="9dp"
app:cardElevation="3dp">
<ImageView
android:id="@+id/girl"
android:layout_width="300dp"
android:layout_height="400dp"
android:scaleType="centerCrop"
android:src="@drawable/girl2" />
</android.support.v7.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="36dp"
android:textColor="@color/gray_333333"
android:text="三吉彩花看完了,记得点赞收藏"
android:textSize="18dp"
android:textStyle="bold" />
</LinearLayout>
效果如下:
在注释1处,通过调用ImageView的post获取ImageView的宽高,日志打印如下:
2022-03-07 15:55:40.369 14922-14922/? D/MainActivity: width =900
2022-03-07 15:55:40.369 14922-14922/? D/MainActivity: height =1200
可以看到已经正确获取到了图片的宽高。接下来我们就结合源码来分析为啥View的post可以实现这样的效果?
分析
先来看下View#post的代码(注意代码基于Android Api 30):
代码片段1
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
//注释1
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.
//注释2
getRunQueue().post(action);
return true;
}
在注释1处对 attachInfo 进行了非空判断,如果非空直接调用Handlder#post(),没啥特殊的就不分析了。注释2处getRunQueue()对View#mRunQueue进行初始化之后调用post 跟进去看下,最终到了 HandlerActionQueue#postDelayed(),如下:
代码片段2
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++;
}
}
可以看到mActions维护了一个执行的数组,postDelayed()只是往数组里面添加一条记录,并没有真正执行,执行的方法在 HandlerActionQueue#executeActions(),如下:
代码片段3
public void executeActions(Handler handler) {
synchronized (this) {
//注释1
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
注释1处遍历mActions,交给 handler去 postDelayed(),接下来看下executeActions()调用,在View#dispatchAttachedToWindow,截取部分关键代码如下:
代码片段4
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
//注释1
mAttachInfo = info;
//注释2
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
注释1处对 mAttachInfo 赋值,注释2处执行HandlerActionQueue#executeActions(),这里需要关注mAttachInfo#mHandler,接下看下dispatchAttachedToWindow()调用的地方,在ViewRootImpl#performTraversals()中,截取关键代码如下:
代码片段5
private void performTraversals() {
//注释1
final View host = mView;
...
//注释2
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
//注释3
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
注释1处的mView的赋值在ViewRootImpl#setView(),注释2处调用dispatchAttachedToWindow(),传入了mAttachInfo,注释3处执行了View的测量流程,这里先埋个疑问,dispatchAttachedToWindow()在测量以及布局之前调用为啥还能获取到View的宽高呢?这个后面解答接下来看下performTraversals()调用点,截取部分关键代码如下:
void doTraversal() {
//注释1处
performTraversals();
}
}
继续看doTraversal的调用点,如下:
代码片段6
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//注释1
doTraversal();
}
}
是个 TraversalRunnable,使用的地方如下:
代码片段7
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//注释1
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//注释2
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
注释1先放一放,先看注释2,最后到Choreographer#postCallbackDelayedInternal()
代码片段8
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
#注释1
msg.setAsynchronous(true);
#注释2
mHandler.sendMessageAtTime(msg, dueTime);
}
}
注释1处 msg.setAsynchronous(true)注释2处交给handle处理,再回到代码片段7,看下scheduleTraversals()的调用点,在ViewRootIml中有多处调用,那到底是哪个呢?在这篇文章中,我们知道 ViewRootImpl 初始化之后调用的第一个方法是setView(),截取部分关键代码如下:
代码片段9
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
//注释1
requestLayout();
}
到这里我们就知道scheduleTraversals()是调用发起点是ViewRootImpl#setView(),通过以上分析我们得到一个完整的调用流程如下:
可以看到最后的处理流向都是 handler,这里handler的的looper可以简单理解为主线程的looper
QA
借助上一节张流程图,我们来解答之前留下的问题,dispatchAttachedToWindow()在测量以及布局之前调用为啥还能获取到View的宽高呢? 从上图可知在ViewRootImpl#scheduleTraversal()调用了Choreographer#postCallBack(),回到代码片段7的注释1如下:
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
,表示插入一个同步屏障,普通的msg在屏障清除之前都将阻塞,只能执行异步的msg,可以通过在代码片段8的注释1处代码的代码如下:
msg.setAsynchronous(true);
将msg设置为异步的msg,使其优先执行。也就是说通过这种手段,是的 mTraversalRunnable 的run方法在整个MessageQueen中具有最高优先级,从上面流程图可以看到dispatchAttachedToWindow()最终也是往Hanlder发送普通的msg,优先级在 mTraversalRunnable 之后,也就是在 performMeasure() 之后,当然可以拿到正确的宽高,上面的疑问就得到解决。
总结
回过头来看下,View#post()流程:
代码片段1
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
//注释1
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.
//注释2
getRunQHandlerActionQueue ().post(action);
return true;
}
注释1处如果 attachInfo != null,说明View的测量流程已经走完,因为mAttachInfo是通过代代码片段5里面的dispatchAttachedToWindow()赋值的,而dispatchAttachedToWindow()是在performTraversal()执行的,结合上面的分析,View的测量流程已经走完了,此时可以获取到View的宽高。注释2处如果 attachInfo 为null,则通过 HandlerActionQueue 以数组的方式保存下来,等到执行 performTraversal() 时通过 HandlerActionQueue#executeActions()执行,同样的道理,View的测量流程已经走完了,此时可以获取到View的宽高。
后记
通过上面结合流程图的分析,相信对View.post()的流程以及原理以及有了比较清晰的理解,最后原创不易,求个三连!