Android後臺服務概述

本篇文章主要講述android servivce相關知識,其中會穿插一些其他的知識點,作爲初學者的教程。老鳥繞路

目錄

爲什麼要用Service

Service及其繼承者IntentService

service的生命週期

IntentService

一個後臺計數器的例子來講述Service

Service如何與UI組件通信

Broadcast

Service與通知欄的通信


爲什麼要用Service

我們接觸android的時候,大部分時候是在和activity打交道,但是有些比如網絡下載、大文件讀取、解析等耗時卻又不需要界面對象的操作。一旦退出界面,那麼可能就會變得不可控(比如界面退出後,線程通知UI顯示進度,但是由於View已經被銷燬導致報錯,或者界面退出後下載中斷,就算你寫得非常完美,什麼異常狀態都考慮到了,還是保證不了系統由於內存緊張把你這個後臺的activity給幹掉,依附於於它的下載線程也中斷。)

這時候Service就有它的用武之地了,不依賴界面,消耗資源少,優先級比後臺activity高,不會輕易被系統幹掉(就算被幹掉,也有標誌位設置可以讓它自動重啓,這也是一些流氓軟件牛皮鮮的招數)、

Service及其繼承者IntentService

service的生命週期

service的生命週期相對activity要簡單不少。

可以看出service有兩條生命線,一條是調用startService,一條是調用bindService
,兩條生命線相互獨立。本文只講startService。

一道選擇題,解釋service生命週期的所有問題:

android通過startService的方式開啓服務,關於service生命週期的onCreate()和onStart() 說法正確的是哪兩項
A.當第一次啓動的時候先後調用 onCreate()和 onStart()方法
B.當第一次啓動的時候只會調用 onCreate()方法
C.如果 service 已經啓動,將先後調用 onCreate()和 onStart()方法
D.如果 service 已經啓動,只會執行 onStart()方法,不在執行 onCreate()方法

答案自己想下,結尾公佈

IntentService

一些容易被忽略的基礎知識:Service運行的代碼是在主線程上的,也就是說,直接在上面運行會卡住UI,這時就Service的繼承者(繼承於Service的子類)IntentService就應運而生。android studio的新建裏面直接就有IntentService的模板,足見其應用之廣。
那麼Service與IntentService的區別在哪呢?
詳見這裏 Android之Service與IntentService的比較

簡單來說就是

  • IntentService內部有個工作線程(Worker Thread),會將startService傳入的intent通過Handler-Message機制傳入工作線程,開發者通過重載onHandleIntent進行服務的具體實現。
  • IntentService在跑完onHandleIntent後,如何Handler隊列裏沒有其他消息,就會自動結束服務,有點像Thread中run函數一樣,跑完run函數之後,線程就結束了。而service需要自己去停止。

一個後臺計數器的例子來講述Service

實戰環節,本文通過一個計數器的例子模擬下載文件的耗時操作。

public void startService(View view){
    Intent intent = new Intent(this,BackgroundService.class);            intent.setAction("com.example.administrator.servicestudy.action.counter");
    intent.putExtra("duration",10);
    intent.putExtra("interval",1.0f);
    startService(intent);
}

上述代碼就是一個啓動service的例子,action相當於做什麼操作(適用於一個service處理多種請求的情況。),extra就是參數。參數中duration代表總時間10秒,interval代碼每隔一秒。

private static final String ACTION_COUNTER = "com.example.administrator.servicestudy.action.counter";

@Override
protected void onHandleIntent(Intent intent) {
    if (intent != null) {
        final String action = intent.getAction();
        if (ACTION_COUNTER.equals(action)) {
            final int duration = intent.getIntExtra(EXTRA_DURATION,0);
            final float interval = intent.getFloatExtra(EXTRA_INTERVAL,0);
            handleActionCounter(duration, interval);
        }
    }
}

private void handleActionCounter(int duration, float interval) {
    for(int i=0; i<duration; i++){
        updateUI(i,duration);
        try {
            Thread.sleep((long) (interval*1000));
        } catch (InterruptedException ignored) {
        }
    }
    updateUI(duration,duration);
}

可以看到重載onHandleIntent處理事件,handleActionCounter表示具體服務。根據傳入的參數決定循環時間和sleep間隔。

當然別忘了在manifest文件中聲明該Service

<service
  android:name=".BackgroundService"
  android:exported="false" />

以上就是最基本的IntentService的用法了,不過爲了代碼獨立性更好,可以將代碼寫成這樣。
Activity

public void startService(View view){
     BackgroundService.startCounterService(this,1,10);
}

Service

public static void startCounterService(@NonNull Context context, int interval, int duration) {
        Intent intent = new Intent(context, BackgroundService.class);
        intent.setAction(ACTION_COUNTER);
        intent.putExtra(EXTRA_DURATION, duration);
        intent.putExtra(EXTRA_INTERVAL, interval);
        context.startService(intent);
    }

在Service裏寫個靜態方法,只將參數傳入,剩餘的全都在Service內實現。雖然代碼寫的位置變了,但是代碼運行的位置沒變(靜態方法依然還是運行在activity端),這樣做將EXTRA_DURATION、EXTRA_INTERVAL等參數也不暴露給外部。做到更好的封裝性和模塊化,推薦這種做法。

Service如何與UI組件通信

那麼Service在後臺努力幹活的時候,如何將當前進度通知給用戶呢,因爲Service不依賴任何界面,所以自身沒辦法操作界面(除非用Toast)。所以Service就要與其他組件進行通信(主要就是activity和通知欄了,但不限於上述兩者)。

android組件間的通信(還記得android四大組件是哪四個不?)。 大部分通過android四大組件之一的Broadcast來通信。
那麼簡要說下Broadcast

Broadcast

生命週期:

就這麼簡單,一旦處理完廣播就被銷燬,沒有onCreate,也沒有onDestory
最重要的一點就是receiver裏不能處理耗時操作,超過5秒(好像是)系統就會報錯

Service

 private void updateUI(int current,int total){
    Intent intent = new Intent(BROADCAST_UPDATE_UI);
    intent.putExtra(EXTRA_CURRENT,current);
    intent.putExtra(EXTRA_TOTAL,total);

    sendBroadcast(intent);
}

可以看到,發個廣播就這麼簡單,把參數填入intent,自定義一個action,send!好了。

Activity

@Override
protected void onResume() {
    super.onResume();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
    registerReceiver(mBackgroundServiceReceiver,intentFilter);
}

@Override
protected void onPause() {
    super.onPause();
    unregisterReceiver(mBackgroundServiceReceiver);    
}

private BroadcastReceiver mBackgroundServiceReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG,"receive:"+intent.getAction());
        if(intent.getAction() == BackgroundService.BROADCAST_UPDATE_UI){
            int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
            int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
            mHint.setText(current+"/"+total);
        }
    }
};

Activity在resume的時候註冊一個廣播接收器,pasue的時候註銷掉。在receiver裏處理更新UI的操作。就這麼簡單

同樣的,爲了代碼更具有封裝性。在Activity中將recevier去掉。放在Service中,看代碼:

<receiver android:name=".BackgroundService$BackgroundServiceReceiver">
   <intent-filter>
       <action android:name="com.example.administrator.servicestudy.action.update_ui" />
   </intent-filter>
</receiver>
public static class BackgroundServiceReceiver extends BroadcastReceiver {
    private static List<UIHandler> mHandlers = new ArrayList<>();

    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.getAction().equals(BROADCAST_UPDATE_UI)){
            int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
            int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
            for (UIHandler handler : mHandlers) {
                handler.onUpdateUI(current,total);
            }
        }
    }
}

public interface UIHandler {
    void onUpdateUI(int current,int total);
}

public static void registerUIHandler(UIHandler handler){
    if(handler != null){
        BackgroundServiceReceiver.mHandlers.add(handler);
    }

}

public static void unregisterUIHandler(UIHandler handler){
    BackgroundServiceReceiver.mHandlers.remove(handler);
}

這裏代碼有點多,一點一點說,

  1. 首先在manifest裏註冊一個靜態廣播接收器,靜態就是表示一直都會接收的,不需要手動register和unregister。一般的receiver都是單獨一個文件,這裏爲了更好地封裝性,寫在Service裏作爲靜態內部類。所以在manifest裏的註冊名字也寫成了.BackgroundService$BackgroundServiceReceiver,注意中間一個美元符號,那就是表示公共靜態內部類的標誌。
  2. 在Service內部實現一個Receiver,具體和Activity裏面的一樣。
  3. 然後寫一個interface,代表具體的UI處理
  4. 寫一個註冊函數和反註冊函數,用以界面組件註冊UI更新事件。
  5. 由於該Service可能不止只更新一個界面組件,所以註冊的Handler是一個列表。在收到廣播後,將所有註冊過的組件都通知更新一遍。

然後在Activity中註冊一下。替換掉註冊廣播的地方。

@Override
protected void onResume() {
    super.onResume();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
//        registerReceiver(mBackgroundServiceReceiver,intentFilter);
    BackgroundService.registerUIHandler(mServiceUIHandler);
}

@Override
protected void onPause() {
    super.onPause();
    BackgroundService.unregisterUIHandler(mServiceUIHandler);
//        unregisterReceiver(mBackgroundServiceReceiver);
}

private BackgroundService.UIHandler mServiceUIHandler = new BackgroundService.UIHandler() {
    @Override
    public void onUpdateUI(int current, int total) {
        Log.d(TAG,"receive: service broadcast");
        mHint.setText(current+"/"+total);
    }
};

這樣就完成了一個Service的封裝,簡化Activity的代碼,我的思想一直都是Activity中,應該只處理和界面有關的代碼。就像C語言的main函數一樣,你不可能把所有代碼都寫在main函數裏吧。或者面函數的同一個文件裏吧。

那麼我們加一個stop Service的函數吧。

Service

public static void stopCounterService(@NonNull Context context){
    Intent intent = new Intent(context, BackgroundService.class);
    intent.setAction(ACTION_COUNTER);
    context.stopService(intent);
}

Activity

public void stopService(View view){
//        Intent intent = new Intent(this,BackgroundService.class);
//        intent.setAction("com.example.administrator.servicestudy.action.counter");
//        stopService(intent);
    BackgroundService.stopCounterService(this);
}

IntentService是以Message爲單位來停止的,也就是說,一定要等到當前消息處理完才能完全stop掉,爲此我們可以加一個標誌位,一旦Service停止,強制循環退出。

Service

@Override
public void onCreate() {
    super.onCreate();
    Log.d(TAG,"onCreate");
    mServiceFinished = false;
}

@Override
public void onDestroy() {
    super.onDestroy();
    Log.d(TAG,"onDestroy");
    mServiceFinished = true;
}

private void handleActionCounter(int duration, float interval) {
   for(int i=0; i<duration; i++){
       if(mServiceFinished){
           break;
       }
       updateUI(i,duration);
       try {
           Thread.sleep((long) (interval*1000));
       } catch (InterruptedException ignored) {
       }
   }
   updateUI(duration,duration);
}

Service與通知欄的通信

至此我們已經完成了Service與Activity的通信,Service與Activity之間通過廣播進行通信。Service負責邏輯處理,Activity負責更新界面顯示。但是到這邊還沒發現Service的獨特之處,就是這個這些代碼完全也可以寫在Activity裏面的,寫在Service裏面無非就是結構更好看點,如果你那麼認爲就錯了。你可以在Activity中退出再進入,可以發現計數器並沒有因爲Activity的退出而終止或者暫停。依然跟着時間走。這點是寫在Activity中完全做不到的。當然你也可以通過一些小技巧來達到同樣的效果,不過我們這個例子是爲了模擬後臺下載用的。所以不扯這些了。

下面進入真正的後臺下載。Service與通知欄的通信。
我們這樣設計一個程序,當Activity退出後,通知欄繼續顯示計數器進度,點擊通知或者再次進入Activity,通知欄取消顯示進度(爲了不重複顯示,也爲了演示代碼)。

爲此我們新建一個新的Service,並在Activity添加如下代碼

NotificationService

public class NotificationService extends Service {
    private static final String TAG = NotificationService.class.getSimpleName();

    public NotificationService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        BackgroundService.registerUIHandler(mUIHandler);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
        BackgroundService.unregisterUIHandler(mUIHandler);
    }
    ......
}

這裏我們新建的是一個普通的service,而不是IntentService,因爲這邊我們不需要耗時操作,我們甚至連onStartCommand都沒有重載,因爲我們只需要在啓動服務的時候註冊一個UI更新的回調就可以了,然後在銷燬服務的時候註銷掉。

Activity

@Override
protected void onResume() {
    super.onResume();
    ...
    stopService(new Intent(this,NotificationService.class));
}

@Override
protected void onPause() {
    super.onPause();
    ...
    startService(new Intent(this,NotificationService.class));
}

我們在Activity Resume的時候關閉通知欄通知服務,在Pause的時候開啓該服務,這樣就能做到我們的設計初衷。

接下來就是通知欄的UI更新操作了,都是通知欄的接口,聽說2.3和4.0以上的接口很不一樣,我們這邊用的是4.0以上的接口。

private BackgroundService.UIHandler mUIHandler = new BackgroundService.UIHandler() {
    @Override
    public void onUpdateUI(int current, int total) {
        Log.d(TAG,"Notification onUpdateUI");
        //點擊通知後,啓動Activity,最後的FLAG_ONE_SHOT,表示只執行一次,具體自行百度。
        PendingIntent pendingIntent = PendingIntent.getActivity(NotificationService.this,
                0,
                new Intent(NotificationService.this,MainActivity.class),
                PendingIntent.FLAG_ONE_SHOT);

        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        Notification.Builder builder = new Notification.Builder(getApplicationContext());
        Notification notification = builder.setContentTitle("Background Service")
                .setTicker("Counting...")//狀態欄上滾動的字符串
                .setContentText("Ongoing")//設置通知的正文
                .setProgress(total, current, false)//設置通知欄的進度條,android真貼心,終於可以不用自定義進度條了。
                .setOngoing(true)//設置可不可以取消該通知
                .setContentIntent(pendingIntent)//點擊該通知後的操作。
                .setDefaults(Notification.DEFAULT_ALL)//通知的音效、震動、呼吸燈全都隨系統設置,當然你也可以自定義
                .setAutoCancel(true)//是不是點擊之後自動取消,否則的話,可能你需要手動調用接口來取消
                .setOnlyAlertOnce(true)//音效震動呼吸燈是否只提醒一下,專門給進度條之類,頻繁更新的通知用的,不設置這個,你可以試試,那鬼畜的音效
                .setSmallIcon(R.mipmap.ic_launcher)//這個不解釋了
                .build();
        //第一個參數爲ID,APP內全局唯一,相同的ID表示相同的通知,不會在通知欄新增一條通知,不同的話,則在通知欄插入一條新的通知。第二個參數就是剛纔配置的通知。
        nm.notify(1234,notification);
    }
};

最後提醒一句,通知不配置PendingIntent是不會顯示的哦

爲了完美模擬後臺下載,我們在下載完成後(服務被銷燬後),發送一個結束廣播,通知UI層。

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