Android how do I wait until a service is actually

2020-01-29 03:48发布

I have an Activity calling a Service defined in IDownloaderService.aidl:

public class Downloader extends Activity {
 IDownloaderService downloader = null;
// ...

In Downloader.onCreate(Bundle) I tried to bindService

Intent serviceIntent = new Intent(this, DownloaderService.class);
if (bindService(serviceIntent, sc, BIND_AUTO_CREATE)) {
  // ...

and within the ServiceConnection object sc I did this

public void onServiceConnected(ComponentName name, IBinder service) {
  Log.w("XXX", "onServiceConnected");
  downloader = IDownloaderService.Stub.asInterface(service);
  // ...

By adding all kinds of Log.xx I found that the code after if(bindService(...)) actually goes BEFORE ServiceConnection.onServiceConnected is being called - that is, when downloader is still null - which gets me into trouble. All the samples in ApiDemos avoid this timing problem by only calling services when triggered by user actions. But what should I do to right use this service after bindService succeeds? How can I wait for ServiceConnection.onServiceConnected being called reliably?

Another question related. Are all the event handlers: Activity.onCreate, any View.onClickListener.onClick, ServiceConnection.onServiceConnected, etc. actually called in the same thread (mentioned in the doc as the "main thread")? Are there interleaves between them, or Android would schedule all events come into being handled one-by-one? Or, When exactly is ServiceConnection.onServiceConnected actually going to be called? Upon completion of Activity.onCreate or sometime when A.oC is still running?

6条回答
Luminary・发光体
2楼-- · 2020-01-29 04:22

I ended up with something like this:

1) to give the auxiliary stuff some scope, I created an internal class. At least, the ugly internals are separated from the rest of the code. I needed a remote service doing something, therefore the word Something in class name

private RemoteSomethingHelper mRemoteSomethingHelper = new RemoteSomethingHelper();
class RemoteSomethingHelper {
//...
}

2) there are two things necessary to invoke a remote service method: the IBinder and the code to execute. Since we don't know which one becomes known first, we store them:

private ISomethingService mISomethingService;
private Runnable mActionRunnable;

Each time we write to one of these fileds, we invoke _startActionIfPossible():

    private void _startActionIfPossible() {
        if (mActionRunnable != null && mISomethingService != null) {
            mActionRunnable.run();
            mActionRunnable = null;
        }
    }
    private void performAction(Runnable r) {
        mActionRunnable = r;
        _startActionIfPossible();
    }

This, of course, assumes that the Runnable has access to mISomethingService, but this is true for runnables created within the methods of the RemoteSomethingHelper class.

It is really good that the ServiceConnection callbacks are called on the UI thread: if we are going to invoke the service methods from the main thread, we do not need to care about synchronization.

ISomethingService is, of course, defined via AIDL.

3) Instead of just passing arguments to methods, we create a Runnable that will invoke the method with these arguments later, when invocation is possible:

    private boolean mServiceBound;
    void startSomething(final String arg1) {
        // ... starting the service ...
        final String arg2 = ...;
        performAction(new Runnable() {
            @Override
            public void run() {
                try {
                    // arg1 and arg2 must be final!
                    mISomethingService.startSomething(arg1, arg2);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
    }

4) finally, we get:

private RemoteSomethingHelper mRemoteSomethingHelper = new RemoteSomethingHelper();
class RemoteSomethingHelper {
    private ISomethingService mISomethingService;
    private Runnable mActionRunnable;
    private boolean mServiceBound;
    private void _startActionIfPossible() {
        if (mActionRunnable != null && mISomethingService != null) {
            mActionRunnable.run();
            mActionRunnable = null;
        }
    }
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        // the methods on this class are called from the main thread of your process.
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mISomethingService = null;
        }
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mISomethingService = ISomethingService.Stub.asInterface(service);
            _startActionIfPossible();
        }
    }
    private void performAction(Runnable r) {
        mActionRunnable = r;
        _startActionIfPossible();
    }

    public void startSomething(final String arg1) {
        Intent intent = new Intent(context.getApplicationContext(),SomethingService.class);
        if (!mServiceBound) {
            mServiceBound = context.getApplicationContext().bindService(intent, mServiceConnection, 0);
        }
        ComponentName cn = context.getApplicationContext().startService(intent);
        final String arg2 = ...;
        performAction(new Runnable() {
            @Override
            public void run() {
                try {
                    mISomethingService.startSomething(arg1, arg2);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

context is a field in my class; in an Activity, you can define it as Context context=this;

I did not need queuing actions; if you do, you can implement it.

You likely will need a result callback in startSomething(); I did, but this is not shown in this code.

查看更多
冷血范
3楼-- · 2020-01-29 04:27

*The basic idea is same with @18446744073709551615, but I will share my code as well.

As a answer of main question,

But what should I do to right use this service after bindService succeeds?

[Original expectation (but not work)]

wait until service connected like below

    @Override
    protected void onStart() {
        bindService(service, mWebServiceConnection, BIND_AUTO_CREATE);
        synchronized (mLock) { mLock.wait(40000); }

        // rest of the code continues here, which uses service stub interface
        // ...
    }

It won't work because both bindService() in onCreate()/onStart() and onServiceConnected() is called at same main thread. onServiceConnected() is never called before wait finishes.

[Alternative solution]

Instead of "wait", define own Runnable to be called after Service Connected and execute this runnable after service connected.

Implement custom class of ServiceConnection as follows.

public class MyServiceConnection implements ServiceConnection {

    private static final String TAG = MyServiceConnection.class.getSimpleName();

    private Context mContext = null;
    private IMyService mMyService = null;
    private ArrayList<Runnable> runnableArrayList;
    private Boolean isConnected = false;

    public MyServiceConnection(Context context) {
        mContext = context;
        runnableArrayList = new ArrayList<>();
    }

    public IMyService getInterface() {
        return mMyService;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.v(TAG, "Connected Service: " + name);
        mMyService = MyService.Stub.asInterface(service);

        isConnected = true;
        /* Execute runnables after Service connected */
        for (Runnable action : runnableArrayList) {
            action.run();
        }
        runnableArrayList.clear();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        try {
            mMyService = null;
            mContext.unbindService(this);
            isConnected = false;
            Log.v(TAG, "Disconnected Service: " + name);
        } catch(Exception e) {
            Log.e(TAG, e.toString());
        }
    }

    public void executeAfterServiceConnected(Runnable action) {
        Log.v(TAG, "executeAfterServiceConnected");
        if(isConnected) {
            Log.v(TAG, "Service already connected, execute now");
            action.run();
        } else {
            // this action will be executed at the end of onServiceConnected method
            Log.v(TAG, "Service not connected yet, execute later");
            runnableArrayList.add(action);
        }
    }
}

And then use it in the following way (in your Activity class or etc),

private MyServiceConnection myServiceConnection = null;

@Override
protected void onStart() {
    Log.d(TAG, "onStart");
    super.onStart();

    Intent serviceIntent = new Intent(getApplicationContext(), MyService.class);
    startService(serviceIntent);
    myServiceConnection = new MyServiceConnection(getApplicationContext());
    bindService(serviceIntent, myServiceConnection, BIND_AUTO_CREATE);

    // Instead of "wait" here, create callback which will be called after service is connected
    myServiceConnection.executeAfterServiceConnected(new Runnable() {
        @Override
        public void run() {
            // Rest of the code comes here.
            // This runnable will be executed after service connected, so we can use service stub interface
            IMyService myService = myServiceConnection.getInterface();
            // ...
        }
    });
}

It worked for me. But there may be more better way.

查看更多
趁早两清
4楼-- · 2020-01-29 04:29

I did something similar before, the only different is I was not binding to service, but just starting it.

I would broadcast an intent from the service to notify the caller/activity about it is started.

查看更多
劳资没心,怎么记你
5楼-- · 2020-01-29 04:29

I figured out that these workarounds are only worth the effort and the wait only if your bound services are running in a different process than your application's main process.

For accessing data and methods in the same process (or application), I ended up implementing singleton classes. If the classes need a context for some methods, I leak the application context to the singleton classes. There is, of course, a bad consequence of it as it breaks the "instant run". But that is an overall better compromise, I think.

查看更多
叼着烟拽天下
6楼-- · 2020-01-29 04:32

How can I wait for ServiceConnection.onServiceConnected being called reliably?

You don't. You exit out of onCreate() (or wherever you are binding) and you put you "needs the connection established" code in onServiceConnected().

Are all the event handlers: Activity.onCreate, any View.onClickListener.onClick, ServiceConnection.onServiceConnected, etc. actually called in the same thread

Yes.

When exactly is ServiceConnection.onServiceConnected actually going to be called? Upon completion of Activity.onCreate or sometime when A.oC is still running?

Your bind request probably is not even going to start until after you leave onCreate(). Hence, onServiceConnected() will called sometime after you leave onCreate().

查看更多
三岁会撩人
7楼-- · 2020-01-29 04:34

I had the same problem. I didn't want to put my bound service dependent code in onServiceConnected, though, because I wanted to bind/unbind with onStart and onStop, but I didn't want the code to run again every time the activity came back to the front. I only wanted it to run when the activity was first created.

I finally got over my onStart() tunnel vision and used a Boolean to indicate whether this was the first onServiceConnected run or not. That way, I can unbindService in onStop and bindService again in onStart without running all the start up stuff each time.

查看更多
登录 后发表回答