天天看點

View.post()為什麼能準确拿到View的寬高

起因:之前一群裡的哥們問

Handler.post()

為什麼會在

Activity

onResume()

之後執行,我找了一遍之後并沒有找到原因,後來從這個問題我想起其他的問題

view.post()

為什麼在

view.post()

之後為什麼可以準确的擷取到

view

的寬高。

疑問🤔️:View.post()為什麼會準确的擷取到View的寬高?

public boolean post(Runnable action) {
    //注釋1:判斷attachInfo如果不為空 直接調用attachInfo内部Handler.post()方法
        //這樣就有一個問題attachInfo在哪裡指派?這個問題先存疑。
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        //注釋2:此時attachInfo是空的
        getRunQueue().post(action);
        return true;
    }
複制代碼      

存疑1:attachInfo在哪裡指派?

我們點進去看一下

getRunQueue().post(action);

是何方神聖。

private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
複制代碼      

上面代碼很簡單,那麼接下來就需要看看

HandlerActionQueue()

是什麼玩意了。

public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;
   //注釋1
    public void post(Runnable action) {
        postDelayed(action, 0);
    }
   //注釋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++;
        }
    }
    public void removeCallbacks(Runnable action) {}
   //假裝不知道這個是執行方法
    public void executeActions(Handler handler) {}
    public int size() {}
    public Runnable getRunnable(int index) {}
    public long getDelay(int index) {}
    private static class HandlerAction {}
}
複制代碼      

先看

注釋1

可以看到這個

post()

方法 其實就是

getRunQueue().post(action);

它内部調用了

注釋2

方法 也就是

postDelayed()

注釋2

簡單看了裡面的代碼 發現大概邏輯隻是對 我們

post

runnable

進行緩存起來,那麼我們從哪裡真正執行這個

runnable

呢? 我發現 我們一路跟進來直到

postDelayed()

方法時并沒有看到對應的執行方法那麼我們就放大招了

影·奧義!時光回溯。

我們看看

getRunQueue()

時 傳回的mRunQueue在哪裡調用的就好了。

//不會上傳圖檔...大小不懂得控制 是以用代碼塊 
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
   void dispatchAttachedToWindow(AttachInfo info, int visibility) {
           mAttachInfo = info;
   //忽略的一些代碼
    if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
   }
複制代碼      

通過檢查對

mRunQueue

的引用 可以看到

view

代碼内隻有這兩個方法有所使用,那麼第一個方法我們以及看過了 是以我們重點看下第二個方法。

從上面第二個方法 可以得知

mAttachInfo

dispatchAttachedToWindow(AttachInfo info, int visibility)

内的

info

指派 那麼

info

又在哪裡生成?先繼續看下去。

通過對

dispatchAttachedToWindow()

的調用關系可以發現以下方法會調用

private void performTraversals() {
    // cache mView since it is used so much below...
        final View host = mView;
    /.../
        //注釋1
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    /.../
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//執行測量
            performLayout(lp, mWidth, mHeight);//執行布局
            performDraw();//執行繪制
 }
複制代碼      

可以看到

host

内會調用這個方法,并且将

mAttachInfo

作為參數傳入,而這個

host

是一個

DecorView

為什麼是

DecorView

? 我們可以反推回去驗證 通過源碼我們得知

host

ViewRootImpl

中的

mView

的成員變量 對

mView

檢查指派的地方 可以看到以下代碼:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        if (mView == null) {
                mView = view;//對mView 指派
                /.../
                }
        }
}
複制代碼      

那什麼時候能調用到

setView()

呢? 可以看到

setView()

并不是靜态方法,是以要調用并需要引用執行個體才可以。

那麼我們可以看看構造方法

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        /.../
        //注釋1
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
    }
複制代碼      

可以看到注釋1處的代碼我們能夠得知

mAttachInfo

是在

ViewRootImpl

構造器中建立出來的。

我們再對構造器檢視調用 可以發現再

WindowManagerGlobal

addView()

方法 會建立

ViewRootImpl

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            /.../
           root = new ViewRootImpl(view.getContext(), display);
     }
複制代碼      

WindowManagerGlobal

addView()

方法 又被

WindowManagerImpl

addView()

調用

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
複制代碼      

再對這個方法進行調用檢查可以看到

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
                 if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView(); //注釋1
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l); //注釋2
                } else {
                    a.onWindowAttributesChanged(l);
                }
            }
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }
            }
複制代碼      

可以在

注釋1

處:會傳回一個

DecorView

并且在注釋2處添加進入。 至此 我們可以得知

ViewRootImpl

處的

mView

就是

DecorView

知識點:并且我們可以從上面代碼看出在

onResume

view

才會被添加在

window

内并且執行

view

的測量布局繪制 這就是為什麼在

onCreate()

時擷取到

view

寬高會是0的原因,因為那時

view

都沒有添加進

window

呢!!!

時間再次穿梭 回到

ViewRootImpl

performTraversals()

既然已知

mView

DecorView

那麼這個

DecorView

是一個繼承于

FrameLayout

ViewGroup

, 我們在

DecorView

FrameLayout

内沒有找到對

dispatchAttachedToWindow()

方法的處理,就自然而然的來到了

ViewGroup

處。

@Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            //周遊調用 這樣最終會回到View的dispatchAttachedToWindow()
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
    }
複制代碼      

這時候我們又回到了

View

dispatchAttachedToWindow()

方法内的

mRunQueue.executeActions(info.mHandler)

;并點選去看源碼

public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;//注釋1
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);//注釋2
            }
            mActions = null;
            mCount = 0;
        }
    }
複制代碼      

注釋1

處 我們可以看到

mActions

其實是我們用

view.post

方法時 傳入的

runnable

的存儲數組。 而

注釋2

處 就會将

runnable

交給

handler.post()

方法并且添加進這個

Handler

所持有的

Looper

内部的

MessageQueue

中。

至此我們可以大概的進行一次總結了。

總結:

1:

View

mAttachInfo

會在

View

dispatchAttachedToWindow()

方法内指派 在

dispatchDetachedFromWindow()

指派為

null

,并且

mAttachInfo

的根本是在

ViewRootImpl

的構造器内建立的,是以我們就可以知道當

view

attchInfo

不為空時 這個

view

是已經被添加進視窗内的,如果為

null

就說明

view

沒有在

window

内。

2: 我們能通過

view.post()

正确的擷取

View

的寬高主要得益于

Android

内的生命周期是被

Handler

所驅動的,是以當

ViewRootImpl

Activity

onResume()

生命周期内被建立時,其實主線程的

Handler

是在執行處理一個

Message

的流程中,雖然我們從上面

ViewRootImpl

performTraversals()

源碼中看到

view

緩存的

runnable

performMeasure(), performLayout(),performDraw()

這些方法前先被

post

出去并且添加到

MessageQueue

連結清單中,但是這些

runnable

是屬于下一個

Message

的,而

performMeasure(), performLayout(),performDraw()

這三個方法是屬于本次

Message

的邏輯,隻有本次消息處理完成

Handler

Looper

才會進行下一次消息的處理,最終保證了

View.post()

能夠正确的拿到

View

作者:FeanCheng

連結:

https://juejin.cn/post/6916024212696236040

來源:掘金

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

繼續閱讀