Kann ich eine synchrone Anfrage mit Volley machen?

133

Stellen Sie sich vor, ich bin in einem Dienst, der bereits einen Hintergrund-Thread hat. Kann ich eine Anfrage mit Volley in demselben Thread stellen, damit Rückrufe synchron erfolgen?

Dafür gibt es zwei Gründe: - Erstens brauche ich keinen weiteren Thread und es wäre eine Verschwendung, ihn zu erstellen. - Zweitens, wenn ich mich in einem ServiceIntent befinde, wird die Ausführung des Threads vor dem Rückruf beendet, und daher werde ich keine Antwort von Volley erhalten. Ich weiß, dass ich meinen eigenen Dienst erstellen kann, der einen Thread mit einem Runloop hat, den ich steuern kann, aber es wäre wünschenswert, diese Funktionalität in Volley zu haben.

Danke dir!

LocoMike
quelle
5
Lesen Sie unbedingt die Antwort von @ Blundell sowie die hoch bewertete (und sehr nützliche) Antwort.
Jedidja

Antworten:

183

Es sieht so aus, als wäre es mit Volleys RequestFutureKlasse möglich. Um beispielsweise eine synchrone JSON-HTTP-GET-Anforderung zu erstellen, haben Sie folgende Möglichkeiten:

RequestFuture<JSONObject> future = RequestFuture.newFuture();
JsonObjectRequest request = new JsonObjectRequest(URL, new JSONObject(), future, future);
requestQueue.add(request);

try {
  JSONObject response = future.get(); // this will block
} catch (InterruptedException e) {
  // exception handling
} catch (ExecutionException e) {
  // exception handling
}
Matt
quelle
5
@ Tasomaniac Aktualisiert. Dies verwendet den JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)Konstruktor. RequestFuture<JSONObject>implementiert sowohl die Listener<JSONObject>als auch die ErrorListenerSchnittstellen und kann daher als die letzten beiden Parameter verwendet werden.
Matt
21
es blockiert für immer!
Mohammed Subhi Sheikh Quroush
9
Hinweis: Wenn Sie future.get () aufrufen, bevor Sie die Anforderung zur Anforderungswarteschlange hinzufügen, wird sie möglicherweise für immer blockiert.
Datum
3
Es wird für immer blockiert, weil Sie möglicherweise einen Verbindungsfehler haben. Lesen Sie Blundell Answer
Mina Gabriel
4
Man sollte sagen, dass Sie dies nicht im Haupt-Thread tun sollten. Das war mir nicht klar. Wenn der Haupt-Thread von blockiert wird, future.get()wird die App blockiert oder es tritt mit Sicherheit eine Zeitüberschreitung auf, wenn dies festgelegt ist.
r00tandy
125

Hinweis: Die Antwort von @Matthews ist korrekt, ABER wenn Sie sich in einem anderen Thread befinden und einen Volley-Anruf tätigen, wenn Sie kein Internet haben, wird Ihr Fehlerrückruf im Haupt-Thread aufgerufen, aber der Thread, in dem Sie sich befinden, wird für immer blockiert. (Wenn es sich bei diesem Thread um einen IntentService handelt, können Sie niemals eine weitere Nachricht an ihn senden, und Ihr Dienst ist im Grunde genommen tot.)

Verwenden Sie die Version get()davon hat eine Zeitüberschreitungfuture.get(30, TimeUnit.SECONDS) und fangen Sie den Fehler ab, um Ihren Thread zu beenden.

Passend zu @Mathews Antwort:

        try {
            return future.get(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // exception handling
        } catch (ExecutionException e) {
            // exception handling
        } catch (TimeoutException e) {
            // exception handling
        }

Unten habe ich es in eine Methode eingewickelt und eine andere Anfrage verwendet:

   /**
     * Runs a blocking Volley request
     *
     * @param method        get/put/post etc
     * @param url           endpoint
     * @param errorListener handles errors
     * @return the input stream result or exception: NOTE returns null once the onErrorResponse listener has been called
     */
    public InputStream runInputStreamRequest(int method, String url, Response.ErrorListener errorListener) {
        RequestFuture<InputStream> future = RequestFuture.newFuture();
        InputStreamRequest request = new InputStreamRequest(method, url, future, errorListener);
        getQueue().add(request);
        try {
            return future.get(REQUEST_TIMEOUT, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Log.e("Retrieve cards api call interrupted.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (ExecutionException e) {
            Log.e("Retrieve cards api call failed.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (TimeoutException e) {
            Log.e("Retrieve cards api call timed out.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        }
        return null;
    }
Blundell
quelle
Das ist ein ziemlich wichtiger Punkt! Ich bin mir nicht sicher, warum diese Antwort nicht mehr positive Stimmen bekommen hat.
Jedidja
1
Beachten Sie auch, dass Sie die Ausnahme zweimal verarbeiten, wenn Sie die ExecutionException an denselben Listener übergeben, den Sie an die Anforderung übergeben haben. Diese Ausnahme tritt auf, wenn während der Anforderung eine Ausnahme aufgetreten ist, welche Salve für Sie an den errorListener weitergeleitet wird.
Stimsoni
@Blundell Ich verstehe deine Antwort nicht. Wenn der Listener auf dem UI-Thread ausgeführt wird, wartet ein Hintergrund-Thread und der UI-Thread, der notifyAll () aufruft, ist also in Ordnung. Ein Deadlock kann auftreten, wenn die Zustellung auf demselben Thread erfolgt, den Sie mit dem zukünftigen get () blockiert haben. Ihre Antwort scheint also sinnlos zu sein.
greywolf82
1
@ greywolf82 IntentServiceist ein Thread-Pool-Executor eines einzelnen Threads, daher wird IntentService für immer blockiert, da es sich in einer Schleife befindet
Blundell
@Blundell Ich verstehe nicht. Es bleibt so lange bestehen, bis eine Benachrichtigung aufgerufen und vom UI-Thread aufgerufen wird. Bis Sie zwei verschiedene Thread haben, kann ich den Deadlock nicht sehen
greywolf82
9

Es wird wahrscheinlich empfohlen, die Futures zu verwenden, aber wenn Sie aus irgendeinem Grund nicht möchten, sollten Sie eine verwenden, anstatt Ihre eigene synchronisierte Blockierungssache zu kochen java.util.concurrent.CountDownLatch. Das würde also so funktionieren ..

//I'm running this in an instrumentation test, in real life you'd ofc obtain the context differently...
final Context context = InstrumentationRegistry.getTargetContext();
final RequestQueue queue = Volley.newRequestQueue(context);
final CountDownLatch countDownLatch = new CountDownLatch(1);
final Object[] responseHolder = new Object[1];

final StringRequest stringRequest = new StringRequest(Request.Method.GET, "http://google.com", new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        responseHolder[0] = response;
        countDownLatch.countDown();
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        responseHolder[0] = error;
        countDownLatch.countDown();
    }
});
queue.add(stringRequest);
try {
    countDownLatch.await();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
if (responseHolder[0] instanceof VolleyError) {
    final VolleyError volleyError = (VolleyError) responseHolder[0];
    //TODO: Handle error...
} else {
    final String response = (String) responseHolder[0];
    //TODO: Handle response...
}

Da die Leute tatsächlich zu versuchen schienen, dies zu tun, und auf einige Probleme stießen, beschloss ich, tatsächlich ein "reales" Arbeitsprobe davon zur Verfügung zu stellen. Hier ist es https://github.com/timolehto/SynchronousVolleySample

Obwohl die Lösung funktioniert, gibt es einige Einschränkungen. Am wichtigsten ist, dass Sie es nicht im Haupt-UI-Thread aufrufen können. Volley führt die Anforderungen zwar im Hintergrund aus, aber standardmäßig verwendet Volley den Hauptteil Looperder Anwendung, um die Antworten zu versenden. Dies führt zu einem Deadlock, da der Haupt-UI-Thread auf die Antwort wartet, der jedoch auf den Abschluss Looperwartet onCreate, bevor die Lieferung verarbeitet wird. Wenn Sie dies wirklich wirklich tun möchten, können Sie anstelle der statischen Hilfsmethoden Ihre eigene RequestQueueInstanz instanziieren, indem Sie Ihre eigene ExecutorDeliveryan eine Handlerverwenden, Looperdie an einen anderen Thread als den Haupt-UI-Thread gebunden ist.

Timo
quelle
Diese Lösung blockiert meinen Thread für immer, hat Thread.sleep anstelle von countDownLatch geändert und das Problem gelöst
snersesyan
Wenn Sie ein vollständiges Beispiel für einen Code bereitstellen können, der auf diese Weise fehlschlägt, können wir möglicherweise herausfinden, wo das Problem liegt. Ich sehe nicht, wie sinnvoll es ist, in Verbindung mit Countdown-Verschlüssen zu schlafen.
Timo
Okay @VinojVetha Ich habe die Antwort ein wenig aktualisiert, um die Situation zu klären, und ein GitHub-Repo bereitgestellt, mit dem Sie den Code einfach klonen und in Aktion ausprobieren können. Wenn Sie weitere Probleme haben, geben Sie bitte eine Abzweigung des Beispiel-Repos an, in der Ihr Problem als Referenz aufgeführt ist.
Timo
Dies ist eine großartige Lösung für synchrone Anfragen.
Bikram
2

Als ergänzende Beobachtung zu den Antworten von @Blundells und @Mathews bin ich mir nicht sicher, ob ein Anruf von Volley an etwas anderes als den Haupt-Thread gesendet wird.

Die Quelle

Bei einem Blick auf die RequestQueueImplementierung scheint RequestQueuees, dass a verwendet wird NetworkDispatcher, um die Anforderung auszuführen, und a ResponseDelivery, um das Ergebnis zu liefern (das ResponseDeliverywird in das injiziert NetworkDispatcher). Das ResponseDeliverywird wiederum mit einem HandlerSpawn aus dem Hauptthread erstellt (irgendwo um Zeile 112 in der RequestQueueImplementierung).

Irgendwo in Zeile 135 der NetworkDispatcherImplementierung scheinen auch erfolgreiche Ergebnisse durch ResponseDeliveryFehler geliefert zu werden. Nochmal; a ResponseDeliverybasierend auf aHandler Spawn aus dem Haupt-Thread.

Begründung

Für den Anwendungsfall, in dem eine Anfrage von einem gestellt werden soll IntentService soll, kann davon ausgegangen werden, dass der Thread des Dienstes blockiert werden sollte, bis wir eine Antwort von Volley erhalten (um einen lebendigen Laufzeitbereich zu gewährleisten, in dem das Ergebnis verarbeitet werden kann).

Lösungsvorschläge

Ein Ansatz wäre , um die Standard Weise außer Kraft setzen ein RequestQueueerstellt wird , in der ein alternativer Konstruktor stattdessen verwendet wird, die Injektion ein , ResponseDeliverydie Spawns aus dem aktuellen Thread eher als der Haupt - Thread. Ich habe die Auswirkungen davon jedoch nicht untersucht.

dbm
quelle
1
Das Implementieren einer benutzerdefinierten ResponseDelivery-Implementierung wird durch die Tatsache erschwert, dass die finish()Methode in der RequestKlasse und die RequestQueueKlasse paketprivat sind. Abgesehen von der Verwendung eines Reflection-Hacks bin ich mir nicht sicher, ob es einen Weg gibt, dies zu umgehen. Am Ende habe ich einen alternativen Looper-Thread eingerichtet (using Looper.prepareLooper(); Looper.loop()) und eine ExecutorDeliveryInstanz RequestQueuemit einem Handler für diesen Looper an den Konstruktor übergeben , um zu verhindern, dass etwas auf dem Hauptthread (UI) ausgeführt wird . Sie haben den Overhead eines anderen Loopers, bleiben aber vom Hauptfaden fern
Stephen James Hand
1

Ich benutze eine Sperre, um diesen Effekt zu erzielen. Jetzt frage ich mich, ob es richtig ist, wie jemand etwas kommentieren möchte.

// as a field of the class where i wan't to do the synchronous `volley` call   
Object mLock = new Object();


// need to have the error and success listeners notifyin
final boolean[] finished = {false};
            Response.Listener<ArrayList<Integer>> responseListener = new Response.Listener<ArrayList<Integer>>() {
                @Override
                public void onResponse(ArrayList<Integer> response) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        mLock.notify();

                    }


                }
            };

            Response.ErrorListener errorListener = new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        System.out.println();
                        mLock.notify();
                    }
                }
            };

// after adding the Request to the volley queue
synchronized (mLock) {
            try {
                while(!finished[0]) {
                    mLock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
Forcewill
quelle
Ich denke, Sie implementieren im Wesentlichen das, was Volley bereits bietet, wenn Sie "Futures" verwenden.
spaaarky21
1
Ich würde empfehlen, catch (InterruptedException e)die while-Schleife zu verwenden. Andernfalls kann der Thread nicht warten, wenn er aus irgendeinem Grund unterbrochen wird
jayeffkay
@ Jayeffkay Ich fange bereits die Ausnahme ab, wenn eine ** InterruptedException ** in der while-Schleife auftritt, die der Catch behandelt.
Forcewill
1

Ich möchte etwas zu Matthews akzeptierter Antwort hinzufügen. WährendRequestFuture könnte einen synchronen Aufruf aus dem Thread zu machen scheinen Sie es erstellt hat , ist es nicht. Stattdessen wird der Aufruf in einem Hintergrundthread ausgeführt.

Soweit ich weiß, werden nach dem Durchlaufen der Bibliothek Anfragen in der folgenden Methode RequestQueueversendet start():

    public void start() {
        ....
        mCacheDispatcher = new CacheDispatcher(...);
        mCacheDispatcher.start();
        ....
           NetworkDispatcher networkDispatcher = new NetworkDispatcher(...);
           networkDispatcher.start();
        ....
    }

Jetzt beide CacheDispatcherundNetworkDispatcher Klassen den Thread. So wird effektiv ein neuer Worker-Thread erzeugt, um die Anforderungswarteschlange aus der Warteschlange zu entfernen, und die Antwort wird an die Erfolgs- und Fehler-Listener zurückgegeben, die intern von implementiert wurdenRequestFuture .

Ihr zweiter Zweck wird zwar erreicht, Ihr erster Zweck jedoch nicht, da immer ein neuer Thread erzeugt wird, unabhängig davon, von welchem ​​Thread Sie ausgeführt werden RequestFuture .

Zusamenfassend, echte synchrone Anforderung ist mit der Standard-Volley-Bibliothek nicht möglich. Korrigieren Sie mich, wenn ich falsch liege.

Vignatus
quelle
0

Sie können eine Synchronisierungsanforderung mit Volley ausführen, müssen jedoch die Methode in einem anderen Thread aufrufen. Andernfalls wird Ihre laufende App blockiert. Dies sollte folgendermaßen aussehen:

public String syncCall(){

    String URL = "http://192.168.1.35:8092/rest";
    String response = new String();



    RequestQueue requestQueue = Volley.newRequestQueue(this.getContext());

    RequestFuture<JSONObject> future = RequestFuture.newFuture();
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, URL, new JSONObject(), future, future);
    requestQueue.add(request);

    try {
        response = future.get().toString();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    }

    return response;


}

Danach können Sie die Methode im Thread aufrufen:

 Thread thread = new Thread(new Runnable() {
                                    @Override
                                    public void run() {

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Slimani Ibrahim
quelle