天天看點

Android多線程實作-Handler詳解Android常用的多線程實作方法總結

文章目錄

  • Android常用的多線程實作方法
    • 1.Java原生的實作
      • 繼承Thread類與實作Runnable接口的差別
      • 實作Runnable接口與實作Callable接口的差別
    • 2.AsyncTask
    • 3.Handler
      • Handler的基本使用
      • Looper、MessageQueue、Message與Handler
  • 總結

Android常用的多線程實作方法

說起多線程程式設計大家都不陌生,所謂線程就是作業系統能夠進行運算排程的最小機關,它被包含在程序之中,是程序中的實際運作機關。在Android開發中,當我們執行一些耗時操作,比如發起一條網絡請求時,我們不知道伺服器何時會響應請求,這類操作需要放在新的線程執行,以免阻塞主線程(UI線程)。在Android開發中常用的實作多線程的方式有以下幾種:

1.Java原生的實作

作為Android開發常用的語言,Java已經為我們提供了多線程的實作,主要有以下三種方式:

1.繼承Thread類,重寫run方法;

2.實作Runnable接口,重寫run方法,實作Runnable接口的實作類的執行個體對象作為Thread構造函數的target;

3.通過Callable和FutureTask建立線程;

繼承Thread類與實作Runnable接口的差別

那麼這三種方法也是各有特點,實作Runnable接口方法相比于繼承Thread類就有這幾個優勢:

1)由于Java不支援多繼承,是以繼承Thread類之後就不可以再繼承别的類了,而實作Runnable接口方法可以解決單繼承而帶來的局限性;

2)增強程式的健壯性,代碼能夠被多個線程共享,代碼與資料是獨立的

3)适合多個相同程式代碼的線程去處理同一資源的情況

4)線程池隻能放入實作Runable或Callable類線程,不能直接放入繼承Thread的類

針對2)和3)這兩點,我們以賣票系統舉個例子,通過繼承Thread類方式實作多線程如下:

public class MutliThreadDemo {  
    public static void main(String[] args) {  
        MutliThread m1=new MutliThread("Window 1");   
        MutliThread m2=new MutliThread("Window 2");   
        MutliThread m3=new MutliThread("Window 3");   
        m1.start();  
        m2.start();  
        m3.start();  
    }  
}  
           
public class MutliThread extends Thread {  
    private int ticket=100;//每個線程都擁有100張票   
  
    public MutliThread (){}  
    public MutliThread (String name){  
        super(name);  
    }  
      
    @Override  
    public void run() {  
        while(ticket>0){   
            System.out.println(ticket--+" is saled by "+Thread.currentThread().getName());   
        }   
    }  
}  
           

從結果可以看到,每個線程分别對應100張電影票,之間并無任何關系,這就說明每個線程之間是平等的,系統會分别為其配置設定記憶體資源。而如果我們要實作資源共享,也就是說這些線程賣同一份票池,使用這種方法就無法實作了,而使用Runnable接口方式就可以解決這個問題:

public class MutliThreadDemo {  
    public static void main(String[] args) {  
        MutliThread m=new MutliThread();   
        Thread t1=new Thread(m);   
        Thread t2=new Thread(m);   
        Thread t3=new Thread(m);   
        t1.start();   
        t2.start();   
        t3.start();   
    }  
} 
           
public class MutliThread implements Runnable{   
    private int ticket=100;//每個線程都擁有100張票   
    public void run(){   
        while(ticket>0){   
            System.out.println(ticket--+" is saled by "+Thread.currentThread());   
        }   
    }   
}  
           

如上所示,程式在記憶體中僅建立了一份資源,而建立的三個線程都是通路同一資源的。

在第一種方法中中,要想開多個線程,就要先new多個自定義類MutliThread對象,每一個自定義類MyThread的對象的成員變量都相同,這樣需要在棧中開辟很多記憶體;

在第二種方法中中,要想開多個線程,隻需要new一個自定義類MutliThread的對象,再new多個Thread類的對象即可,這樣就大大節約了記憶體。

是以,我們說Runnable接口方式相比于繼承Thread類方式,适合多個相同程式的代碼去處理同一個資源的情況,能夠把線程同程式的代碼和資料有效分離(即耦合性降低),較好的展現了Java面向對象的設計思想。

實作Runnable接口與實作Callable接口的差別

實作Runnable接口和實作Callable接口的差別主要有以下幾點:

1)Runnable是自從java1.1就有了,而Callable是1.5之後才加上去的

2)實作Callable接口的任務線程能傳回執行結果(需要調用FutureTask.get()方法實作,但會阻塞主線程直到擷取傳回結果),而實作Runnable接口的任務線程不能傳回結果

3)Callable接口的call()方法允許抛出異常,而Runnable接口的run()方法的異常隻能在内部消化,不能繼續上抛

4)加入線程池運作,Runnable使用ExecutorService的execute方法,Callable使用submit方法

我們看到相比于Runnable接口方法,Callable接口可以傳回執行結果。是以如果你希望任務在完成的能傳回一個值,那麼可以通過Callable接口實作。Callable接口使用泛型去定義它的傳回類型,它的參數類型表示的是從方法call()(不是run())中傳回的值,下面簡單說一下用用法。

首先定義一個類實作java.util.concurrent包下的Callable接口,然後重寫裡面的call方法(相當于其他方法的run()函數),建立ExecutorService線程池,将自定義類的對象放入線程池裡面,即可擷取線程的傳回結果,最後,關閉線程池,不再接收新的線程,未執行完的線程不會被關閉。簡單的示例如下:

public static void main(String[] args) {
        
    //建立線程池對象
    ExecutorService pool = Executors.newFixedThreadPool(2) ;
        
    //送出任務
    pool.submit(new MyCallable()) ;
    pool.submit(new MyCallable()) ;
        
    //關閉線程池
    pool.shutdown();
}
           
public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        for(int x = 0 ; x < 100 ; x ++) {
            System.out.println(Thread.currentThread().getName()+":"+x);
        }        
        return null;
    }    
}
           

2.AsyncTask

AsyncTask是一個Android 已封裝好的輕量級的異步類,可以用來實作多線程及多線程通信,通常用來實作一些較簡單的耗時操作(如:倒計時,下載下傳等)。它是一個抽象類,需要我們在使用的時候做具體實作。

public abstract class AsyncTask<Params, Progress, Result> { 
 ... 
 }
           

如上所示,AsyncTask擁有三個泛型參數:

  • Params 啟動任務執行的輸入參數,比如下載下傳URL;
  • Progress 背景任務執行的百分比,比如下載下傳進度;
  • Result 背景執行任務最終傳回的結果,比如下載下傳結果。

AsyncTask的常用方法有以下幾個:

onPreExecute方法:

異步任務開始執行時,系統最先調用此方法。此方法運作在主線程中,可以對控件進行初始化等操作。

dolnBackground方法:

執行完onPreExecute方法後,系統執行此方法。此方法運作在子線程中,比較耗時的操作放在此方法中執行。

onProgressUpdate方法:

1.顯示目前進度,适用于下載下傳或掃描這類需要實時顯示進度的需求。

2.此方法運作在主線程中,可以修改控件狀态,例如: 顯示百分比。

3.觸發此方法,需要在dolnBackground中使用publishProgress方法。

publishProgress方法:

在doInBackground中使用。用于觸發onProgressUpdate方法。

on PostExecute方法:

當異步任務執行完成後,系統會調用此方法。此方法運作在主線程中,可以修改控件狀态,例如: 下載下傳完成。

那麼AsyncTask使用起來也非常簡單,主要分為三步:

  1. 建立 AsyncTask 子類 & 根據需求實作核心方法
  2. 建立 AsyncTask子類的執行個體對象(即 任務執行個體)
  3. 手動調用execute(()進而執行異步線程任務

看起來還是很簡單的,下面是一個網上找的簡單例子。

/**
  * 步驟1:建立AsyncTask子類
  * 注: 
  *   a. 繼承AsyncTask類
  *   b. 為3個泛型參數指定類型;若不使用,可用java.lang.Void類型代替
  *   c. 根據需求,在AsyncTask子類内實作核心方法
  */

 public class MainActivity extends AppCompatActivity {

    // 線程變量
    MyTask mTask;

    // 主布局中的UI元件
    Button button,cancel; // 加載、取消按鈕
    TextView text; // 更新的UI元件
    ProgressBar progressBar; // 進度條
    
    /**
     * 步驟1:建立AsyncTask子類
     * 注:
     *   a. 繼承AsyncTask類
     *   b. 為3個泛型參數指定類型;若不使用,可用java.lang.Void類型代替
     *      此處指定為:輸入參數 = String類型、執行進度 = Integer類型、執行結果 = String類型
     *   c. 根據需求,在AsyncTask子類内實作核心方法
     */
    private class MyTask extends AsyncTask<String, Integer, String> {

        // 方法1:onPreExecute()
        // 作用:執行 線程任務前的操作
        @Override
        protected void onPreExecute() {
            text.setText("加載中");
            // 執行前顯示提示
        }


        // 方法2:doInBackground()
        // 作用:接收輸入參數、執行任務中的耗時操作、傳回 線程任務執行的結果
        // 此處通過計算進而模拟“加載進度”的情況
        @Override
        protected String doInBackground(String... params) {

            try {
                int count = 0;
                int length = 1;
                while (count<99) {

                    count += length;
                    // 可調用publishProgress()顯示進度, 之後将執行onProgressUpdate()
                    publishProgress(count);
                    // 模拟耗時任務
                    Thread.sleep(50);
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        // 方法3:onProgressUpdate()
        // 作用:在主線程 顯示線程任務執行的進度
        @Override
        protected void onProgressUpdate(Integer... progresses) {

            progressBar.setProgress(progresses[0]);
            text.setText("loading..." + progresses[0] + "%");

        }

        // 方法4:onPostExecute()
        // 作用:接收線程任務執行結果、将執行結果顯示到UI元件
        @Override
        protected void onPostExecute(String result) {
            // 執行完畢後,則更新UI
            text.setText("加載完畢");
        }

        // 方法5:onCancelled()
        // 作用:将異步任務設定為:取消狀态
        @Override
        protected void onCancelled() {

            text.setText("已取消");
            progressBar.setProgress(0);

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 綁定UI元件
        setContentView(R.layout.activity_main);

        button = (Button) findViewById(R.id.button);
        cancel = (Button) findViewById(R.id.cancel);
        text = (TextView) findViewById(R.id.text);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);

        /**
         * 步驟2:建立AsyncTask子類的執行個體對象(即 任務執行個體)
         * 注:AsyncTask子類的執行個體必須在UI線程中建立
         */
        mTask = new MyTask();

        // 加載按鈕按按下時,則啟動AsyncTask
        // 任務完成後更新TextView的文本
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                /**
                 * 步驟3:手動調用execute(Params... params) 進而執行異步線程任務
                 * 注:
                 *    a. 必須在UI線程中調用
                 *    b. 同一個AsyncTask執行個體對象隻能執行1次,若執行第2次将會抛出異常
                 *    c. 執行任務中,系統會自動調用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
                 *    d. 不能手動調用上述方法
                 */
                mTask.execute();
            }
        });

        cancel = (Button) findViewById(R.id.cancel);
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 取消一個正在執行的任務,onCancelled方法将會被調用
                mTask.cancel(true);
            }
        });
    }

}
           

布局檔案我沒有貼,主要包括兩個button和一個進度條,這個例子實作了一個進度條的實時更新,點選一個按鈕開始,調用AsyncTask的execute()方法開始異步請求,然後再doInBackground方法中增加進度,然後通過publishProgress方法将進度發送,這樣onProgressUpdate(progresses)中可以通過progresses擷取目前緊毒并對UI進行更新,最後,點選停止按鈕時調用cancel方法取消這個任務。

AsyncTask很容易引起記憶體洩漏,如果AsyncTask被聲明為Activity的非靜态的内部類,那麼AsyncTask會保留一個對建立了AsyncTask的Activity的引用。如果Activity已經被銷毀,AsyncTask的背景線程還在執行,它将繼續在記憶體裡保留這個引用,導緻Activity無法被回收,引起記憶體洩露。

為了避免記憶體洩漏,我們在使用時要特别注意:第一,在Activity生命周期結束前,去cancel AsyncTask。第二,如果一定要寫成内部類的形式,對context采用WeakRefrence,在使用之前判斷是否為空。

3.Handler

AsyncTask使用的優點是簡單,快捷,過程可控。使用的缺點是在使用多個異步操作和并需要進行UI變更時,就會變得複雜起來,而接下來要說的Handler,結構清晰,對于多個背景任務時簡單清晰。是以一般進行資料量多且複雜的操作就使用Handler,Handler也是Android面試經常考查的知識點。

Handler是Android 提供的一種異步通信機制,我們經常用它将工作線程中需更新UI的操作資訊 傳遞到 UI主線程(隻有主線程能更新UI),實作工作線程對UI的更新處理,例如:在UI界面上進行某項操作後要執行一段很耗時的代碼,需要根據執行結果來更新UI,由于這是一個耗時操作,我們不知道什麼時候才能完成,這個時候Handler就派上用場了。

Handler的基本使用

Handler有兩種實作方式,一種是調用sendMessage方式,一種是通過post方式。基本實作如下:

sendMessage方式:

android.os.Handler handler = new Handler(){
  @Override
  public void handleMessage(final Message msg) {
    //這裡接受并處理消息,如更新UI等操作
  }
};
//發送消息
handler.sendMessage(message);
           

post方式:

// 在主線程中建立Handler執行個體
    private Handler mhandler = new mHandler();

    // 在工作線程中發送消息到消息隊列中并進行更新UI的操作
    mHandler.post(new Runnable() {
            @Override
            public void run() {
                // 需執行的UI操作 
            }
    });
           

Handler使用起來非常簡單友善,但實作原理我們也是要知道的,這裡就涉及到Looper、MessageQueue、Message這幾個Handler中的重要概念,下面來詳細介紹:

Looper、MessageQueue、Message與Handler

Looper、MessageQueue、Message和Handler是handler中重要的四個類,他們分工合作實作消息的發送和處理,下面是各自的作用:

Looper:消息循環器,循環取出消息(Message),将取出的消息分發給對應的處理者(Handler)。

Message:消息,線程間通訊的資料單元。

MessageQueue:消息隊列,存儲Handler發送過來的消息(Message),如其名字,是一個先進先出的隊列。

Handler:消息處理器,将消息傳遞給消息隊列,然後再由對應線程從消息隊列中逐個取出資料并執行。

這裡有一張圖比較直覺的說明了它們的關系:

Android多線程實作-Handler詳解Android常用的多線程實作方法總結

那麼這裡就有一個問題了,在上面handler的基本使用中并沒有看到Looper相關調用,那Looper是如何工作的呢。其實是在主線程使用Handler系統在啟動App時已經把這些提前為我們做好了,是以我們平常在主線程不需要手動調用,在ActivityThread.main方法中會調用到Looper.prepareMainLooper()和Looper.loop(),必須執行這兩步使Looper完成啟動才可以正常的使用,如果我們在子線程裡使用Handler,必須手動執行這兩個方法,否則會報錯。

按照這個流程,我們先來看首先調用的Looper.prepare()裡面都做了什麼:

public static void prepare() {
        prepare(true);
    }
    private static void prepare(boolean quitAllowed) {
        // 規定了一個線程隻有一個Looper,也就是一個線程隻能調用一次Looper.prepare()
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 如果目前線程沒有Looper,那麼就建立一個,存到sThreadLocal中
        sThreadLocal.set(new Looper(quitAllowed));
    }
           

從上面的代碼可以看出,一個線程隻能有一個Looper對象。當沒有Looper對象時,去建立一個Looper,并存放到sThreadLocal中,sThreadLocal是一個ThreadLocal對象,ThreadLocal是一個線程内部的存儲類,它提供了線程記憶體儲變量的能力,這些變量不同之處在于每一個線程讀取的變量是對應的互相獨立的。通過get和set方法就可以得到目前線程對應的值。在Handler裡它用來存儲Looper對象的副本,并且可以通過它取得目前線程在之前存儲的Looper的副本。

在Looper.prepare()中建立了一個Looper對象,下面我們來看Looper的構造方法:

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
           

這裡的實作很簡單,主要就是建立了消息隊列MessageQueue,并讓它供Looper持有,因為一個線程最大隻有一個Looper對象,是以一個線程最多也隻有一個消息隊列。然後再把目前線程指派給mThread。而MessageQueue的構造方法就是建立了一個消息隊列,用于存放Message。

Looper就緒後就可以建立Handler了,Handler最基本的構造方法如下:

public Handler() {
        this(null, false);
    }

    public Handler(Callback callback, boolean async) {
      // 不相關代碼
       ......
        //得到目前線程的Looper,其實就是調用的sThreadLocal.get
        mLooper = Looper.myLooper();
        // 如果目前線程沒有Looper就報運作時異常
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        // 把得到的Looper的MessagQueue讓Handler持有
        mQueue = mLooper.mQueue;
        // 初始化Handler的Callback
        mCallback = callback;
        mAsynchronous = async;
    }
           

首先,調用了Looper.myLooper,其實就是調用sThreadLocal.get方法,來得到目前線程調用儲存的Looper對象,并讓Handler持有它。接下來對該Looper對象進行非空判斷,避免如沒有調用Looper.prepare()建立Looper對象。然後,讓Handler持有Looper對象的MessageQueue,最後設定處理回調的Callback對象。這些都準備好後,我們來看loop()方法:

public static void loop() {
        // 得到目前線程的Looper對象
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 得到目前線程的MessageQueue對象
        final MessageQueue queue = me.mQueue;
        
        // 無關代碼
        ......
        
        // 死循環
        for (;;) {
            // 不斷從目前線程的MessageQueue中取出Message,當MessageQueue沒有元素時,方法阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // Message.target是Handler,其實就是發送消息的Handler,這裡就是調用它的dispatchMessage方法
            msg.target.dispatchMessage(msg);
            // 回收Message
            msg.recycleUnchecked();
        }
    }
           

首先還是判斷了目前線程是否有Looper,然後得到目前線程的MessageQueue。接下來,就是最關鍵的代碼了,寫了一個死循環,不斷調用MessageQueue的next方法取出MessageQueue中的Message,拿到Message以後,會調用它的target的dispatchMessage方法,這個target其實就是發送消息時用到的Handler。是以就是調用Handler的dispatchMessage方法,這個方法最終繪調用到handleMessage方法,也就是我們使用時重寫的那個方法。

最後就是發送消息了,上面說過使用Handler發送消息主要有兩種,不過兩種方式最後都會調用到sendMessageDelayed方法,是以我們就以最簡單的sendMessage方法來分析。

public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        // 這裡拿到的MessageQueue其實就是建立時的MessageQueue,預設情況是目前線程的Looper對象的MessageQueue
        // 也可以指定
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        // 調用enqueueMessage,把消息加入到MessageQueue中
        return enqueueMessage(queue, msg, uptimeMillis);
    }
           

可見其主要實作是調用enqueueMessage來實作的,這裡面會調用MessageQueue的enqueueMessage方法,我們來看一下它的實作:

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        // 一個Message,隻能發送一次
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                msg.recycle();
                return false;
            }
            // 标記Message已經使用了
            msg.markInUse();
            msg.when = when;
            // 得到目前消息隊列的頭部
            Message p = mMessages;
            boolean needWake;
            // 我們這裡when為0,表示立即處理的消息
            if (p == null || when == 0 || when < p.when) {
                // 把消息插入到消息隊列的頭部
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // 根據需要把消息插入到消息隊列的合适位置,通常是調用xxxDelay方法,延時發送消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                // 把消息插入到合适位置
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // 如果隊列阻塞了,則喚醒
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
           

内容很多,總結一下就是把Message加入到MessageQueue并按照目前的情況放到合适的位置。

總結

以上就是對三種常用的多線程實作的一個學習總結,相信這樣總結過後對它們都有了更深刻的了解,由于Handler在我平常項目使用上頻率較高,而且據說是面試經常考察的點,本篇文章也是對其着墨較多,并特别的從源碼的角度進行了一定了解,但是使用時也要注意,由于Handler一般定義為Activity的内部類,會持有外部類的引用,是以很容易引起記憶體洩漏,這裡官方給出了避免引起記憶體洩漏的建議是:

将Handler聲明為靜态類; 在外部類中,執行個體化WeakReference到外部類并在執行個體化Handler時将此對象傳遞給Handler,使用WeakReference對象建立對外部類成員的所有引用。