Bind to service from new Context for configuration changes or bind from app context?

不想你离开。 提交于 2019-12-02 19:37:24

So after doing some digging I think I have come up with an (as yet) untested solution.

Firstly, based on Diane's suggestion here: https://groups.google.com/forum/#!topic/android-developers/Nb58dOQ8Xfw I should be binding to the application context - so my problem of losing the context is gone - I can maintain my ServiceConnection across configuration changed with a Non-UI fragment - great. Then when I am done I can use the app context to hand back the service connection and unbind. I shouldn't receive any leaky service connection warnings. (I should probably point out that this is a standard and recommended way to maintain instances across config changes)

The final crux of this problem was I was unsure of whether I could bind multiple times from the same context - the documentations on bindings imply there is some dependence between the binding and the context's lifecycle and so I was worried I would have to do my own form of reference counting. I had a look at the source code and ended up here: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.2_r1/android/app/LoadedApk.java#LoadedApk.forgetServiceDispatcher%28android.content.Context%2Candroid.content.ServiceConnection%29

Crucially, these lines:

sd = map.get(c);
    if (sd != null) {
        map.remove(c);
        sd.doForget();
        if (map.size() == 0) {
            mServices.remove(context);
        }

Reveal that the map is being used for the reference counting I was worried about.

SO the take home is this:

  • Bound service will work fine with the app context and we SHOULD do this to prevent leaking a service connection from one activity to another during a config change
  • I can keep my service connection on a non-UI fragment safely and use it to unbind when I am done

I'll try and post some tested code soon.

UPDATE and tested solution: I've made some code to test this and published here: https://github.com/samskiter/BoundServiceTest

It seems to work quite well and the non-ui fragment (data fragment) acts as a nice proxy listener during rotation changes to catch results from the service (the intention of the listeners is to closely bind the requests to the UI in order to guarantee it stays responsive. Obviously any model changes can be propagated to the UI via observers.)

Edit: I thought I should explicitly answer the questions in the OP...

  • should I use the first method (activities with temporary contexts)? Or the second (just bind service to the app context)? The second

  • Am I right in thinking the app context can bind to the service multiple times and then unbind from it the same number of times? (I.e. that you can have multiple valid bindings PER context)? Yes

  • Could using my own context (new Context()) in the first solution cause any issues? This is not even possible

A final summary:

This pattern should be pretty powerful - I can prioritise network IO (or other tasks) coming from a variety of sources across my app. I could have a foreground activity making some small io the user has asked for, simultaneously I could have kicked of a foreground service to sync all my users data. Both the foregrounds service and the activity can be bound to the same Network service to get their requests done.

All this while making sure the service lives only exactly as long as it needs to - i.e. it plays nicely with android.

I'm excited to get this into an app soon.

UPDATE: I've tried to write this up and give some context to the wider problem of background work in a blog entry here: http://blog.airsource.co.uk/2014/09/10/android-bound-services/

Can you not just select configurations that you would like to handle with the configChanges attribute in your manifest and do the orientation changes in the UI manually? In this case you only need to bind to the service in onCreate and then unBind in onDestroy.

or maybe try something like this ( I have not done proper error checking):

  

    class MyServiceConnection implements ServiceConnection,Parcelable {
                public static final Parcelable.Creator CREATOR
                = new Parcelable.Creator() {
                    public MyServiceConnection createFromParcel(Parcel in) {
                        return new MyServiceConnection(in);
                    }

                    public MyServiceConnection[] newArray(int size) {
                        return new MyServiceConnection[size];
                    }
                };

                @Override
                public int describeContents() {
                    return 0;
                }

                @Override
                public void writeToParcel(Parcel dest, int flags) {

                }

                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {

                }

                @Override
                public void onServiceDisconnected(ComponentName name) {

                }
            }
            MyServiceConnection myServiceConnection;
            boolean configChange = false;

            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                if (savedInstanceState != null) {
                    myServiceConnection = savedInstanceState.getParcelable("serviceConnection");
                } else {
                    myServiceConnection = new MyServiceConnection();
                }

            }
            @Override
            protected void onSaveInstanceState(Bundle outState) {
                super.onSaveInstanceState(outState);
                if (myServiceConnection != null) {
                    outState.putParcelable("serviceConnection",myServiceConnection);
                    configChange = true;
                }
            }
            @Override
            protected void onDestroy() {
                super.onDestroy();
                if (!configChange && myServiceConnection != null){
                    unbindService(myServiceConnection);
                }
            }
        }

Rarw

There is a much easier way to handle this situation called an IntentService which you can read more about here. From the android site:

"The IntentService class provides a straightforward structure for running an operation on a single background thread. This allows it to handle long-running operations without affecting your user interface's responsiveness. Also, an IntentService isn't affected by most user interface lifecycle events, so it continues to run in circumstances that would shut down an AsyncTask"

Rather than binding your service to your activity you can start long-running actions on a background thread by simply using an intent that starts your IntentService

public class RSSPullService extends IntentService {

    @Override
    protected void onHandleIntent(Intent workIntent) {
    // Gets data from the incoming Intent
    String dataString = workIntent.getDataString();
    ...
    // Do work here, based on the contents of dataString
    ...
    }
}

This is an example taken from the android docs. You would send an intent with the relevant data then handle that data within the service to do what you want. For example, you could just add a priority flag to your intent so your service knows which requests come before others.

The benefit of an intent service is that it runs on a background thread and is not tied to the lifecycle of the starting activity. That means you configuration changes should not have an effect on the service execution.

When your service is done you can report work status by using a local broadcast - either sending the results directly back to the activity (via broadcast receiver) or possibly even through onNewIntent() (though getting that to work is a bit more clunky.

Edit - answer questions in comment

IntentService is a relatively small class. This makes it easy to modify. The stock code for IntentService calls stopSelf() and dies when it runs out of work to do. This can be easily fixed. Examining the source for the IntentService (see the previous link) you can see that it pretty much works off a queue already, receiving messages in onStart() and then executing them in the order received as described in the comment. Overriding onStart() will allow you to implement a new queue structure to meet your needs. Use the example code there for how to handle the incoming message and get the Intent then just create your own data structure for handling priority. You should be able to start/stop your web requests in the IntentService the same way you would in a Service. Thus by overriding onStart() and onHandleIntent() you should be able to do what you want.

I had a similar problem, where I have a Bound Service used in an Activity. Inside the activity I define a ServiceConnection, mConnection, and inside onServiceConnected I set a class field, syncService that's a reference to the Service:

private SynchronizerService<Entity> syncService;

(...)

/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        // We've bound to LocalService, cast the IBinder and get
        // LocalService instance
        Log.d(debugTag, "on Service Connected");
        LocalBinder binder = (LocalBinder) service;
        //HERE
        syncService = binder.getService();
        //HERE
        mBound = true;
        onPostConnect();
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
        Log.d(debugTag, "on Service Disconnected");
        syncService = null;
        mBound = false;
    }
};

Using this method, whenever the orientation changed I would get a NullPointerException when referencing the syncService variable, despite the fact the service was running, and I tried several methods that never worked.

I was about to implement the solution proposed by Sam, using a retained fragment to keep the variable, but first remembered to try a simple thing: setting the syncService variable to static.. and the connection reference is maintained when the orientation changes!

So now I have

private static SynchronizerService<Entity> syncService = null;

...

/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        // We've bound to LocalService, cast the IBinder and get
        // LocalService instance
        Log.d(debugTag, "on Service Connected");
        LocalBinder binder = (LocalBinder) service;
        //HERE
        if(syncService == null) {
            Log.d(debugTag, "Initializing service connection");
            syncService = binder.getService();
        }
        //HERE
        mBound = true;
        onPostConnect();
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
        Log.d(debugTag, "on Service Disconnected");
        syncService = null;
        mBound = false;
    }
};
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!