转载

View的post函数详析

基于Android 28

View本身也提供了post接口来在主线程中处理异步任务。但是不同的是,view中做了保障机制:即所有的任务都是在绘制第一帧的时候才能进入MainLooper。

下面看具体分析:

HandlerActionQueue

那么就先从入口往里面一层层看,下面是post函数的具体实现。

View.post()函数

// frameworks/base/core/java/android/view/View.java
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那么直接使用它里面的Handler发送消息。这同正常的Handler机制是一致的。
  • 如果不存在,那么获取一个RunQueue并post给它。

至于AttachInfo什么情况下不为空,一会会跟下面的RunQueue会合起来看。这里主要看一下RunQueue。

getRunQueue()

// frameworks/base/core/java/android/view/View.java
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

getRunQueue返回的是本地缓存的一个HandlerActionQueue类的实例mRunQueue,并且mRunQueue本身是lazyInit的。

下面再看来看mRunQueue如何处理postDelayed():

postDelayed

// frameworks/base/core/java/android/view/HandlerActionQueue.java
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++;
    }
}

这里其实也非常明显,首先HandlerActionQueue会将进来的Runnable对象封装成HandlerAction。最后将HandlerAction对象加入到本地的mActions(即HandlerAction数组中)缓存起来。

到这里其实调用就结束了,那么什么时候才会执行到呢?

dispatchAttachedToWindow

其实稍微注意一下源码就可以发现,在View的dispatchAttachedToWindow方法中会设置AttachInfo并且处理mRunQueue中的队列:

// frameworks/base/core/java/android/view/View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    onAttachedToWindow();
    // ...
}

上面提到的mAttachInfo就在这里赋值的,经过这一步之后再执行View的post方法就会直接使用Handler发送消息了。

之后会读取mRunQueue(默认情况下没人调用post等相关方法这个变量会是null),并将mAttachInfo中的mHandler对象传递到其内部的executeActions中,同时在View里将mRunQueue置null。

下面来看HandlerActionQueue的executeActions函数

executeActions

// frameworks/base/core/java/android/view/HandlerActionQueue.java
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];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        mActions = null;
        mCount = 0;
    }
}

这里其实就是读取HandlerAction数组并通过View中传递过来的mHandler执行postDelay的行为。

其实不论是在什么时机调用post最终都会用到mAttachInfo的mHandler对象来发送消息到MainLooper中。

虽然知道view.post的任务最终的一定会通过AttachInfo的mHandler对象post出去,但是他是哪里来的呢?

doTraversal()

我们都知道Android上面View的绘制都是在doTraversal的完成的。

// frameworks/base/core/java/android/view/ViewRootImpl.java
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

而doTraversal()函数本身会调用 performTraversals()来完成具体的绘制调用,因为绘制涉及到非常多的流程,所以这一步其实非常庞大,但是还是可以找到上面提到的dispatchAttachedToWindow相关的行为:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performTraversals() {
// ...
    Rect frame = mWinFrame;
    if (mFirst) {
        // ...
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
    // ...
    }
    // ...
    mFirst = false;
    // ...
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw && !newSurface) {
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }

        performDraw();
    } else {
        // ..
    }
    mIsInTraversal = false;
}

可以看到mFirst为true时,即准备开始第一次绘制时会调用mHost的dispatchAttachedToWindow函数,经过ViewGroup对各个子View进行dispatchAttachedToWindow事件的层层风发,最终执行到调用post方法的那个view。

其中mHost其实就是在Activity执行setContentView之后经过PhoneWindow最后创建ViewRootImpl并设置进来的DecorView。

具体doTraversal是如何运行的,那就是View本身绘制流程了。要想讲清楚需要挺大篇幅,这里不再展开了。

延伸

与直接创建Handler的不同

仅考虑是在主线程内部创建Handler的情况。

其实通过上面的分析已经可以得出非常明显的结论:那就是,view.post如果在没有绘制第一帧的情况下,所有的任务都不会被执行,并且会缓存在HandlerActionQueue中等待dispatchAttachToWindow时机。而在HandlerActionQueue被执行时,还会再次讲先前的任务post一遍。

而Handler最直接,在你发送任务的时候就会立马进入MainLooper等待下次被调起。这个过程其实同View的绘制流程其实是脱节的。

与ViewTreeObserver的不同

ViewTreeObserver行为发生在View的绘制过程中,并且可以对当前view绘制事件进行拦截。简单来说,当我知道你的view尺寸之后我可以作出改变并且要求你停止此次绘制(见上面performTraversals函数最终的cancelDraw)。

如果想要第一事件获取View的宽高并且及时对自身的UI作出改变,则非常建议使用ViewTreeObserver。使用view.post很有可能是发生在MainLooper执行完绘制第一帧之后,从而导致节目的抖动或者闪烁。

其实究其原因还是ViewTreeObserver同Handler本身完全不是一回事。放在这里提是提醒一下,想在View的绘制周期中干活其实是有更好的选择。

By@hyongbai 共5323个字

本文链接

原文  http://yourbay.me/all-about-tech/2019/08/27/android-28-view-post/
正文到此结束
Loading...