Service的一些迷思
通過之前對Service銷燬流程的分析,stopService
和unbindService
最終都會進入到ActiveServices.bringDownServiceIfNeededLocked
方法中,該方法會判斷當前的Service
是否滿足銷燬條件,其中的核心方法便是isServiceNeeded
。
private final boolean isServiceNeeded(ServiceRecord r, boolean knowConn, boolean hasConn) { // Are we still explicitly being asked to run? if (r.startRequested) { return true; } // Is someone still bound to us keepign us running? if (!knowConn) { hasConn = r.hasAutoCreateConnections(); } if (hasConn) { return true; } return false; } 複製程式碼
有兩個非常關鍵的變數:ServiceRecord.startRequested
和hasConn
,前者與start
有關,後者與bind
有關,只有兩者都為false
才能銷燬一個Service
。
我們先來看看startRequested
ServiceRecord.startRequested
通過全域性搜尋發現,該欄位只有在ActiveServices.startServiceLocked
方法中,也即是start
流程中會被置為true
。
在ActiveServices.stopServiceLocked
、ActiveServices.stopServiceTokenLocked
、ActiveServices.killServicesLocked
這三個方法中會被置為false,ActiveServices.stopServiceTokenLocked
是在Service
呼叫stopSelf
時會觸發的,而ActiveServices.killServicesLocked
則是在清理應用(記憶體不足等場景)的時候觸發。
簡單來說ServiceRecord.startRequested
會在start
流程中被置為true
,在stop
流程中置為false
。因此,無論你之前呼叫過多少次startService
,只要你調了一次stopService
(之後沒有再呼叫startService
),那麼startRequested
就被置為了false
。**startRequested
的值取決於最後一次呼叫的是startService
還是stopService
。
hasConn
該欄位的值跟ServiceRecord.hasAutoCreateConnection
方法的返回值有關
public boolean hasAutoCreateConnections() { // XXX should probably keep a count of the number of auto-create // connections directly in the service. for (int conni=connections.size()-1; conni>=0; conni--) { ArrayList<ConnectionRecord> cr = connections.valueAt(conni); for (int i=0; i<cr.size(); i++) { //這個flags就是呼叫bindService時使用的flags if ((cr.get(i).flags&Context.BIND_AUTO_CREATE) != 0) { return true; } } } return false; } 複製程式碼
該方法內部會遍歷所有bind
至當前服務的連線,如果還存在任一連線,其呼叫bindService
時使用的flags
包含BIND_AUTO_CREATE
標誌,則返回true
,否則返回false
。
總結
我們以具體場景來分析怎樣才能銷燬一個服務:
-
只是用了
startService
來啟動服務。 這種場景下,只需要呼叫stopService
就可以正常銷燬服務 -
只是用了
bindService
啟動服務 這種場景下,只需要呼叫對應的unbindService
即可、 -
同時使用了
startService
和bindService
這種場景想要關閉服務的話,首先要呼叫stopService
,其次還需要確保之前使用BIND_AUTO_CREATE
進行繫結的客戶端解綁(unbindService
)即可。
2.為啥多次呼叫bindServcie,而onBind只觸發了一次
在Service啟動流程中有一個realStartServiceLocked
方法,在服務程序啟動完畢之後,會呼叫該方法繼續服務啟動的流程。realStartServiceLocked
內部呼叫了一個名為requestServiceBindingsLocked
的方法處理bind
請求。重新貼一下該方法程式碼:
private final void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg) throws TransactionTooLargeException { for (int i=r.bindings.size()-1; i>=0; i--) { IntentBindRecord ibr = r.bindings.valueAt(i); //該方法內部會通過跨程序呼叫ApplicationThread.scheduleBindService //來回調Service.onBind方法 if (!requestServiceBindingLocked(r, ibr, execInFg, false)) { break; } } } 複製程式碼
可以看到這裡有一個for
迴圈,這說明了Service.onBind
被多次回撥是可能的。那麼問題就變成了ServiceRecord.bindings
什麼時候會儲存多個值呢?
對bindings
欄位的put
操作只發生在retrieveAppBindingLocked
方法中,該方法是在bind
流程中的ActiveServices.bindServiceLocked
方法中被呼叫的。
貼下程式碼
public AppBindRecord retrieveAppBindingLocked(Intent intent,//客戶端發起bind請求所使用的Intent ProcessRecord app) {//客戶端程序記錄 Intent.FilterComparison filter = new Intent.FilterComparison(intent); IntentBindRecord i = bindings.get(filter); if (i == null) { i = new IntentBindRecord(this, filter); bindings.put(filter, i); } AppBindRecord a = i.apps.get(app); if (a != null) { return a; } a = new AppBindRecord(this, i, app); i.apps.put(app, a); return a; } 複製程式碼
可以看到該方法首先將intent
封裝成了一個FilterComparison
物件作為key
,然後去bindings
中檢索,如果沒有對應的值就會建立一個值。
再來看看FilterComparison.equals
方法,因為只有創建出不同的FilterComparison
例項,bindings
中才會儲存多個值。
//Intent$FilterComparison.java public boolean equals(Object obj) { if (obj instanceof FilterComparison) { Intent other = ((FilterComparison) obj).mIntent; return mIntent.filterEquals(other); } return false; } //Intent.java public boolean filterEquals(Intent other) { if (other == null) { return false; } if (!Objects.equals(this.mAction, other.mAction)) return false; if (!Objects.equals(this.mData, other.mData)) return false; if (!Objects.equals(this.mType, other.mType)) return false; if (!Objects.equals(this.mPackage, other.mPackage)) return false; if (!Objects.equals(this.mComponent, other.mComponent)) return false; if (!Objects.equals(this.mCategories, other.mCategories)) return false; return true; } 複製程式碼
可以看到,FilterComparison
的比較其實是跟Intent
密切相關的。Intent
內部mAction
、mData
、mType
、mPackage
、mComponent
、mCategories
中的任意欄位發生變化,就會產生兩個不同的FilterComparison
例項。
結論
在呼叫bindService
時,改變一下Intent
內部的一些值,就可以觸發多次Service.onBind
。
覆盤
知道了結論,我們來複盤一下,多次使用同一個Intent
來bindService
的問題
通常我們是以下面這種方式來構造Intent
的
Intent intent = new Intent(activity, DemoService.class); //Intent.java public Intent(Context packageContext, Class<?> cls) { mComponent = new ComponentName(packageContext, cls); } 複製程式碼
這種方式初始化Intent
,最終會將建構函式的入參儲存成mComponent
。
第一次進入bind
流程之後,呼叫retrieveAppBindingLocked
肯定會為bindings
生成一條新的IntentBindRecord
記錄。
這時候如果服務已經啟動,就會馬上進入requestServiceBindingLocked
方法
private final boolean requestServiceBindingLocked(ServiceRecord r, IntentBindRecord i, boolean execInFg, boolean rebind) throws TransactionTooLargeException { //... //requested此時為false if ((!i.requested || rebind) && i.apps.size() > 0) { try { //... r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind, r.app.repProcState); if (!rebind) { //觸發onBind之後requested被置為了true i.requested = true; } i.hasBound = true; i.doRebind = false; } catch (TransactionTooLargeException e) { //... } catch (RemoteException e) { //... } } return true; } 複製程式碼
由此可見,如果使用相同的Intent
請求bind
,那麼第二次進來requested
已經是true
了,便不會觸發Service.onBind
。