Carson帶你學Android:為什么view.post()能保證獲取到view的寬高?

前言

為什么view.post()能保證獲取到view的寬高?本文將手把手帶你深入源碼了解view.post() 原理。


背景

  • 業務需求代碼開始時機一般是在:Activity的生命周期onCreate()
  • 視圖View 繪制時機:Activity的生命周期onResume()之后
    (注:ActivityThread 的 handleResumeActivity()執行順序:先回調 Activity 生命周期 onResume() - 再開始 View 的繪制任務)

矛盾

  • 業務需求代碼需獲取寬高的時機 跟 View的繪制時機 存在時序問題
  • 一般來說,業務需求代碼開始時就需要獲取View的相關信息(如寬、高),但:View 繪制時機在Activity.onResume()之后,即在Activity.onCreate()之后 = 業務需求代碼開始后。

解決方案

將需執行的任務傳入到View.post() 。這個操作大家一定很熟悉。那么 其內部原理是什么呢?


結論

以Handler為基礎,View.post() 將傳入任務的執行時機調整到View 繪制完成之后。下面我將從源碼的角度進行分析。


源碼解析

我們直接從view.post()入手:

public boolean post(Runnable action) {
    
    // 僅貼出關鍵代碼
    // ...
    
    // 判斷AttachInfo是否為null
    final AttachInfo attachInfo = mAttachInfo;

    // 過程1:若不為null,直接調用其內部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 過程2:若為null,則加入當前View的等待隊列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表傳入的要執行的任務
    // 即調用了HandlerActionQueue.post() ->> 分析1
    return true;
}

此處分成兩個過程講解:

  1. 當AttachInfo不為null時,直接調用其內部Handler的post;
  2. 當AttachInfo為null時,則將任務加入當前View的等待隊列中。

此處為了方便理解,我會先講解過程2


過程2:當AttachInfo為null時,則將任務加入當前View的等待隊列中。

public boolean post(Runnable action) {
    
    // 僅貼出關鍵代碼
    // ...
    
    // 判斷AttachInfo是否為null
    final AttachInfo attachInfo = mAttachInfo;

    // 過程1:若不為null,直接調用其內部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 過程2:若為null,則加入當前View的等待隊列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表傳入的要執行的任務
    // 即調用了HandlerActionQueue.post() ->> 分析1
    return true;
}

/**
  * 分析1:HandlerActionQueue.post()
  */
public void post(Runnable action) {
    // ...
    postDelayed(action, 0); 
    // ->> 分析2 
}

/**
  * 分析2
  */
public void postDelayed(Runnable action, long delayMillis) {
    // 1. 將傳入的任務runnable封裝成HandlerAction  ->>分析3
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        // 2. 將要執行的HandlerAction 保存在 mActions 數組中
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }

        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

/**
  * 分析3:HandlerAction 表示一個待執行的任務
  * 內部持有要執行的 Runnable 和延遲時間
  */
private static class HandlerAction {
    // post的任務
    final Runnable action;
    // 延遲時間
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }
   // ...
}
// 回到分析原處

結論:

  1. 將傳入的任務封裝成HandlerAction對象
  2. 創建一個默認長度為4的 HandlerAction數組,用于保存通過post()添加的任務

注:此時只是保存了通過post()添加的任務,并沒執行。


過程1:當AttachInfo不為null時,直接調用其內部Handler的post()

public boolean post(Runnable action) {
    
    // ...
    
    // 判斷AttachInfo是否為null
    final AttachInfo attachInfo = mAttachInfo;

    // 過程1:若不為null,直接調用其內部Handler的post ->>分析1
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // 過程2:若為null,則加入當前View的等待隊列
    getRunQueue().post(action); 
    return true;
}

/**
  * 分析1:AttachInfo的賦值過程 -> dispatchAttachedToWindow()
  * 注:AttachInfo持有當前渲染線程Handler
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    // ...

    // 給當前View賦值AttachInfo,此時同一個ViewRootImpl內的所有View共用同一個AttachInfo
    mAttachInfo = info;

    //  mRunQueue,即 前面說的 HandlerActionQueue
    // 其內部保存了當前View.post的任務
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        // 執行使用View.post的任務,post到渲染線程的Handler中
        // ->> 分析2
    }

    // 在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);
        }
    }

    // ... 
}

/**
  * 分析2:HandlerActionQueue.executeActions()
  */
  public void executeActions(Handler handler) {
    synchronized (this) {
        
        final HandlerAction[] actions = mActions;
        // 任務隊列
        // mActions即為前面過程1 保存了通過post()添加的任務 的數組

        // 遍歷所有任務
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            // 發送到Handler中,等待執行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        // 此時不再需要后續的post,將被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}
// ->> 回到分析原處

結論

在AttachInfo被賦值時(即不為null),就會遍歷 前面過程1保存了通過post()添加的任務 的數組,將每個任務發送到handler中等待執行。


下面,我們繼續看,AttachInfo的賦值過程 -> dispatchAttachedToWindow()是什么時候被調用的。

答:dispatchAttachedToWindow()調用時機是在 View 繪制流程的開始階段,即 ViewRootImpl.performTraversals()

/**
  * 基礎:
  * 1. AttachInfo的創建是在ViewRootImpl的構造方法中
  * 2. 同一個 View Hierachy 樹結構中所有View共用一個AttachInfo
  */
  mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);

/**
  * dispatchAttachedToWindow()調用時機 = View繪制流程開始
  * 即ViewRootImpl.performTraversals()
  */
    private void performTraversals() {

        // ...

        // mView是DecorView
        // host的類型是 DecorView(繼承自 FrameLayout)
        // 每個Activity都有一個關聯的 Window(當前窗口),每個窗口內部又包含一個 DecorView 對象(描述窗口的xml視圖布局)
        final View host = mView;

        // 調用DecorVIew的dispatchAttachedToWindow()
        // ->> 分析1
        host.dispatchAttachedToWindow(mAttachInfo, 0);
       
       getRunQueue().executeActions(mAttachInfo.mHandler);

       // 開始View繪制流程的測量、布局、繪制階段
       performMeasure();
       performLayout();
       performDraw();
       ...

    }

/**
  * 分析1:DecorVIew.dispatchAttachedToWindow()
  * 注:DecorView并無重寫該方法,而是在其父類ViewGroup里 
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    super.dispatchAttachedToWindow(info, visibility);

    // 子View的數量
    final int count = mChildrenCount;
    final View[] children = mChildren;

    // 遍歷所有子View,調用所有子View的dispatchAttachedToWindow() & 為每個子View關聯AttachInfo
    // 子View 的 dispatchAttachedToWindow()在前面過程1已經分析過:
    // 即 遍歷 前面過程1保存了通過post()添加的任務 的數組,將每個任務發送到handler中等待執行。
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
    }
   
    // ...
}

結論

  • 通過View.post()添加的任務是在View繪制任務里 - 開始繪制階段時添加到消息隊列的尾部的;
  • 所以,View.post() 添加的任務的執行是在View繪制任務后才執行,即在View繪制流程結束之后執行
  • 即View.post() 添加的任務能夠保證在所有 View繪制流程結束之后才被執行,所以 執行View.post() 添加的任務時可以正確獲取到 View 的寬高。

額外延伸

a. 問題描述

若只是創建一個 View & 調用它的post(),那么post的任務會不會被執行?

final View view = new View(this);

    view.post(new Runnable() {
        @Override
        public void run() {
            // ...
        }
    });

b. 答案

不會。主要原因是:
每個View中post() 需執行的任務,必須得添加到窗口視圖-執行繪制流程 - 任務才會被post到消息隊列里去等待執行,即依賴于dispatchAttachedToWindow ();

若View未添加到窗口視圖,那么就不會走繪制流程,post() 添加的任務最終不會被post到消息隊列里,即得不到執行。(但會保存到HandlerAction數組里)

上述例子,因為它沒有被添加到窗口視圖,所以不會走繪制流程,所以該任務最終不會被post到消息隊列里 & 執行

c. 解決方案

此時只需要添加將View添加到窗口,那么post()的任務即可被執行

// 因為此時會重新發起繪制流程,post的任務會被放到消息隊列里,所以會被執行
contentView.addView(view);

至此,關于view.post()原理講解完畢


總結

View.post()的原理:以Handler為基礎,View.post() 將傳入任務添加到 View繪制任務所在的消息隊列尾部,從而保證View.post() 任務的執行時機是在View 繪制任務完成之后的。 其中,幾個關鍵點:

1-View.post()實際操作:將view.post()傳入的任務保存到一個數組里 /
2-View.post()添加的任務 添加到 View繪制任務所在的消息隊列尾部的時機:View 繪制流程的開始階段,即 ViewRootImpl.performTraversals()
3-View.post()添加的任務執行時機:在View繪制任務之后

接下來推出的文章,我將繼續講解Android的相關知識,感興趣的讀者可以繼續關注我的博客哦:Carson_Ho的Android博客

相關系列文章閱讀
Carson帶你學Android:學習方法
Carson帶你學Android:四大組件
Carson帶你學Android:自定義View
Carson帶你學Android:異步-多線程
Carson帶你學Android:性能優化
Carson帶你學Android:動畫


歡迎關注Carson_Ho的簡書

不定期分享關于安卓開發的干貨,追求短、平、快,但卻不缺深度


請點贊!因為你的鼓勵是我寫作的最大動力!

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