文章目錄
- 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使用起來也非常簡單,主要分為三步:
- 建立 AsyncTask 子類 & 根據需求實作核心方法
- 建立 AsyncTask子類的執行個體對象(即 任務執行個體)
- 手動調用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:消息處理器,将消息傳遞給消息隊列,然後再由對應線程從消息隊列中逐個取出資料并執行。
這裡有一張圖比較直覺的說明了它們的關系:
那麼這裡就有一個問題了,在上面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對象建立對外部類成員的所有引用。