runOnUiThread 、Handler.post、View.post分析

Burjal本尊發表於2017-12-21

本文原始碼基於 Android API 26 Platform

一、 示例

首先,看如下程式碼,請判斷輸出結果:

public class MainThreadTestActivity extends AppCompatActivity {

  private static final String TAG = MainThreadTestActivity.class.getSimpleName();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main_thread_test);

    View view = new View(this);
    view.post(new Runnable() {
      @Override
      public void run() {
        Log.i(TAG, "[view.post] >>>> 1 ");
      }
    });

    new Handler(Looper.getMainLooper()).post(new Runnable() {
      @Override
      public void run() {
        Log.i(TAG, "[handler.post] >>>> 2");
      }
    });

    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Log.i(TAG, "[runOnUiThread] >>>>> 3");
      }
    });

    new Thread(new Runnable() {
      @Override
      public void run() {
        runOnUiThread(new Runnable() {
          @Override
          public void run() {
            Log.i(TAG, "[runOnUiThread from thread] >>>> 4");
          }
        });
      }
    }).start();
  }
}
複製程式碼

首先預測下,輸出結果會是怎樣的呢?

下面給出執行結果:

...I/MainThreadTestActivity: [runOnUiThread] >>>>> 3
...I/MainThreadTestActivity: [handler.post] >>>> 2
...I/MainThreadTestActivity: [runOnUiThread from thread] >>>> 4
複製程式碼

那麼,問題來了:

  • 第一步的 View.post 為什麼沒有執行?
  • 為什麼 runOnUiThread 會執行的比 Handler.post 快?
  • 在正常情況下,runOnUiThreadHandler.postView.post 這三者的執行順序又會是怎樣的呢?

下面我們分別進行解析。

二、 解析

2.1 View.post

2.1.1 View.post 不執行問題

首先,我們來看 View.post 原始碼:

//View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        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);
    return true;
}
複製程式碼

即:當執行 View.post 方法時,如果 AttachInfo 不為空,則通過 AttachInfoHandler 來執行 Runnable;否則,將這個 Runnable 拋到 View 的執行佇列 HandlerActionQueue 中。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
        //...
}
複製程式碼

也就是隻有當 View attch 到 Window 後,才會給 AttachInfo 賦值。所以,在 示例 裡的程式碼會直接走入 getRunQueue().post(action) 。我們繼續順著原始碼看下去。

View 裡,通過 HandlerActionQueue 封裝了可執行請求佇列。官方給它的註釋是 Class used to enqueue pending work from Views when no Handler is attached.,也就是view還沒有 Handler 來執行後續任務(沒有attach 到 Window上),將所有請求入隊。

// HandlerActionQueue.java
public class HandlerActionQueue {

    public void removeCallbacks(Runnable action) {
        synchronized (this) {
            final int count = mCount;
            int j = 0;

            final HandlerAction[] actions = mActions;
            for (int i = 0; i < count; i++) {
                if (actions[i].matches(action)) {
                    // Remove this action by overwriting it within
                    // this loop or nulling it out later.
                    continue;
                }

                if (j != i) {
                    // At least one previous entry was removed, so
                    // this one needs to move to the "new" list.
                    actions[j] = actions[i];
                }

                j++;
            }

            // The "new" list only has j entries.
            mCount = j;

            // Null out any remaining entries.
            for (; j < count; j++) {
                actions[j] = null;
            }
        }
    }

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

            mActions = null;
            mCount = 0;
        }
    }

}

複製程式碼

而在 View 中,

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
       ...
        // Transfer all pending runnables.
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        ...
}
複製程式碼

綜上我們就可以分析出: 由於 View view = new View(this); 沒有將view attch到window上,所以執行的 View.post 方法將可執行請求都快取到請求佇列裡。

所以,示例 中的程式碼可改為:

View view = new View(this);
    rootView.addView(view);
    view.post(new Runnable() {
複製程式碼

輸出結果為:

...I/MainThreadTestActivity: [runOnUiThread] >>>>> 3
...I/MainThreadTestActivity: [handler.post] >>>> 2
...I/MainThreadTestActivity: [runOnUiThread from thread] >>>> 4
...I/MainThreadTestActivity: [view.post] >>>> 1 
複製程式碼

成功執行 View.post 方法,修改正確。


View.post() 只有在 View attachedToWindow 的時候才會立即執行

2.1.2 View.post 原始碼解析

通過 2.1.1 節我們知道,View.post() 只有在 View attachedToWindow 的時候才會立即執行 。通過執行 ViewRootImplViewRootHandler 執行。

2.2 runOnUiThread

//Activity.java
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}
複製程式碼

注意:

  • 如果當前執行緒是 主執行緒 , 請求會立即執行
  • 如果當前執行緒不是 主執行緒 , 請求會發到 UI 執行緒的時間佇列。

即,在非主執行緒呼叫該方法,存線上程切換的開銷

2.3 Handler.post

需要先把請求加入到 Handler 佇列,然後執行。

三、執行順序分析

綜上,當在主執行緒分別呼叫 View.postHandler.postrunOnUiThread , new Thread() - [runOnUiThread] 四個方法執行順序從快到慢為:

runOnUiThread - Handler.post - new Thread() - [runOnUiThread] - View.post

(符合前面驗證結果)

分析:

  • runOnUiThread 因為當前執行在 UI執行緒,無需執行緒切換,直接執行
  • Handler.post 需要將請求加入 UI執行緒 Handler , 多了 入隊出隊 時間
  • new Thread() - [runOnUiThread] 開啟新執行緒,在啟動完成後將請求加入 UI執行緒 Handler, 多了 執行緒切換入隊出隊 時間
  • View.post 需要在view attach 到 Window 後,通過 ViewRootImplViewRootHandler 執行請求。執行緒切換時間遠小於UI渲染時間,所以執行最慢

擴充套件: 當 示例 中程式碼在 非UI執行緒 執行時,runOnUiThreadHandler.post 開銷幾乎一樣,所以執行結果也是順序完成。 故當在 非UI執行緒 分別呼叫 View.postHandler.postrunOnUiThread , new Thread() - [runOnUiThread] 四個方法執行順序從快到慢為:

Handler.post - runOnUiThread - new Thread() - [runOnUiThread] - View.post

相關文章