Android執行緒間通訊

flueky發表於2019-11-16

0. 前言

Android 系統中,應用在執行時是一個獨立的程式,但是每個程式中可以包含多個執行緒提高執行效率。在多執行緒開發中,有一個很重要的原則:不能在子執行緒中更新 UI 。

Only the original thread that created a view hierarchy can touch its views.

為解決這個問題,目前有多重方案實現子執行緒和主執行緒(UI 執行緒)之間的通訊。

1. 判斷程式碼執行的執行緒

在一些簡單程式碼邏輯中,也許能夠很清晰的辨別出執行在子執行緒或主執行緒中。通常在複雜的類關係依賴、函式巢狀呼叫中,可能需要花費很大精力去閱讀程式碼之後去判斷。不過,巧法子也是有的,一行程式碼解決。

Log.d("TAG","test");
複製程式碼

日誌內容中,2368-2393 表示是在子執行緒中輸出日誌。

11-16 01:08:31.584 2368-2393/com.flueky.demo D/TAG: test

其中 2368 表示 PID 指程式id, 2393 表示 TID 指執行緒id 。如果 TID 也是 2368 ,則表示日誌輸出在主執行緒中。

可能也有人聽過 UID ,應用第一次安裝在裝置上時,系統會分配一個序號給應用,作為其唯一標識。UID 在覆蓋安裝時不會變化,解除安裝安裝時系統會重新分配一個。

下面是在程式碼中獲取三個 id 的方式。

// 獲取 tid
Process.myTid()
// 獲取 pid
Process.myPid()
// 獲取 uid
Process.myUid()
複製程式碼

遇到需要在子執行緒中更新 UI 操作時,可以通過下面的這些方式解決。

2. 使用 View.post

子執行緒程式碼執行在 Activity 或 Fragment 中,能獲取到任意 view 的引用時,可以使用此方式將需要實現的程式碼放在主執行緒中執行。

// post 方法在子執行緒中呼叫
textView.post(new Runnable() {
    @Override
    public void run() {
        // 此處程式碼會在 UI 執行緒執行
    }
});
複製程式碼

3. 使用 Activity.runOnUiThread

如果能夠直接獲取到 Activity 例項,使用 runOnUiThread 方法。

// runOnUiThread 方法在子執行緒中呼叫
activity.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // 此處程式碼會在 UI 執行緒執行
    }
});
複製程式碼

4. 使用 Handler.post

使用 Handler 比較講究,因為需要考慮到 Handler 例項初始化的位置。

// post 方法在子執行緒中呼叫
handler.post(new Runnable() {
    @Override
    public void run() {
        // handler 在主執行緒中初始化時,此處程式碼在主執行緒中執行
        // handler 在子執行緒中初始化事,此處程式碼在子執行緒中執行
    }
});
複製程式碼

以上說法其實不夠嚴謹,存在下面的情況,初始化 handler 例項時傳入 Looper.getMainLooper() ,則 handler.post 也在主執行緒中執行。

// 下面的程式碼在子執行緒中執行
Looper.prepare();
handler = new Handler(Looper.getMainLooper());
Looper.loop();
複製程式碼

5. 使用 EventBus

EventBus 出自 greenrobot ,通過訂閱的方式,告知函式執行在哪個執行緒中。為使訂閱函式在主執行緒中執行,使用註解 MAINMAIN_ORDERED

/**
 * eventbus 簡單示例
 */
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 訂閱函式,
     * ThreadMode.MAIN 表示在主執行緒中執行,可能會阻塞子執行緒。
     * ThreadMode.MAIN_ORDERED 表示在主執行緒中執行,不會阻塞子執行緒。
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(Object event) {
        if(event instanceof Runnable)
            ((Runnable)event).run();
    }

    @Override
    protected void onStart() {
        super.onStart();
        // 註冊 eventbus 監聽
        EventBus.getDefault().register(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        // 登出 eventbus 監聽
        EventBus.getDefault().unregister(this);
    }
}

// 在子執行緒中傳送訊息
EventBus.getDefault().post(new Runnable() {
    @Override
    public void run() {
        // 此處程式碼會在 UI 執行緒執行
    }
});
複製程式碼

6. 傳遞資料

前面四種方式演示瞭如何在子執行緒中做更新 UI 操作。 AsyncTask 也具備相同用法,但是有點牽強,因為只有 execute 方法在主執行緒中執行,onPostExecute 才會在主執行緒中呼叫。由於 onPostExecute 可以接收到子執行緒傳遞的任意型別的物件資料,所以 AsyncTask 可以作為執行緒間的資料互動的載體。對此 HandlerEventBus 表示不服。

EventBus 如之前所示,可以將 Runnable 物件換成任意例項。

Handler 也可以通過 sendMessage 方法傳送 Message 物件。其中 Message.obj 用作傳遞物件資料的載體。

建議使用 Message.obtain() 方法複用 Message 例項。

順便提下,BroadcastReceiver 也可以作為此類用途,只不過沒有 EventBusHandler 方便。

覺得有用?那打賞一個唄。去打賞

個人主頁已經更新 ,歡迎收藏flueky.github.io/

相關文章