web-dev-qa-db-fra.com

Android comment puis-je attendre qu'un service soit réellement connecté?

J'ai une activité appelant un service défini dans IDownloaderService.aidl:

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

Dans Downloader.onCreate (Bundle), j'ai essayé de lierService

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

et dans l'objet ServiceConnection sc je l'ai fait

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

En ajoutant toutes sortes de Log.xx, j'ai trouvé que le code après si (bindService (...)) va réellement AVANT ServiceConnection.onServiceConnected est appelé - c'est-à-dire, lorsque le téléchargeur est toujours nul - ce qui me pose des problèmes. Tous les exemples d'ApiDemos évitent ce problème de synchronisation en n'appelant des services que lorsqu'ils sont déclenchés par des actions utilisateur. Mais que dois-je faire pour utiliser correctement ce service après la réussite de bindService? Comment puis-je attendre que ServiceConnection.onServiceConnected soit appelé de manière fiable?

Une autre question était liée. Tous les gestionnaires d'événements: Activity.onCreate, tout View.onClickListener.onClick, ServiceConnection.onServiceConnected, etc. sont-ils réellement appelés dans le même thread (mentionné dans le document comme "thread principal")? Y a-t-il des entrelacements entre eux, ou Android planifierait tous les événements à gérer un par un? Ou, Quand exactement ServiceConnection.onServiceConnected sera-t-il réellement appelé? À la fin de l'activité. onCreate ou parfois lorsque A.oC est toujours en cours d'exécution?

44
Ryan

Comment puis-je attendre que ServiceConnection.onServiceConnected soit appelé de manière fiable?

Non. Vous sortez de onCreate() (ou partout où vous liez) et vous mettez votre code "a besoin de la connexion établie" dans onServiceConnected().

Tous les gestionnaires d'événements: Activity.onCreate, tout View.onClickListener.onClick, ServiceConnection.onServiceConnected, etc. sont-ils réellement appelés dans le même thread

Oui.

Quand exactement ServiceConnection.onServiceConnected va-t-il réellement être appelé? À la fin de Activity.onCreate ou à un moment où A.oC est toujours en cours d'exécution?

Votre demande de liaison ne va probablement même pas démarrer tant que vous n'aurez pas quitté onCreate(). Par conséquent, onServiceConnected() sera appelée quelque temps après avoir quitté onCreate().

49
CommonsWare

J'ai eu le même problème. Je ne voulais pas mettre mon code dépendant du service lié dans onServiceConnected, car je voulais lier/dissocier avec onStart et onStop, Mais je ne voulais pas code pour s'exécuter à nouveau chaque fois que l'activité revient au premier plan. Je voulais seulement qu'elle s'exécute lors de la création de l'activité.

J'ai finalement récupéré ma vision tunnel onStart() et utilisé un booléen pour indiquer si c'était la première onServiceConnected exécution ou non. De cette façon, je peux délierService dans onStop et bindService à nouveau dans onStart sans exécuter tous les trucs de démarrage à chaque fois.

2
froman

Je me suis retrouvé avec quelque chose comme ça:

1) pour donner une certaine portée aux éléments auxiliaires, j'ai créé une classe interne. Au moins, les internes laids sont séparés du reste du code. J'avais besoin d'un service distant faisant quelque chose , donc le mot Something dans le nom de la classe

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

2) il y a deux choses nécessaires pour invoquer une méthode de service à distance: l'IBinder et le code à exécuter. Puisque nous ne savons pas lequel devient connu en premier, nous les stockons:

private ISomethingService mISomethingService;
private Runnable mActionRunnable;

Chaque fois que nous écrivons dans l'un de ces fichiers, nous invoquons _startActionIfPossible():

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

Bien entendu, cela suppose que Runnable a accès à mISomethingService, mais cela est vrai pour les runnables créés dans les méthodes de la classe RemoteSomethingHelper.

Il est vraiment bon que les rappels ServiceConnectionsoient appelés sur le thread d'interface utilisateur : si nous allons invoquer les méthodes de service à partir du thread principal, nous n'avons pas besoin de nous soucier de la synchronisation.

ISomethingService est, bien entendu, défini via AIDL.

3) Au lieu de simplement passer des arguments aux méthodes, nous créons un Runnable qui invoquera la méthode avec ces arguments plus tard, lorsque l'invocation est 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) enfin, on obtient:

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 est un champ de ma classe; dans une activité, vous pouvez la définir comme Context context=this;

Je n'avais pas besoin d'actions en file d'attente; si vous le faites, vous pouvez l'implémenter.

Vous aurez probablement besoin d'un rappel de résultat dans startSomething (); Je l'ai fait, mais cela n'est pas indiqué dans ce code.

2

J'ai fait quelque chose de similaire auparavant, la seule différence est que je n'étais pas lié au service, mais que je le commençais.

Je diffuserais une intention du service pour informer l'appelant/l'activité qu'elle a commencé.

1
xandy

J'ai compris que ces solutions de contournement ne valaient l'effort et l'attente que si vos services liés étaient exécutés dans un processus différent de celui de votre application.

Pour accéder aux données et méthodes dans le même processus (ou application), j'ai fini par implémenter des classes singleton. Si les classes ont besoin d'un contexte pour certaines méthodes, je divulgue le contexte d'application aux classes singleton. Il y a, bien sûr, une mauvaise conséquence car il rompt le "run instantané". Mais c'est un meilleur compromis global, je pense.

0
Hamid Khan

* L'idée de base est la même avec @ 18446744073709551615, mais je partagerai également mon code.

En réponse à la question principale,

Mais que dois-je faire pour utiliser correctement ce service après la réussite de bindService?

[Attente d'origine (mais ne fonctionne pas)]

attendez que le service soit connecté comme ci-dessous

    @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
        // ...
    }

Cela ne fonctionnera pas car bindService() dans onCreate()/onStart() et onServiceConnected() est appelé à même thread principal. onServiceConnected() n'est jamais appelée avant la fin de l'attente.

[Solution alternative]

Au lieu d '"attendre", définissez votre propre Runnable à appeler après Service Connected et exécutez ce runnable après service connecté.

Implémentez la classe personnalisée de ServiceConnection comme suit.

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);
        }
    }
}

Et puis utilisez-le de la manière suivante (dans votre classe d'activité ou 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();
            // ...
        }
    });
}

Ça a marché pour moi. Mais il peut y avoir une meilleure façon.

0
corochann