onCreate中獲取View的寬高為0?

開發的過程中,我們有時候需要在Activity啟動以后第一時間獲取某個View的寬高,并作相應處理,當我們在onCreate中通過View.getWidth(或getMeasuredWidth)和View.getHeight(或getMeasuredHeight)方法獲取的時候,你會發現它們都返回0。我們猜測是因為,這個時候View還沒有布局完成。解決的辦法有很多種,比如在ViewOnGlobalLayoutListener中獲取,在onLayout中獲取等等,具體請參考StackOverflow:
https://stackoverflow.com/questions/18861585/get-content-view-size-in-oncreate

有另外一個方法,有些人應該也知道,在onCreate中通過View.post,在其中的Runnable中就能獲取到正確的寬高。那么,這里的原理是什么,很多人認為這里就是一個時間差,等Viewmeasurelayout完了就有寬和高了,有些人甚至使用postDelay延遲幾秒來確保萬無一失。那么這里面到底會不會有不穩定的因素呢?這篇文章告訴你答案。

本篇文章分析的是Android 6.0系統的源碼。閱讀之前,請先了解Android中HandlerLooperMessageQueue等概念,推薦文章:http://blog.csdn.net/lmj623565791/article/details/38377229/

詳細分析:

請看View.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;
}

onCreate的方法中執行的時候,attachInfo==null(為什么?請自行驗證),所以post方法執行到了

ViewRootImpl.getRunQueue().post(action)。

這個方法是將action先保存到ViewRootImpl中的一個靜態隊列中,保存起來什么時候用呢?

ViewRootImpl.perfromTravesals方法中可以看到這個隊列調用的代碼,

// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();

大家都知道performTraversals是布局遍歷的方法,所以我們知道了,post的Runnable是在布局遍歷的時候執行的。然而,它是在performMeasure、performLayout之前執行的。也就是說,這個Runnable是在View的測量和布局之前執行。View測量和布局完成之前是獲取不到寬高的,那我們的Runnable是怎么獲取到寬高的呢。

事實上,getRunQueue().executeActions(mAttachInfo.mHandler) 也不是直接執行這些Runnable,而是往主線程消息隊列添加對應的消息進去,那么我們的這個消息執行的時機是怎么保證在View布局完成之后呢?研究過performTraversals源碼的話,應該知道,performTraversals這個方法也是被附到一個消息上添加到消息隊列等待執行的。下面看分析。

首先從View布局的終極方法performTraversals開始分析,這個方法到底是怎么執行的,在哪執行的。一路追過去,

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

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

        performTraversals();

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

perfromTraversalsdoTraversal調用,

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal方法在TraversalRunnable調用,那么這個Runnable的對象在哪執行的呢?

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

可以看到,mTraversalRunnable出現在這個方法scheduleTraversals里:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }

這里面mTraversalRunnable也是被post了,這個post方法叫做postCallback,猜測是不是也是加到消息隊列去了?

繼續看mChoreographer.postCallback方法,這個方法最終會到下面這個方法中:

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }

    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

注意到熟悉的一句代碼:mHandler.sendMessageAtTime,這個方法就是將消息加入到消息隊列,而這個Handler對應的消息隊列是什么?
我們從 mHandler這個對象著手,看看它是怎么來的。

private Choreographer(Looper looper) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;

    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

在這個構造函數中,可以看到mHandler構造的時候傳入一個Looper對象,那就要看下這個Looper是從哪里來的。

// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        return new Choreographer(looper);
    }
};

這里又看到熟悉的代碼了Looper looper = Looper.myLooper(); 獲取了當前線程(主線程)的Looper對象。

所以,performTraversals方法實際上也是被加入到消息隊列中去執行的,而我們最初post的Runnable,是在performTraversals方法中將其附加到一個消息上并加入到消息隊列里。所以,我們post的Runnable的消息,肯定是在performTraversals的消息之后執行的,也就是我們post的Runnable肯定是在performTraversals之后執行,而performTraversals之后,View的寬和高便計算出來了。

需要注意的是, 在onCreate(以及onStart, onResume)方法中必須保證是在主線程調用View.post方法,因為ViewRootImpl.getRunQueue()在不同線程獲取到的是不同的對象(為什么?請研究getRunQueue()的類型),所以在onCreate方法內新建線程并在里面post的時候,這個post是成功不了的。在Android 7.0中修復了這個問題,也就說7.0系統中,在onCreate方法中新建線程并在里面post,這個Runnable是可以被執行到的。感興趣的看源碼。

一句話總結:

onCreate中調用View.post的時候,post的Runnable被保存起來,在下一次遍歷布局的時候重新加到消息隊列里(Android 7.0是在attach window的時候重新加到消息隊列里),而且布局遍歷本身也是加到消息隊列執行的,所以我們post的Runnable所在的消息肯定是在布局遍歷之后,所以在Runnable里必定可以獲取到正確的寬高。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容