4.5 Android 中多線程的用法大全

當我們需要執(zhí)行一些耗時操作,比如說發(fā)起一條網(wǎng)絡(luò)請求時,考慮到網(wǎng)速等其他原因,服務(wù)器未必會立刻響應(yīng)我們的請求,如果不將這類操作放在子線程里去運行,就會導致主線程被阻塞住,從而影響用戶對軟件的正常使用,那么這就需要用到異步處理的知識,下面我們就開始相關(guān)的學習。

本節(jié)例程下載地址:WillFlowThread

一、線程的基本用法

1、繼承 Thread

Android 多線程編程其實并不比 Java 多線程編程特珠,基本都是使用相同的語法。比如定義一個線程只需要新建一個類繼承自 Thread,然后重寫父類的 run() 方法,并在里面編寫耗時邏輯即可,如下所示:

        class MyThread extends Thread {
            @Override
            public void run() {
                // 處理具體的邏輯
            }
        }

那么該如何啟動這個線程呢?其實也很簡單,只需要 new 出 MyThread 的實例,然后調(diào)用它的 start() 方法,這樣 run() 方法中的代碼就會在子線程當中運行了,如下所示:

new MyThread().start();

2、實現(xiàn) Runnable 接口

當然,使用繼承的方式耦合性有點高,更多的時候我們都會選擇使用實現(xiàn) Runnable 接口的方式來定義一個線程,如下所示:

        class MyThread implements Runnable {
            @Override
            public void run() {
                // 處理具體的邏輯
            }
        }

如果使用了這種寫法,啟動線程的方法也需要進行相應(yīng)的改變,如下所示:

MyThread myThread = new MyThread();
new Thread(myThread).start();

可以看到, Thread 的構(gòu)造函數(shù)接收一個 Runnable 參數(shù),而我們 new 出的 MyThread 正是一個實現(xiàn)了 Runnable 接口的對象,所以可以直接將它傳入到 Thread 的構(gòu)造函數(shù)里。接著調(diào)用 Thread 的 start() 方法, run() 方法中的代碼就會在子線程當中運行了。

當然,如果你不想專門再定義一個類去實現(xiàn) Runnable 接口,也可以使用匿名類的方式,這種寫法更為常見,如下所示:

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 處理具體的邏輯
            }
        }).start();

了解了線程的基本用法后,下面我們來看一下 Android 多線程編程與 Java 多線程編程不同的地方。

二、在子線程中更新 UI

和許多其他的 GUI 庫一樣, Android 的 UI 也是線程不安全的。也就是說,如果想要更新應(yīng)用程序里的 UI 元素,則必須在主線程中進行,否則就會出現(xiàn)異常。

首先修改 activity_main.xml 中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wgh.willflowthread.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textColor="#0583fa"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button_thread"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="異步請求" />

</android.support.constraint.ConstraintLayout>

布局文件中定義了兩個控件, TextView 用于在屏幕的正中央顯示一個 Hello world 字符串, Button 用于改變 TextView 中顯示的內(nèi)容,我們希望在點擊 Button 后可以把 TextView 中顯示的字符串改成 Nice to meet you。

接下來修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {

    private Button mButton;
    private TextView mTextView;

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

        mTextView = (TextView) findViewById(R.id.text);
        mButton = (Button) findViewById(R.id.button_thread);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTextView.setText("Nice to meet you");
                    }
                }).start();
            }
        });
    }
}

可以看到,我們在 Change Text 按鈕的點擊事件里面開啟了一個子線程,然后在子線程中調(diào)用 TextView 的 setText() 方法將顯示的字符串改成 Nice to meet you。代碼的邏輯非常簡單,只不過我們是在子線程中更新 UI 的。

現(xiàn)在運行一下程序,并點擊異步請求按鈕,你會發(fā)現(xiàn)程序果然崩潰了!

然后觀察 LogCat 中的錯誤日志,可以看出是由于在子線程中更新 UI 所導致的:


由此證實了 Android 確實是不允許在子線程中進行 UI 操作的。但是有些時候,我們必須在子線程里去執(zhí)行一些耗時任務(wù),然后根據(jù)任務(wù)的執(zhí)行結(jié)果來更新相應(yīng)的 UI 控件。對于這種情況, Android 提供了一套異步消息處理機制,完美地解決了在子線程中進行UI 操作的問題,接下來我們看一下它的用法。

修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
    
    public static final int UPDATE_TEXT = 0;
    ......
    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在這里可以進行UI操作
                    mTextView.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ......
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = UPDATE_TEXT;
                        mHandler.sendMessage(message); // 將Message對象發(fā)送出去
                    }
                }).start();
            }
        });
    }
}

可以看到,這次我們并沒有在子線程里直接進行 UI 操作,而是創(chuàng)建了一個 Message(android.os.Message)對象,并將它的 what 字段的值指定為 UPDATE_TEXT,然后調(diào)用 Handler 的 sendMessage() 方法將這條 Message 發(fā)送出去。很快,Handler 就會收到這條 Message,并在 handleMessage() 方法中對它進行處理。注意此時 handleMessage() 方法中的代碼就是在主線程當中運行的了,所以我們可以放心地在這里進行 UI 操作。接下來對 Message 攜帶的 what 字段的值進行判斷,如果等于UPDATE_TEXT,就將 TextView 顯示的內(nèi)容改成 Nice to meet you。

現(xiàn)在重新運行程序,可以看到點擊按鈕后的效果:

這樣我們就已經(jīng)掌握了 Android 異步消息處理的基本用法,使用這種機制就可以出色地解決掉在子線程中更新 UI 的問題。不過恐怕我們對它的工作原理還不是很清楚,下面我們就來分析一下 Android 異步消息處理機制到底是如何工作的。

三、解析異步消息處理機制

Android 中的異步消息處理主要由四個部分組成, Message、 Handler、 MessageQueue 和 Looper。其中 Message 和 Handler 在剛才我們已經(jīng)接觸過了,而 MessageQueue 和 Looper 對于我們來說還是全新的概念,下面我就對這四個部分進行一下簡要的介紹。

1. Message

Message 是在線程之間傳遞的消息,它可以在內(nèi)部攜帶少量的信息,用于在不同線程之間交換數(shù)據(jù)。剛才我們使用到了 Message 的 what 字段,除此之外我們還可以使用 arg1 和 arg2 字段來攜帶一些整型數(shù)據(jù),使用 obj 字段攜帶一個 Object 對象。

2. Handler

Handler 顧名思義也就是處理者的意思,它主要是用于發(fā)送和處理消息的。發(fā)送消息一般是使用 Handler 的 sendMessage() 方法,而發(fā)出的消息經(jīng)過一系列地輾轉(zhuǎn)處理后,最終會傳遞到 Handler 的 handleMessage() 方法中。

3. MessageQueue

MessageQueue 是消息隊列的意思,它主要用于存放所有通過 Handler 發(fā)送的消息。這部分消息會一直存在于消息隊列中等待被處理,每個線程中只會有一個 MessageQueue 對象。

4. Looper

Looper 是每個線程中的 MessageQueue 的管家,調(diào)用 Looper 的 loop() 方法后,就會進入到一個無限循環(huán)當中,然后每當發(fā)現(xiàn) MessageQueue 中存在一條消息,就會將它取出,并傳遞到 Handler 的 handleMessage( )方法中,每個線程中也只會有一個 Looper 對象。

了解了 Message、 Handler、 MessageQueue 以及 Looper 的基本概念后,我們再來對異步消息處理的整個流程梳理一遍:

  • 首先需要在主線程當中創(chuàng)建一個 Handler 對象,并重寫 handleMessage() 方法。然后當子線程中需要進行 UI 操作時,就創(chuàng)建一個 Message 對象,并通過 Handler 將這條消息發(fā)送出去。
  • 之后這條消息會被添加到 MessageQueue 的隊列中等待被處理,而 Looper 則會一直嘗試從 MessageQueue 中取出待處理消息,最后分發(fā)回 Handler 的 handleMessage() 方法中。
  • 由于 Handler 是在主線程中創(chuàng)建的,所以此時 handleMessage() 方法中的代碼也會在主線程中運行,于是我們在這里就可以安心地進行 UI 操作了。
整個異步消息處理機制的流程示意圖:

![Upload 選區(qū)_131.png failed. Please try again.]

一條 Message 經(jīng)過這樣一個流程的輾轉(zhuǎn)調(diào)用后,也就從子線程進入到了主線程,從不能更新 UI 變成了可以更新 UI,整個異步消息處理的核心思想也就是如此。

四、使用 AsyncTask

為了更加方便我們在子線程中對 UI 進行操作, Android 還提供了另外一些好用的工具, AsyncTask 就是其中之一。借助 AsyncTask,即使我們對異步消息處理機制完全不了解,也可以十分簡單地從子線程切換到主線程。當然, AsyncTask 背后的實現(xiàn)原理也是基于異步消息處理機制的,只是 Android 幫我們做了很好的封裝而已。

首先來看一下 AsyncTask 的基本用法,由于 AsyncTask 是一個抽象類,所以如果我們想使用它,就必須要創(chuàng)建一個子類去繼承它。在繼承時我們可以為 AsyncTask 類指定三個泛型參數(shù),這三個參數(shù)的用途如下:

1. Params

在執(zhí)行 AsyncTask 時需要傳入的參數(shù),可用于在后臺任務(wù)中使用。

2. Progress

后臺任務(wù)執(zhí)行時,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位。

3. Result

當任務(wù)執(zhí)行完畢后,如果需要對結(jié)果進行返回,則使用這里指定的泛型作為返回值類型。

所以一個最簡單的自定義 AsyncTask 就可以寫成如下方式:

    class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
        ……
    }

這里我們把 AsyncTask 的第一個泛型參數(shù)指定為 Void,表示在執(zhí)行 AsyncTask 的時候不需要傳入?yún)?shù)給后臺任務(wù)。第二個泛型參數(shù)指定為 Integer,表示使用整型數(shù)據(jù)來作為進度顯示單位。第三個泛型參數(shù)指定為 Boolean,則表示使用布爾型數(shù)據(jù)來反饋執(zhí)行結(jié)果。

當然,目前我們自定義的 DownloadTask 還是一個空任務(wù),并不能進行任何實際的操作,我們還需要去重寫 AsyncTask 中的幾個方法才能完成對任務(wù)的定制。經(jīng)常需要去重寫的方法有以下四個:

1. onPreExecute()

這個方法會在后臺任務(wù)開始執(zhí)行之前調(diào)用,用于進行一些界面上的初始化操作,比如顯示一個進度條對話框等。

2. doInBackground(Params...)

這個方法中的所有代碼都會在子線程中運行,我們應(yīng)該在這里去處理所有的耗時任務(wù)。任務(wù)一旦完成就可以通過 return 語句來將任務(wù)的執(zhí)行結(jié)果返回,如果 AsyncTask 的第三個泛型參數(shù)指定的是 Void,就可以不返回任務(wù)執(zhí)行結(jié)果。注意,在這個方法中是不可以進行 UI 操作的,如果需要更新 UI 元素,比如說反饋當前任務(wù)的執(zhí)行進度,可以調(diào)用 publishProgress(Progress...) 方法來完成。

3. onProgressUpdate(Progress...)

當在后臺任務(wù)中調(diào)用了 publishProgress(Progress...) 方法后,這個方法就會很快被調(diào)用,方法中攜帶的參數(shù)就是在后臺任務(wù)中傳遞過來的。在這個方法中可以對 UI 進行操作,利用參數(shù)中的數(shù)值就可以對界面元素進行相應(yīng)地更新。

4. onPostExecute(Result)

當后臺任務(wù)執(zhí)行完畢并通過 return 語句進行返回時,這個方法就很快會被調(diào)用。返回的數(shù)據(jù)會作為參數(shù)傳遞到此方法中,可以利用返回的數(shù)據(jù)來進行一些 UI 操作,比如說提醒任務(wù)執(zhí)行的結(jié)果,以及關(guān)閉掉進度條對話框等。

所以一個比較完整的自定義 AsyncTask 就可以寫成如下方式:

/**
 * Created by   : WGH.
 */
public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {

    @Override
    protected void onPreExecute() {
        progressDialog.show(); // 顯示進度對話框
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        try {
            while (true) {
                int downloadPercent = doDownload();
                publishProgress(downloadPercent);
                if (downloadPercent >= 100) {
                    break;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 在這里更新下載進度
        progressDialog.setMessage("Downloaded " + values[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean result) {
        progressDialog.dismiss(); // 關(guān)閉進度對話框
        // 在這里提示下載結(jié)果
        if (result) {
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show();
        }
    }
}

在這個 DownloadTask 中,我們在 doInBackground() 方法里去執(zhí)行具體的下載任務(wù)。這個方法里的代碼都是在子線程中運行的,因而不會影響到主線程的運行。doDownload() 這個方法用于計算當前的下載進度并返回,我們假設(shè)這個方法已經(jīng)存在了。在得到了當前的下載進度后,下面就該考慮如何把它顯示到界面上了,由于 doInBackground() 方法是在子線程中運行的,在這里肯定不能進行 UI 操作,所以我們可以調(diào)用 publishProgress() 方法并將當前的下載進度傳進來,這樣 onProgressUpdate() 方法就會很快被調(diào)用,在這里就可以進行 UI 操作了。

當下載完成后, doInBackground() 方法會返回一個布爾型變量,這樣 onPostExecute() 方法就會很快被調(diào)用,這個方法也是在主線程中運行的。然后在這里我們會根據(jù)下載的結(jié)果來彈出相應(yīng)的 Toast 提示,從而完成整個 DownloadTask 任務(wù)。

總之使用 AsyncTask 的訣竅就是:在 doInBackground()方法中去執(zhí)行具體的耗時任務(wù),在 onProgressUpdate() 方法中進行 UI 操作,在 onPostExecute() 方法中執(zhí)行一些任務(wù)的收尾工作。

如果想要啟動這個任務(wù),只需編寫以下代碼即可:

new DownloadTask().execute();

以上就是 AsyncTask 的基本用法,我們并不需要去考慮什么異步消息處理機制,也不需要專門使用一個 Handler 來發(fā)送和接收消息,只需要調(diào)用一下 publishProgress() 方法就可以輕松地從子線程切換到 UI 線程了。

注意:雖然 AsyncTask 讓我們的異步工作變得如此簡單,但是使用工作線程時可能會遇到另一個問題,即:運行時配置變更(例如:用戶更改了屏幕方向)導致 Activity 意外重啟,這可能會銷毀工作線程。關(guān)于如何在這種重啟情況下堅持執(zhí)行任務(wù),以及如何在 Activity 被銷毀時正確地取消任務(wù),我們會在接下來的文章當中說明和解決。

五、線程安全方法

在某些情況下,我們實現(xiàn)的方法可能會從多個線程調(diào)用,因此編寫這些方法時必須確保其滿足線程安全的要求。

這一點主要適用于可以遠程調(diào)用的方法,如綁定服務(wù)中的方法。如果對 IBinder 中所實現(xiàn)方法的調(diào)用源自運行 IBinder 的同一進程,則該方法在調(diào)用方的線程中執(zhí)行。但是,如果調(diào)用源自其他進程,則該方法將在從線程池選擇的某個線程中執(zhí)行(而不是在進程的 UI 線程中執(zhí)行),線程池由系統(tǒng)在與 IBinder 相同的進程中維護。例如,即使服務(wù)的 onBind() 方法將從服務(wù)進程的 UI 線程調(diào)用,在 onBind() 返回的對象中實現(xiàn)的方法仍會從線程池中的線程調(diào)用。 由于一個服務(wù)可以有多個客戶端,因此可能會有多個池線程在同一時間使用同一 IBinder 方法。因此,IBinder 方法必須實現(xiàn)為線程安全方法。

同樣,我們之前講解的內(nèi)容提供程序也可接收來自其他進程的數(shù)據(jù)請求。盡管 ContentResolver 和 ContentProvider 類隱藏了如何管理進程間通信的細節(jié),但響應(yīng)這些請求的 ContentProvider 方法(query()、insert()、delete()、update() 和 getType() 方法)將從內(nèi)容提供程序所在進程的線程池中調(diào)用,而不是從進程的 UI 線程調(diào)用。 由于這些方法可能會同時從任意數(shù)量的線程調(diào)用,因此它們也必須實現(xiàn)為線程安全方法。

關(guān)于線程安全方法我們會在之后的文章當中加以說明和解決。

點此進入:GitHub開源項目“愛閱”。

感謝優(yōu)秀的你跋山涉水看到了這里,歡迎關(guān)注下讓我們永遠在一起!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內(nèi)容