9.1 服務是什么
服務(Service)是 Android 中實現程序后台運行的解決方案,它非常適合用於去執行那 些不需要和用戶交互而且還要求長期運行的任務。服務的運行不依賴於任何用戶界面,即使 當程序被切換到后台,或者用戶打開了另外一個應用程序,服務仍然能夠保持正常運行。
不過需要注意的是,服務並不是運行在一個獨立的進程當中的,而是依賴於創建服務 時所在的應用程序進程。當某個應用程序進程被殺掉時,所有依賴於該進程的服務也會停 止運行。
另外,也不要被服務的后台概念所迷惑,實際上服務並不會自動開啟線程,所有的代碼 都是默認運行在主線程當中的。也就是說,我們需要在服務的內部手動創建子線程,並在這 里執行具體的任務,否則就有可能出現主線程被阻塞住的情況。那么本章的第一堂課,我們 就先來學習一下關於 Android 多線程編程的知識。
9.2 Android 多線程編程
熟悉 Java 的你,對多線程編程一定不會陌生吧。當我們需要執行一些耗時操作,比如說發起一條網絡請求時,考慮到網速等其他原因,服務器未必會立刻響應我們的請求,如果
不將這類操作放在子線程里去運行,就會導致主線程被阻塞住,從而影響用戶對軟件的正常 使用。那么就讓我們從線程的基本用法開始學習吧。
9.2.1 線程的基本用法
Android 多線程編程其實並不比 Java 多線程編程特珠,基本都是使用相同的語法。比如 說,定義一個線程只需要新建一個類繼承自 Thread,然后重寫父類的 run()方法,並在里面 編寫耗時邏輯即可,如下所示:
class MyThread extends Thread {
@Override
public void run() {
// 處理具體的邏輯
}
}
那么該如何啟動這個線程呢?其實也很簡單,只需要 new 出 MyThread 的實例,然后調 用它的 start()方法,這樣 run()方法中的代碼就會在子線程當中運行了,如下所示:
new MyThread().start();
當然,使用繼承的方式耦合性有點高,更多的時候我們都會選擇使用實現 Runnable 接 口的方式來定義一個線程,如下所示:
class MyThread implements Runnable {
@Override
public void run() {
// 處理具體的邏輯
}
}
如果使用了這種寫法,啟動線程的方法也需要進行相應的改變,如下所示:
MyThread myThread = new MyThread();
new Thread(myThread).start();
可以看到,Thread 的構造函數接收一個 Runnable 參數,而我們 new 出的 MyThread 正是 一個實現了 Runnable 接口的對象,所以可以直接將它傳入到 Thread 的構造函數里。接着調用 Thread 的 start()方法,run()方法中的代碼就會在子線程當中運行了。當然,如果你不想專門再定義一個類去實現 Runnable 接口,也可以使用匿名類的方式, 這種寫法更為常見,如下所示:
new Thread(new Runnable() {
@Override
public void run() {
// 處理具體的邏輯
}
}).start();
以上幾種線程的使用方式相信你都不會感到陌生,因為在 Java 中創建和啟動線程也是 使用同樣的方式。了解了線程的基本用法后,下面我們來看一下 Android 多線程編程與 Java 多線程編程不同的地方。
9.2.2 在子線程中更新 UI
和許多其他的 GUI 庫一樣,Android 的 UI 也是線程不安全的。也就是說,如果想要更 新應用程序里的 UI 元素,則必須在主線程中進行,否則就會出現異常。眼見為實,讓我們通過一個具體的例子來驗證一下吧。新建一個 AndroidThreadTest 項 目,然后修改 activity_main.xml 中的代碼,如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" >
<Button android:id="@+id/change_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Change Text" />
<TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Hello world" android:textSize="20sp" />
</RelativeLayout>
布局文件中定義了兩個控件,TextView 用於在屏幕的正中央顯示一個 Hello world 字符 串,Button 用於改變 TextView 中顯示的內容,我們希望在點擊 Button 后可以把 TextView 中 顯示的字符串改成 Nice to meet you。
接下來修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends Activity implements OnClickListener {
private TextView text;
private Button changeText;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text);
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;
}
}
}
可以看到,我們在 Change Text 按鈕的點擊事件里面開啟了一個子線程,然后在子線程中調用 TextView 的 setText()方法將顯示的字符串改成 Nice to meet you。代碼的邏輯非常簡 單,只不過我們是在子線程中更新 UI 的。現在運行一下程序,並點擊 Change Text 按鈕,你 會發現程序果然崩潰了,如圖 9.1 所示。
圖 9.1
然后觀察 LogCat 中的錯誤日志,可以看出是由於在子線程中更新 UI 所導致的,如圖 9.2所示。
圖 9.2
由此證實了 Android 確實是不允許在子線程中進行 UI 操作的。但是有些時候,我們必 須在子線程里去執行一些耗時任務,然后根據任務的執行結果來更新相應的 UI 控件,這該 如何是好呢?
對於這種情況,Android 提供了一套異步消息處理機制,完美地解決了在子線程中進行 UI 操作的問題。本小節中我們先來學習一下異步消息處理的使用方法,下一小節中再去分 析它的原理。
修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends Activity implements OnClickListener {
public static final int UPDATE_TEXT = 1;
private TextView text;
private Button changeText;
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
// 在這里可以進行UI操作
text.setText("Nice to meet you");
break;
default:
break;
}
}
};
……
@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對象發送出去
}
}).start();
break;
default:
break;
}
}
}
這里我們先是定義了一個整型常量 UPDATE_TEXT,用於表示更新 TextView 這個動作。 然后新增一個 Handler 對象,並重寫父類的 handleMessage 方法,在這里對具體的 Message 進行處理。如果發現 Message 的 what 字段的值等於 UPDATE_TEXT,就將 TextView 顯示的 內容改成 Nice to meet you。
下面再來看一下 Change Text 按鈕的點擊事件中的代碼。可以看到,這次我們並沒有在 子線程里直接進行 UI 操作,而是創建了一個 Message(android.os.Message)對象,並將它 的 what 字段的值指定為 UPDATE_TEXT ,然后調用 Handler 的 sendMessage() 方法將這條 Message 發送出去。很快,Handler 就會收到這條 Message,並在 handleMessage()方法中對它 進行處理。注意此時 handleMessage()方法中的代碼就是在主線程當中運行的了,所以我們可 以放心地在這里進行 UI 操作。接下來對 Message 攜帶的 what 字段的值進行判斷,如果等於 UPDATE_TEXT,就將 TextView 顯示的內容改成 Nice to meet you。
現在重新運行程序,可以看到屏幕的正中央顯示着 Hello world。然后點擊一下 ChangeText 按鈕,顯示的內容着就被替換成 Nice to meet you,如圖 9.3 所示。
圖 9.3
這樣你就已經掌握了 Android 異步消息處理的基本用法,使用這種機制就可以出色地解決掉在子線程中更新 UI 的問題。不過恐怕你對它的工作原理還不是很清楚,下面我們就來 分析一下 Android 異步消息處理機制到底是如何工作的。
9.2.3 解析異步消息處理機制
Android 中的異步消息處理主要由四個部分組成,Message、Handler、MessageQueue 和 Looper。其中 Message 和 Handler 在上一小節中我們已經接觸過了,而 MessageQueue 和 Looper 對於你來說還是全新的概念,下面我就對這四個部分進行一下簡要的介紹。
1. Message
Message 是在線程之間傳遞的消息,它可以在內部攜帶少量的信息,用於在不同線 程之間交換數據。上一小節中我們使用到了 Message 的 what 字段,除此之外還可以使 用 arg1 和 arg2 字段來攜帶一些整型數據,使用 obj 字段攜帶一個 Object 對象。
2. Handler
Handler 顧名思義也就是處理者的意思,它主要是用於發送和處理消息的。發送消 息一般是使用 Handler 的 sendMessage()方法,而發出的消息經過一系列地輾轉處理后, 最終會傳遞到 Handler 的 handleMessage()方法中。
3. MessageQueue
MessageQueue 是消息隊列的意思,它主要用於存放所有通過 Handler 發送的消息。 這部分消息會一直存在於消息隊列中,等待被處理。每個線程中只會有一個 MessageQueue 對象。
4. Looper
Looper 是每個線程中的 MessageQueue 的管家,調用 Looper 的 loop()方法后,就會 進入到一個無限循環當中,然后每當發現 MessageQueue 中存在一條消息,就會將它取 出,並傳遞到 Handler 的 handleMessage()方法中。每個線程中也只會有一個 Looper 對象。 了解了 Message、Handler、MessageQueue 以及 Looper 的基本概念后,我們再來對異步
消息處理的整個流程梳理一遍。首先需要在主線程當中創建一個 Handler 對象,並重寫 handleMessage()方法。然后當子線程中需要進行 UI 操作時,就創建一個 Message 對象,並 通過 Handler 將這條消息發送出去。之后這條消息會被添加到 MessageQueue 的隊列中等待 被處理,而 Looper 則會一直嘗試從 MessageQueue 中取出待處理消息,最后分發回 Handler 的 handleMessage()方法中。由於 Handler 是在主線程中創建的,所以此時 handleMessage()方 法中的代碼也會在主線程中運行,於是我們在這里就可以安心地進行 UI 操作了。整個異步 消息處理機制的流程示意圖如圖 9.4 所示。
圖 9.4
一條 Message 經過這樣一個流程的輾轉調用后,也就從子線程進入到了主線程,從不能 更新 UI 變成了可以更新 UI,整個異步消息處理的核心思想也就是如此。