详解Android的View.post()

494 阅读5分钟

前言

 在开发过程中,我们一般会遇到在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>

效果如下:

Screenshot_20220307_155344_com.example.myapplication.jpg

在注释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(),通过以上分析我们得到一个完整的调用流程如下:

image.png

可以看到最后的处理流向都是 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()的流程以及原理以及有了比较清晰的理解,最后原创不易,求个三连!