Android多線程編程

(內(nèi)容來自《Android第一行代碼(第二版)》)

本文目錄

1. 線程的基本用法

2. 在子線程中更新UI

3. 解析異步消息處理機(jī)制

4. 使用AsyncTask


分割線


當(dāng)我們?cè)诔绦蛑袌?zhí)行一些耗時(shí)操作時(shí),比如發(fā)起一條網(wǎng)絡(luò)請(qǐng)求,考慮到網(wǎng)速等原因,服務(wù)器未必會(huì)立刻響應(yīng)我們的請(qǐng)求,此時(shí)我們就需要將這些操作放在子線程中去運(yùn)行,以防止主線程被阻塞。

1. 線程的基本用法

新建一個(gè)類繼承自Thread,然后重寫父類的run()方法

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

如何啟動(dòng)這個(gè)線程呢,只需要new出一個(gè)MyThread的實(shí)例,然后調(diào)用它的start()方法,這樣run()方法中的代碼就會(huì)在子線程中運(yùn)行了

new MyThread().start();

更多的時(shí)候我們會(huì)選擇使用實(shí)現(xiàn)Runnable接口的方式來定義一個(gè)線程

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

啟動(dòng)該線程的方法

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

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


當(dāng)然如果不想專門定義一個(gè)類去實(shí)現(xiàn)Runnable接口,也可以使用匿名類的方式實(shí)現(xiàn)

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

2. 在子線程中更新UI

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

下面通過一個(gè)具體的例子來驗(yàn)證一下。
新建一個(gè)AndroidThreadTest項(xiàng)目

  • 修改activity_main.xml中的代碼
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/change_text"
        android:text="Change Text"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/text"
        android:layout_centerInParent="true"
        android:textSize="20sp"/>

</RelativeLayout>

布局文件中定義了兩個(gè)控件:
TextView用于在屏幕的正中央顯示一個(gè)Hello World字符串
Button用于改變TextView中顯示的內(nèi)容

我們希望在點(diǎn)擊Button后可以把TextView中顯示的字符串改成Nice to meet you

  • 修改MainActivity中的代碼
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private TextView text;

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

        text = (TextView)findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.change_text:
                new Thread(new Runnable(){
                    @Override
                    public void run(){
                        text.setText("Nice to meet you");
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

我們?cè)?code>Change Text按鈕點(diǎn)擊事件里開啟了一個(gè)子線程,然后在子線程中調(diào)用TextView的setText()方法將顯示的字符串改成Nice to meet you。
代碼邏輯很簡(jiǎn)單,不過我們是在子線程中更新UI的。
運(yùn)行程序發(fā)現(xiàn)程序崩潰了

圖片.png

從日志中我們可以看出是由于在子線程中更新UI導(dǎo)致的。

對(duì)于這種情況,Android提供了一套異步消息處理機(jī)制,完美的解決了在子線程中進(jìn)行UI操作的問題

3. 解析異步消息處理機(jī)制

首先來學(xué)習(xí)一下使用方法

  • 修改MainActivity中的代碼
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private TextView text;

    public static final int UPDATE_TEXT = 1;

    private Handler handler = new Handler(){
        public void handleMessage(Message msg){
            switch (msg.what){
                case UPDATE_TEXT:
                    //在這里進(jìn)行UI操作
                    text.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };

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

        text = (TextView)findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.change_text:
                new Thread(new Runnable(){
                    @Override
                    public void run(){
                       Message message = new Message();
                        message.what = UPDATE_TEXT;
                        handler.sendMessage(message);//將Message對(duì)象發(fā)送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

這里我們先是定義了一個(gè)整型常量UPDATE_TEXT,用于表示更新TextView這個(gè)動(dòng)作。然后新增一個(gè)Handler對(duì)象,并重寫父類的handleMessage()方法,在這里對(duì)具體的Message進(jìn)行處理。如果發(fā)現(xiàn)Message的what字段的值等于UPDATE_TEXT,就將 TextView顯示的內(nèi)容改成Nice to meet you

下面再來看一下 Change Text按鈕的點(diǎn)擊事件中的代碼??梢钥吹?,這次我們并沒有在子線程里直接進(jìn)行UI操作

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

現(xiàn)在重新運(yùn)行程序,發(fā)現(xiàn)UI確實(shí)更新了


20190513_154145.gif

下面來分析一下Android異步消息處理機(jī)制是如何工作的
Android異步消息處理主要由4個(gè)部分組成:Message、Handle、MessageQueue和Looper。

1.Message

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

2.Handler

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

3.MessageQueue

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

4.Looper

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

了解了 Message、 Handler、 Messagequeue以及 Looper的基本概念后,我們?cè)賮戆旬惒较⑻幚淼恼麄€(gè)流程梳理一遍。

  • 首先需要在主線程當(dāng)中創(chuàng)建一個(gè)Handler對(duì)象,并重寫handMessage()方法。
  • 然后當(dāng)子線程中需要進(jìn)行UI操作時(shí),就創(chuàng)建一個(gè) Message對(duì)象,并通過Handler將這條消息發(fā)送出去。
  • 之后這條消息會(huì)被添加到 MessageQueue的隊(duì)列中等待被處理,而Looper則會(huì)一直嘗試從MessageQueue中取出待處理消息,最后分發(fā)回Handler的handleMessage()方法中。

由于Handler是在主線程中創(chuàng)建的,所以此時(shí)handleMessage()方法中的代碼也會(huì)在主線程中運(yùn)行,于是我們?cè)谶@里就可以安心地進(jìn)行UI操作了。

整個(gè)異步消息處理機(jī)制的流程示意圖如圖所示:


異步消息處理機(jī)制的流程示意圖.png

4. 使用AsyncTask

為了更加方便在子線程中對(duì)UI進(jìn)行更新,Android提供了一些好用的工具,比如AsyncTask。
借助AsyncTask,即使對(duì)異步消息處理機(jī)制完全不了解,也可以很簡(jiǎn)單的從子線程切換到主線程。

AsyncTask的基本用法

由于AsyncTask是一個(gè)抽象類,所以如果我們想使用它,就必須創(chuàng)建一個(gè)子類去繼承它。
在繼承時(shí)我們可以為AsyncTask類指定3個(gè)泛型參數(shù)
Params:在執(zhí)行AsyncTask時(shí)需要傳入的參數(shù),可用于在后臺(tái)任務(wù)中使用
Progress:后臺(tái)任務(wù)執(zhí)行時(shí),如果需要在界面上顯示當(dāng)前進(jìn)度,則使用這里指定的泛型作為進(jìn)度單位
Result:當(dāng)任務(wù)執(zhí)行完畢后,如果需要對(duì)結(jié)果進(jìn)行返回,則使用這里指定的泛型作為返回值類型

因此一個(gè)最簡(jiǎn)單的自定義AsyncTask就可以寫成如下方式:

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

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

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

  • onPreExecute()

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

  • doInBackground(Params ..)

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

  • onProgressUpdate(Progress ...)

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

  • onPostExecute(Result)

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

因此,一個(gè)比較完整的自定義AsyncTask就可以寫成如下方式:

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

    @Override
    protected void onPreExecute(){
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(Void... params){
        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){
        //這里更新下載進(jìn)度
        progressDialog.setMessage("Downloaded "+values[0]+"%");
    }

    @Override
    protected void onPostExecute(Boolean result){
        progressDialog.dismiss();
        if(result){
            Toast.makeText(context,"Download succeeded",Toast.LENGTH_SHORT).show();
        }else {
            Toast.makeText(context,"Download failed",Toast.LENGTH_SHORT).show();
        }
    }
}

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

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

簡(jiǎn)單來說,使用AsyncTask的訣竅就是,在doInBackground()方法中執(zhí)行具體的耗時(shí)任務(wù),在onProgressUpdate()方法中進(jìn)行UI操作,在onPostExecute()方法中執(zhí)行一些任務(wù)的收尾工作。

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

new DownloadTask().execute();

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

Demo實(shí)踐環(huán)節(jié)(對(duì)這個(gè)下載功能進(jìn)行完整的實(shí)現(xiàn))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 毫無防備下就降溫了!北風(fēng)呼呼,有些刺骨!冷,只是單調(diào)的冷而已,最多也就連綿幾場(chǎng)雨,時(shí)而寥寥,時(shí)而傾盆,無非也就多添...
    壞心眼的淑女閱讀 348評(píng)論 0 0
  • 《次弟花開》,這本書終于讀完了, 為什么加終于? 說實(shí)話,我從小跟著父母去基督教堂的, 但這本書是讀藏傳佛教的一本...
    小趣大凡閱讀 3,821評(píng)論 1 4
  • 相思銘月閱讀 185評(píng)論 0 0
  • 我曾有一把鋤頭 有一畝田園 那時(shí)候,我便有明明朗朗的春夏秋冬 一粒種子,一顆蔬菜,一株莊稼,便是一年 那時(shí)候,我二...
    瓦力LY閱讀 278評(píng)論 0 1
  • 《影子》 影子很美 回頭看 方便我躲在影子里 方便你看到我心里 —————————— 不能言說的秘密 無可回避的記...
    天鏡泊興閱讀 388評(píng)論 5 4

友情鏈接更多精彩內(nèi)容