[Android] Service服務詳解以及如何使service服務不被殺死

Services

  服務是一個應用程序組件,可以在後臺執行長時間運行的操作,不提供用戶界面。一個應用程序組件可以啓動一個服務,它將繼續在後臺運行,即使用戶切換到另一個應用程序。此外,一個組件可以綁定到一個服務與它交互,甚至執行進程間通信(IPC)。例如,一個服務可能處理網絡通信,播放音樂,執行文件I/O,或與一個內容提供者交互,都在後臺執行。

一個服務本質上講有兩種形式:

Started 啓動的

started形式的服務是指當一個應用組件(比如activity)通過startService()方法開啓的服務。一旦開啓,該服務就可以無限期地在後臺運行,哪怕開啓它的組件被銷燬掉。 通常,開啓的服務執行一個單獨的操作且並不向調用者返回一個結果。 比如,可能從網絡進行下載或者上傳一個文件。當任務完成,服務就該自我停止。

Bound 綁定的

bound形式的服務是指一個應用組件通過調用 bindService() 方法與服務綁定。一個綁定的服務提供一個客戶-服務端接口,以允許組件與服務交互,發送請求,獲得結果,甚至執行進程間通信。一個綁定的服務只和與其綁定的組件同時運行。多個組件可以同時綁定到一個服務,但當全部接觸綁定後,服務就被銷燬。

  雖然分這兩類,但是一個服務可以同時使用這兩種方式——可以用started無限期的運行,同時允許綁定。只需要在服務中實現兩個回調方法:onStartCommand()允許組件開啓服務,onBind()允許綁定。

  不論應用程序是怎麼起服務的,任何應用程序都可以用這個服務。同樣的,任何組件可以使用一個Activity通過傳遞Intent開啓服務。你也可以在配置文件設置服務爲私有來防止其他應用訪問該服務。

注意:一個服務在進程中的主線程運行——一個服務不會創建自己的線程,也不會在另外的進程運行(除非另外指定)。這意味着,如果服務需要做一些頻繁佔用CPU的工作或者會發生阻塞的操作,你需要在服務中另開線程執行任務。這可以降低產生ANR的風險,提高用戶體驗。

基礎

  創建一個服務需要建立一個Service相關的子類,然後需要實現一些回調方法,好在不同的生命週期內做對應處理和綁定服務,比較重要的方法如下:

  • onStartCommand() 當其他組件,如 activity 請求服務啓動時,系統會調用這個方法。一旦這個方法執行,服務就開始並且無限期的執行。如果實現這個方法,當這個服務完成任務後,需要你來調用 stopSelf() 或者 stopService() 停掉服務。如果只想提供綁定,不需要自己實現這個方法。
  • onBind() 當有其他組件想通過 bindService() 方法綁定這個服務時系統就會調用此方法。在實現的方法裏面,必須添加一個供客戶端使用的接口通過返回一個IBinder來與服務通信,這個方法必須實現。當然不想允許綁定的話,返回null即可。
  • onCreate() 服務第一次建立的時候會調用這個方法,執行一次性設置程序,在上面2個方法執行前調用。如果服務已存在,則不執行該方法。
  • onDestroy() 服務不再使用則使用該方法。服務應該實現這個方法來清理諸如線程,註冊的監聽器等資源。這是最後調用的方法。

  安卓系統只會在內存佔用很高,必須恢復系統資源供當前運行程序的情況下強制停掉一個運行中的服務。如果服務綁定在當前運行的程序中,就幾乎不會被殺掉,如果服務聲明瞭在前臺運行(其實在後臺,只是給系統一個錯的信息來提高優先級),就幾乎不會被殺掉。另外,如果一個服務正在運行,且運行了很久,系統就會根據運行時間把其排在後臺任務列表的後面,則這個服務很容易被殺掉。根據onStartCommand() 的返回值設置,服務被殺掉後仍可以在資源充足的條件下立即重啓。

是用一個服務好還是開一個線程好 一個服務就是一個可以忽略交互,在後臺獨立運行的組件,如果你需要這樣就用服務 如果你需要在用戶與程序交互時在主線程外執行任務,那就開個線程吧。 比如想播放音樂,但只在程序運行時播放,你可能在 onCreate() 開一個線程,在 onStart() 中開啓它,在 onStop() 停止它。也可以考慮使用AsyncTask或者HandlerThread取代一般的線程。 記住,如果使用一個服務,它還是默認在主線程中運行,如果會發生阻塞,還是要在服務中另開線程的。

在 manifest 文件聲明服務

要使用服務就必須在 manifest 文件聲明要用的所有服務,只用在<application>標籤內添加子標籤<service>即可。

<manifest ...>
   	...
       <application ...>
       	<service android:name=".ExampleService"
           	android:enabled=["true" | "false"]
        		android:exported=["true" | "false"]
        		android:isolatedProcess=["true" | "false"]
        		android:label="string resource"
        		android:icon="drawable resource"
        		android:permission="string"
        		android:process="string" >
   			...
		</service>
       </application>
   </manifest>

下面對service標籤屬性做說明

  • android:name 你所編寫的服務類的類名,可填寫完整名稱,包名+類名,如com.example.test.ServiceA,也可以忽略包名,用.開頭,如.ServiceA,因爲在manifest文件開頭會定義包名,它會自己引用。

一旦你發佈應用,你就不能改這個名字(除非設置android:exported="false"),另外name沒有默認值,必須定義。

  • android:enabled 是否可以被系統實例化,默認爲true
因爲父標籤`<application>`也有`enable`屬性,所以必須兩個都爲默認值`true`的情況下服務纔會被激活,否則不會激活。
  • android:exported 其他應用能否訪問該服務,如果不能,則只有本應用或有相同用戶ID的應用能訪問。當然除了該屬性也可以在下面permission中限制其他應用訪問本服務。 這個默認值與服務是否包含意圖過濾器intent filters有關。如果一個也沒有則爲false
  • android:isolatedProcess 設置true意味着,服務會在一個特殊的進程下運行,這個進程與系統其他進程分開且沒有自己的權限。與其通信的唯一途徑是通過服務的API(binding and starting)。
  • android:label 可以顯示給用戶的服務名稱。如果沒設置,就用<application>lable。不管怎樣,這個值是所有服務的意圖過濾器的默認lable。定義儘量用對字符串資源的引用。
  • android:icon 類似label,是圖標,儘量用drawable資源的引用定義。
  • android:permission 是一個實體必須要運行或綁定一個服務的權限。如果沒有權限,startService()bindService()stopService()方法將不執行,Intent也不會傳遞到服務。 如果屬性未設置,會由<application>權限設置情況應用到服務。如果兩者都未設置,服務就不受權限保護。
  • android:process 服務運行所在的進程名。通常爲默認爲應用程序所在的進程,與包名同名。<application>元素的屬性process可以設置不同的進程名,當然組件也可設置自己的進程覆蓋應用的設置。 如果名稱設置爲冒號開頭,一個對應用程序私有的新進程會在需要時和運行到這個進程時建立。如果名稱爲小寫字母開頭,服務會在一個相同名字的全局進程運行,如果有權限這樣的話。這允許不同應用程序的組件可以分享一個進程,減少了資源的使用。

創建“啓動的”服務

  啓動的(started)服務由startService(Intent)方法啓動,在服務中的onStartCommand()方法裏獲得Intent信息。關閉則由服務自己的方法stopSelf()或者由啓動服務的地方調用stopService(Intent)方法來關閉。並不會因爲啓動服務的應用程序銷燬而關閉。

  示例,一個應用需要保存數據到遠程數據庫,這時啓動一個服務,通過創建啓動的服務給服務傳遞數據,由服務執行保存行爲,行爲結束再自我銷燬。因爲服務跟啓動它的應用在一個進程的主線程中,所以對於耗時的操作要起一個新的線程去做。

//activity中
Intent intent = new Intent(MainActivity.this, ServiceA.class);
intent.putExtra("name", strName);
startService(intent);

//service中
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
	// TODO Auto-generated method stub
    // 獲取數據
	String strName = intent.getStringExtra("name");
	// ... 數據庫操作
    new Thread(new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				耗時的操作
			}
		}).run();
	return Service.START_STICKY;
}

寫服務有2種,繼承service或者IntentService。後者是前者的子類。前者包含上面介紹的各種方法,用於普通的服務。後者可以自己開一個工作線程一個接一個處理多個請求。

繼承IntentService

大多數服務不需要同時處理多個請求,繼承IntentService是最好的選擇

IntentService處理流程

  • 創建默認的一個worker線程處理傳遞給onStartCommand()的所有intent,不佔據應用的主線程
  • 創建一個工作隊列一次傳遞一個intent到你實現的onHandleIntent()方法,避免了多線程
  • 在所以啓動請求被處理後自動關閉服務,不需要調用stopSelf()
  • 默認提供onBind()的實現,並返回null
  • 默認提供onStartCommand()的實現,實現發送intent到工作隊列再到你的onHandleIntent()方法實現。

這些都加入到IntentService中了,你需要做的就是實現構造方法和onHandleIntent(),如下:

public class HelloIntentService extends IntentService {

  /**
   * A constructor is required, and must call the super IntentService(String)
   * constructor with a name for the worker thread.
   */
  public HelloIntentService() {
      super("HelloIntentService");
  }

  /**
   * The IntentService calls this method from the default worker thread with
   * the intent that started the service. When this method returns, IntentService
   * stops the service, as appropriate.
   */
  @Override
  protected void onHandleIntent(Intent intent) {
      // Normally we would do some work here, like download a file.
      // For our sample, we just sleep for 5 seconds.
      long endTime = System.currentTimeMillis() + 5*1000;
      while (System.currentTimeMillis() < endTime) {
          synchronized (this) {
              try {
                  wait(endTime - System.currentTimeMillis());
              } catch (Exception e) {
              }
          }
      }
  }
}

如果需要重寫其他回調方法,如onCreate(),onStartCommand()等,一定要調用super()方法,保證IntentService正確處理worker線程,只有onHandleIntent()onBind()不需要這樣。如:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
    return super.onStartCommand(intent,flags,startId);
}

繼承Service

繼承Service就可以實現對請求多線程的處理,前面介紹了service的生命週期,可以按照生命週期實現方法。就不放示例了。

onStartCommand()的返回值 返回一個整型值,用來描述系統在殺掉服務後是否要繼續啓動服務,返回值有三種:

  • START_NOT_STICKY 系統不重新創建服務,除非有將要傳遞來的intent。這是最安全的選項,可以避免在不必要的時候運行服務。
  • START_STICKY 系統重新創建服務並且調用onStartCommand()方法,但並不會傳遞最後一次傳遞的intent,只是傳遞一個空的intent。除非存在將要傳遞來的intent,那麼就會傳遞這些intent。這個適合播放器一類的服務,不需要執行命令,只需要獨自運行,等待任務。
  • START_REDELIVER_INTENT 系統重新創建服務並且調用onStartCommand()方法,傳遞最後一次傳遞的intent。其餘存在的需要傳遞的intent會按順序傳遞進來。這適合像下載一樣的服務,立即恢復,積極執行。

如果想從服務獲得結果,可以用廣播來處理

創建“綁定的”服務

  • bindService()方法將應用組件綁定到服務,建立一個長時間保持的聯繫。
  • 如果需要在activity或其他組件和服務交互或者通過進程間通信給其他應用程序提供本應用的功能,就需要綁定的服務。
  • 建立一個綁定的服務需要實現onBind()方法返回一個定義了與服務通信接口的IBinder對象。其他應用程序組件可以調用bindService()方法獲取接口並且調用服務上的方法。
  • 創建一個綁定的服務,第一件事就是定義一個說明客戶端與服務通信方式的接口。這個接口必須是IBinder的實現,並且必須要從onBind()方法返回。一旦客戶端接收到了IBinder,就可以通過這個接口進行交互。
  • 多個客戶端可以綁定到一個服務,可以用unbindService()方法解除綁定,當沒有組件綁定在服務上,這個服務就會被銷燬。
//activity中
private ServiceConnection connB = new ServiceConnection() {

	@Override
	public void onServiceDisconnected(ComponentName name) {
		// TODO Auto-generated method stub
		Log.v(tag, "Service B disconnected");
	}

	@Override
	public void onServiceConnected(ComponentName name, IBinder service) {
		// TODO Auto-generated method stub
		Log.v(tag, "Service B connected");
		MyBinderB binder = (MyBinderB) service;
		ServiceB SB = binder.getService();
		SB.showLog();
	}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);

	findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
		@Override
		public void onClick(View v) {
			// TODO Auto-generated method stub
			Intent a = new Intent(MainActivity.this, ServiceB.class);
			bindService(a, connB, BIND_AUTO_CREATE);
	}
}


//ServiceB
public class ServiceB extends Service {
	public void showLog() {
		Log.i(tag, "serviceB-->showLog()");
	}

	public class MyBinderB extends Binder {

		public ServiceB getService() {
			return ServiceB.this;
		}
	}

	private MyBinderB myBinderB = new MyBinderB();

	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return myBinderB;
	}
}

啓動前臺服務

  前臺服務是被認爲是用戶已知的正在運行的服務,當系統需要釋放內存時不會優先殺掉該進程。前臺進程必須發一個notification在狀態欄中顯示,直到進程被殺死。因爲前臺服務會一直消耗一部分資源,但不像一般服務那樣會在需要的時候被殺掉,所以爲了能節約資源,保護電池壽命,一定要在建前臺服務的時候發notification,提示用戶。當然,系統提供的方法就是必須有notification參數的,所以不要想着怎麼把notification隱藏掉。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
	// TODO Auto-generated method stub
	Intent notificationIntent = new Intent(this, MainActivity.class);
	PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
	Notification noti = new Notification.Builder(this)
				.setContentTitle("Title")
				.setContentText("Message")
				.setSmallIcon(R.drawable.ic_launcher)
				.setContentIntent(pendingIntent)
				.build();
	startForeground(12346, noti);
	return Service.START_STICKY;
}

startForeground()方法就是將服務設爲前臺服務。參數12346就是這個通知唯一的id,只要不爲0即可。

服務的生命週期

  • 啓動的服務: startService()->onCreate()->onStartCommand()->running->stopService()/stopSelf()->onDestroy()->stopped 其中,服務未運行時會調用一次onCreate(),運行時不調用。
  • 綁定的服務: bindService()->onCreate()->onBind()->running->onUnbind()->onDestroy()->stopped

服務起始於onCreate(),終止於onDestory() 服務的開關過程,只有onStartCommand()可多次調用,其他在一個生命週期只調用一次。

這兩個過程並不完全獨立,也可以綁定一個由startService()啓動過的服務

關於怎樣讓服務不被殺死

  這個倒是有點流氓軟件的意思,但有些特定情況還是需要服務能保持開啓不被殺死,當然這樣做我還是在程序裏添加了關閉服務的按鈕,也就是開啓了就殺不死,除非在軟件裏關閉。

服務不被殺死分3種來討論 1.系統根據資源分配情況殺死服務 2.用戶通過settings->Apps->Running->Stop方式殺死服務 3.用戶通過settings->Apps->Downloaded->Force Stop方式殺死服務

第一種情況:   用戶不干預,完全靠系統來控制,辦法有很多。比如onStartCommand()方法的返回值設爲START_STICKY,服務就會在資源緊張的時候被殺掉,然後在資源足夠的時候再恢復。當然也可設置爲前臺服務,使其有高的優先級,在資源緊張的時候也不會被殺掉。

第二種情況:   用戶干預,主動殺掉運行中的服務。這個過程殺死服務會通過服務的生命週期,也就是會調用onDestory()方法,這時候一個方案就是在onDestory()中發送廣播開啓自己。這樣殺死服務後會立即啓動。如下:

@Override
public void onCreate() {
	// TODO Auto-generated method stub
	super.onCreate();

	mBR = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			// TODO Auto-generated method stub
			Intent a = new Intent(ServiceA.this, ServiceA.class);
			startService(a);
		}
	};
	mIF = new IntentFilter();
	mIF.addAction("listener");
	registerReceiver(mBR, mIF);
}

@Override
public void onDestroy() {
	// TODO Auto-generated method stub
	super.onDestroy();

	Intent intent = new Intent();
	intent.setAction("listener");
	sendBroadcast(intent);

	unregisterReceiver(mBR);
}

  當然,從理論上來講這個方案是可行的,實驗一下也可以。但有些情況下,發送的廣播在消息隊列中排的靠後,就有可能服務還沒接收到廣播就銷燬了(這是我對實驗結果的猜想,具體執行步驟暫時還不瞭解)。所以爲了能讓這個機制完美運行,可以開啓兩個服務,相互監聽,相互啓動。服務A監聽B的廣播來啓動B,服務B監聽A的廣播來啓動A。經過實驗,這個方案可行,並且用360殺掉後幾秒後服務也還是能自啓的。到這裏再說一句,如果不是某些功能需要的服務,不建議這麼做,會降低用戶體驗。

第三種情況:   強制關閉就沒有辦法。這個好像是從包的level去關的,並不走完整的生命週期。所以在服務里加代碼是無法被調用的。處理這個情況的唯一方法是屏蔽掉force stopuninstall按鈕,讓其不可用。方法自己去找吧。當然有些手機自帶的清理功能就是從這個地方清理的,比如華爲的清理。所以第三種情況我也沒有什麼更好的辦法了。

  最後再說一句,別在這上面太折騰,弄成流氓軟件就不好了。我就是討厭一些軟件亂髮通知,起服務才轉而用iPhone的。不過下一代Android好像可以支持用戶選擇是否開啓軟件設置的權限了,倒是可以期待一下。 推薦一篇文章:Diamonds Are Forever. Services Are Not.

補充:檢測服務是否在運行的方法,就是獲取所有正在運行的服務,一一與相應的服務名稱做比較:

public static boolean isServiceRunning(String serviceClassName){ 
        final ActivityManager activityManager = (ActivityManager)Application.getContext().getSystemService(Context.ACTIVITY_SERVICE); 
        final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE); //這個value取任意大於1的值,但返回的列表大小可能比這個值小。
 
        for (RunningServiceInfo runningServiceInfo : services) { 
            if (runningServiceInfo.service.getClassName().equals(serviceClassName)){ 
                return true; 
            } 
        } 
        return false; 
     }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章