Android view.post

Android view.post,第1张

1.view.post()

在开发过程中用到过几次view.post(),但是对它的原理不甚了解,今天就从源码看看它是怎么实现的吧。

其实view.post()的内部也是调用了Handler,它主要用于更新UI *** 作、获取view的实际宽高。简单来说,view.post()对任务的运行时机做了调整。

举个例子:在Activity中,view绘制流程的开始时机是在ActivityThread的handleResumeActivity方法中,该方法首先完成Activity生命周期onResume方法回调,然后开始view绘制任务。也就是说,view的绘制流程要在onResume方法之后,但是我们绝大多数业务是在onCreate方法,比如要获取某个view的实际宽高,由于view的绘制任务还未开始,所以就无法正确获取。此时大家肯定使用过view.post()来解决问题(当然也可以使用ViewTreeObserver或更长延迟的postDelayed()方法)。注意view绘制流程也是向Handler添加任务,如果在onCreate方法直接使用Handler.post(),则该任务一定在view绘制任务之前(同一个线程队列机制)。

现在带着3个问题去看源码:

①为什么View.post()可以对UI进行 *** 作呢,即使在子线程中调用View.post()?

②View.post()执行时,View的宽高已经计算完毕,所以经常看见在Activity的onCreate()里调用View.post()来解决获取View宽高为0的问题,为什么可以这样做呢?

③用 View.postDelay() 会导致内存泄漏吗?

 

2.源码

View.java:

/**Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread.

param: action - The Runnable that will be executed.

return: Returns true if the Runnable was successfully placed in to the message queue. Returns false on failure, usually because the looper processing the message queue is exiting.*/

public boolean post(Runnable action) {

    final AttachInfo attachInfo = mAttachInfo;

    if(attachInfo != null) {

        //attachInfo不为空,直接调用其内部Handler的post方法

        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); //attachInfo为空,则加入当前view的等待队列

    return true;

}

AttachInfo是View的静态内部类,每个View都会持有一个AttachInfo,它默认为null。

从源码中可以看到,pos方法里分了两种情况:

①mAttachInfo != null 时,post方法最终是由attachInfo中的mHandler调用post来处理,从而保证在UI线程中执行,所以从根本上来说之后的整个流程就是Handler的处理机制流程,那么mAttachInfo又是什么时候赋值的呢?搜索源码看到:

/**param: info the android.view.View.AttachInfo to associated with this view*/

void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    mAttachInfo = info;

    if (mOverlay != null) {

        mOverlay.getOverlayView(). dispatchAttachedToWindow(info, visibility);

    }

    mWindowAttachCount++;

    ……

    //Transfer all pending runnable.

    if(mRunQueue != null) {

        mRunQueue.executeActions(info.mHandler);

        mRunQueue = null;

    }

    ……

}

mAttachInfo是在dispatchAttachedToWindow方法中调用的,而dispatchAttachedToWindow是在 ViewRootImpl类的performTraversals调用的,而这个方法在view初始化的时候会被调用。

②m AttachInfo == null时,调用getRunQueue().post(action),来看下这个getRunQueue()的源码:

/**Returns the queue of runnable for this view.

return: the queue of runnables for this view*/

private HandlerActionQueue getRunQueue() {

    if (mRunQueue == null) {

        mRunQueue = new HandlerActionQueue();

    }

    return mRunQueue;

 }

getRunQueue()返回的是HandlerActionQueue,也就是调用了HandlerActionQueue的post()方法:

HandlerActionQueue.java:

public void post(Runnable action) {

    postDelayed(action, 0);

}

public void postDelayed(Runnable action, long delayMillis) {

    final HandlerAction handlerAction = new HandlerAction(action, delayMillis); 

    synchronized (this) {

        if (mActions == null) { //保存HandlerAction的数组

            mActions = new HandlerAction[4];

        }

        mActions = GrowingArrayUtils.append( mActions, mCount, handlerAction); //要执行的任务HandlerAction保存在mActions数组中

        mCount++; //mActions的数组下标加1

    }

}

HandlerAction表示一个待执行的任务,内部持有要执行的Runnable和延迟时间。类声明如下:

private static class HandlerAction {  

    final Runnable action; // post的任务  

    final long delay; // 延迟时间

    public HandlerAction(Runnable action, long delay) {

        this.action = action;

        this.delay = delay;

    }

    // 比较是否是同一个任务,用于匹配某个 Runnable 和对应的HandlerAction

    public boolean matches(Runnable otherAction ) {

        return otherAction == null && action == null || action != null && action.equals(otherAction);

    }

}

postDelayed()创建一个默认长度为4的 HandlerAction数组,用于保存post()添加的任务。跟踪到这,大家是否有这样的疑惑:View.post()添加的任务没有被执行?

实际上,此时回过头来重新看下AttachInfo的创建过程,先看下它的构造方法:

AttachInfo(IWindowSession session, IWindow window, Display display, ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer, Context context) {

    mSession = session;

    mWindow = window;

    mWindowToken = window.asBinder();

    mDisplay = display;

    // 持有当前ViewRootImpl

    mViewRootImpl = viewRootImpl;

    mHandler = handler; // 当前渲染线程Handler

    mRootCallbacks = effectPlayer;

    // 为其创建一个ViewTreeObserver

    mTreeObserver = new ViewTreeObserver( context);

}

可以看到AttachInfo中持有当前线程的Handler。翻阅View源码,发现仅有两处对mAttachInfor赋值 *** 作,一处是为其赋值,另一处是将其置为 null。

mAttachInfo赋值过程:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    //给当前View赋值AttachInfo,此时所有的View共用同一个AttachInfo(同一个ViewRootImpl内)

    mAttachInfo = info;   

    if (mOverlay != null) { //View浮层

        // 任何一个View都有一个ViewOverlay,ViewGroup的是ViewGroupOverlay,它区别于直接在类似RelativeLaout/FrameLayout添加View,通过ViewOverlay添加的元素没有任何事件。此时主要分发给这些View浮层

        mOverlay.getOverlayView(). dispatchAttachedToWindow(info, visibility);

    }

    mWindowAttachCount++;

     // ... …

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {

        mAttachInfo.mScrollContainers.add(this);

        mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;

    }

    // mRunQueue,就是在前面的 getRunQueue().post(),实际类型是 HandlerActionQueue,内部保存了当前View.post的任务

    if (mRunQueue != null) {

        // 执行使用View.post的任务。注意这里是post到渲染线程的Handler中

        mRunQueue.executeActions(info.mHandler);

        // 保存延迟任务的队列被置为null,因为此时所有的View共用AttachInfo

        mRunQueue = null;

    }

    performCollectViewAttributes(mAttachInfo, visibility);

    // 回调View的onAttachedToWindow方法。该方法在Activity的onResume方法中调用,但是在View绘制流程之前

    onAttachedToWindow();

    ListenerInfo li = mListenerInfo;

    final CopyOnWriteArrayList< OnAttachStateChangeListener> listeners = li != null ? li.mOnAttachStateChangeListeners : null;

    if (listeners != null && listeners.size() > 0) {

        for (OnAttachStateChangeListener listener : listeners) {

            // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener(); 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小

            listener.onViewAttachedToWindow(this);

        }

    }

    // ... …

    // 回调View的onVisibilityChanged。注意这时候View绘制流程还未真正开始

    onVisibilityChanged(this, visibility);

    // ... …

}

方法最开始为当前View赋值AttachInfo。注意 mRunQueue就是保存了View.post()任务的 HandlerActionQueue。此时调用它的 executeActions方法如下:

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);//发送到Handler中,等待执行

        }

        mActions = null; //此时不再需要,后续的post,将被添加到AttachInfo中

        mCount = 0;

    }

}

遍历所有已保存的任务,发送到Handler中排队执行,所以刚刚缓存起来的runnable最终还是通过handler来执行的。最后将保存任务的mActions置为null,因为后续View.post()直接添加到AttachInfo内部的Handler 。注意,调用executeActions执行runnable的地方正是在dispatchAttachedToWindow方法里。

现在清楚了:打开一个activity时,如果调用post方法时还没有开始执行dispatchAttachedToWindow就先调用getRunQueue().post(action)方法将runnable先缓存起来,当执行到dispatchAttachedToWindow时再执行handler的post;如果到达就直接执行handler.post。

 

继续向下分析:

同一个View Hierachy树结构中所有View共用一个 AttachInfo,AttachInfo的创建是在ViewRootImpl 的构造方法中:

ViewRootImpl.java:

mAttachInfo = new View.AttachInfo( mWindowSession, mWindow, display, this, mHandler, this, context);

一般Activity包含多个View形成View Hierachy的树形结构,只有最顶层的DecorView才是对 WindowManagerService “可见的”。

view的dispatchAttachedToWindow()的调用时机是在View绘制流程的开始阶段。在ViewRootImpl 的performTraversals方法,在该方法将会依次完成View绘制流程的三大阶段:测量、布局和绘制。

// View绘制流程开始在ViewRootImpl

private void performTraversals() {

    final View host = mView; //mView是DecorView

    if (mFirst) {

        .....

        // host为DecorView,调用DecorVIew的 dispatchAttachedToWindow,并且把mAttachInfo给子view

       host.dispatchAttachedToWindow( mAttachInfo, 0);

        mAttachInfo.mTreeObserver. dispatchOnWindowAttachedChange(true);

        dispatchApplyInsets(host);

        .....

    } 

   mFirst=false

   ...

   // Execute enqueued actions on every traversal in case a detached view enqueued an action

   getRunQueue().executeActions( mAttachInfo.mHandler);

   performMeasure(); // View绘制流程的测量阶段

   performLayout();  // View绘制流程的布局阶段

   performDraw(); // View绘制流程的绘制阶段

   ...

}

host的实际类型是DecorView,DecorView继承自FrameLayout。

每个Activity都有一个关联的Window对象,用来描述应用程序窗口,每个窗口内部又包含一个DecorView对象,DecorView对象用来描述窗口的视图 — xml布局。通过setContentView()设置的View布局最终添加到DecorView的content容器中。

跟踪DecorView的dispatchAttachedToWindow方法的执行过程,DecorView并没有重写该方法,而是在其父类ViewGroup中:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    mGroupFlags |= FLAG_PREVENT_DISPATCH_ ATTACHED_TO_WINDOW;

    super.dispatchAttachedToWindow(info, visibility);

    mGroupFlags &= ~FLAG_PREVENT_DISPATCH _ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;//子View数量

    final View[] children = mChildren;  

    for (int i = 0; i < count; i++) { //遍历所有子View

        final View child = children[i];

        //遍历调用所有子View的 dispatchAttachedToWindow方法,为每个子View关联AttachInfo

        child.dispatchAttachedToWindow(info, combineVisibility(visibility, child.getVisibility()));

    }

    // ...

}

for循环遍历当前ViewGroup的所有childView,为其关联AttachInfo。子View的 dispatchAttachedToWindow方法在前面已经分析过了:首先为当前View关联AttachInfo,然后将之前View.post()保存的任务添加到AttachInfo内部的Handler。

注意回到ViewRootImpl的performTraversals方法,咋一看,这个过程好像没有太多新奇的地方。不过你是否注意到这一过程是在View的绘制任务中。

通过View.post()添加的任务,是在View绘制流程的开始阶段,将所有任务重新发送到消息队列的尾部,此时相关任务的执行已经在View绘制任务之后,即View绘制流程已经结束,此时便可以正确获取到 View 的宽高了。

View.post()添加的任务能够保证在所有View(同一个View Hierachy内)绘制流程结束之后才被执行。

 

碎片化问题来了,如果只是创建一个View,调用它的post方法,它会不会被执行呢?比如:

final ImageView view = new ImageView(this);

view.post(new Runnable() {

    @Override

    public void run() {

        // do something

    }

});

答案是否定的,因为它没有添加到窗口视图,不会走绘制流程,自然也就不会被执行。此时只需要添加如下代码即可:

// 将View添加到窗口,此时重新发起绘制流程,post任务会被执行

contentView.addView(view);

不过该问题在API Level 24之前不会发生,看下之前的代码实现:

// API Level 24之前的post实现

public boolean post(Runnable action) {

    // 这里的逻辑与API Level 24及以后一致

    final AttachInfo attachInfo = mAttachInfo;

    if (attachInfo != null) {

        return attachInfo.mHandler.post(action);

    }

    // 主要是这里,此时管理待执行的任务直接交给了ViewRootImpl中。 而在API Level 24及以后,每个View自行维护待执行任务队列, 所以如果View不添加到Window视图,dispatchAttachedToWindow不会被调用,View中的post任务将永远得不到执行

    ViewRootImpl.getRunQueue().post(action);

    return true;

}

在API Level 24之前,通过View.post()任务被直接添加到ViewRootImpl中,在24及以后,每个View自行维护待执行的post()任务,它们要依赖于 dispatchAttachedToWindow方法,如果View未添加到窗口视图,post()添加的任务将永远得不到执行。

这样的碎片化问题在Android中可能数不胜数,这也告诫我们如果对某项功能点了解的不够充分,最后可能导致程序未按照意愿执行。

至此,View.post()的原理就算搞清楚了,不过还是有必要跟踪下AttachInfo的释放过程。

mAttachInfo置null 的过程:

先看下表示DecorView的 dispatchDetachedFromWindow方法,实际是调用其父类ViewGroup中:

ViewGroup.java:

void dispatchDetachedFromWindow() {

    // ... …

    final int count = mChildrenCount;

    final View[] children = mChildren;

    for (int i = 0; i < count; i++) { //遍历所有子view

        //通知childView的 dispatchDetachedFromWindow

        children[i].dispatchDetachedFromWindow();

    }

    // ... …

    super.dispatchDetachedFromWindow();

}

ViewGroup的dispatchDetachedFromWindow 方法会遍历所有childView。

View.java:

void dispatchDetachedFromWindow() {

    AttachInfo info = mAttachInfo;

    if (info != null) {

        int vis = info.mWindowVisibility;

        if (vis != GONE) {

            // 通知 Window显示状态发生变化

            onWindowVisibilityChanged(GONE);

            if (isShown()) {

                onVisibilityAggregated(false);

            }

        }

    }

    // 回调View的onDetachedFromWindow

    onDetachedFromWindow();

    onDetachedFromWindowInternal();

    // ... …

    ListenerInfo li = mListenerInfo;

    final CopyOnWriteArrayList< OnAttachStateChangeListener> listeners = li != null ? li.mOnAttachStateChangeListeners : null;

    if (listeners != null && listeners.size() > 0) {

        // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();

        for (OnAttachStateChangeListener listener : listeners) {

            // 通知回调 onViewDetachedFromWindow

            listener.onViewDetachedFromWindow( this);

        }

    }

    // ... …

    // 将AttachInfo置为null

    mAttachInfo = null;

    if (mOverlay != null) {

        // 通知浮层View

        mOverlay.getOverlayView(). dispatchDetachedFromWindow();

    }

    notifyEnterOrExitForAutoFillIfNeeded(false);

}

可以看到在dispatchDetachedFromWindow方法,首先回调View的onDetachedFromWindow(),然后通知所有监听者onViewDetachedFromWindow(),最后将 mAttachInfo置为null。

由于dispatchAttachedToWindow方法是在ViewRootImpl中完成,此时很容易想到它的释放过程肯定也在ViewRootImpl,跟踪发现如下调用过程:

void doDie() {

    checkThread(); // 检查执行线程

    synchronized (this) {

        if (mRemoved) {

            return;

        }

        mRemoved = true;

        if (mAdded) {

            // 回调View的dispatchDetachedFromWindow

            dispatchDetachedFromWindow();

        }

        if (mAdded && !mFirst) {

            destroyHardwareRenderer();  

            if (mView != null) {  // mView是DecorView

                int viewVisibility = mView.getVisibility();

                // 窗口状态是否发生变化

                boolean viewVisibilityChanged = mViewVisibility != viewVisibility;

                if (mWindowAttributesChanged || viewVisibilityChanged) {

                    try {

                        if ((relayoutWindow( mWindowAttributes, viewVisibility, false) &    WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {

                            mWindowSession.finishDrawing( mWindow);

                        }

                    } catch (RemoteException e) {

                    }

                }

                // 释放画布

                mSurface.release();

            }

        }

        mAdded = false;

    }

    // 将其从WindowManagerGlobal中移除

    // 移除DecorView

    // 移除DecorView对应的ViewRootImpl

    // 移除DecorView

    WindowManagerGlobal.getInstance(). doRemoveView(this);

}

可以看到dispatchDetachedFromWindow方法被调用,注意方法最后将ViewRootImpl从WindowManager中移除。

经过前面的分析已经知道AttachInfo的赋值 *** 作是在View绘制任务的开始阶段,而它的调用者是 ActivityThread的handleResumeActivity方法,即Activity生命周期onResume方法之后。那它是在Activity的哪个生命周期阶段被释放的呢?在Android中,Window是View的容器,而WindowManager则负责管理这些窗口。因此直接找到管理应用进程窗口的 WindowManagerGlobal,查看DecorView的移除工作:

//将DecorView从WindowManager中移除

public void removeView(View view, boolean immediate) {

    if (view == null) {

        throw new IllegalArgumentException("view must not be null");

    }

    synchronized (mLock) {

        // 找到保存该DecorView的下标,true表示找不到要抛出异常

        int index = findViewLocked(view, true);

       //找到对应的ViewRootImpl,内部的DecorView

       View curView = mRoots.get(index).getView();

        // 从WindowManager中移除该DecorView,immediate 表示是否立即移除

        removeViewLocked(index, immediate);

        if (curView == view) {

            // 判断要移除的与WindowManager中保存的是否为同一个

            return;

        }

        //如果不是同一个View(DecorView),抛异常

        throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView);

    }

}

根据要移除的DecorView找到在WindowManager中保存的ViewRootImpl,真正移除是在removeViewLocked方法:

private void removeViewLocked(int index, boolean immediate) {

    // 找到对应的ViewRootImpl

    ViewRootImpl root = mRoots.get(index);

    // 该View是DecorView

    View view = root.getView();

    // ... …

    // 调用ViewRootImpl的die,并且将当前ViewRootImpl在WindowManagerGlobal中移除

    boolean deferred = root.die(immediate);

    if (view != null) {

        // 断开DecorView与ViewRootImpl的关联

        view.assignParent(null);

        if (deferred) {

            //返回true表示延迟移除,加入待死亡队列

            mDyingViews.add(view);

        }

    }

}

可以看到调用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:

boolean die(boolean immediate) {

    // immediate表示立即执行,mIsInTraversal表示是否正在执行绘制任务

    if (immediate && !mIsInTraversal) {

        // 内部调用了View的dispatchDetachedFromWindow

        doDie();

        // return false 表示已经执行完成

        return false;

    }

    if (!mIsDrawing) {

        // 释放硬件加速绘制

        destroyHardwareRenderer();

    } 

    // 如果正在执行遍历绘制任务,此时需要等待遍历任务完成

    // 故发送消息到尾部

    mHandler.sendEmptyMessage(MSG_DIE);

    return true;

}

注意doDie方法(源码在前面已经贴出),它最终会调用 dispatchDetachedFromWindow 方法。

最后,移除 Window 窗口任务是通过 ActivityThread 完成的,具体调用在 handleDestoryActivity 方法完成:

private void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance) {

    // 回调 Activity 的 onDestory 方法

    ActivityClientRecord r = performDestroyActivity (token, finishing, configChanges, getNonConfigInstance);

    if (r != null) {

        cleanUpPendingRemoveWindows(r, finishing);

        // 获取当前Window的WindowManager, 实际是WindowManagerImpl

        WindowManager wm = r.activity.getWindowManager();

        // 当前Window的DecorView

        View v = r.activity.mDecor;

        if (v != null) {

            if (r.activity.mVisibleFromServer) {

                mNumVisibleActivities--;

            }

            IBinder wtoken = v.getWindowToken();

            // Window 是否添加过,到WindowManager

            if (r.activity.mWindowAdded) {

                if (r.mPreserveWindow) {

                    r.mPendingRemoveWindow = r.window;

                    r.mPendingRemoveWindowManager = wm;

                    r.window.clearContentView();

                } else {

                    // 通知 WindowManager,移除当前 Window窗口

                    wm.removeViewImmediate(v);

                }

            }

performDestoryActivity()将完成Activity生命周期onDestory方法回调。然后调用WindowManager的removeViewImmediate():

WindowManagerImpl.java:

@Override

public void removeViewImmediate(View view) {

    //调用WindowManagerGlobal的removeView方法

    mGlobal.removeView(view, true);

}

即AttachInfo的释放 *** 作是在Activity生命周期onDestory方法之后,在整个Activity的生命周期内都可以正常使用View.post()任务。

 

3.总结

①关于View.post()要注意在 API Level 24 前后的版本差异,不过该问题也不用过于担心,试想,会有哪些业务场景需要创建一个 View 却不把它添加到窗口视图呢?

②View.post()任务能够保证在所有View绘制流程结束之后被调用,故如果需要依赖View绘制任务,此时可以优先考虑使用该机制。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/794032.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-06
下一篇 2022-05-06

发表评论

登录后才能评论

评论列表(0条)

保存