通过View.post()获取View的宽高引发的两个问题:1post的Runnable何时被执行,2为何View需要layout两次;以及发现Android的一个小bug

来源:互联网 发布:idc源码 编辑:程序博客网 时间:2024/06/05 21:55

前言
在Android里,获取View宽高的时机是个老生常谈的话题了。众所周知,在Oncreate里直接调用View.getWidth或者View.getMeasuredWidth返回都是0。所以获取宽高时机很重要,对于这个问题的解决方法概括起来有四种之多,具体可以看看任玉刚老师的《Android开发艺术探索》中的View章节或者网上也有非常丰富的资料。
而本文主要讨论的是其中的一个解决方法。

View.post()获取View宽高
这个方法相信大家都很熟悉了,就是这样:

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mTextView = (TextView) findViewById(R.id.my_text);        // 下面这一行log打印的是0,0        Log.d("test", "mTextView width : " + mTextView.getMeasuredWidth() + " - height : " + mTextView.getMeasuredHeight());        mTextView.post(new Runnable() {            @Override            public void run() {                // 下面这一行log打印的是TextView测量后的宽高                Log.d("test", "mTextView width : " + mTextView.getMeasuredWidth() + " - height : " + mTextView.getMeasuredHeight());            }        });    }

这个方法我们都用到烂了,但是用的时候不会有疑惑吗?我是非常疑惑,主要是:
1、执行View.post()的时候,此时View是已经开始被meaure?还是在meaure之前执行呢?如果在meaure之前执行,而post又没有进行延时,那么这个Runnable又是如何在被放在测量之后再执行的呢?

2、这个方法得到的结果是否真的百分百准确呢??

下面就围绕这两个疑问进行研究。

View.post()的执行流程
以前我一直以为要用View.postDelayed(new Runnable(), 200);延时两三百毫秒才能获取要宽高,原因就是上面的疑问一,我总觉得如果马上post的话,它根本还没有被measure,所以是不会有测量后的宽高值。但是现在的结果明显表明我的想法的错误的。那么究竟如何保证在View被measure之后再去执行Runnable,然后获取到正确的宽高呢?我们从源码方向入手(本文的源码是Android4.4.2的)。

首先View.post():

    /**     * <p>Causes the Runnable to be added to the message queue.     * The runnable will be run on the user interface thread.</p>     *     * @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.     *     * @see #postDelayed     * @see #removeCallbacks     */    public boolean post(Runnable action) {        final AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            return attachInfo.mHandler.post(action);         ①        }        // Assume that post will succeed later        ViewRootImpl.getRunQueue().post(action);        ②        return true;    }

如果mAttachInfo不为null的时候,会执行①;否则会执行②;
所以现在问题是究竟这个时候View中mAttachInfo是否为null?答案是此时(onCreate的时候)它确实为null。下面验证这个说法。

View中能给mAttachInfo赋值的地方只有一处,在dispatchAttachedToWindow()方法里赋值:

/**     * @param info the {@link android.view.View.AttachInfo} to associated with     *        this view     */    void dispatchAttachedToWindow(AttachInfo info, int visibility) {        //System.out.println("Attached! " + this);        mAttachInfo = info;        ①        if (mOverlay != null) {            mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);        }        mWindowAttachCount++;        // We will need to evaluate the drawable state at least once.        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;        if (mFloatingTreeObserver != null) {            info.mTreeObserver.merge(mFloatingTreeObserver);            mFloatingTreeObserver = null;        }        if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {            mAttachInfo.mScrollContainers.add(this);            mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;        }        performCollectViewAttributes(mAttachInfo, visibility);        onAttachedToWindow();        ②         ListenerInfo li = mListenerInfo;        final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =                li != null ? li.mOnAttachStateChangeListeners : null;        if (listeners != null && listeners.size() > 0) {            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to            // perform the dispatching. The iterator is a safe guard against listeners that            // could mutate the list by calling the various add/remove methods. This prevents            // the array from being modified while we iterate it.            for (OnAttachStateChangeListener listener : listeners) {                listener.onViewAttachedToWindow(this);            }        }        int vis = info.mWindowVisibility;        if (vis != GONE) {            onWindowVisibilityChanged(vis);        }        if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {            // If nobody has evaluated the drawable state yet, then do it now.            refreshDrawableState();        }        needGlobalAttributesUpdate(false);    }

可以看到,在①处给mAttachInfo赋值了,并且后面②的地方调用了onAttachedToWindow()方法,所以我们可以在onAttachedToWindow打印log,来验证在onCreate处调用view.post()时,view的mAttachInfo是否为null。

下面我用个超级简单的例子证明:
我的MainActivity:

public class MainActivity extends Activity {    private MyTextView mMyTextView = null;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mMyTextView = (MyTextView) findViewById(R.id.my_textView);        Log.d("test", "1 post前 : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());        mMyTextView.post(new Runnable() {            @Override            public void run() {                Log.d("test", "3 post Runnable : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());            }        });        Log.d("test", "2 post后 : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());    }}

我的activity_main.xml:

<com.example.viewposttest2.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.example.viewposttest2.MainActivity"     android:background="@drawable/ic_launcher">    <com.example.viewposttest2.MyTextView        android:id="@+id/my_textView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="@string/hello_world" /></com.example.viewposttest2.MyRelativeLayout>

MyRelativeLayout:

public class MyRelativeLayout extends RelativeLayout {    private static String TAG = "test MyRelativeLayout";    public MyRelativeLayout(Context context, AttributeSet attrs,            int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    public MyRelativeLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    public MyRelativeLayout(Context context) {        super(context);    }    @Override    protected void onAttachedToWindow() {        Log.d(TAG, "onAttachedToWindow");        super.onAttachedToWindow();    }}

MyTextView:

public class MyTextView extends TextView {    private static String TAG = "test MyTextView";    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    public MyTextView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public MyTextView(Context context) {        super(context);    }    @Override    protected void onAttachedToWindow() {        Log.d(TAG, "onAttachedToWindow");        super.onAttachedToWindow();    }}

以上就是全部代码,里面除了打印一些log,基本上什么都没做。然后我们运行程序,可以看到下面的log:
这里写图片描述

可以看到这里,Runnable已经被post之后才执行onAttachToWindow,按照上面的源码分析,在执行post()的时候,mAttachInfo是为null的,然后在执行onAttachToWindow()之后,mAttachInfo才被赋值。
如果不相信的话,可以自行用反射的方法去把View的mAttachInfo读出来验证下。

回到源码继续分析,所以按照上面所说,那么post时:

    public boolean post(Runnable action) {        final AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            return attachInfo.mHandler.post(action);        }        // Assume that post will succeed later        ViewRootImpl.getRunQueue().post(action);        return true;    }

这个时候将会执行 ViewRootImpl.getRunQueue().post(action);这行代码。继续跟踪过去,首先是getRunQueue():

    static RunQueue getRunQueue() {        RunQueue rq = sRunQueues.get();        if (rq != null) {            return rq;        }        rq = new RunQueue();        sRunQueues.set(rq);        return rq;    }

这个方法先不分析,后面会单独说明,因为这里发现了Android的一个小bug。这个方法的作用就是返回一个RunQueue类,这个类的作用是当所有View还没attach之前,保存那些将要被执行的Runnable,这里有源码及其注释:

/**     * The run queue is used to enqueue pending work from Views when no Handler is     * attached.  The work is executed during the next call to performTraversals on     * the thread.     * @hide     */    static final class RunQueue {        private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();        void post(Runnable action) {            postDelayed(action, 0);        }        void postDelayed(Runnable action, long delayMillis) {            HandlerAction handlerAction = new HandlerAction();            handlerAction.action = action;            handlerAction.delay = delayMillis;            synchronized (mActions) {                mActions.add(handlerAction);            }        }        void removeCallbacks(Runnable action) {            final HandlerAction handlerAction = new HandlerAction();            handlerAction.action = action;            synchronized (mActions) {                final ArrayList<HandlerAction> actions = mActions;                while (actions.remove(handlerAction)) {                    // Keep going                }            }        }        void executeActions(Handler handler) {            synchronized (mActions) {                final ArrayList<HandlerAction> actions = mActions;                final int count = actions.size();                for (int i = 0; i < count; i++) {                    final HandlerAction handlerAction = actions.get(i);                    handler.postDelayed(handlerAction.action, handlerAction.delay);                }                actions.clear();            }        }        private static class HandlerAction {            Runnable action;            long delay;            @Override            public boolean equals(Object o) {                if (this == o) return true;                if (o == null || getClass() != o.getClass()) return false;                HandlerAction that = (HandlerAction) o;                return !(action != null ? !action.equals(that.action) : that.action != null);            }            @Override            public int hashCode() {                int result = action != null ? action.hashCode() : 0;                result = 31 * result + (int) (delay ^ (delay >>> 32));                return result;            }        }    }

所以在onCreate执行View.post()的方法时,那些Runnable并没有马上被执行,而是保存到RunQueue里面。那么它们在什么时候被执行呢?下面继续分析。

执行的接口就是RunQueue.executeActions,其内部也看到是调用Handler执行的,RunQueue.executeActions()这个接口在整个ViewRootImpl里只有一个地方调用,就是在performTraversals():

 private void performTraversals() {        // cache mView since it is used so much below...        final View host = mView;    // 这里面做了一些初始化的操作,第一次执行和后面执行的操作不一样,这里不关    // 心过多的东西,主要关心attachInfo在此处被初始化完成        // Execute enqueued actions on every traversal in case a detached view enqueued an action        getRunQueue().executeActions(attachInfo.mHandler);    ...    performMeasure();    ...    performLayout();    ...    performDraw(); }

performTraversals()相信大家都听过,它非常重要,它的作用就是遍历整个View树,并且按照要求进行measure,layout和draw流程,仅仅这一个方法就有800多行的代码。

刚才的post的Runnable就是在这里被执行了!但是你们也看到,它是先执行Runnable后才进行measure,layout和draw流程。(注意:这里所说的执行是指RunQueue.executeActions()用这个方法执行)假设按顺序执行的话,此时得到的VIew宽高肯定也是0、0,因为measure流程还没开始,但是结果明显不是,那么它的执行的顺序是如何保证呢?

我们再看回头看看RunQueue.executeActions()这个方法:

        void executeActions(Handler handler) {            synchronized (mActions) {                final ArrayList<HandlerAction> actions = mActions;                final int count = actions.size();                for (int i = 0; i < count; i++) {                    final HandlerAction handlerAction = actions.get(i);                    handler.postDelayed(handlerAction.action, handlerAction.delay);                }                actions.clear();            }        }

这里面其实也是调用Handler去post我们的Runnable,而ViewRootImpl的Handler就是主线程的Handler,因此在performTraversals()被执行的Runnable事实上是被主线程的Handler的post到它的执行队列里面了

这里需要说明下,Android的运行其实是一个消息驱动模式,可以看看ActivityThread的main函数源码,每个应用程序启动第一个执行的方法就是这个main方法,具体可以看看我另一篇文章《源码分析Android 应用进程的启动过程 》:

    public static void main(String[] args) {        SamplingProfilerIntegration.start();        // CloseGuard defaults to true and can be quite spammy.  We        // disable it here, but selectively enable it later (via        // StrictMode) on debug builds, but using DropBox, not logs.        CloseGuard.setEnabled(false);        Environment.initForCurrentUser();        // Set the reporter for event logging in libcore        EventLogger.setReporter(new EventLoggingReporter());        Security.addProvider(new AndroidKeyStoreProvider());        Process.setArgV0("<pre-initialized>");        Looper.prepareMainLooper();        ActivityThread thread = new ActivityThread();        thread.attach(false);        if (sMainThreadHandler == null) {            sMainThreadHandler = thread.getHandler();        }        AsyncTask.init();        if (false) {            Looper.myLooper().setMessageLogging(new                    LogPrinter(Log.DEBUG, "ActivityThread"));        }        Looper.loop();        throw new RuntimeException("Main thread loop unexpectedly exited");    }

可以看到这里创建了主线程的Handler和Looper,并且最后执行了Looper.loop()进入循环。它一直在等待消息去驱动它继续执行下去。而像我们post的那个Runnable就是消息的一种了。

所以此时我们需要等待主线程的Handler执行完当前的任务,就会去执行我们post的那个Runnable。那么当前正在执行了什么任务呢?正如我刚才所说的,Android是消息驱动,所以它执行performTraversals()其实是由另一个消息去驱动执行了,而这个消息是TraversalRunnable,具体可以看ViewRootImpl的源码,里面有TraversalRunnable的定义。

因此,这个时候Handler正在执行着TraversalRunnable这个Runnable,而我们post的Runnable要等待TraversalRunnable执行完才会去执行,而TraversalRunnable这里面又会进行measure,layout和draw流程,所以等到执行我们的Runnable时,此时的View就已经被measure过了,所以获取到的宽高就是measure过后的宽高。

RunQueue里发现Android的小bug
这里简单的说说刚才没说的RunQueue,回到ViewRootImpl.getRunQueue()方法的源码:

    static RunQueue getRunQueue() {        RunQueue rq = sRunQueues.get();        if (rq != null) {            return rq;        }        rq = new RunQueue();        sRunQueues.set(rq);        return rq;    }

这里首先会去sRunQueues拿之前保存的RunQueue实例,可是看看sRunQueues的类型,你会知道哪里出问题了:

static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

这里可以看到sRunQueues是个ThreadLocal类型(关于ThreadLocal可以自行度娘),我们可以百分百确定performTraversals的执行是在主线程执行的,那么这行代码

getRunQueue().executeActions(attachInfo.mHandler);

肯定也在主线程执行,这样问题就出现了。
如果我们在onCreate里在主线程调用view.post(),那么这个Runnable会被正常执行;但是如果我们在子线程调用view.post(),那么Runnable很有可能就没有被执行了。原因看上面的代码就知道了。

下面我们简单验证一下,修改下MainActivity的代码:

public class MainActivity extends Activity {    private MyTextView mMyTextView = null;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        mMyTextView = (MyTextView) findViewById(R.id.my_textView);        Log.d("test", "1 post前 : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());        new Thread(new Runnable() {            @Override            public void run() {                Log.d("test", "Thread 被执行了");                mMyTextView.post(new Runnable() {                    @Override                    public void run() {                        Log.d("test", "3 post Runnable : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());                    }                });            }        }).start();        Log.d("test", "2 post后 : " + mMyTextView.getMeasuredWidth() + " - height : " +  mMyTextView.getMeasuredHeight());    }

这里仅仅把post放在子线程调用,然后我们看看log日志:
这里写图片描述

果不其然,我们的子线程被执行了,但是里面的post没有被执行。
我们看看post源码注释:

    /**     * <p>Causes the Runnable to be added to the message queue.     * The runnable will be run on the user interface thread.</p>     *     * @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.     *     * @see #postDelayed     * @see #removeCallbacks     */    public boolean post(Runnable action) {        final AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            return attachInfo.mHandler.post(action);        }        // Assume that post will succeed later        ViewRootImpl.getRunQueue().post(action);        return true;    }

注释只是说任务会在主线程执行,但并没有让我们注意什么不友好的操作,所以这个应该是android的bug。可能有人注意到这个方法是有返回值告诉我们是否成功加入执行队列,但是你们可以自行写一遍代码打印看看,这里是返回true的。

最后关于这个bug需要说明的是:
不是不可以在子线程中调用View.post,大家千万误会。相反的,这个方法的作用就是可以让我们在子线程中把任务放到主线程中执行。但是要注意,要在View.onAttachToWindow之后在执行;否则,例如在onCreate里的子线程执行的话,那么这个Runnable将永远不会被执行了。

View.post里获取到的View宽高是否准确?
关于这个问题,我还完全分析出来,至少现在多次试验来说的准确的,但不能完全保证它的准确度。为什么这么说呢?我说明下。

由于这篇文章已经写了很长了,下面尽量简单说明情况。

首先,如果你在刚才的例子了,打印自定义View的measure,layout的日志,你会发现一个神奇的问题,下面是我的打印的log:
这里写图片描述
注意到了吗?measure和layout过程被执行了两次,中间穿插了post的Runnable的执行结果,然后在第二次的layout之后才会去执行draw流程!

关于这点,网上有好多说法,感觉大部分都是错的,我也研究了很久,也没完全理解,我把我研究到的部分说说。

首先,从上面的分析,可以知道,第一次layout和第二次layout是两个不同的任务,因为中间穿插了post的Runnable的执行结果,刚才我也说了这些都是在Handler排队执行的,所以这里有三个任务,第一次performTraversals,我们的Runnable和第二次performTraversals三个任务。那么为什么会执行两次performTraversals呢?

我的调研结果如下:
在第一次执行performTraversals时,分配给该Activity的窗口的显存还没分配好,这个显存在ViewRootImpl有个类来管理:Surface类,关于这个类可以看看这篇文章http://mobile.51cto.com/android-259922_all.htm ,不过文章比较旧。

然后在执行performTraversals时,初始化mSurface,分配了内存,这时才能在上面绘制东西,展示界面出来。第一次初始化mSurface后,需要重新把之前的东西绘制在上面,所以需要重新执行一遍performTraversals,下面是源码的部分注释:

                        // If we are creating a new surface, then we need to                        // completely redraw it.  Also, when we get to the                        // point of drawing it we will hold off and schedule                        // a new traversal instead.  This is so we can tell the                        // window manager about all of the windows being displayed                        // before actually drawing them, so it can display then                        // all at once.

因此会执行了两遍performTraversals。但是问题不在于此,需要说明的是初始化mSurface其实是在measure,layout之前就理应搞好了,那么后续measure,layout执行一遍后为何还要执行一遍呢?这点我不理解。

还有一点:performTraversals并非每次都会重新执行measure,layout和draw过程的,ViewRootImpl会判断是否需要全部重新一遍或者仅仅执行某一部分(当然有measure过程必定会有layout过程),如果没有必要重新来,那么就可以省下那些步骤。那么刚才的log也看到,ViewRootImpl认为第二次需要重新再来一遍measure,layout和draw过程,那么也就是说ViewRootImpl认为第一次测量的结果可能不准确。

因此对于“View.post里获取到的View宽高是否准确”的问题,不能给出一个正确的答案,要解答这个问题,首先要理解透performTraversals和为什么performTraversals需要执行两次。关于上述的问题,如果有自己的理解的可以回复一起讨论一下,谢谢!

目前来说,通过大量的测试,发现这个方法的结果还是准确的,如果你遇到不准确的情况也欢迎一起讨论。

总结:
Android是消息驱动的模式,View.post的Runnable任务会被加入任务队列,并且等待第一次TraversalRunnable执行结束后才执行,此时已经执行过一次measure,layout过程了,所以在后面执行post的Runnable时,已经有measure的结果,因此此时可以获取到View的宽高。

2 0