如何正確的使用 Service?

簡介

Service(服務)是 Android 四大組件之一,它的主要做用是執行後臺操做,Activity 提供了 UI 界面來跟用戶交互,而 Service 則沒有 UI 界面,全部的操做都是在後臺完成。java

Service 跟 Activity 同樣也能夠由其它應用程序啓動,即便用戶切換到了其它應用,Service 仍然保持在後臺運行。android

此外,一個組件能夠與 Service 進行綁定(bind)來跟 Service 進行交互,甚至是進行進程間通訊(IPC)。git

一般狀況下可使用 Service 進行網絡請求、播放音樂、文件 I/O 等操做。github

建立服務

要建立一個 Service 首先須要繼承 Service 來實現一個子類。網絡

public class TestService extends Service {

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

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

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

  @Override
  public boolean onUnbind(Intent intent) {
    return super.onUnbind(intent);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
  }
}
複製代碼

相似於 Activity,全部的 Service 都要在 Manifest 裏面進行聲明,以下:app

<manifest ... >
  ...
  <application ... >
    <service android:name="xxx.xxxs.TestService" />
    ...
  </application>
</manifest>
複製代碼

經過在 <service> 標籤裏將 android:exported 設置爲 false。能夠防止其餘的程序來啓動你的 Service。框架

啓動服務

一般狀況下有兩種方式來啓動 Service,startService()bindService()異步

startService()

Intent intent = new Intent(this, TestService.class);
startService(intent); // 開啓服務
stopService(intent); // 中止服務
複製代碼

當組件經過調用 startService() 啓動 Service 後,Service 就能夠在後臺無限期的運行,即便啓動 Service 的組件被銷燬也不受影響。ide

通常狀況下 startService() 是執行單一操做,而且不會將執行結果返回給調用者。例如,它多是下載文件或者上傳文件,一般操做完成後會自動中止。函數

該方式容許多個組件同時對相同的 Service 進行 startService() 操做,可是若是隻要有其中有一個組件調用了 stopSelf()stopService(), 該 Service 就會被銷燬。

bindService()

Intent intent = new Intent(this, TestService.class);
ServiceConnection connection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder service) {
  }

  @Override
  public void onServiceDisconnected(ComponentName name) {
  }
};
// 綁定服務
bindService(intent, connection, Context.BIND_AUTO_CREATE);
// 解綁服務
unbindService(aidlConnection);
複製代碼

當組件經過調用 bindService() 啓動 Service 後,Service 就處於綁定狀態了。這種方式提供了 client - service 的接口,可讓調用者與 Service 進行發送請求和返回結果的操做,甚至能夠進行進程間的通訊(IPC)。

只要有一個組件對該 Service 進行了綁定,那該 Service 就不會銷燬。若是多個組件能夠同時對一個 Service 進行綁定,只有全部綁定的該 Service 的組件都解綁後,該 Service 纔會銷燬。

儘管兩種方式是分開討論的,可是並非互斥的關係,使用 startService() 啓動了 Service 後,也是能夠進行綁定的。

注意:雖然 Service 是在後臺運行的,但其實仍是在主線程中進行全部的操做。Service 啓動時除非單獨進行了定義,不然沒有單獨開啓線程或者進程都是運行在主線程中。

因此任何能阻塞主線程的操做(例如:播放音樂或者網絡請求),都應該在 Service 中單獨開啓新的線程來進行操做,不然很容易出現 ANR。

系統方法

在建立一個 Service 時,必需要去繼承 Service,而且須要重寫父類的一些方法來實現功能。如下是主要方法的介紹。

onStartCommand()

當另外一個組件(如:Activity)經過調用 startService() 來啓動 Service 時,系統會調用該方法。一旦執行該方法,Service 就會啓動並在後臺無限期執行。

若是實現該方法,在 Service 執行完後,須要調用 stopSelf() 或 stopService() 來停結束Service。

若是隻是會經過綁定的方式(bind)的方式來啓動 Service 則不須要重寫該方法。

onBind()

系統會調用這個函數當某個組件(例如:activity,fragment)經過調用 bindService() 綁定的方式來啓動 Service 的時候。在實現這個函數的時候,必需要返回一個 IBinder 的繼承類,來與 Service 進行通訊。

這個函數是默認必需要重寫的,可是若是不想經過綁定的方式來啓動 Service,則能夠直接返回 null

onCreate()

系統會調用此方法在第一次啓動 Service 的時候,用於初始化一些一次性的變量。若是 Service 已經啓動了,則此方法就不會再別調用。

onDestroy()

系統在 Service 已經不須要準備被銷燬的時候會調用此方法。Service 中若有用到 thread、listeners、receivers 等的時候,應該將這些的清理方法寫在此方法內。

生命週期

與 Activity 相似,Service 也有生命週期回調方法,能夠實現這些方法來監控 Service 狀態的變化來執行相關操做。

Service 生命週期

startService()

onCreate() -> onStartCommand() -> onDestroy()

bindService()

onCreate() -> onBind() -> onUnbind() -> onDestroy()

系統資源回收

當系統內存不足的時候,系統會強制回收一些 Activity 和 Service 來獲取更多的資源給那些用戶正在交互的程序或頁面。當資源充足的時候能夠經過 onStartCommand() 的返回值,來實現 Service 自動重啓。

public int onStartCommand(Intent intent, int flags, int startId) {
  return START_NOT_STICKY | START_STICKY | START_REDELIVER_INTENT;
}
複製代碼

START_NOT_STICKY

當系統因回收資源而銷燬了 Service,當資源再次充足時再也不自動啓動 Service,除非有未處理的 Intent 準備發送。

START_STICKY

當系統因回收資源而銷燬了 Service,當資源再次充足時自動啓動 Service。並且再次調用 onStartCommand() 方法,可是不會傳遞最後一次的 Intent,相反系統在回調 onStartCommand() 的時候會傳一個空 Intent,除非有未處理的 Intent 準備發送。

START_REDELIVER_INTENT

當系統因回收資源而銷燬了 Service,當資源再次充足時自動啓動 Service,而且再次調用 onStartCommand() 方法,並會把最後一次 Intent 再次傳遞給 onStartCommand(),相應的在隊列裏的 Intent 也會按次序一次傳遞。此模式適用於下載等服務。

IntentService

Service 自己默認是運行在主線程裏的,因此若是在 Service 要進行一些會堵塞線程的操做,必定要將這些操做放在一個新的線程裏。

爲了知足後臺運行異步線程的需求,Android 的框架提供了 IntentService。

IntentService 是 Service 的子類,而且全部的請求操做都是在異步線程裏。若是不須要 Service 來同時處理多個請求的話,IntentService 將會是最佳的選擇。

使用該服務只須要繼承並重寫 IntentService 中的 onHandleIntent() 方法,就能夠對接受到的 Intent 作後臺的異步線程操做了。

public class TestIntentService extends IntentService {

  public TestIntentService() {
    super("TestIntentService");
  }

  public TestIntentService(String name) {
    super(name);
  }

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

  @Override
  protected void onHandleIntent(@Nullable Intent intent) {
    //TODO: 耗時操做,運行在子線程中
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
  }
}
複製代碼

前臺服務

什麼是前臺服務?

前臺服務是那些被認爲用戶知道(用戶所承認的),且在系統內存不足的時候不容許系統殺死的服務。

前臺服務必須給狀態欄提供一個通知,它被放到正在運行(Ongoing)標題之下 —— 這就意味着通知只有在這個服務被終止或從前臺主動移除通知後才能被解除。

爲何要使用前臺服務?

在通常狀況下,Service 幾乎都是在後臺運行,一直默默地作着辛苦的工做。但這種狀況下,後臺運行的Service系統優先級相對較低,當系統內存不足時,在後臺運行的 Service 就有可能被回收。

那麼,若是咱們但願 Service 能夠一直保持運行狀態,且不會在內存不足的狀況下被回收時,能夠選擇將須要保持運行的 Service 設置爲前臺服務。

例如:App中的音樂播放服務應被設置在前臺運行(前臺服務)——在 App 後臺運行時,便於用戶明確知道它的當前操做、在狀態欄中指明當前歌曲信息、提供對應操做。

如何建立一個前臺服務?

新建一個服務。

public class ForegroundService extends Service {
  
  private static final int RESULT_CODE = 0;
  private static final int ID = 1;
  
  public ForegroundService() { }

  @Override
  public void onCreate() {
    super.onCreate();
    Intent intent = new Intent(this, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, RESULT_CODE, 
                              intent, PendingIntent.FLAG_UPDATE_CURRENT);
    NotificationCompat.Builder builder;
    // 兼容 Android 8.0
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      String channelId = "foreground_service";
      NotificationChannel channel = new NotificationChannel(channelId, 
                              "channel_1", NotificationManager.IMPORTANCE_HIGH);
      channel.enableLights(true);
      channel.setLightColor(Color.GREEN);
      channel.setShowBadge(true);
      NotificationManager notificationManager = 
        											getSystemService(NotificationManager.class);
      notificationManager.createNotificationChannel(channel);
      builder = new NotificationCompat.Builder(this, channelId);
    } else {
      builder = new NotificationCompat.Builder(this);
    }
    builder.setContentIntent(pendingIntent)
      .setContentTitle("這是前臺通知標題")
      .setContentText("這是內容")
      .setWhen(System.currentTimeMillis())
      .setSmallIcon(R.mipmap.ic_launcher_round)
      .setLargeIcon(BitmapFactory.decodeResource(getResources(), 
                                                 R.mipmap.ic_launcher))
      .setPriority(NotificationManager.IMPORTANCE_HIGH)
      .setDefaults(Notification.DEFAULT_SOUND);

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

  @Override
  public IBinder onBind(Intent intent) {
    return super.onBind(intent);
  }
}
複製代碼

啓動與中止前臺服務

Intent foregroundIntent = new Intent(this, ForegroundService.class);
startService(foregroundIntent); // 啓動前臺服務
stopService(foregroundIntent); // 中止前臺服務
複製代碼

前臺服務與普通服務的區別

  • 前臺 Service 的系統優先級更高、不易被回收;
  • 前臺 Service 會一直有一個正在運行的圖標在系統的狀態欄顯示,下拉狀態欄後能夠看到更加詳細的信息,很是相似於通知的效果。

服務保活

經過前面的介紹咱們瞭解到 Service 是後臺服務來執行一些特定的任務,可是當後臺服務在系統資源不足的時候可能會回收銷燬掉 Service。

那麼如何讓後臺服務儘可能不被殺死呢?基本解決思路以下:

提高 Service 的優先級

爲防止 Service 被系統回收,能夠嘗試經過提升服務的優先級解決。優先級數值最高爲 1000,數字越小,優先級越低。

<service android:name=".ui.service.TestService" >
  <intent-filter android:priority="1000"/>
</service>
複製代碼

persistent 屬性

在 Manifest.xml 文件中設置 persistent 屬性爲 true,則可以使該服務免受 out-of-memory killer 的影響。可是這種作法必定要謹慎,系統服務太多將嚴重影響系統的總體運行效率。

<application android:persistent="true">
</application>
複製代碼

該屬性的特色以下:

  • 在系統啓動的時候會被系統啓動起來。
  • 在該 App 被強制殺掉後系統會從新啓動該 App,這種狀況只針對系統內置App,第三方安裝的 App 不會被重啓。

將服務改爲前臺服務

重寫 onStartCommand 方法,使用 startForeground(int, Notification) 方法來啓動 Service。利用 Android 的系統廣播

利用 Android 的系統廣播檢查 Service 的運行狀態,若是被殺掉就重啓。系統廣播是 Intent.ACTION_TIME_TICK,這個廣播每分鐘發送一次。咱們能夠每分鐘檢查一次 Service 的運行狀態,若是已經被銷燬了,就從新啓動 Service。

參考資料

個人 GitHub

github.com/jeanboydev

個人公衆號

歡迎關注個人公衆號,分享各類技術乾貨,各類學習資料,職業發展和行業動態。

Android 波斯灣

技術交流羣

歡迎加入技術交流羣,來一塊兒交流學習。

QQ 技術交流羣

QQ 技術交流羣
相關文章
相關標籤/搜索