我能用Volley进行同步请求吗?

141

假设我已经在一个有后台线程的服务中,我能否在同一线程中使用Volley进行请求,以便回调同步发生?

这样做有两个原因:

  • 首先,我不需要另一个线程,创建它将是浪费。
  • 其次,如果我在ServiceIntent中,线程的执行将在回调之前完成,因此我将无法从Volley获得响应。我知道我可以创建自己的Service,并拥有一些带有可控制的运行循环的线程,但希望在Volley中实现此功能。

6
请务必阅读@Blundell的回答以及受到高票支持且非常有用的答案。 - Jedidja
8个回答

194

看起来使用Volley的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
已更新。这个使用了JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)构造函数。 RequestFuture<JSONObject>实现了Listener<JSONObject>ErrorListener接口,因此可以将其用作最后两个参数。 - Matthew
24
它一直被阻塞了! - Mohammed Subhi Sheikh Quroush
10
如果在将请求添加到请求队列之前调用future.get(),可能会永远阻塞。 - dy_
3
由于您可能存在连接错误,它将永久地被阻止,阅读布兰德尔的答案。 - Mina Gabriel
4
应该说你不应该在主线程上做这件事。这对我来说并不清楚。因为如果主线程被 future.get() 阻塞,那么如果设置了超时时间,应用程序肯定会停止或超时。 - r00tandy
显示剩余6条评论

132

注意:@Matthews的回答是正确的,但如果你在另一个线程上并且在没有网络连接时进行了调用,则会在主线程上调用错误回调,但你所在的线程将永远被阻塞。(因此,如果该线程是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;
    }

1
这是一个相当重要的观点!不确定为什么这个答案没有得到更多的赞同。 - Jedidja
1
值得注意的是,如果您将ExecutionException传递给与请求相同的侦听器,则会两次处理异常。此异常发生在请求期间发生异常时,Volley会将其传递给errorListener。 - Stimsoni
@Blundell,我不理解你的回复。如果监听器在UI线程上执行,则会有一个等待的后台线程和调用notifyAll()的UI线程,因此没问题。如果交付是在您被future get()阻塞的同一线程上完成,则可能会发生死锁。因此,您的回复似乎毫无意义。 - greywolf82
1
@ greywolf82 IntentService 是一个单线程的线程池执行器,因此如果它坐在循环中,那么该 IntentService 将永远被阻塞。 - Blundell
@greywolf82,这不是死锁,而是死线程,get()会阻塞线程,如果它是后台线程,那么就会阻塞该线程。 - Blundell
显示剩余4条评论

9

虽然建议使用 Futures,但如果出于某种原因不想使用它们,而不是自己编写同步阻塞操作,您应该使用 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实例化静态辅助方法,将其传递给与主 UI 线程不同线程相关联的LooperHandler,然后再将其绑定到自己的ExecutorDelivery上。

这个解决方案会永远阻塞我的线程,改用Thread.sleep代替countDownLatch就解决了问题。 - snersesyan
如果您可以提供一个完整的代码示例,以该方式失败,也许我们就可以找出问题所在。我不明白如何同时使用睡眠和倒计时门闩是有意义的。 - Timo
好的 @VinojVetha,我稍微更新了答案以澄清情况,并提供了一个GitHub仓库,您可以轻松克隆并尝试其中的代码。如果您有更多问题,请提供一个派生的示例存储库作为参考来演示您的问题。 - Timo
这是一个非常出色的同步请求解决方案。 - bikram

3
你可以使用 kotlin 协程来实现这一点。
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()
   }
}

2
作为对@Blundells和@Mathews回答的补充观察,我不确定Volley将任何调用传递给除主线程以外的任何东西。
源代码实现表明,RequestQueue使用NetworkDispatcher执行请求,并使用ResponseDelivery传递结果(ResponseDelivery被注入到NetworkDispatcher中)。ResponseDelivery又是由从主线程产生的Handler创建的(在RequestQueue实现的大约112行左右)。
在NetworkDispatcher实现的大约135行左右,成功结果似乎也是通过相同的ResponseDelivery传递的,就像任何错误一样。同样,它是基于从主线程生成的Handler的ResponseDelivery。
对于从IntentService发出请求的用例,可以合理地假设服务线程应该阻塞,直到我们从Volley获得响应(以保证运行时范围处于活动状态以处理结果)。
一种方法是重写默认创建RequestQueue的方式,使用替代构造函数,注入一个ResponseDelivery,该ResponseDelivery从当前线程而不是主线程生成。但我还没有调查这样做的影响。

1
实现自定义的ResponseDelivery实现是很复杂的,因为Request类和RequestQueue类中的finish()方法是包私有的,除了使用反射技巧,我不确定是否有其他方法可以解决这个问题。为了防止任何东西在主(UI)线程上运行,我最终设置了一个替代的Looper线程(使用Looper.prepareLooper(); Looper.loop()),并将一个带有该looper处理程序的ExecutorDelivery实例传递给RequestQueue构造函数。你会有另一个looper的开销,但可以避免主线程。 - Stephen James Hand

2

I want to add something to Matthew's accepted answer. While RequestFuture might seem to make a synchronous call from the thread you created it, it does not. Instead, the call is executed on a background thread.

From what I understand after going through the library, requests in the RequestQueue are dispatched in its start() method:

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

Now both CacheDispatcher and NetworkDispatcher classes extend thread. So effectively a new worker thread is spawned for dequeuing the request queue and the response is returned to the success and error listeners implemented internally by RequestFuture.

Although your second purpose is attained but you first purpose is not since a new thread is always spawned, no matter from which thread you execute RequestFuture.

In short, true synchronous request is not possible with default Volley library. Correct me if I am wrong.


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

我认为当你使用“futures”时,你实际上正在实现Volley已经提供的东西。 - spaaarky21
1
我建议将 catch (InterruptedException e) 放在 while 循环内部。否则,如果线程因某种原因被中断,则无法等待。 - jayeffkay
@jayeffkay,如果在 while 循环中发生 InterruptedException 异常,我已经捕获了该异常并使用 catch 处理它。 - forcewill

0

你可以使用Volley进行同步请求,但必须在不同的线程中调用该方法,否则您的运行应用程序将被阻塞,应该像这样:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接