ฉันสามารถขอซิงโครนัสกับวอลเลย์ได้หรือไม่?


134

สมมติว่าฉันอยู่ในบริการที่มีเธรดพื้นหลังอยู่แล้ว ฉันสามารถร้องขอโดยใช้วอลเลย์ในเธรดเดียวกันเพื่อให้การโทรกลับเกิดขึ้นพร้อมกันได้หรือไม่?

มี 2 ​​เหตุผลสำหรับสิ่งนี้: - ประการแรกฉันไม่ต้องการเธรดอื่นและจะเป็นการสิ้นเปลืองในการสร้าง - ประการที่สองถ้าฉันอยู่ใน ServiceIntent การดำเนินการของเธรดจะเสร็จสิ้นก่อนการติดต่อกลับและด้วยเหตุนี้ฉันจะไม่ได้รับคำตอบจาก Volley ฉันรู้ว่าฉันสามารถสร้าง Service ของตัวเองที่มีเธรดบางส่วนที่มี runloop ที่ฉันสามารถควบคุมได้ แต่ก็เป็นที่พึงปรารถนาที่จะมีฟังก์ชันนี้ในการวอลเลย์

ขอบคุณ!


5
อย่าลืมอ่านคำตอบของ @ Blundell รวมถึงคำตอบที่ได้รับการโหวตสูง (และมีประโยชน์มาก)
Jedidja

คำตอบ:


184

ดูเหมือนว่าจะเป็นไปได้ด้วยRequestFutureคลาสของวอลเลย์ ตัวอย่างเช่นในการสร้างคำขอ JSON HTTP GET แบบซิงโครนัสคุณสามารถทำสิ่งต่อไปนี้:

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 ปรับปรุง. สิ่งนี้ใช้ตัวJsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)สร้าง RequestFuture<JSONObject>ใช้ทั้งอินเทอร์เฟซListener<JSONObject>และErrorListenerอินเทอร์เฟซดังนั้นจึงสามารถใช้เป็นพารามิเตอร์สองตัวสุดท้ายได้
Matt

21
มันปิดกั้นตลอดไป!
Mohammed Subhi Sheikh Quroush

9
คำแนะนำ: อาจถูกบล็อกตลอดไปหากคุณเรียก future.get () ก่อนที่คุณจะเพิ่มคำขอลงในคิวคำขอ
datayeah

3
จะถูกบล็อกตลอดไปเนื่องจากคุณอาจมีข้อผิดพลาดในการเชื่อมต่ออ่านคำตอบของ Blundell
Mina Gabriel

4
ควรบอกว่าคุณไม่ควรทำเช่นนี้ในเธรดหลัก นั่นไม่ชัดเจนสำหรับฉัน ทำให้หากเธรดหลักถูกบล็อกโดยfuture.get()แอปจะหยุดทำงานหรือหมดเวลาแน่นอนหากตั้งค่าไว้เช่นนั้น
r00tandy

126

หมายเหตุ @ แมทธิวส์คำตอบนั้นถูกต้อง แต่ถ้าคุณอยู่ในเธรดอื่นและคุณทำการโทรแบบวอลเลย์เมื่อคุณไม่มีอินเทอร์เน็ตการโทรกลับข้อผิดพลาดของคุณจะถูกเรียกในเธรดหลัก แต่เธรดที่คุณอยู่จะถูกบล็อกตลอดไป (ดังนั้นหากเธรดนั้นเป็น IntentService คุณจะไม่สามารถส่งข้อความอื่นไปหามันได้และบริการของคุณจะถูกปิดโดยทั่วไป)

ใช้เวอร์ชันget()ที่หมดเวลาfuture.get(30, TimeUnit.SECONDS)และตรวจจับข้อผิดพลาดเพื่อออกจากเธรดของคุณ

เพื่อให้ตรงกับคำตอบของ @Mathews:

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

ด้านล่างฉันห่อด้วยวิธีการและใช้คำขออื่น:

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

นั่นเป็นจุดสำคัญพอสมควร! ไม่แน่ใจว่าทำไมคำตอบนี้จึงไม่ได้รับการโหวตเพิ่มขึ้น
Jedidja

1
สิ่งที่น่าสังเกตก็คือถ้าคุณส่ง ExecutionException ไปยัง Listener เดียวกับที่คุณส่งผ่านไปยังคำขอคุณจะประมวลผลข้อยกเว้นสองครั้ง ข้อยกเว้นนี้เกิดขึ้นเมื่อมีข้อยกเว้นเกิดขึ้นระหว่างการร้องขอซึ่งวอลเลย์จะส่งผ่านไปยัง errorListener สำหรับคุณ
Stimsoni

@Blundell ฉันไม่เข้าใจคำตอบของคุณ หากมีการเรียกใช้ Listener บนเธรด UI คุณมีเธรดพื้นหลังรออยู่และเธรด UI ที่เรียกใช้ alertAll () ดังนั้นก็โอเค การชะงักงันสามารถเกิดขึ้นได้หากการส่งเสร็จสิ้นบนเธรดเดียวกันกับที่คุณถูกบล็อกด้วย get () ในอนาคต ดังนั้นคำตอบของคุณจึงดูเหมือนไร้เหตุผล
greywolf82

1
@ greywolf82 IntentServiceเป็นตัวดำเนินการพูลเธรดของเธรดเดียวดังนั้น IntentService จะถูกบล็อกก่อนที่มันจะอยู่ในลูป
Blundell

@Blundell ฉันไม่เข้าใจ อยู่จนกว่าจะมีการเรียกการแจ้งเตือนและจะถูกเรียกจากเธรด UI จนกว่าคุณจะมีสองเธรดที่แตกต่างกันฉันมองไม่เห็นทางตัน
greywolf82

9

ขอแนะนำให้ใช้ Futures แต่ถ้าคุณไม่ต้องการด้วยเหตุผลใดก็ตามแทนที่จะทำสิ่งปิดกั้นที่ซิงโครไนซ์ของคุณเองคุณควรใช้ a java.util.concurrent.CountDownLatch. จึงจะได้ผลเช่นนี้ ..

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

เนื่องจากผู้คนดูเหมือนจะพยายามทำสิ่งนี้จริง ๆ และพบปัญหาบางอย่างฉันจึงตัดสินใจว่าจะให้ตัวอย่างการใช้งาน "ในชีวิตจริง" ของสิ่งนี้ที่ใช้งานจริง นี่คือhttps://github.com/timolehto/SynchronousVolleySample

ถึงแม้ว่าโซลูชันจะใช้ได้ผล แต่ก็มีข้อ จำกัด บางประการ ที่สำคัญที่สุดคือคุณไม่สามารถเรียกมันบนเธรด UI หลักได้ Volley ดำเนินการตามคำขอบนพื้นหลัง แต่โดยค่าเริ่มต้น Volley จะใช้Looperแอปพลิเคชันหลักในการส่งคำตอบ สิ่งนี้ทำให้เกิดการหยุดชะงักเนื่องจากเธรด UI หลักกำลังรอการตอบสนอง แต่ไฟล์LooperกำลังรอonCreateให้เสร็จสิ้นก่อนประมวลผลการจัดส่ง หากคุณต้องการทำสิ่งนี้จริงๆคุณสามารถทำได้แทนที่จะใช้วิธีการผู้ช่วยแบบคงที่ให้สร้างอินสแตนซ์ของRequestQueueคุณเองโดยExecutorDeliveryเชื่อมโยงกับการHandlerใช้Looperเธรดที่เชื่อมโยงกับเธรดอื่นจากเธรด UI หลัก


วิธีนี้บล็อกเธรดของฉันตลอดไปเปลี่ยน Thread.sleep แทน countDownLatch และแก้ไขปัญหา
snersesyan

หากคุณสามารถให้ตัวอย่างทั้งหมดของรหัสที่ล้มเหลวในลักษณะนี้เราอาจจะเข้าใจได้ว่าปัญหาคืออะไร ฉันไม่เห็นว่าการนอนร่วมกับสลักนับถอยหลังเข้าท่าขนาดไหน
Timo

เอาล่ะ @VinojVetha ฉันได้อัปเดตคำตอบเล็กน้อยเพื่อชี้แจงสถานการณ์และให้ repo GitHub ซึ่งคุณสามารถโคลนและลองใช้โค้ดได้อย่างง่ายดาย หากคุณมีปัญหาเพิ่มเติมโปรดระบุส่วนแยกของ repo ตัวอย่างที่แสดงถึงปัญหาของคุณเป็นข้อมูลอ้างอิง
Timo

นี่เป็นทางออกที่ยอดเยี่ยมสำหรับคำขอแบบซิงโครนัสจนถึงตอนนี้
bikram

2

ในฐานะที่เป็นข้อสังเกตเสริมสำหรับทั้งคำตอบของ @Blundells และ @Mathews ฉันไม่แน่ใจว่ามีการโทรไปยังสิ่งใดนอกจากเธรดหลักโดย Volley

แหล่งที่มา

เมื่อดูการRequestQueueใช้งานดูเหมือนว่าRequestQueueกำลังใช้ a NetworkDispatcherเพื่อดำเนินการตามคำขอและ a ResponseDeliveryเพื่อส่งมอบผลลัพธ์ ( ResponseDeliveryจะถูกฉีดเข้าไปในNetworkDispatcher) ResponseDeliveryเป็นในทางกลับสร้างขึ้นด้วยHandlerวางไข่จากหัวข้อหลัก (บางรอบเส้น 112 ในRequestQueueการดำเนินงาน)

บางแห่งเกี่ยวกับบรรทัด 135 ในการNetworkDispatcherใช้งานดูเหมือนว่าผลลัพธ์ที่ประสบความสำเร็จจะถูกส่งผ่านเช่นเดียวResponseDeliveryกับข้อผิดพลาดใด ๆ อีกครั้ง; กResponseDeliveryขึ้นอยู่กับการHandlerวางไข่จากหัวข้อหลัก

เหตุผล

สำหรับกรณีการใช้งานที่ต้องทำคำขอจากไฟล์ IntentServiceมันเป็นธรรมที่จะถือว่าเธรดของบริการควรบล็อกจนกว่าเราจะได้รับคำตอบจาก Volley (เพื่อรับประกันขอบเขตรันไทม์ที่มีชีวิตเพื่อจัดการกับผลลัพธ์)

วิธีแก้ปัญหาที่แนะนำ

แนวทางหนึ่งคือการแทนที่วิธีเริ่มต้นที่RequestQueueสร้างขึ้นโดยที่ใช้คอนสตรัคเตอร์ทางเลือกแทนโดยฉีดสิ่งResponseDeliveryที่เกิดจากเธรดปัจจุบันแทนที่จะเป็นเธรดหลัก อย่างไรก็ตามฉันยังไม่ได้ตรวจสอบผลกระทบของสิ่งนี้


1
การใช้งาน ResponseDelivery แบบกำหนดเองนั้นมีความซับซ้อนเนื่องจากfinish()วิธีการในRequestคลาสและRequestQueueคลาสนั้นเป็นแพ็กเกจส่วนตัวนอกเหนือจากการใช้แฮ็กสะท้อนฉันไม่แน่ใจว่ามีวิธีแก้ไข สิ่งที่ฉันทำเพื่อป้องกันไม่ให้มีสิ่งใดที่ทำงานบนเธรดหลัก (UI) คือการตั้งค่าเธรด Looper ทางเลือก (ใช้Looper.prepareLooper(); Looper.loop()) และส่งExecutorDeliveryอินสแตนซ์ไปยังตัวRequestQueueสร้างด้วยตัวจัดการสำหรับลูปเกอร์นั้น คุณมีค่าใช้จ่ายในการเล่นลูปอื่น แต่อย่าอยู่นอกเธรดหลัก
Stephen James Hand

2

ฉันต้องการเพิ่มบางอย่างในคำตอบที่ยอมรับของมัทธิว ในขณะที่RequestFutureอาจดูเหมือนจะโทรแบบซิงโครนัสจากเธรดที่คุณสร้างขึ้น แต่ก็ไม่ได้ แต่การเรียกจะดำเนินการบนเธรดพื้นหลัง

จากสิ่งที่ฉันเข้าใจหลังจากอ่านไลบรารีคำขอในห้องสมุดRequestQueueจะถูกส่งด้วยstart()วิธีการ:

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

ตอนนี้ทั้งสองCacheDispatcherและNetworkDispatcherชั้นเรียนขยายเธรด ดังนั้นเธรดผู้ปฏิบัติงานใหม่จึงถูกสร้างขึ้นอย่างมีประสิทธิภาพสำหรับการยกเลิกคิวการร้องขอและการตอบกลับจะถูกส่งกลับไปยังความสำเร็จและตัวฟังข้อผิดพลาดที่ดำเนินการภายในโดยRequestFutureคิวการร้องขอและการตอบสนองจะถูกส่งกลับไปสู่ความสำเร็จและความผิดพลาดฟังการดำเนินการภายในโดย

แม้ว่าจุดประสงค์ที่สองของคุณจะบรรลุ แต่จุดประสงค์แรกของคุณไม่ใช่เนื่องจากเธรดใหม่จะเกิดขึ้นเสมอไม่ว่าคุณจะดำเนินการจากเธรดใดก็ตาม RequestFutureใดก็ตาม

ในระยะสั้นการร้องขอแบบซิงโครนัสที่แท้จริงเป็นไปไม่ได้กับไลบรารี Volley เริ่มต้น แก้ไขฉันถ้าฉันผิด


1

ฉันใช้การล็อคเพื่อให้ได้เอฟเฟกต์นั้นตอนนี้ฉันสงสัยว่ามันถูกต้องตามที่ใคร ๆ ต้องการแสดงความคิดเห็นหรือไม่?

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

ฉันคิดว่าคุณกำลังนำสิ่งที่วอลเลย์ให้มาใช้เป็นหลักเมื่อคุณใช้ "ฟิวเจอร์ส"
spaaarky21

1
ฉันอยากจะแนะนำให้มีcatch (InterruptedException e)วงใน while มิฉะนั้นเธรดจะไม่สามารถรอได้หากถูกขัดจังหวะด้วยเหตุผลบางประการ
jayeffkay

@jayeffkay ฉันพบข้อยกเว้นอยู่แล้วหาก ** InterruptedException ** เกิดขึ้นในขณะที่ลูปจับมัน
forcewill

0

คุณสามารถทำการร้องขอการซิงค์กับวอลเลย์ได้ แต่คุณต้องเรียกใช้เมธอดในเธรดอื่นมิฉะนั้นแอปที่ทำงานอยู่ของคุณจะบล็อกมันควรเป็นดังนี้:

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;


}

หลังจากนั้นคุณสามารถเรียกใช้เมธอดในเธรด:

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

                                        String response = syncCall();

                                    }
                                });
                                thread.start();

0

คุณบรรลุเป้าหมายนี้ด้วยคอตลินโครูทีน

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

และโทรติดต่อกับ

CoroutineScope(Dispatchers.IO).launch {
    val response = request(CONTEXT, "https://www.google.com")
    withContext(Dispatchers.Main) {
       Toast.makeText(CONTEXT, response,Toast.LENGTH_SHORT).show()
   }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.