後臺默默的勞動者,探究服務
記得在幾年前,iPhone 屬於少數人才擁有的稀有物品,Android 甚至還沒面世,那個時候全球的手機市場是由諾基亞的 Symbian 作業系統做得特別出色,因為比起一般的手機,它可以支援後臺功能。那個時候能夠一邊打著電話、聽著音樂,一邊在後臺掛著 QQ 是件非常酷的事情。所以我也曾經單純地認為,支援後臺的手機就是智慧手機。
而如今,Symbian 已經風光不再,Android 和 IOS 佔據了大部分的只能市場份額,Windows Phone 也佔據了一部分,目前已是三分天下的局面。在這三大智慧手機作業系統中,IOS 是不支援後臺的,當應用程式不在前臺執行時就會進入到掛起狀態。Android 則是沿用了 Symbian 的老習慣,加入了後臺功能,這使得應用程式即使在關閉的情況下仍然可以在後臺繼續執行。而 Windows Phone 則是經歷了一個由不支援到支援後臺的過程,目前 Windows Phone 8 系統也是具備後臺功能的。這裡我們不會花時間去辯論到底誰的方案更好,既然 Android 提供了這個功能,而且是一個非常重要的元件,那我們自然要去學習一下它的用法了。
1. 服務是什麼
服務(Service)是 Android 中實現程式後臺執行的解決方案,它非常適合用於去執行那些不需要和使用者互動還要求長期執行的任務。服務的執行不依賴於任何使用者介面,即使當程式被切換到後臺,或者使用者打開了另外一個應用程式,服務仍然能夠保持正常執行。
不過需要注意的是,服務並不是執行在一個獨立的程序當中的,而是依賴於建立服務時所在的應用程式程序。當某個應用程式程序被殺掉時,所有依賴於該程序的服務也會停止執行。
另外,也不要被服務的後臺概念所迷惑,實際上服務並不會自動開啟執行緒,所有的程式碼都是預設執行在主執行緒當中的。也就是說,
2. Android 多執行緒程式設計
熟悉 Java 的你,對多執行緒程式設計一定不會陌生吧。當我們需要執行一些耗時操作,比如說發起一條網路請求時,考慮到網速等其他原因,伺服器未必會立刻響應我們的請求,如果不將這類操作放在子執行緒裡去執行,就會導致主執行緒被阻塞住,從而影響使用者對軟體的正常使用。那麼久讓我們從執行緒的基本用法開始學習吧。
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 多執行緒程式設計不同的地方。
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 按鈕,你會發現程式果然崩潰了。然後觀察 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
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() {
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。然後點選一下 Change Text 按鈕,顯示的內容就被替換成 Nice to meet you。
這樣你就已經掌握了 Android 非同步訊息處理的基本用法,使用這種機制就可以出色地解決掉在子執行緒中更新 UI 的問題。不過恐怕你對它的工作原理還不是很清楚,下面我們就來分析一下 Android 非同步訊息處理機制到底是如何工作的。
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,整個非同步訊息處理的核心思想也就是如此。
2.4 使用 AsyncTask
不過為了更加方便我們在子執行緒中對 UI 進行操作,Android 還提供了另外一些好用的工具,AsyncTask 就是其中之一。藉助 AsyncTask,即使你對非同步訊息處理機制完全不瞭解,也可以十分簡單地從子執行緒切換到主執行緒。當然,AsyncTask 背後的實現原理也是基於非同步訊息處理機制的,只是 Android 幫我們做了很好的封裝而已。
首先來看一下 AsyncTask 的基本用法,由於 AsyncTask 是一個抽象類,所以如果我們想使用它,就必須要建立一個子類去繼承它。在繼承時我們可以為 AsyncTask 類指定三個泛型引數,這三個引數的用途如下:
- Params
在執行 AsyncTask 時需要傳入的引數,可用於在後臺任務中使用。
- Progress
後臺任務執行時,如果需要在介面上顯示當前的進度,則使用這裡指定的泛型作為進度單位。
- Result
當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。
因此,一個最簡單的自定義 AsyncTask 就可以寫成如下方式:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
......
}
這裡我們把 AsyncTask 的第一個泛型引數指定為 Void,表示在執行 AsyncTask 的時候不需要傳入引數給後臺任務。第二個泛型引數指定為 Integer,表示使用整型資料來作為進度顯示單位。第三個泛型引數指定為 Boolean,則表示使用布林型資料來反饋執行結果。
當然,目前我們自定義的 DownloadTask 還是一個空任務,並不能進行任何實際的操作,我們還需要去重寫 AsyncTask 中的幾個方法才能完成對任務的定製。經常需要去重寫的方法有以下四個。
- onPreExecute()
這個方法會在後臺任務開始執行之前呼叫,用於進行一些介面上的初始化操作,比如顯示一個進度條對話方塊等。
- doInBackground(Params...)
這個方法中的所有程式碼都會在子執行緒中執行,我們應該在這裡去處理所有的耗時任務。任務一旦完成就可以通過 return 語句來將任務的執行結果返回,如果 AsyncTask 的第三個泛型引數指定的是 Void,就可以不返回任務執行結果。注意,在這個方法中是不可以進行 UI 操作的,如果需要更新 UI 元素,比如說反饋當前任務的執行進度,可以呼叫 publishProgress(Progress...)方法來完成。
- onProgressUpdate(Progress...)
當在後臺任務中呼叫 publishProgress(Progress...) 方法後,這個方法就會很快被呼叫,方法中攜帶的引數就是在後臺任務中傳遞過來的。在這個方法中可以對 UI 進行操作,利用引數中的數值就可以對介面元素進行相應地更新。
- onPostExecute(Result)
當後臺任務執行完畢並通過 return 語句進行返回時,這個方法就很快會被呼叫。返回的資料會作為引數傳遞到此方法中,可以利用返回的資料來進行一些 UI 操作,比如說提醒任務執行的結果,以及關閉掉進度條對話方塊等。
因此,一個比較完整的自定義 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) {
// 在這裡更新下載進度
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();
}
}
}
在這個 DownloadTask 中,我們在 doInBackground() 方法裡去執行具體的下載任務。這個方法裡的程式碼都是在子執行緒中執行的,因而不會影響到主執行緒的執行。注意這裡虛構了一個 doDownload() 方法,這個方法用於計算當前的下載進度並返回,我們假設這個方法已經存在了。在得到了當前的下載進度後,下面就該考慮如何把它顯示到介面上了,由於 doInBackground() 方法是在子執行緒中執行的,在這裡肯定不能進行 UI 操作,所以我麼可以呼叫 publishProgress() 方法並將當前的下載進度傳進來,這樣 onProgressUpdate() 方法就會很快被呼叫,在這裡就可以進行 UI 操作了。
當下載完成後,doInBackground() 方法會返回一個布林型變數,這樣 onPostExecute() 方法就會很快被呼叫,這個方法也是在主執行緒中執行的。然後在這裡我們會根據下載的結果來彈出相應的 Toast 提示,從而完成整改 DownloadTask 任務。
簡單來說,使用 AsyncTask 的訣竅就是,在 donInBackground() 方法中去執行具體的耗時任務,在 onProgressUpdate() 方法中進行 UI 操作,在 onPostExecute() 方法中執行一些任務的收尾工作。
如果想要啟動這個任務,只需編寫以下程式碼即可:
new DownloadTask().execute();
以上就是 AsyncTask 的基本用法,怎麼樣,是不是感覺簡單方便了許多?我們並不需要去考慮什麼非同步訊息處理機制,也不需要專門使用一個 Handler 來發送和接收訊息,只需要呼叫一下 publishProgress() 方法就可以輕鬆地從子執行緒切換到 UI 執行緒了。
3. 服務的基本用法
瞭解了 Android 多執行緒程式設計的技術之後,下面就讓我們計入到本章的正題,開始對服務的相關內容進行學習。作為 Android 四大元件之一,服務也少不了有很多非常重要的知識點,那我們自然要從最基本的用法開始學習了。
3.1 定義一個服務
首先看一下如何在專案中定義一個服務。新建一個 ServiceTest 專案,然後在這個專案中新增一個名為 MyService 的類,並讓它繼承自 Service,完成後的程式碼如下所示:
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
目前 MyService 中可以算是空空如也,但有一個 onBind() 方法特別醒目。這個方法是 Service 中唯一一個抽象方法,所以必須要在子類裡實現。我們會在後面的小節中使用到 onBind() 方法,目前可以暫時將它忽略掉。
既然是定義一個服務,自然應該在服務中去處理一些事情了,那處理事情的邏輯應該寫在哪裡呢?這是就可以重寫 Service 中的另外一些方法了,如下所示:
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
可以看到,這裡我們又重寫了 onCreate()、onStartCommand() 和onDestroy()這三個方法,它們是每個服務中最常用到的三個方法了。其中onCreate()
方法會在服務銷燬的時候呼叫,onStartCommand() 方法會在每次服務啟動的時候呼叫,onDestroy() 方法會在服務銷燬的時候呼叫。
通常情況下,如果我們希望服務一旦啟動就立刻去執行某個動作,就可以將邏輯寫在 onStartCommand() 方法裡。而當服務銷燬時,我們又應該在 onDestroy() 方法中去回收那些不再使用的資源。
另外需要注意,每一個服務都需要在 AndroidManifest.xml 檔案中進行註冊才能生效,不知道你有沒有發現,這是 Android 四大元件共有的特點。於是我們還應該修改 AndroidManifest.xml 檔案,程式碼如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest"
android:versionCode="1"
android:versionName="1.0" >
......
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
<service android:name=".MyService" >
</service>
</application>
</manifest>
這樣的話,就已經將一個服務完全定義好了。
3.2 啟動和停止服務
定義好了服務之後,接下來就應該考慮如何去啟動以及停止這個服務。啟動和停止的方法當然你也不會陌生,主要是藉助 Intent 來實現的,下面就讓我們在 ServiceTest 專案中嘗試去啟動以及停止 MyService 這個服務。
首先修改 activity_main.xml 中的程式碼,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/start_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Service" />
<Button
android:id="@+id/stop_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Service" />
</LinearLayout>
這裡我們在佈局檔案中加入了兩個按鈕,分別是用於啟動服務和停止服務的。
然後修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity implements OnClickListener {
private Button startService;
private Button stopService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService = (Button) findViewById(R.id.start_service);
stopService = (Button) findViewById(R.id.stop_service);
startService.setOnClickListener(this);
stopService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 啟動服務
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服務
break;
default:
break;
}
}
}
可以看到,這裡在 onCreate() 方法中分別獲取到了 Start Service 按鈕和 Stop Service 按鈕的例項,並給它們註冊了點選事件。然後在 Start Service 按鈕的點選事件裡,我們構建出了一個 Intent 物件,並呼叫 startService() 方法來啟動 MyService 這個服務。在 Stop Service 按鈕的點選事件裡,我們同樣構建出了一個 Intent 物件,並呼叫 stopService() 方法來停止 MyService 這個服務。startService() 和 stopService() 方法都是定義在 Context 類中的,所以我們在 Activity 裡可以直接呼叫這兩個方法。注意,這裡完全是由 Activity 來決定服務何時停止的,如果沒有點選 Stop Service 按鈕,服務就會一直處於執行狀態。那服務有沒有什麼辦法讓自己停止下來呢?當然可以,只需要在 MyService 的任何一個位置呼叫 stopSelf() 方法就能讓這個服務停止下來了。
那麼接下來又有一個問題需要細看了,我們如何才能證實服務已經成功啟動或者停止了呢?最簡單的方法就是在 MyService 的幾個方法中加入列印日誌,如下所示:
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService", "onStartCommand executed");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService", "onDestroy executed");
}
}
現在可以執行一下程式來進行測試了,程式的主介面如圖 9.5 所示。
圖 9.5
點選一下 Start Service 按鈕,觀察 LogCat 中的列印日誌如圖 9.6 所示。
圖 9.6
MyService 中的 onCreate() 和 onStartCommand() 方法都執行了,說明這個服務確實已經啟動成功了,並且你還可以在正在執行的服務列表中找到它,如圖 9.7 所示。
圖 9.7
然後再點選一下 Stop Service 按鈕,觀察 LogCat 中的列印日誌如圖 9.8 所示。
圖 9.8
由此證明,MyService 確實已經成功停止下來了。
話說回來,雖然我們已經學會了啟動服務以及停止服務額方法,不知道你心裡現在有沒有一個疑惑,那就是 onCreate() 方法和 onStartCommand() 到底有什麼區別呢?因為剛剛點選 Start Service 按鈕後兩個方法都執行了。
其實 onCreate() 方法是在服務第一次建立的時候呼叫的,而onStartCommand() 方法則再每次啟動服務的時候都會呼叫,由於剛才我們是第一次點選 Start Service 按鈕,服務此時還未建立過,所以兩個方法都會執行,之後如果你再連續多點選幾次 Start Service 按鈕,你就會發現只有 onStartCommand() 方法可以得到執行了。
3.3 活動和服務進行通訊
上一小節中我們學習了啟動和停止服務的方法,不知道你有沒有發現,雖然服務是在活動裡啟動的,但在啟動服務之後,活動與服務基本就沒有什麼關係了。確實如此,我們在活動裡呼叫了 startService() 方法來啟動 MyService 這個服務,然後 MyService 的 onCreate() 和 onStartCommand() 方法就會得到執行。之後服務會一直處於執行狀態,但具體執行的是什麼邏輯,活動就控制不了了。這就類似於活動通知了服務一下:“你可以啟動了!”然後服務就去忙自己的事情了,但活動並不知道服務到底去做了什麼事情,以及完成的如何。
那麼有沒有什麼辦法能讓活動和服務的關係更緊密一些呢?例如在活動中指揮服務去幹什麼,服務就去幹什麼。當然可以,這就需要藉助我們剛剛忽略的 onBind() 方法了。
比如說目前我們希望在 MyService 裡提供一個下載功能,然後在活動中可以決定何時開始下載,以及隨時檢視下載進度。實現這個功能的思路是建立一個專門的 Binder 物件來對下載功能進行管理,修改 MyService 中的程式碼,如下所示:
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload() {
Log.d("MyService", "startDownload executed");
}
public int getProgress() {
Log.d("MyService", "getProgress executed");
return 0;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
......
}
可以看到,這裡我們新建了一個 DownloadBinder 類,並讓它繼承自 Binder,然後在它的內部提供了開始下載以及檢視下載進度的方法。當然這只是模擬方法,並沒有實現真正的功能,我們在這兩個方法中分別列印了一行日誌。
接著,在 MyService 中建立了 DownloadBinder 的例項,然後在onBind() 方法裡返回了這個例項,這樣 MyService 中的工作就全部完成了。
下面就要看一看,在活動中如何去呼叫服務裡的這些方法了。首先需要在佈局檔案裡新增兩個按鈕,修改 activity_main.xml 中的程式碼,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
......
<Button
android:id="@+id/bind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bind Service" />
<Button
android:id="@+id/unbind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unbind Service" />
</LinearLayout>
這兩個按鈕分別是用於繫結服務和取消繫結服務的,那到底誰需要去和服務繫結呢?當然就是活動了。當一個活動和服務綁定了之後,就可以呼叫該服務裡的 Binder 提供的方法了。修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity implements OnClickListener {
private Button startService;
private Button stopService;
private Button bindService;
private Button unbindService;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
......
bindService = (Button) findViewById(R.id.bind_service);
unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
......
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE); // 繫結服務
break;
case R.id.unbind_service:
unbindService(connection); // 解綁服務
break;
default:
break;
}
}
}
可以看到,這裡我們首先建立了一個 ServiceConnection 的匿名類,在裡面重寫了onServiceConnected() 方法和onServiceDisconnected() 方法,這兩個方法分別會在活動與服務成功繫結以及解除繫結的時候呼叫。在 onServiceConnected() 方法中,我們又通過向下轉型得到了 DownloadBinder 的例項,有了這個例項,活動和服務之間的關係就變得非常緊密了。現在我們可以在活動中根據具體的場景來呼叫 DownloadBinder 中的任何 public 方法,即實現了指揮服務幹什麼,服務就去幹什麼的功能。這裡仍然只是做了個簡單的測試,在 onServiceConnected() 方法中呼叫了 DownloadBinder 的 startDownload() 和 getProgress() 方法。
當然,現在活動和服務其實還沒進行繫結呢,這個功能是在 Bind Service 按鈕的點選事件裡完成的。可以看到,這裡我們仍然是構建出了一個 Intent 物件,然後呼叫 bindService() 方法將 MainActivity 和 MyService 進行繫結。bindService() 方法接收三個引數,第一個引數就是剛剛構建出的 Intent 物件,第二個引數是前面創建出的 ServiceConnection 的例項,第三個引數是一個標誌位,這裡傳入 BIND_AUTO_CREATE 表示在活動和服務進行繫結後自動建立服務。這會使得 MyService 中的 onCreate() 方法得到執行,但 onStartCommand() 方法不會執行。
然後如果我們想解除活動和服務之間的繫結該怎麼辦呢?呼叫一下 unbindService() 方法就可以了,這也是 Unbind Service 按鈕的點選事件裡實現的功能。
現在讓我們重新執行一下程式吧,介面如圖 9.9 所示。
圖 9.9
點選一下 Bind Service 按鈕,然後觀察 LogCat 中的列印日誌如圖 9.10 所示:
圖 9.10
可以看到,首先是 MyService 的 onCreate() 方法得到了執行,然後 startDownload() 和 getProgress() 方法都得到了執行,說明我們確實已經在活動裡成功呼叫了服務裡提供的方法了。
另外需要注意,任何一個服務在整個應用程式範圍內都是通用的,即MyService 不僅可以和 MainActivity 繫結,還可以和任何一個其他的活動進行繫結,而且在繫結完成後它們都可以獲取到相同的 DownloadBinder 例項。
4. 服務的生命週期
之前我們學習過了活動以及碎片的生命週期。類似地,服務也有自己的生命週期,前面我們使用到的 onCreate()、onStartCommand()、onBind() 和onDestroy()等方法都是在服務的生命週期內可能回撥的方法。
一旦在專案的任何位置呼叫了 Context 的 startService() 方法,相應的服務就會啟動起來,並回調 onStartCommand() 方法。如果這個服務之前還沒有建立過,onCreate() 方法會先於 onStartCommand() 方法執行。服務啟動了之後還沒有建立過,onCreate() 方法會先於 onStartCommand() 方法執行。服務啟動了之後會一直保持執行狀態,知道 stopService() 或 stopSelf() 方法,服務就會停止下來了。
另外,還可以呼叫 Context 的 bindService() 來獲取一個服務的持久連線,這時就會回撥服務中的 onBind() 方法。類似地,如果這個服務之前還沒有建立過,onCreate() 方法會先於 onBind() 方法執行。之後,呼叫方可以獲取到 onBind() 方法裡返回的 IBinder 物件的例項,這樣就能自由地和服務進行通訊了。只要呼叫方和服務之間的連線沒有斷開,服務就會一直保持執行狀態。
當呼叫了 startService() 方法後,又去呼叫 stopService() 方法,這時服務中的 onDestroy() 方法就會執行,表示服務已經銷燬了。類似地,當呼叫了 bindService() 方法後,又去呼叫 unbindService() 方法,onDestroy() 方法也會執行,這兩種情況都很好理解。但是需要注意,我們是完全有可能對一個服務既呼叫了 startService(),又呼叫了 bindService() 方法的,這種情況下該如何才能讓服務銷燬掉呢?根據 Android 系統的機制,一個服務只要被啟動或者綁定了之後,就會一直處於執行狀態,必須要讓以上兩種條件同時不滿足,服務才能被銷燬。所以,這種情況下要同時呼叫 stopService() 和 unbindService() 方法,onDestroy() 方法才會執行。
這樣你就已經把服務的生命週期完整地走了一遍。
5. 服務的更多技巧
以上所學的都是關於服務最基本的一些用法和概念,當然也是最常用的。不過,僅僅滿足於此顯然是不夠的,服務的更多高階使用技巧還在等著我們呢,下面就趕快去看一看吧。
5.1 使用前臺服務
服務幾乎都是在後臺執行的,一直以來它都是默默地做著辛苦的工作。但是服務的系統優先順序還是比較低的,當系統出現記憶體不足的情況時,就有可能會回收掉正在後臺執行的服務。如果你希望服務可以一直保持執行狀態,而不會由於系統記憶體不足的原因導致被回收,就可以考慮使用前臺服務。前臺服務和普通服務最大的區別就在於,它會一直有一個正在執行的圖示在系統的狀態列顯示,下拉狀態列後可以看到更加詳細的資訊,非常類似於通知的效果。當然有時候你也可能不僅僅是為了防止服務被回收掉才使用前臺服務的,有些專案由於特殊的需求必須使用前臺服務,比如說墨跡天氣,它的服務在後臺更新天氣資料的同時,還會在系統狀態列一直顯示當前的天氣資訊。
那麼我們就來看一下如何才能建立一個前臺服務吧,其實並不複雜,修改 MyService 中的程式碼,如下所示:
public class MyService extends Service {
......
@Override
public void onCreate() {
super.onCreate();
Notification notification = new Notification(R.drawable.ic_launcher,
"Notification comes", System.currentTimeMillis());
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
notificationIntent, 0);
notification.setLatestEventInfo(this, "This is title", "This is content",
pendingIntent);
startForeground(1, notification);
Log.d("MyService", "onCreate executed");
}
......
}
可以看到,這裡只是修改了 onCreate() 方法中的程式碼,相信這部分的程式碼你會非常眼熟。沒錯!這就是我們在豐富你的程式,運用手機多媒體 中學習的建立通知的方法。只不過這次在構建出 Notification 物件後並沒有使用 NotificationManager 來將通知顯示出來,而是呼叫了 startForeground() 方法。這個方法接收兩個引數,第一個引數是通知的 id,類似於 notify() 方法的第一個引數,第二個引數則是構建出的 Notification 物件。呼叫startForeground() 方法後就會讓 MyService 變成一個前臺服務,並在系統狀態列顯示出來。
現在重新執行一下程式,並點選 Start Service 或 Bind Service 按鈕,MyService 就會以前臺服務的模式啟動了,並且在系統狀態列會顯示一個通知圖示,下拉狀態列後可以看到該通知的詳細內容,如圖 9.12 所示。
圖 9.12
前臺服務的用法就這麼簡單,只要你將通知的用法掌握好了,學習本節的知識一定會特別輕鬆。
5.2 使用 IntentService
話說回來,在一開始講解服務的時候我們就已經知道,服務中的程式碼都是預設執行在主執行緒當中的,如果直接在服務裡去處理一些耗時的邏輯,就很容易出現 ANR(Application Not Responding)的情況。
所以這個時候就需要用到 Android 多執行緒程式設計的技術了,我們應該在服務的每個具體方法裡開啟一個子執行緒,然後在這裡取處理那些耗時的邏輯,因此,一個比較標準的服務就可以寫成如下形式:
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 處理具體的邏輯
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
}
但是,這種服務一旦啟動之後,就會一直處於執行狀態,必須呼叫stopService() 或者stopSelf() 方法才能讓服務停止下來。所以,如果想要實現讓一個服務在執行完畢後自動停止的功能,就可以這樣寫:
public class MyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 處理具體的邏輯
stopSelf();
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
}
雖然這種寫法並不複雜,但是總會有一些程式設計師忘記開啟執行緒,或者忘記呼叫 stopSelf()方法。為了可以簡單地建立一個非同步的、會自動停止的服務,Android 專門提供了一個 IntentService 類,這個類就很好地解決了前面所提到的兩種尷尬,下面我們就來看一下它的用法。
新建一個 MyIntentService 類繼承自 IntentService,程式碼如下所示:
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService"); // 呼叫父類的有參建構函式
}
@Override
protected void onHandleIntent(Intent intent) {
// 列印當前執行緒的 id
Log.d("MyIntentService", "Thread id is " + Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestroy executed");
}
}
這裡首先是要提供一個無參的建構函式,並且必須在其內部呼叫父類的有參建構函式。然後要在子類中去實現onHandleIntent() 這個抽象方法,在這個方法中可以去處理一些具體的邏輯,而且不用擔心 ANR 的問題,因為這個方法已經是在子執行緒中執行的了。這裡為了證實一下,我們在 onHandleIntent() 方法中列印了當前執行緒的 id。另外根據 IntentService 的特性,這個服務在執行結束後應該是會自動停止的,所以我們又重寫了 onDestroy() 方法,在這裡也列印了一行日誌,以證實服務是不是停止掉了。
接下來修改 activity_main.xml 中的程式碼,加入一個用於啟動 MyIntentService 這個服務的按鈕,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
......
<Button
android:id="@+id/start_intent_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start IntentService" />
</LinearLayout>
然後修改 MainActivity 中的程式碼,如下所示:
public class MainActivity extends Activity implements OnClickListener {
......
private Button startIntentService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
......
startIntentService = (Button) findViewById(R.id.start_intent_service);
startIntentService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
......
case R.id.start_intent_service:
// 列印主執行緒的 id
Log.d("MainActivity", "Thread id is " + Thread.currentThread().getId());
Intent intentService = new Intent(this, MyIntentService.class);
startService(intentService);
break;
default:
break;
}
}
}
可以看到,我們在 Start IntentService 按鈕的點選事件裡面去啟動 MyIntentService 這個服務,並在這裡列印了主執行緒的 id,稍後用於和 IntentService 進行比對。你會發現,其實 IntentService 的用法和普通的服務沒什麼兩樣。
最後仍然不要忘記,服務都是需要在 AndroidManifest.xml 裡註冊的,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest"
android:versionCode="1"
android:versionName="1.0" >
......
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
......
<service android:name=".MyIntentService"></service>
</application>
</manifest>
現在重新執行一下程式,介面如圖 9.13 所示。
圖 9.13
點選 Start IntentService 按鈕後,觀察 LogCat 中的列印日誌,如圖 9.14 所示。
圖 9.14
可以看到,不僅 MyIntentService 和 MainActivity 所在的執行緒 id 不一樣,而且 onDestroy() 方法也得到了執行,說明 MyIntentService 在執行完畢後確實自動停止了。集開啟執行緒和自動停止於一身,IntentService 還是博得了不少程式設計師的喜愛。
摘自《第一行程式碼》