preface

Here’s an interview question: What’s the difference between startService and bindService? Why can bindService interact with the Activity lifecycle?

The first question can be answered quickly: different life cycles, different endings, different interactions.

The Activity destroyed the Service while it was being destroyed. So why doesn’t startService work? How does that work? Without studying the source code, it seems impossible to give a convincing answer, hence this article.

The difference between startup and binding

Whether you start an Activity or a Service, The basic process is Context -> ActivtityManagerService -> some intermediate classes (Activity is ActivityStarter, ActivityStack, etc.) Service is ActiveServices) -> ActivityThread. The specific code flow is long, and much of it is irrelevant to the topic of this article, so I will not analyze the startup or binding process in detail, and will only preserve some of the source code relevant to this article.

startService

Start with ContextImpl’s startService method:

class ContextImpl extends Context {

    @Override
    public ComponentName startService(Intent service) {
        warnIfCallingFromSystemProcess();
        return startServiceCommon(service, false, mUser); } private ComponentName startServiceCommon(Intent service, boolean requireForeground, UserHandle user) {try {// Check Intent validateServiceIntent(service); service.prepareToLeaveProcess(this); / / start the Service the ComponentName cn = ActivityManager. GetService () startService (mMainThread. GetApplicationThread (), service, service.resolveTypeIfNeeded( getContentResolver()), requireForeground, getOpPackageName(), user.getIdentifier()); // Check the resultif(cn ! = null) { ... }returncn; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}}Copy the code

ActivityManager. GetService () returns is AMS itself, and AMS only play the role of a transit, except for some parameter judgment, AMS directly call the ActiveServices startServiceLocked:

public final class ActiveServices {

    final ActivityManagerService mAm;

    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
            int callingPid, int callingUid, boolean fgRequired, String callingPackage, Final int userId) throws TransactionTooLargeException {/ / and the Activity also have a Record to Record the corresponding component ServiceRecord r = res. Record;  . R.activity = systemclock. uptimeMillis(); r.activity = systemclock. uptimeMillis(); r.startRequested =true;
        r.delayedStop = false;
        r.fgRequired = fgRequired;
        r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(), service, neededGrants, callingUid)); . CMP = ComponentName CMP = startServiceInnerLocked(smap, service, r,callerFg, addToStarting);
        returncmp; }}Copy the code

The startService process is now covered, but there is nothing special about it. At the end of the day, ActivityThread will create the Service object, call back the related lifecycle methods, and so on.

bindService

Here is an implementation of bindService:

class ContextImpl extends Context {

    final @NonNull LoadedApk mPackageInfo;
    private final @Nullable IBinder mActivityToken;

    @Override
    public boolean bindService(Intent service, ServiceConnection conn,
            int flags) {
        warnIfCallingFromSystemProcess();
        return bindServiceCommon(service, conn, flags, mMainThread.getHandler(),
                Process.myUserHandle());
    }
    
    private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags, Handler
            handler, UserHandle user) {
        ...
        IServiceConnection sd;
        if(mPackageInfo ! = null) {/ / sd = mPackageInfo. Note here getServiceDispatcher (conn, getOuterContext (), the handler, flags); }else {
            throw new RuntimeException("Not supported in system context"); } try {// This token is created when the Activity starts, corresponding to the mToken member of the Activity IBinder Token = getActivityToken(); . / / perform binding process int res = ActivityManager. GetService () bindService (mMainThread. GetApplicationThread (), getActivityToken (), service, service.resolveTypeIfNeeded(getContentResolver()), sd, flags, getOpPackageName(), user.getIdentifier()); . } catch (RemoteException e) { ... } } @Override public IBindergetActivityToken() {
        returnmActivityToken; }}Copy the code

BindService is different from startService when ContextImpl is executed. In addition to obtaining the token of the Activity, Another key call is LoadedApk’s getServiceDispatcher method:

public final class LoadedApk {

    private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
        = new ArrayMap<>();

    public final IServiceConnection getServiceDispatcher(ServiceConnection c,
            Context context, Handler handler, int flags) {
        synchronized (mServices) {
            LoadedApk.ServiceDispatcher sd = null;
            ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);
            if(map ! = null) { sd = map.get(c); }if (sd == null) {
                sd = new ServiceDispatcher(c, context, handler, flags);
                if(map == null) { map = new ArrayMap<>(); mServices.put(context, map); } map.put(c, sd); // record ServiceConnection}else {
                sd.validate(context, handler);
            }
            returnsd.getIServiceConnection(); }}}Copy the code

ServiceDispatcher can be ignored and focuses on the mServices member, which records the ServiceConnection to be bound to the Activity.

Moving on to the binding process, AMS also skips it and goes straight to the implementation of ActiveServices:

public final class ActiveServices {

    final ActivityManagerService mAm;
    final ArrayMap<IBinder, ArrayList<ConnectionRecord>> mServiceConnections = new ArrayMap<>();

    int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service, String resolvedType, final IServiceConnection connection, int flags, String callingPackage, final int userId) throws TransactionTooLargeException {/ / obtain information on the application process final ProcessRecordcallerApp = mAm.getRecordForAppLocked(caller);
        if (callerApp == null) {
            throw new SecurityException(
                    "Unable to find app for caller " + caller
                    + " (pid=" + Binder.getCallingPid()
                    + ") when binding service "+ service); ActivityRecord Activity = null;if(token ! = null) { activity = ActivityRecord.isInStackLocked(token);if (activity == null) {
                Slog.w(TAG, "Binding with unknown activity: " + token);
                return0; ServiceRecord s = res.record; boolean permissionsReviewRequired =false; // Start the Activity, and then start the Serviceif (mAm.mPermissionReviewRequired) {
            if (mAm.getPackageManagerInternalLocked().isPermissionsReviewRequired(
                    s.packageName, s.userId)) {

                RemoteCallback callback = new RemoteCallback(
                        new RemoteCallback.OnResultListener() {
                    @Override
                    public void onResult(Bundle result) {
                        synchronized(mAm) {
                            final long identity = Binder.clearCallingIdentity();
                            try {
                                ...
                                if(...). {try {// Start Service bringUpServiceLocked(...) ; } catch (RemoteException e) { /* ignore -local call */
                                    }
                                } else{... } } finally { ... }}}}); final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSIONS); PutExtra (intent.extra_remote_callback, callback); // Start the Activity and call callback mam.mhandler. post(new)Runnable() {
                    @Override
                    public void run() { mAm.mContext.startActivityAsUser(intent, new UserHandle(userId)); }}); } } final long origId = Binder.clearCallingIdentity(); try { ... ActivityRecord ConnectionRecord c = new ConnectionRecord(b, Activity, Connection, flags, clientLabel, clientIntent); // Connection type IServiceConnection IBinder binder = connection.asbinder (); // Let ActivityRecord record connections informationif(activity ! = null) {if(activity.connections == null) { activity.connections = new HashSet<ConnectionRecord>(); } activity.connections.add(c); ArrayList<ConnectionRecord> clist = s.connections.get(binder);if(clist == null) { clist = new ArrayList<ConnectionRecord>(); s.connections.put(binder, clist); } clist.add(c); Clist = mServiceConnections. Get (binder);if(clist == null) { clist = new ArrayList<ConnectionRecord>(); mServiceConnections.put(binder, clist); } clist.add(c); .if((flags&Context.BIND_AUTO_CREATE) ! = 0) {// Automatically start s.lastActivity = systemclock. uptimeMillis(); / / start the Serviceif (bringUpServiceLocked(s, service.getFlags(), callerFg, false, permissionsReviewRequired) ! = null) {return0; }}if(s.app ! = null &&b.intent. Received) {// The Service is already running, // Service is already running; Connection.try {// call onServiceConnected c.connected (s.name, connected); b.intent.binder,false); } catch (Exception e) { ... }... }else if(! B.i ntent. Requested) {/ / callback onBind, internal calls the scheduleBindService requestServiceBindingLocked (s, b.i ntent,callerFg, false);
            }

        } finally {
            Binder.restoreCallingIdentity(origId);
        }

        return1; }}Copy the code

As you can see, bindService does some extra work before starting the Service compared to startService:

  1. Notify LoadedApk to record ServiceConnection
  2. Get an ActivityRecord based on ActivityToken
  3. Add ConnectionRecord to ActivityRecord and ServiceRecord

The main differences between startService and bindService in the source code implementation are described above. Let’s start by analyzing the Activity’s Finish method to see how the Service is destroyed when the Activity is destroyed.

How is a Service destroyed when an Activity is destroyed?

The Activity -> ActivityManager -> ActivitiyStack -> ActivityThread process is the same as the start process. So look directly at the implementation of ActivityThread:

public final class ActivityThread { final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>(); private void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, Boolean getNonConfigInstance () {// The callback lifecycle method ActivityClientRecord r = performDestroyActivity(Token, finishing, configChanges, getNonConfigInstance);if(r ! = null) {/ / cleaning Windows resources cleanUpPendingRemoveWindows (r, finishing); / / delete DecorView WindowManager wm = state Richard armitage ctivity. GetWindowManager (); View v = r.activity.mDecor;if(v ! = null) { IBinder wtoken = v.getWindowToken();if (r.activity.mWindowAdded) {
                    if (r.mPreserveWindow) {
                        r.window.clearContentView();
                    } else{ wm.removeViewImmediate(v); }} // Clear the record, which can be referenced in ViewRootImplsetThe View methodif(wtoken ! = null && r.mPendingRemoveWindow == null) { WindowManagerGlobal.getInstance().closeAll(wtoken, r.activity.getClass().getName(),"Activity");
                } else if(r.mPendingRemoveWindow ! = null) { WindowManagerGlobal.getInstance().closeAllExceptView(token, v, r.activity.getClass().getName(),"Activity");
                }
                r.activity.mDecor = null;
            }
            
            if (r.mPendingRemoveWindow == null) {
                WindowManagerGlobal.getInstance().closeAll(token,
                        r.activity.getClass().getName(), "Activity"); } / / using the Base Context performing a final cleaning steps Context c = state Richard armitage ctivity. GetBaseContext ();if (c instanceof android.app.ContextImpl) {
                ((ContextImpl) c).scheduleFinalCleanup(
                        r.activity.getClass().getName(), "Activity"); }} // Notify AMSif (finishing) {
            try {
                ActivityManager.getService().activityDestroyed(token);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
        mSomeActivitiesChanged = true; }}Copy the code

As you can see, in ActivityThread, the Activity destruction process has four steps:

  1. Callback lifecycle methods such as onPause, onStop, and onDestroy
  2. Close Windows, remove the DecorView, and clean up Windows Manager records
  3. Call ContextImpl to perform the final cleanup step
  4. Notify AMS Activity that it has been destroyed

The unbinding logic for the Service is hidden in ContextImpl.

class ContextImpl extends Context { final @NonNull ActivityThread mMainThread; final @NonNull LoadedApk mPackageInfo; ActivityThread final void scheduleFinalCleanup(String who, String what) { mMainThread.scheduleContextCleanup(this, who, what); Final void performFinalCleanup(String who, String what) {//Log."Cleanup up context: "+ this); mPackageInfo.removeContextRegistrations(getOuterContext(), who, what); }}Copy the code

As you can see, ContextImpl only serves as a relay and is ultimately delivered to LoadedApk for execution. RemoveContextRegistrations names can infer from the method, its purpose is to clean up the registration to the Context of resources:

public final class LoadedApk { private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers = new ArrayMap<>(); private final ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mUnregisteredReceivers = new ArrayMap<>(); private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices = new ArrayMap<>(); private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices = new ArrayMap<>(); public void removeContextRegistrations(Context context, String who, String what) { final boolean reportRegistrationLeaks = StrictMode.vmRegistrationLeaksEnabled(); Synchronized (mReceivers) {// Get BroadcastReceiver registered in Context LoadedApk.ReceiverDispatcher> rmap = mReceivers.remove(context);if(rmap ! = null) {// iterate over and log out one by onefor(int i = 0; i < rmap.size(); i++) { LoadedApk.ReceiverDispatcher rd = rmap.valueAt(i); IntentReceiverLeaked = new IntentReceiverLeaked(what +"" + who + " has leaked IntentReceiver "
                            + rd.getIntentReceiver() + " that was " +
                            "originally registered here. Are you missing a " +
                            "call to unregisterReceiver()?");
                    leak.setStackTrace(rd.getLocation().getStackTrace());
                    Slog.e(android.app.ActivityThread.TAG, leak.getMessage(), leak);
                    if(reportRegistrationLeaks) { StrictMode.onIntentReceiverLeaked(leak); } / / notify the AMS logout BroadcastReceiver try {ActivityManager. GetService () unregisterReceiver (rd) getIIntentReceiver ()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } mUnregisteredReceivers.remove(context); } synchronized (mServices) {// obtain ServiceConnection bound to Context ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> smap = mServices.remove(context);if(smap ! = null) {// iterate, unbind one by onefor(int i = 0; i < smap.size(); i++) { LoadedApk.ServiceDispatcher sd = smap.valueAt(i); ServiceConnectionLeaked = new ServiceConnectionLeaked(what +"" + who + " has leaked ServiceConnection "
                            + sd.getServiceConnection() + " that was originally bound here");
                    leak.setStackTrace(sd.getLocation().getStackTrace());
                    Slog.e(android.app.ActivityThread.TAG, leak.getMessage(), leak);
                    if(reportRegistrationLeaks) { StrictMode.onServiceConnectionLeaked(leak); } / / notify the AMS unbundling ServiceConnection try {ActivityManager. GetService () unbindService (sd) getIServiceConnection ()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } sd.doForget(); } } mUnboundServices.remove(context); }}}Copy the code

Sure enough, removeContextRegistrations the role of registered/bind to the Context of BroadcastReceiver, ServiceConnection to logout/solution, and to throw an exception information, tell the user should actively cancel/unbundling. The unbindService, unregisterReceiver process ignores it and simply removes some records from the associated list (such as ActivityThread) and notifies the ActivityThread to perform the final logout logic.

conclusion

After analyzing the code above, you can now confidently answer this interview question:

  1. When the bindService method is executed, LoadedApk records ServiceConnection information
  2. When the Activity executes the Finish method, LoadedApk checks to see if there are BroadcastReceiver and ServiceConnection unregistered/unbound. If there are, AMS will be notified to derestrate/unbind the BroadcastReceiver and Service, and an exception message will be printed to inform the user that the derestrate/unbind operation should be performed voluntarily