第九章 後臺默默的勞動者,探究服務
9.1服務是什麼
服務( Service)是 Android 中實現程式後臺執行的解決方案,它適合用於去執行不需要和使用者互動而且還要求長期執行的任務。服務的執行不依賴於任何使用者介面,當程式被切換到後臺,服務仍然能夠保持正常執行。但服務並不是執行在一個獨立的程序當中的,而是依賴於建立服務時所在的應用程式程序。當某個應用程式程序被殺掉時,所有依賴於該程序的服務也會停止執行。另外,服務並不會自動開啟執行緒,所有的程式碼都是預設執行在主執行緒當中的。也就是說,我們需要在服務的內部手動建立子執行緒,並在這裡執行具體的任務,否則就有可能出現主執行緒被阻塞住的情況。
Android多執行緒程式設計
Android多執行緒程式設計與Java類似.新建一個類繼承Thread,重寫run(),其中編寫耗時邏輯.啟動時只需new出例項,呼叫start()方法.或者實現Runable介面,實現run()方法.啟動時通過
new Thread(myThread).start();
實現.或者通過匿名類實現
new Thread(new Runnable() {
@Override
public void run() {
// 處理具體的邏輯
}
}).start();
9.2.2在子執行緒中更新UI
在子執行緒中更新UI是不安全的,因此必須在主執行緒中進行.
子執行緒更新UI一次:
Process: com.wjoker.androidthreadtest, PID: 8037
android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.
Android提供了非同步訊息處理機制,用於解決子執行緒中進行UI操作的問題.
9.2.3解析非同步訊息處理機制
Android 中的非同步訊息處理主要由四個部分組成, Message、 Handler、 MessageQueue 和Looper。
1. Message 是線上程之間傳遞的訊息,它可以在內部攜帶少量的資訊,用於在不同執行緒之間交換資料。上一小節中我們使用到了 Message 的 what 欄位,除此之外還可以使用 arg1 和 arg2 欄位來攜帶一些整型資料,使用 obj 欄位攜帶一個 Object 物件。
2. Handler 顧名思義也就是處理者的意思,它主要是用於傳送和處理訊息的。傳送訊息一般是使用 Handler 的 sendMessage()方法,而發出的訊息經過一系列地輾轉處理後,最終會傳遞到 Handler 的 handleMessage()方法中。
3. MessageQueue 是訊息佇列的意思,它主要用於存放所有通過 Handler 傳送的訊息。這部分訊息會一直存在於訊息佇列中,等待被處理。每個執行緒中只會有一個 MessageQueue物件。
4. Looper 是每個執行緒中的 MessageQueue 的管家,呼叫 Looper 的 loop()方法後,就會進入到一個無限迴圈當中,然後每當發現 MessageQueue 中存在一條訊息,就會將它取出,並傳遞到 Handler 的 handleMessage()方法中。每個執行緒中也只會有一個 Looper 物件。
首先需要在主執行緒當中建立一個 Handler 物件,並重寫handleMessage()方法。
然後當子執行緒中需要進行 UI 操作時,就建立一個 Message 物件,並通過 Handler 將這條訊息傳送出去。
之後這條訊息會被新增到 MessageQueue 的佇列中等待被處理,而 Looper 則會一直嘗試從 MessageQueue 中取出待處理訊息,最後分發回 Handler的 handleMessage()方法中。
由於 Handler 是在主執行緒中建立的,所以此時 handleMessage()方法中的程式碼也會在主執行緒中執行,於是我們在這裡就可以安心地進行 UI 操作了。
9.2.4使用AsyncTask
Android提供AsyncTask在子執行緒中對UI進行操作,其原理是基於非同步訊息處理機制.
由於 AsyncTask 是一個抽象類,所以如果我們想使用它,就必須要建立一個子類去繼承它。在繼承時我們可以為 AsyncTask 類指定三個泛型引數,這三個引數的用途如下。
1. Params:在執行 AsyncTask 時需要傳入的引數,可用於在後臺任務中使用。
2. Progress:後臺任務執行時,如果需要在介面上顯示當前的進度,則使用這裡指定的泛型作為進度單位。
3. Result:當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。
示例:
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 操作,比如說提醒任務執行的結果,以及關閉掉進度條對話方塊等。
示例:
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 的訣竅就是,在 doInBackground()方法中去執行具體的耗時任務,在 onProgressUpdate()方法中進行 UI 操作,在 onPostExecute()方法中執行一些任務的收尾工作。如果想要啟動這個任務,只需編寫以下程式碼即可:
new DownloadTask().execute();
9.3服務的基本用法
9.3.1定義一個服務
定義一個類,繼承Service.實現onBind,onCreate,onStartCommand//啟動,onDestroy方法,
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();
}
}
要註冊服務後才可使用:
9.3.2啟動和停止服務.
啟動:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 啟動服務
停止:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服務
9.3.3活動和服務進行通訊
活動和服務之間的通訊可以通過onBind方法進行
首先建立了一個 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.4服務的生命週期
前面我們使用到的 onCreate()、 onStartCommand()、 onBind()和 onDestroy()等方法都是在服務的生命週期內可能回撥的方法。
一旦在專案的任何位置呼叫了 Context 的 startService()方法,相應的服務就會啟動起來,並回調 onStartCommand()方法。如果這個服務之前還沒有建立過, onCreate()方法會先於onStartCommand()方法執行。服務啟動了之後會一直保持執行狀態,直到 stopService()或stopSelf()方法被呼叫。注意雖然每呼叫一次 startService()方法, onStartCommand()就會執行一次,但實際上每個服務都只會存在一個例項。所以不管你呼叫了多少次 startService()方法,只需呼叫一次 stopService()或 stopSelf()方法,服務就會停止下來了。
另外,還可以呼叫 Context 的 bindService()來獲取一個服務的持久連線,這時就會回撥服務中的 onBind()方法。類似地,如果這個服務之前還沒有建立過, onCreate()方法會先於onBind()方法執行。之後,呼叫方可以獲取到 onBind()方法裡返回的 IBinder 物件的例項,這樣就能自由地和服務進行通訊了。只要呼叫方和服務之間的連線沒有斷開,服務就會一直保持執行狀態。
當呼叫了 startService()方法後,又去呼叫 stopService()方法,這時服務中的 onDestroy()方法就會執行,表示服務已經銷燬了。類似地,當呼叫了 bindService()方法後,又去呼叫unbindService()方法, onDestroy()方法也會執行,這兩種情況都很好理解。但是需要注意,我們是完全有可能對一個服務既呼叫了 startService()方法,又呼叫了 bindService()方法的,這種情況下該如何才能讓服務銷燬掉呢?根據 Android 系統的機制,一個服務只要被啟動或者被綁定了之後,就會一直處於執行狀態,必須要讓以上兩種條件同時不滿足,服務才能被銷燬。所以,這種情況下要同時呼叫 stopService()和 unbindService()方法, onDestroy()方法才會執行。
9.5服務的更多技巧
9.5.1使用前臺服務
大部分服務都在後臺執行,但服務的系統優先順序較低,當記憶體不足時可能回收後臺執行的服務,如果需要服務一直保持執行,可以使用前臺服務.
前臺服務會有一個正在執行的圖示在系統的狀態列顯示,下拉後可以看到更加詳細的資訊.
在Service的onCreate()中使用通知,最後使用startForeground()方法.該方法接受兩個引數,一個是通知的ID,另一個接受構建的Notification物件.
public void onCreate(){
Log.e("wyxjoker", "onCreate executed");
super.onCreate();
Intent notificationIntent = new Intent(this,MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,notificationIntent,0);
Notification notification = new Notification.Builder(this)
.setAutoCancel(true)
.setContentTitle("This is a title")
.setContentText("This is content")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(System.currentTimeMillis())
.build();
startForeground(1,notification);
}
9.5.2使用IntentService
服務中的嗲嗎都是預設執行在主執行緒中,如果服務裡處理耗時操作,容易出現ANR(Application Not Responding).因此需要服務的每個具體方法裡開啟子執行緒,處理耗時操作.
@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);
}
Android提供了IntentService類用於建立非同步,會自動停止的服務.
新建類,繼承IntentService在onHandleIntent(Intent intent)中寫邏輯.
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");
}
}