Wyobraź sobie, że jestem w usłudze, która ma już wątek w tle. Czy mogę wykonać żądanie przy użyciu woleja w tym samym wątku, aby wywołania zwrotne były synchroniczne?
Są ku temu 2 powody: - Po pierwsze, nie potrzebuję kolejnego wątku, a tworzenie go byłoby marnotrawstwem. - Po drugie, jeśli jestem w ServiceIntent, wykonywanie wątku zakończy się przed wywołaniem zwrotnym i dlatego nie otrzymam odpowiedzi od Volley. Wiem, że mogę stworzyć własną usługę, która ma wątek z pętlą startową, którą mogę kontrolować, ale byłoby pożądane, aby ta funkcja była dostępna w trybie salwy.
Dziękuję Ci!
java
android
android-volley
LocoMike
źródło
źródło
Odpowiedzi:
Wygląda na to, że jest to możliwe z
RequestFuture
klasą Volleya. Na przykład, aby utworzyć synchroniczne żądanie JSON HTTP GET, możesz wykonać następujące czynności: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 }
źródło
JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)
konstruktora.RequestFuture<JSONObject>
implementuje oba interfejsyListener<JSONObject>
iErrorListener
, więc może być używany jako ostatnie dwa parametry.future.get()
aplikacja zatrzyma się lub na pewno przekroczy limit czasu, jeśli tak jest ustawiony.Uwaga Odpowiedź @Matthews jest prawidłowa, ALE jeśli jesteś w innym wątku i wykonujesz wolej, gdy nie masz internetu, twoje wywołanie zwrotne błędu zostanie wywołane w głównym wątku, ale wątek, w którym jesteś, zostanie ZAWSZE zablokowany.(Dlatego jeśli ten wątek jest IntentService, nigdy nie będziesz mógł wysłać do niego kolejnej wiadomości, a Twoja usługa będzie w zasadzie martwa).
Użyj wersji,
get()
która ma limit czasufuture.get(30, TimeUnit.SECONDS)
i złap błąd, aby wyjść z wątku.Aby dopasować @Mathews odpowiedź:
try { return future.get(30, TimeUnit.SECONDS); } catch (InterruptedException e) { // exception handling } catch (ExecutionException e) { // exception handling } catch (TimeoutException e) { // exception handling }
Poniżej zawinąłem to w metodę i używam innego żądania:
/** * 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; }
źródło
IntentService
jest wykonawcą puli wątków pojedynczego wątku, dlatego IntentService zostanie zablokowany na zawsze, ponieważ znajduje się w pętliPrawdopodobnie zaleca się korzystanie z kontraktów futures, ale jeśli z jakiegoś powodu nie chcesz, zamiast gotować własne zsynchronizowane blokowanie, powinieneś użyć pliku
java.util.concurrent.CountDownLatch
. Więc to działałoby w ten sposób ...//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... }
Ponieważ ludzie rzeczywiście próbowali to zrobić i wpadli w kłopoty, zdecydowałem, że faktycznie dostarczę próbkę roboczą tego „z prawdziwego życia”. Tutaj jest https://github.com/timolehto/SynchronousVolleySample
Teraz, mimo że rozwiązanie działa, ma pewne ograniczenia. Co najważniejsze, nie możesz tego wywołać w głównym wątku interfejsu użytkownika. Volley wykonuje żądania w tle, ale domyślnie Volley używa głównej
Looper
części aplikacji do wysyłania odpowiedzi. Powoduje to zakleszczenie, ponieważ główny wątek interfejsu użytkownika oczekuje na odpowiedź, aleLooper
czeka naonCreate
zakończenie przed przetworzeniem dostawy. Jeśli naprawdę chcesz to zrobić, możesz zamiast statycznych metod pomocniczych utworzyć własne wystąpienie,RequestQueue
przekazując je własne,ExecutorDelivery
powiązane zHandler
użyciem a,Looper
które jest powiązane z innym wątkiem z głównego wątku interfejsu użytkownika.źródło
Jako obserwacja uzupełniająca do odpowiedzi zarówno @Blundells, jak i @Mathews, nie jestem pewien, czy jakikolwiek telefon zostanie dostarczony do niczego innego niż główny wątek przez Volley.
Źródło
Patrząc na
RequestQueue
implementację , wydaje się, żeRequestQueue
używa aNetworkDispatcher
do wykonania żądania iResponseDelivery
do dostarczenia wyniku (ResponseDelivery
jest wstrzykiwany doNetworkDispatcher
).ResponseDelivery
Jest z kolei utworzone zHandler
ikry od głównego wątku (gdzieś w okolicach linii 112 wRequestQueue
realizacji).Gdzieś w linii 135 w
NetworkDispatcher
implementacji wydaje się, że również pomyślne wyniki są dostarczane przez to samo,ResponseDelivery
co wszelkie błędy. Jeszcze raz; aResponseDelivery
oparty naHandler
spawnie z głównego wątku.Racjonalne uzasadnienie
W przypadku użycia, w którym żądanie ma być wykonane z poziomu
IntentService
, można założyć, że wątek usługi powinien blokować się do czasu otrzymania odpowiedzi od Volley (aby zagwarantować żywy zakres środowiska wykonawczego do obsługi wyniku).Sugerowane rozwiązania
Jednym podejściem byłoby zastąpić domyślny sposób, w jaki
RequestQueue
jest utworzony , gdy alternatywą konstruktora używany zamiast wstrzykiwaniaResponseDelivery
, które ikra z obecnym wątku niż nici głównej. Nie zbadałem jednak konsekwencji tego.źródło
finish()
metoda wRequest
klasie iRequestQueue
klasie są pakietami prywatnymi, poza używaniem hackowania refleksji Nie jestem pewien, jak to obejść. To, co ostatecznie zrobiłem, aby zapobiec działaniu wszystkiego w głównym wątku (UI), to skonfigurowanie alternatywnego wątku Looper (przy użyciuLooper.prepareLooper(); Looper.loop()
) i przekazanieExecutorDelivery
instancji doRequestQueue
konstruktora z obsługą tego looper. Masz narzut innego looper, ale trzymaj się z dala od głównego wątkuChcę coś dodać do zaakceptowanej odpowiedzi Matthew. Chociaż
RequestFuture
może wydawać się, że wykonuje synchroniczne wywołanie z wątku, który utworzyłeś, tak nie jest. Zamiast tego wywołanie jest wykonywane w wątku w tle.Z tego co rozumiem po przejściu przez bibliotekę, żądania w katalogu
RequestQueue
wysyłane są w jegostart()
sposób:public void start() { .... mCacheDispatcher = new CacheDispatcher(...); mCacheDispatcher.start(); .... NetworkDispatcher networkDispatcher = new NetworkDispatcher(...); networkDispatcher.start(); .... }
Teraz obie
CacheDispatcher
iNetworkDispatcher
klasy rozszerzają wątek. W efekcie nowy wątek roboczy jest tworzony w celu dekolejkowania kolejki żądań, a odpowiedź jest zwracana do detektorów sukcesu i błędów zaimplementowanych wewnętrznie przezRequestFuture
.Chociaż twój drugi cel został osiągnięty, ale twój pierwszy cel nie jest, ponieważ zawsze pojawia się nowy wątek, bez względu na to, z którego wątku wykonujesz
RequestFuture
.Krótko mówiąc, prawdziwe żądanie synchroniczne nie jest możliwe z domyślną biblioteką Volley. Popraw mnie, jeśli się mylę.
źródło
Używam zamka, aby osiągnąć ten efekt, teraz zastanawiam się, czy to poprawne w moim stylu, ktoś chce komentować?
// 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(); } }
źródło
catch (InterruptedException e)
wewnętrzną pętlę while. W przeciwnym razie wątek nie będzie czekał, jeśli z jakiegoś powodu zostanie przerwanyMożesz wykonać żądanie synchronizacji za pomocą volleya, ale musisz wywołać metodę w innym wątku lub Twoja uruchomiona aplikacja zostanie zablokowana, powinno wyglądać tak:
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; }
potem możesz wywołać metodę w wątku:
Thread thread = new Thread(new Runnable() { @Override public void run() { String response = syncCall(); } }); thread.start();
źródło
Osiągasz to z kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
private suspend fun request(context: Context, link : String) : String{ return suspendCancellableCoroutine { continuation -> val queue = Volley.newRequestQueue(context) val stringRequest = StringRequest(Request.Method.GET, link, { response -> continuation.resumeWith(Result.success(response)) }, { continuation.cancel(Exception("Volley Error")) }) queue.add(stringRequest) } }
I dzwoń z
CoroutineScope(Dispatchers.IO).launch { val response = request(CONTEXT, "https://www.google.com") withContext(Dispatchers.Main) { Toast.makeText(CONTEXT, response,Toast.LENGTH_SHORT).show() } }
źródło