Android handler 消息通訊實踐(附計時器 demo)

**segmentfault 對 mackdown 語法的支持不是很好,有些圖片都顯示不出來,你們能夠去個人掘金查看這篇文章。java

1、Android 中的 UI 線程概述

<font face="黑體">Android 的 UI 線程是線程不安全的,也就是說想要更新應用程序中的 UI 元素,則必須在主線程中進行。因此主線程又叫作 UI 線程。若在子線程中更新 UI 程序會報錯。可是咱們常常有這樣一種需求:須要在子線程中完成一些耗時任務後根據任務執行結果來更新相應的UI。這就須要子線程在執行完耗時任務後向主線程發送消息,主線程來更新UI。也就是線程之間的通訊,線程間通訊方法有不少,今天咱們主要來說利用 Handler 來實現線程之間的通訊。android

2、經常使用類

一、Handler

<font face="黑體">Handler 是 Android 消息機制的上層接口,經過 handler,能夠將一個任務切換到 handler 所在的線程中執行,咱們一般使用 handler 來更新 UI,但更新 UI 僅僅是的使用場景之一,handler 並非僅僅用來更新 UI 的。segmentfault

1)、在子線程發送消息去主線程更新 UI

<font face="黑體">咱們都知道 Android 中的網絡操做是要在子線程中進行的,當咱們請求到網絡數據之後,確定須要展現數據到界面上。咱們這裏的網絡請求就以 HttpURLConnection 那篇博文中的網絡請求爲例子。安全

<font face="黑體">實現效果以下所示:
Handler通訊
<font face="黑體">具體步驟以下:網絡

<font face="黑體">一、建立 Handler併發

// 1:實例化
// 2:在子線程中發送(空)消息
// 3:由 Handler 對象接收消息,並處理

// 只要 Handler 發消息了,必然會觸發該方法,而且會傳入一個 Message 對象
@SuppressLint("HandlerLeak")
// import android.os.Handler;  注意是 os 包下的 Handler
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        }
    }
};

<font face="黑體">二、網絡請求到數據後發送(空)消息dom

new Thread() {
    @Override
    public void run() {
        super.run();
        strMsg = get();
        Log.e("MainActivityTAG", strMsg + "==========");

        // 發空消息
        mHandler.sendEmptyMessage(1001);
    }
}.start();

<font face="黑體" color = red>注意:這裏的 get() 請求源碼就是 HttpURLConnection 裏面的get() 請求。異步

<font face="黑體">三、Handler 中接收消息並處理ide

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 1001:
                txt.setText(strMsg);
                break;
            }
        }
    }
};

<font face="黑體">這裏的1001就是<font color=red> mHandler.sendEmptyMessage(1001) </font>裏面發送過來的那個1001。oop

二、Message

<font face="黑體"> Message 是在線程之間傳遞的消息,它能夠在內部攜帶少許的信息,用於在不一樣線程之間交換數據。經常使用的屬性有:

  1. <font face="黑體">what 屬性 <font face="黑體">用於區分Handler發送消息的不一樣線程來源
  2. <font face="黑體">arg1 屬性 <font face="黑體">子線程向主線程傳遞的整型數據
  3. <font face="黑體">obj 屬性 <font face="黑體">Object

1)、在子線程發送 Message 消息去主線程

<font face="黑體"> 咱們能夠在 Message 對象中裝載一些數據,攜帶在消息中,發送給須要接收消息的地方。

<font face="黑體">實現效果以下所示:
裝載message對象
<font face="黑體">發送消息具體代碼以下所示:

new Thread() {
    @Override
    public void run() {
        super.run();

        // what        用於區分Handler發送消息的不一樣線程來源
        // arg1, arg2  若是子線程須要向主線程傳遞整型數據, 則可用這些參數
        // obj         Object
        Message msg = new Message();
        msg.what = 1002;
        msg.arg1 = 666;
        msg.arg2 = 2333;
        msg.obj = new Random();  // 這裏只是爲了演示msg能夠發送對象信息

        mHandler.sendMessage(msg);
    }
}.start();

<font face="黑體">接收消息具體代碼以下所示:

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 1001:
                txt.setText(strMsg);
                break;
            case 1002:
                String str2 = "發送過來的Message數據爲:" +"what: " + msg.what + " arg1: " + msg.arg1 + " arg2: " + msg.arg2 +
                        ", 隨機數" + ((Random) msg.obj).nextInt();
                Toast.makeText(MainActivity.this, str2, Toast.LENGTH_LONG).show();
                break;
        }
    }
};

三、MessageQueue

<font face="黑體">MessageQueue 就是消息隊列的意思,它主要用於存放全部經過 Handler 發送過來的消息。這部分消息會一直存放於消息隊列當中,等待被處理。每一個線程只會有一個 MessageQueue 對象。咱們上面的看到的 handleMessage() 方法其實就是從這個 MessageQueue 裏面把方法提取出來去處理。咱們在開發的過程當中是沒法直接的接觸 MessageQueue 的,可是它確一直在起做用。

<font face="黑體">問題:爲何 Handler 發送過來的 Message 不能直接在handleMessage() 方法中處理。而是要先存放到 MessageQueue 裏面?
<font face="黑體">答:其實緣由就是同一個線程一下只能處理一個消息,並不具備併發的功能。因此就先經過隊列將全部的消息都保存下來並安排每一個消息的處理順序(隊列的特色:先進先出),而後咱們的 UI 線程在挨個將它們拿出來處理。

四、Looper

<font face="黑體">Looper 是每一個線程中 MessageQueue 的管家,調用 Looper 的 loop() 方法,就會進入到一個無限循環當中,而後每當 MessageQueue 中存在一條消息,Looper 就會將這條消息取出,並將它傳遞到 Handler 的 handleMessage() 方法中。每一個線程只有一個 Looper 對象,並且僅對應一個消息隊列。那麼 Looper 究竟是怎麼工做的,接下來咱們就要經過代碼來實現一下 Looper 的用法。

<font face="黑體">上面咱們都是在子線程中發送消息到主線程去處理,那麼反過來能夠嗎?就是在主線程發送消息到子線程去處理,答案是能夠的。咱們用代碼來實現一下,具體步驟以下:

<font face="黑體">一、首先咱們須要在子線程中定義一個 Handler 來接收消息

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    private Handler mHandler2;
    
    new Thread() {
        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            super.run();
            
            mHandler2 = new Handler() {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    super.handleMessage(msg);
                    Log.e("MainActivityTAG", "由主線程傳遞過來的Message,它的what是:" + msg.what);
                }
            };
        }
    }.start();
}

<font face="黑體">二、而後由主線程發送消息

case R.id.btn3:
    mHandler2.sendEmptyMessage(10000);
    break;

<font face="黑體">咱們如今來回顧一下這個流程,首先咱們點擊按鈕的時候調用 sendEmptyMessage() 方法,這個時候這個消息就進入到 MessageQueue 裏面,而後由 Looper 讀出這一個消息並交由 handleMessage() 這個方法來處理,因此 MessageQueue 和 Looper 都是在幕後工做的。

<font face="黑體">首先能夠確定的是主線程必定有本身的 Looper,那麼子線程是否有它本身的 Looper呢?要回答這個問題,咱們先來運行一下上面的代碼,看看能不能成功的實現由主線程向子線程發送消息。

<font face="黑體">運行結果以下:
錯誤
<font face="黑體">運行上述代碼報了一個<font color = red> "Can't create handler inside thread Thread[Thread-6,5,main] that has not called Looper.prepare()"</font> 的錯誤。意思就是不能再尚未調用 <font color = red>Looper.prepare() </font>的線程裏面去建立 Handler。那麼爲何主線程中咱們沒有調用這個方法不會報錯呢?其實答案就是系統會自動的爲主線程建立 <font color = red>Looper.prepare() </font>這個方法。那麼咱們就手動的爲子線程建立一下這個方法。

<font face="黑體">添加上<font color = red> Looper.prepare() </font>這個方法以後,咱們上述的代碼就不會報錯了。可是這個時候咱們仍是沒法在子線程接收到消息。緣由就是在子線程中咱們不只要手動加上 <font color = red> Looper.prepare() </font>這個方法,還要加上<font color = red> Looper.loop() </font>這個方法。

<font face="黑體"><font color = red> Looper.prepare() </font>這個方法的意思是準備開始一個循環,而<font color = red> Looper.loop() </font>這個方法纔是真正的循環,做用就是使得 handleMessage() 一直處於等待狀態,不會被結束掉。

<font face="黑體">咱們來看下代碼:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    private Handler mHandler2;
    
    new Thread() {
        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            super.run();
            
            Looper.prepare(); // 準備, 開啓一個消息循環. 系統會自動爲主線程開啓消息循環
            mHandler2 = new Handler() {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    super.handleMessage(msg);
                    Log.e("MainActivityTAG", "由主線程傳遞過來的Message,它的what是:" + msg.what);
                }
            };
            Looper.loop(); // 循環. 至關於產生了一個while(true){...}
        }
    }.start();
}

<font face="黑體">如今咱們來看一下運行結果:
主線程發送消息到子線程
如今咱們就已經成功的實現了由主線程發送消息到子線程。

3、運行機制

<font face="黑體">以上呢咱們已經將 Handler 機制裏面所涉及的四個類都講完了。如今咱們來聊一下它的運行機制。

<font face="黑體">下面是我從網上找到的一張 Handler 運行機制圖片,咱們就看着這張圖片再來說一下 Handler 的運行機制。

百度圖片
<font face="黑體">首先咱們來看下左邊這一部分,一個 LooperThread 線程,這個線程裏面有一個 Looper,而後 Looper 內部有 MessageQueue 消息隊列,這三者是一一對應的。

<font face="黑體">只有主線程纔會自動的由 Looper 來管理,而其餘線程的話必需要顯示的調用 Looper.prepare() 這個方法。同時若是但願子線程處於持續接收消息的狀態,咱們還須要調用 Looper.loop() 方法使其處於等待的狀態。

<font face="黑體">咱們建立的 Handler 方法在發送消息的時候實際上是將消息發送到 MessageQueue 消息隊列中。咱們看右邊 MessageQueue 中有多待處理的消息。而後經過 Looper 不斷的取出消息到對應的 Handler 的 handleMessage() 方法中去處理。這就是整個 Handler 的運行機制。

4、利用 Handler 實現計時器案例

<font face="黑體">先來看一下效果,以下圖所示:

計時器案例
<font face="黑體">這個計時器案例主要功能就是按下播放按鍵的時候,計時器就開始計時工做,按下暫停鍵計時器就中止工做,並顯示當前用時。首先計時功能確定是在子線程完成的。而計時功能確定須要改變界面的 UI ,因此這裏咱們就利用 Handler 來將子線程的信息發送到 UI 線程。

<font face="黑體">實現步驟以下所示:

  1. <font face="黑體">在主線程建立 Handler,並覆寫 handleMessage() 方法。

    @SuppressLint("HandlerLeak")
       private Handler mHandler = new Handler() {
       @Override
       public void handleMessage(@NonNull Message msg) {
           super.handleMessage(msg);
       }
       };
  2. <font face="黑體">每隔 1s,時間++,而且發送消息到主線程更新 UI,咱們這裏先用 Thread.sleep() 這個方法來實現每隔1s,一會咱們回去優化這個代碼。

    new Thread() {
        @Override
        public void run() {
            super.run();
            int i = 1;
            while (flag) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                Message msg = new Message();
                msg.arg1 = i;
                mHandler.sendMessage(msg);
    
                // 時間 ++
                i++;
            }
        }
    }.start();
  3. <font face="黑體">在主線程中用 00:00 的形式來顯示計時時間。

    @SuppressLint("HandlerLeak")
       private Handler mHandler = new Handler() {
       @Override
       public void handleMessage(@NonNull Message msg) {
           super.handleMessage(msg);
           int min = msg.arg1 / 60;
           int sec = msg.arg1 % 60;
           // 00:00
           String time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
           timer.setText(time);
    
           if (!flag) {
               title.setText("計時器");
               state.setImageResource(R.mipmap.start);
               txt.setText("用時: " + time);
           }
       }
       };

<font face="黑體">完整代碼以下所示:

public class TimerActivity extends AppCompatActivity {

    private TextView title, timer, txt;
    private ImageView state;

    private boolean flag = false;  // 1: 用於區別當前對按鈕的點擊是屬於開啓計時器仍是中止計時器  2: 控制while循環

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            int min = msg.arg1 / 60;
            int sec = msg.arg1 % 60;
            // 00:00
            String time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
            timer.setText(time);

            if (!flag) {
                title.setText("計時器");
                state.setImageResource(R.mipmap.start);
                txt.setText("用時: " + time);
            }
        }
    };

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

        title = findViewById(R.id.title);
        timer = findViewById(R.id.timer);
        txt = findViewById(R.id.txt);

        state = findViewById(R.id.state);
        state.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (!flag) {
                    // 原本是禁止狀態, 即將開始計時
                    flag = true;
                    title.setText("工做中");
                    state.setImageResource(R.mipmap.stop);
                    txt.setText("");
                    new Thread() {
                        @Override
                        public void run() {
                            super.run();
                            int i = 1;
                            while (flag) {
                                try {
                                    sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                                Message msg = new Message();
                                msg.arg1 = i;
                                mHandler.sendMessage(msg);

                                // 時間 ++
                                i++;
                            }
                        }
                    }.start();
                } else {
                    flag = false;
                }
            }
        });
    }
}

<font face="黑體" color = red>注意:上面 flag 標誌位有兩個做用:

  1. <font face="黑體">用於區別當前對按鈕的點擊是屬於開啓計時器仍是中止計時器;
  2. <font face="黑體">控制 while 循環。

5、利用 Handler 的 postDelayed 方案優化計時器案例

<font face="黑體">其實這個優化方案就是用 Handler.postDelayed() 的方案來代替原來的 Thread.sleep() 方法。

<font face="黑體">咱們直接來看完整的代碼:

public class TimerActivity2 extends AppCompatActivity {

    private TextView title, timer, txt;
    private ImageView state;

    private boolean flag = false;

    private String time;

    private int i;
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            int min = i / 60;
            int sec = i % 60;
            time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
            timer.setText(time);
            i++;

            mHandler.postDelayed(runnable, 1000);
        }
    };

    // post postDelay postAtTime
    private Handler mHandler = new Handler();

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

        title = findViewById(R.id.title);
        timer = findViewById(R.id.timer);
        txt = findViewById(R.id.txt);

        state = findViewById(R.id.state);
        state.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!flag) {
                    flag = true;
                    title.setText("工做中");
                    state.setImageResource(R.mipmap.stop);
                    txt.setText("");

                    i = 1;
                    new Thread() {
                        @Override
                        public void run() {
                            super.run();
                            mHandler.postDelayed(runnable, 1000);
                        }
                    }.start();
                } else {
                    flag = false;
                    title.setText("計時器");
                    state.setImageResource(R.mipmap.start);
                    txt.setText("用時: " + time);

                    mHandler.removeCallbacks(runnable);
                }
            }
        });
    }
}

<font face="黑體">上面代碼裏面實際上是利用了遞歸的思想實現計時原理的,由於咱們在外層的 Runnable 對象中又遞歸調用了 runnable。因此當咱們中止計時功能時,須要移除回調操做,就是將咱們的 runnable對象移除掉,須要調用 mHandler.removeCallbacks(runnable)。

6、Handler 內存溢出問題

<font face="黑體">能夠看到上面咱們在用 handler 的時候都加了一個註解 <font color=red> @SuppressLint("HandlerLeak")。</font>其實緣由就是 Handler 在Android 中用於消息的發送與異步處理時,經常在 Activity 中做爲一個匿名內部類來定義,此時 Handler 會隱式地持有一個外部類對象(一般是一個Activity)的 <font color=red>強引用</font>。當 Activity 已經被用戶關閉時,因爲 Handler 還持有 Activity 的引用形成 Activity 沒法被 GC 回收,從而形成內存泄露。解決辦法通常有兩種;

  1. <font face="黑體">主動的清除全部的 Message,當 Activity 銷燬的時候咱們調用 mHandler.removeCallbacksAndMessages(null) 這個方法;
  2. <font face="黑體">利用弱引用來解決這個問題,定義一個 MyHandler 的靜態內部類(此時不會持有外部類對象的引用),在構造方法中傳入 Activity並對 Activity 對象增長一個弱引用,這樣 Activity 被用戶關閉以後,即使異步消息還未處理完畢,Activity 也可以被 GC 回收,從而避免了內存泄露。寫法以下所示:

    private MyHandler handler = new MyHandler(this);
       static class MyHandler extends Handler {
       WeakReference weakReference;
       public MyHandler(SecondActivity activity) {
           weakReference = new WeakReference(activity);
       }
    
       @Override
       public void handleMessage(Message msg) {
           
       }
       }

7、小結

<font face="黑體">到此爲止跟 Handler 有關的問題咱們大體上都已經講完了,而且也實現了一個小案例。咱們講了跟 Handler 有關的幾個經常使用類,而後根據這些經常使用類理解了 Handler 的運行機制,又利用 Handler 實現了一個計時器,並優化了代碼。最後咱們又簡單的說了一下 Handler 可能會形成內存泄漏的問題。

<font face="黑體">項目源碼下載地址

相關文章
相關標籤/搜索