Android - 聊聊 Service

學習 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() 的時候,系統會回調 ServiceonStartCommand 方法:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return super.onStartCommand(intent, flags, startId);
}

這裏我們關心的是 startId 參數。多次 startService() 後,系統每次調用 onStartCommand()startId 都不同。在 stopSelf() 的時候,我們還可以把 startId 傳遞給它:

stopSelf(startId);

在這種情況下,只有當傳入的 startIdonStartCommand() 最後收到的那個時,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 來綁定它(注:這裏假定我們現在 ServiceActivity 處於同一個進程中。不同進程的情況下,需要使用 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

加下來,我們需要實現 ServiceonBind() 方法。,我們可以這樣:

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 裏面處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章