Service的一些迷思

更木小八發表於2018-11-09

1.為什麼呼叫stopService/unbindService之後Service沒有被銷燬?

通過之前對Service銷燬流程的分析,stopServiceunbindService最終都會進入到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.startRequestedhasConn,前者與start有關,後者與bind有關,只有兩者都為false才能銷燬一個Service。 我們先來看看startRequested

ServiceRecord.startRequested

通過全域性搜尋發現,該欄位只有在ActiveServices.startServiceLocked方法中,也即是start流程中會被置為true。 在ActiveServices.stopServiceLockedActiveServices.stopServiceTokenLockedActiveServices.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

總結

我們以具體場景來分析怎樣才能銷燬一個服務:

  1. 只是用了startService來啟動服務。 這種場景下,只需要呼叫stopService就可以正常銷燬服務
  2. 只是用了bindService啟動服務 這種場景下,只需要呼叫對應的unbindService即可、
  3. 同時使用了startServicebindService 這種場景想要關閉服務的話,首先要呼叫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內部mActionmDatamTypemPackagemComponentmCategories中的任意欄位發生變化,就會產生兩個不同的FilterComparison例項。

結論

在呼叫bindService時,改變一下Intent內部的一些值,就可以觸發多次Service.onBind

覆盤

知道了結論,我們來複盤一下,多次使用同一個IntentbindService的問題 通常我們是以下面這種方式來構造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

相關文章