Posso fare una richiesta sincrona con volley?


133

Immagina di essere in un servizio che ha già un thread in background. Posso fare una richiesta usando volley nello stesso thread, in modo che i callback avvengano in modo sincrono?

Ci sono 2 ragioni per questo: - Innanzitutto, non ho bisogno di un altro thread e sarebbe uno spreco per crearlo. - In secondo luogo, se mi trovo in un ServiceIntent, l'esecuzione del thread terminerà prima del callback e quindi non riceverò risposta da Volley. So che posso creare il mio servizio che ha qualche thread con un runloop che posso controllare, ma sarebbe desiderabile avere questa funzionalità nel pallavolo.

Grazie!


5
Assicurati di leggere la risposta di @ Blundell e la risposta altamente votata (e molto utile).
Jedidja,

Risposte:


183

Sembra che sia possibile con la RequestFutureclasse di Volley . Ad esempio, per creare una richiesta GET HTTP JSON sincrona, è possibile effettuare le seguenti operazioni:

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
}

5
@tasomaniac Aggiornato. Questo utilizza il JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)costruttore. RequestFuture<JSONObject>implementa entrambe le interfacce Listener<JSONObject>e ErrorListener, quindi può essere usato come gli ultimi due parametri.
Matt,

21
sta bloccando per sempre!
Mohammed Subhi Sheikh Quroush,

9
Suggerimento: potrebbe essere bloccato per sempre se si chiama future.get () PRIMA di aggiungere la richiesta alla coda delle richieste.
datayeah

3
sarà bloccato per sempre perché potresti avere un errore di connessione leggere la risposta Blundell
Mina Gabriel

4
Si dovrebbe dire che non dovresti farlo sul thread principale. Questo non mi era chiaro. Causa se il thread principale è bloccato da future.get()allora l'app si bloccherà o si verificherà in timeout se impostato in questo modo.
r00tandy,

125

Nota La risposta @Matthews è corretta MA se ti trovi su un altro thread e fai una chiamata al volo quando non hai internet, il tuo callback di errore verrà chiamato sul thread principale, ma il thread su cui ti trovi verrà bloccato PER SEMPRE. (Pertanto, se quel thread è un IntentService, non sarai mai in grado di inviargli un altro messaggio e il tuo servizio sarà praticamente morto).

Usa la versione get()che ha un timeout future.get(30, TimeUnit.SECONDS)e rileva l'errore per uscire dal thread.

Per abbinare la risposta di @Mathews:

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

Di seguito l'ho avvolto in un metodo e utilizzo una richiesta diversa:

   /**
     * 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;
    }

Questo è un punto abbastanza importante! Non sono sicuro del perché questa risposta non abbia ottenuto più voti.
Jedidja,

1
Vale anche la pena notare che se si passa ExecutionException allo stesso listener passato nella richiesta, si elaborerà l'eccezione due volte. Questa eccezione si verifica quando si è verificata un'eccezione durante la richiesta che il volley passerà attraverso il ErrorListener per te.
Stimsoni

@Blundell Non capisco la tua risposta. Se il listener viene eseguito sul thread dell'interfaccia utente, hai un thread in background in attesa e il thread dell'interfaccia utente che chiama notificationAll (), quindi va bene. Un deadlock può verificarsi se la consegna viene eseguita sullo stesso thread bloccato con il futuro get (). Quindi la tua risposta sembra senza senso.
greywolf82,

1
@ greywolf82 IntentServiceè un esecutore di pool di thread di un singolo thread, pertanto IntentService verrà bloccato per sempre mentre si trova in un ciclo
Blundell,

@Blundell non capisco. Rimane fino a quando verrà chiamato un avviso e verrà chiamato dal thread dell'interfaccia utente. Fino a quando non avrai due thread diversi, non riesco a vedere lo stallo
greywolf82,

9

Probabilmente si consiglia di utilizzare i Futures, ma se per qualsiasi motivo non si desidera, invece di cucinare la propria cosa di blocco sincronizzato si dovrebbe usare un java.util.concurrent.CountDownLatch. Quindi funzionerebbe così ..

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

Dal momento che le persone sembravano effettivamente provare a fare questo e si sono imbattuti in alcuni problemi, ho deciso che avrei effettivamente fornito un esempio di "vita reale" di questo in uso. Qui è https://github.com/timolehto/SynchronousVolleySample

Ora, anche se la soluzione funziona, presenta alcune limitazioni. Ancora più importante, non puoi chiamarlo sul thread dell'interfaccia utente principale. Volley esegue le richieste in background, ma per impostazione predefinita Volley utilizza il principale Looperdell'applicazione per inviare le risposte. Ciò provoca un deadlock poiché il thread dell'interfaccia utente principale è in attesa della risposta, ma Looperè in attesa onCreatedi terminare prima di elaborare la consegna. Se vuoi davvero farlo, potresti, al posto dei metodi di supporto statici, creare un'istanza del tuo RequestQueuepassaggio facendolo ExecutorDeliverylegato a un Handleruso Looperche è legato a un thread diverso dal thread dell'interfaccia utente principale.


Questa soluzione blocca il mio thread per sempre, ha cambiato Thread.sleep invece di countDownLatch e il problema è stato risolto
snersesyan

Se puoi fornire un esempio completo di un codice che non riesce in questo modo, forse possiamo capire qual è il problema. Non vedo come abbia senso dormire insieme ai fermi del conto alla rovescia.
Timo,

Va bene @VinojVetha Ho aggiornato un po 'la risposta per chiarire la situazione e ho fornito un repository GitHub che puoi facilmente clonare e provare il codice in azione. In caso di ulteriori problemi, fornire un fork del repository di esempio che mostri il problema come riferimento.
Timo,

Questa è un'ottima soluzione per la richiesta sincrona finora.
bikram

2

Come osservazione complementare alle risposte sia di @Blundells sia di @Mathews, non sono sicuro che qualsiasi chiamata venga recapitata a tutto tranne che al thread principale di Volley.

La fonte

Dando un'occhiata RequestQueueall'implementazione sembra che RequestQueuestia usando a NetworkDispatcherper eseguire la richiesta e a ResponseDeliveryper consegnare il risultato (il ResponseDeliveryviene iniettato nel NetworkDispatcher). A ResponseDeliverysua volta, viene creato con una Handlerspawn dal thread principale (da qualche parte attorno alla linea 112 inRequestQueue implementazione).

Da qualche parte circa la linea 135 NetworkDispatchernell'implementazione sembra che anche i risultati positivi vengano forniti attraverso gli stessi ResponseDeliverydi eventuali errori. Ancora; a ResponseDeliverybasato su una Handlerspawn dal thread principale.

Fondamento logico

Per il caso d'uso in cui una richiesta deve essere fatta da un IntentServiceè giusto supporre che il thread del servizio dovrebbe bloccarsi fino a quando non abbiamo una risposta da Volley (per garantire un ambito di runtime vivente per gestire il risultato).

Soluzioni suggerite

Un approccio sarebbe quello di sovrascrivere il modo predefinito in cui RequestQueueviene creato un , in cui viene utilizzato un costruttore alternativo, iniettando un ResponseDeliveryche viene generato dal thread corrente anziché dal thread principale. Non ho studiato le implicazioni di questo, comunque.


1
L'implementazione di un'implementazione personalizzata di ResponseDelivery è complicata dal fatto che il finish()metodo nella Requestclasse e nella RequestQueueclasse sono pacchetti privati, a parte l'uso di un hack di riflessione non sono sicuro che ci sia un modo per aggirare questo. Quello che ho finito per impedire che qualcosa funzionasse sul thread principale (UI) era impostare un thread Looper alternativo (usando Looper.prepareLooper(); Looper.loop()) e passare ExecutorDeliveryun'istanza al RequestQueuecostruttore con un gestore per quel looper. Hai il sovraccarico di un altro crochet ma stai lontano dal thread principale
Stephen James Hand,

1

Uso un lucchetto per ottenere questo effetto ora mi chiedo se è corretto nel mio modo che qualcuno voglia commentare?

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

Penso che stai essenzialmente implementando ciò che Volley fornisce già quando usi "futures".
spaaarky21,

1
Consiglierei di avere l' catch (InterruptedException e)interno del ciclo while. Altrimenti il ​​thread non attenderà se interrotto per qualche motivo
jayeffkay

@jayeffkay Sto già rilevando l'eccezione se nel ciclo while si verifica una ** InterruptedException **.
Forcewill

1

Voglio aggiungere qualcosa alla risposta accettata di Matthew. MentreRequestFuture potrebbe sembrare che effettui una chiamata sincrona dal thread che hai creato, non lo fa. Invece, la chiamata viene eseguita su un thread in background.

Da quello che ho capito dopo aver attraversato la libreria, le richieste nel RequestQueuevengono inviate nel suo start()metodo:

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

Ora entrambe CacheDispatchere le NetworkDispatcherclassi estendono il thread. In tal modo viene generato un nuovo thread di lavoro per la rimozione della coda della richiesta e la risposta viene restituita ai listener di errori ed errori implementati internamente da RequestFuture.

Sebbene il tuo secondo scopo sia raggiunto, ma il tuo primo scopo non lo è poiché viene sempre generato un nuovo thread, indipendentemente dal thread che esegui RequestFuture .

In breve, la vera richiesta sincrona non è possibile con la libreria Volley predefinita. Correggimi se sbaglio.


0

Puoi fare la richiesta di sincronizzazione con volley ma devi chiamare il metodo in thread diversi o l'app in esecuzione si bloccherà, dovrebbe essere così:

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;


}

dopodiché puoi chiamare il metodo nel thread:

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

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.