學習 Android 應用開發時,我們首先接觸到的是 Activity
。簡單地理解,一個 Activity
就是 Android 應用的一個頁面。而在某些情況下,我們可能需要在“頁面”不可見,仍然執行某些操作。這個時候,我們就需要 Service
了。
和 Activity
類似,Service
也是 Android 非常重要的組件之一,主要用於在後臺執行某些任務。比方說,我們下載一個文件時,即使用戶不在下載頁面,我們也希望下載任務能夠繼續進行。
使用 Service
非常簡單,我們只需要簡單繼承 Service
並實現 onBind()
方法,然後在 manifest
裏註冊即可:
public class MyService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
// AndroidManifest.xml
<service android:name=".MyService" />
跟啓動 Activity
類似,我們構造一個 Intent
並 start 它:
Intent intent = new Intent(this, MyService.class);
startService(intent);
我們使用 startService()
啓動它。在這種情況下,我們稱這個 Service
是一個 started service。started service 主要用於執行某個任務的場合。比如,我們要下載一個文件,就可以 startService()
,然後讓 service
在後臺幫我們去下載。文件下載結束後,service
就可以自己停止自己的生命:
stopSelf();
但是,這裏存在一個問題。我們可能使用這個 service
同時下載多個文件。這種情況下,就不能簡單地調用 stopSelf()
了。因爲一個文件下載完成,並不意味着所有文件都下載結束。
這種情況的一個可選辦法是,在下載完成後,顯式檢查是否還有其他任務,如果沒有,就 stopSelf()
:
private final Object mLock = new Object();
private int mNumTask;
private void stopSelfIfAllDone() {
synchronized (mLock) {
if (--mNumTask == 0) {
stopSelf();
}
}
}
上面介紹的這種結束方式,適用於在 Service
啓動多個工作線程的情況。另一種情況是,我們只使用一個工作線程,每個任務都由該線程串行執行。這種情況下,我們有理由相信,最後一個 task 執行完,就沒有其他任務了。此時,我們可以直接使用 Android 提供的 IntentService
。即便你不想用 IntentService
,看看它的實現方式,也是非常值得的。
當我們 startService()
的時候,系統會回調 Service
的 onStartCommand
方法:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
這裏我們關心的是 startId
參數。多次 startService()
後,系統每次調用 onStartCommand()
的 startId
都不同。在 stopSelf()
的時候,我們還可以把 startId
傳遞給它:
stopSelf(startId);
在這種情況下,只有當傳入的 startId
是 onStartCommand()
最後收到的那個時,service
纔會真的停止。
假設有這麼一個場景,我們 startService()
3 次,得到的 startId
分別爲 1, 2, 3
。接下來,在每個任務完成後,我們都 stopSelf(startId)
。由於最後收到的 startId
是 3。所以前兩個 stopSelf(1), stopSelf(2)
都不會真的停止 Service
。
這就是 IntentService
的工作方式了,如果你查看它的源碼,你會發現這個:
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
聊完了 started service,接下來我們聊聊 bound service。
繼續我們上面的下載文件 Service
。現在我們不止希望它能夠在後臺悄悄幫我們下載文件,還想要有一個下載管理頁面。在這個下載管理頁面,我們需要看到下載的進度。
這時我們就可以使用 bound service 了。這裏的 “bound” 其實是 bind 的過去式,指的是被綁定過的 service
。和 started service 類似,我們通過 intent 來綁定它(注:這裏假定我們現在 Service
跟 Activity
處於同一個進程中。不同進程的情況下,需要使用 AIDL,不在本篇討論)
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
MyService.MyBinder binder = (MyService.MyBinder) service;
MyService myService = binder.getService();
// put your code here
}
@Override
public void onServiceDisconnected(ComponentName name) {
// put your code here
}
};
Intent intent = new Intent(this, MyService.class);
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
由於在我們綁定 Service
的時候,Service
可能還沒有啓動,所以,這裏需要使用一個回調 ServiceConnection
。
加下來,我們需要實現 Service
的 onBind()
方法。,我們可以這樣:
private IBinder mBinder = new MyBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class MyBinder extends Binder {
public MyService getService() {
return MyService.this;
}
}
這樣一來,我們能夠在 Activity
中拿到 Service
的引用。由於他們處於同一個進程,Activity
中可以調用 Service
的任意方法(別忘了,Service
也是一個 Java 對象,我們持有它的引用,當然可以調用它的方法!)
對於 bound service,service
相當於一個服務的提供者,activity
在這裏則是客戶。這種情況下,往往只有客戶知道什麼時候需要使用服務、什麼時候不再需要。跟 started service 不同,bound service 需要客戶顯式地 unbind:
unbindService(mServiceConnection);
這個時候,你可能會有點疑惑,對於我們的文件下載服務,我們既 startService
,又 bindService
。那麼,unbindService
的時候,服務會停止嗎?stopSelf
呢?
其實,這個問題完全是我們多慮了,Android 已經幫我們考慮到了這種情況。當既有 startService()
,又有 bindService
時,僅當我們 stopSelf()
並且 unbindService
時,服務纔會停止。
最後,對於一些初學者可能無法理解的是,即便我們使用了一個文件下載服務,但實際的下載過程,還是需要在後臺線程進行(Service
的所有方法都運行在主線程)。那爲什麼不直接在 activity
啓動線程來下載就可以了?
問題在於,當 activity
退出後,即便還有後臺線程在運行,如果所有的 Android 組件都退出了,系統完全有可能會殺死這個進程,以回收設備的資源。而如果還有 service
在運行,則可以避免這種情況。
使用廣播的時候,也可能會遇到類似的問題。Android 系統對廣播的執行時間有嚴格的要求。如果收到某個廣播後,需要做一些很耗時的操作(比如,還是下載文件),不能直接 new Thread
,而因爲 startService
然後在 Service
裏面處理。