Android 使用 RequestFuture.get() 时截击超时异常

Android volley Timeout Exception when using RequestFuture.get()

在我的片段中,我正在尝试使用 TMDB 的开放电影数据库来获取有关 "Now Playing" 电影的详细信息。

如果我使用 RequestFuture.get(time, TimeUnit) 方法来执行此 volley 请求,我总是会收到超时错误。如果我在 Safari 中手动测试相同的 Url,我会立即得到结果。

我知道的:

1.) 这不是任何 JSON 解析错误。(程序甚至没有进行到解析步骤)

2.) AVD 没有互联网问题。 (原因稍后解释)。

3.) 我的 volley 单例 class 或我的请求队列不是问题。 (原因稍后解释)。

所以我假设我在使用 volley/Request Future 时犯了一些其他错误。

片段代码如下:

public class BoxOffice extends android.support.v4.app.Fragment {
    private VolleySingleton volleySingleton;
    private RequestQueue requestQueue;
    private ImageLoader imageLoader;
    private ArrayList<MyMovie> movieList;
    private MyUriBuilder mBuilder;

    public BoxOffice() {
        // Required empty public constructor
        volleySingleton = VolleySingleton.getInstance();
        requestQueue = volleySingleton.getRequestQueue();
        mBuilder = new MyUriBuilder();
        movieList = new ArrayList<>();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
        StepA();
    }

    public void StepA() {
        String url = mBuilder.getURL("box");
        Log.d("RT", "StepA initiated - "+ url); // Url is perfect - works when copied in Safari.
        RequestFuture<JSONObject> futureA = RequestFuture.newFuture();
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, (String) null, futureA, futureA);
        requestQueue.add(request);

        try {
            JSONObject response = futureA.get(30, TimeUnit.SECONDS);
            Log.d("RT", "StepA - response received"); //Never reaches this step
            parseJsonFeed(response);
        } catch (InterruptedException e) {
            Log.e("RT", "StepA - InterruptedException - " + e);
            e.printStackTrace();
        } catch (ExecutionException e) {
            Log.e("RT", "StepA - ExecutionException - " + e);
            e.printStackTrace();
        } catch (TimeoutException e) {
            Log.e("RT", "StepA - TimeoutException - " + e);
            e.printStackTrace();
        }
        Log.d("RT", "StepA END");
    }

    public void parseJsonFeed(JSONObject response) {
        Log.d("RT", "StepA - parseFeed Begin");
        if (response == null || response.length() == 0) {
            return;
        }
        MyMovie currentMovie = null;
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

        try {
            if (response.has("results")) {
                Log.d("RT", "StepA - results");
                JSONArray resultList = response.getJSONArray("results");
                for (int i = 0; i < 3; i++) {
                    Log.d("RT", "movie " + i);
                    JSONObject movieElement = resultList.getJSONObject(i);
                    if (movieElement.has("id") && movieElement.has("title")) {
                        currentMovie = new MyMovie();
                        currentMovie.setTmdb_id(movieElement.getString("id"));
                        currentMovie.setTitle(movieElement.getString("title"));
                        if (movieElement.has("release_date")) {
                            currentMovie.setReleaseDate(dateFormat.parse(movieElement.getString("release_date")));
                        } else {
                            currentMovie.setReleaseDate(dateFormat.parse("0000-00-00"));
                        }
                        movieList.add(i, currentMovie);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.d("RT", "StepA - parseFeed END");
    }
}

Logcat 使用标签过滤器 "RT":

05-30 15:17:51.710  D/RT﹕ TL - Constructor Called
05-30 15:17:51.800  D/RT﹕ StepA initiated - https://api.themoviedb.org/3/movie/now_playing?api_key=##### (link works fine)
05-30 15:18:21.820  E/RT﹕ StepA - TimeoutException - java.util.concurrent.TimeoutException
05-30 15:18:21.820  D/RT﹕ StepA END

在使用 RequestFuture 方法之前,我基本上在我的 Fragment oncreate(而不是 StepA();) 中实现了我自己的 Response.Listener 和 Response.ErrorListener,并且成功了!

下面是代码片段:

JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, mBuilder.getURL("box"), (String) null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                parseJsonFeed(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(getActivity(), error.toString(), Toast.LENGTH_LONG).show();
            }
        });
        requestQueue.add(request);

所以我的问题是为什么当我实现请求的未来方法时它不起作用?

如果你问我为什么要进行同步排球实施;这是因为在这之后我必须有两个截击请求,这取决于这个请求是否完全、成功完成。而且我也在学习:)

遗憾的是没有人可以帮助回答这个问题,但我设法解决了这个问题,如下所示:

如果 RequestFuture.get() 与 UI 线程在同一线程上,则会发生超时。我已经更改了请求的机制,以便请求在单独的异步线程(不是 UI 线程)上完成,并且响应也在与请求不同的线程上接收,如下所示:

private void StepA() {
        Log.d("RT", "StepA initiated");
        final CountDownLatch latchA = new CountDownLatch(1);

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("RT", "Thread t Begins");
                ThreadA threadA = new ThreadA();
                try {
                    JSONObject jsonObject = threadA.execute().get(10, TimeUnit.SECONDS);
                    parseA(jsonObject);
                    latchA.countDown();
                    Log.d("RT", "Thread t Ends");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try {
            latchA.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d("RT", "StepA END");
    }

下面是请求的异步任务代码:

protected class ThreadA extends AsyncTask<Void, Void, JSONObject> {
    final String url = mBuilder.getURL("box");

    public ThreadA() {
    }

    @Override
    protected JSONObject doInBackground(Void... params) {
        final RequestFuture<JSONObject> future = RequestFuture.newFuture();
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, (String) null, future, future);
        requestQueue.add(request);
        try {
            return future.get(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return null;
    }
}

我添加了倒计时锁存器,因为它们很棒,而且我的程序中很少有像这样的请求依赖于此代码段的响应。因此,它们可以更同步地帮助 运行 程序。

rapidclock 的回答很好。我个人更喜欢使用 IntentService,因为它们非常棒。另外 Google 推荐它: https://www.youtube.com/watch?v=xHXn3Kg2IQE&t=1852s

这是我的 IntentService:

// 
//http://afzaln.com/volley/com/android/volley/toolbox/RequestFuture.html
//
// 
package org.peacekeeper.service;

import android.app.IntentService;
import android.content.Intent;
import android.os.*;

import com.android.volley.RequestQueue;

import org.json.JSONObject;
import org.peacekeeper.app.R;
import org.peacekeeper.service.pkRequest.pkURL;
import org.peacekeeper.util.*;
import org.slf4j.*;

import java.util.concurrent.*;


/**
 Asynchronously handles an intent using a worker thread. Receives a ResultReceiver object and a
 location through an intent. Tries to fetch the address for the location using a Geocoder, and
 sends the result to the ResultReceiver.
 */
public class RESTIntentService extends IntentService{
//begin static
//Intent putextra ID's
static public final String 
        RECEIVER   = "RESTIntentServiceRCVR",
        JSONResult = "JSONResult",
        REQUEST    = "RESTIntentServiceRequest";
protected final static pkUtility    mUtility      = pkUtility.getInstance();
protected final static RequestQueue mRequestQueue = mUtility.getRequestQueue();
private final static   long         TIMEOUT       = 5;

//end static
private static final Logger mLog = LoggerFactory.getLogger( RESTIntentService.class );
//The receiver where results are forwarded from this service.
private ResultReceiver mReceiver;

//This constructor is required, and calls the super IntentService(String) constructor with the name for a worker thread.
public RESTIntentService(){ super( "RESTIntentService" ); }


@Override protected void onHandleIntent( Intent intent ){
    String errorMessage = "";

    mReceiver = intent.getParcelableExtra( RECEIVER );

    if ( mReceiver == null ){// Check if receiver was properly registered.
        mLog.error( "No RESTIntentService receiver received. There is nowhere to send the results." );
        return;
    }


    // Get the pkRequest passed to this service through an extra.
    pkRequest.pkURL URL = pkURL.valueOf( intent.getStringExtra( REQUEST ) );
    mLog.debug( "RESTIntentService URL: " + URL.toString() );
    // Make sure that the location data was really sent over through an extra. If it wasn't,
    // send an error message and return.
    if ( URL == null ){
        errorMessage = getString( R.string.no_pkRequest_provided );
        mLog.error( errorMessage );
        deliverResultToReceiver( Constants.FAILURE_RESULT, errorMessage );
        return;
    }


    //Request retval = null;
    JSONObject response = null;

    pkRequest request = new pkRequest( URL );
    mLog.debug( "onHandleIntent:\n" + request.toString() );

    request.submit();

    try{
        //while (!request.mFuture.isDone()) {;}
// TODO THIS BLOCKS the service but not the main UI thread. Consider wrapping in an asynch task:
// see 
        response = request.mFuture.get( TIMEOUT, TimeUnit.SECONDS );

        mLog.debug( "onHandleIntent:\n" + response.toString() );

    }catch ( InterruptedException | ExecutionException | TimeoutException x ){
        errorMessage = getString( R.string.failed_future_request );
        mLog.error( errorMessage, x );
        x.printStackTrace();
    }

    if ( errorMessage.isEmpty() ){
        deliverResultToReceiver( Constants.SUCCESS_RESULT,
                                 response.toString() );
    }
    else{ deliverResultToReceiver( Constants.FAILURE_RESULT, errorMessage ); }
}//onHandleIntent()

// Sends a resultCode and message to the receiver.
private void deliverResultToReceiver( int resultCode, String message ){
    Bundle bundle = new Bundle();
    bundle.putString( JSONResult, message );
    mReceiver.send( resultCode, bundle );
}
}//class RESTIntentService

使用 IntentService 的缺点是 IT(但不是主 UI 线程)将被 future.get(...) 阻塞。 (请参阅代码回复中的评论:future.get 块)因此,如果您正在对它进行 REST 调用,那么您可能会考虑继续使用它并按照 rapidclock 的建议将您的调用包装在异步中。

要使用上面的 IntentService,请将其放在主要 UI(或任何地方)中:

protected void startRESTService( final pkRequest.pkURL aURL ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.

    startService(
            new Intent( this, RESTIntentService.class )
                    .putExtra( RESTIntentService.RECEIVER, mRESTResultReceiver )
                    .putExtra( RESTIntentService.REQUEST, aURL.name() )
                );
}//startRESTService()

//Receiver for data sent from RESTIntentService.
class RESTResultReceiver extends ResultReceiver{
    public RESTResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from RESTIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        String snippet = resultData.getString( RESTIntentService.JSONResult );
        mLog.debug( "RESTResultReceiver:\t" + snippet );

    }//onReceiveResult
}//class RESTResultReceiver

哦好吧...这是我的activity(请不要因为我过于详细而对我说...我爱爱爱爱 Whosebug 但好事必有报应....):

//
package org.peacekeeper.app;

import android.Manifest;
import android.content.*;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.*;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.*;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.*;
import com.google.android.gms.maps.*;
import com.google.android.gms.maps.GoogleMap.*;
import com.google.android.gms.maps.model.*;
import com.google.android.gms.maps.model.Marker;

import org.json.JSONObject;
import org.peacekeeper.rest.LinkedRequest;
import org.peacekeeper.service.*;
import org.peacekeeper.service.pkRequest.pkURL;
import org.peacekeeper.util.pkUtility;
import org.slf4j.*;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.joran.spi.JoranException;

public class actGeocoder extends AppCompatActivity
        implements OnMapReadyCallback,
                   GoogleApiClient.ConnectionCallbacks,
                   GoogleApiClient.OnConnectionFailedListener,
                   LocationListener,
                   OnMapLongClickListener,
                   OnMarkerClickListener{

//begin static
private static final LoggerContext mLoggerContext =
        (LoggerContext) LoggerFactory.getILoggerFactory();
private static final ContextInitializer mContextInitializer =
        new ContextInitializer( mLoggerContext );
private static final Logger mLog = LoggerFactory.getLogger( actGeocoder.class );

private static final int MY_PERMISSIONS_REQUEST_LOCATION = 99;
//end static


private GoogleMap mGoogleMap;
private SupportMapFragment mapFrag;
private LocationRequest mLocationRequest;
private GoogleApiClient mGoogleApiClient;
private MarkerOptions mMarkerOptions;
private Marker mMarker;
private AddressResultReceiver mResultReceiver = new AddressResultReceiver( new Handler() );
private RESTResultReceiver mRESTResultReceiver = new RESTResultReceiver( new Handler() );
private pkUtility mUtility;

public void newPeaceKeeperStatus(){
    startRESTService( pkRequest.pkURL.status );
}




@Override protected void onCreate( Bundle savedInstanceState ){
    super.onCreate( savedInstanceState );
    mUtility = pkUtility.getInstance( this );
    newPeaceKeeperStatus();
    setContentView( R.layout.geocoder );

    getSupportActionBar().setTitle( R.string.RegisterYourLocn );
    buildGoogleApiClient();
    mapFrag = (SupportMapFragment) getSupportFragmentManager().findFragmentById( R.id.geocoder );
    mapFrag.getMapAsync( this );
}//onCreate


@Override public void onResume(){
    super.onResume();
    mGoogleApiClient.connect();
}


@Override protected void onRestart(){
    super.onRestart();
    // Reload Logback log: 
    mLoggerContext.reset();

    //I prefer autoConfig() over JoranConfigurator.doConfigure() so I don't need to find the file myself.
    try{ mContextInitializer.autoConfig(); }
    catch ( JoranException X ){ X.printStackTrace(); }
}//onRestart()

@Override protected void onStop(){
    mGoogleApiClient.disconnect();
    mLoggerContext.stop();//flush log
    super.onStop();
}

@Override public void onDestroy(){
    mLog.trace( "onDestroy():\t" );
    mLoggerContext.stop();//flush log
    super.onDestroy();
}

@Override public void onRequestPermissionsResult( int requestCode, String permissions[], int[] grantResults ){
    switch ( requestCode ){
    case MY_PERMISSIONS_REQUEST_LOCATION:{
        // If request is cancelled, the result arrays are empty.
        if ( grantResults.length > 0
             && grantResults[ 0 ] == PackageManager.PERMISSION_GRANTED ){

            // permission was granted, yay! Do the location-related task you need to do.
            if ( ContextCompat.checkSelfPermission( this,
                                                    Manifest.permission.ACCESS_FINE_LOCATION )
                 == PackageManager.PERMISSION_GRANTED ){

                if ( mGoogleApiClient == null ){ buildGoogleApiClient(); }
                mGoogleMap.setMyLocationEnabled( true );
            }

        }
            // permission denied. Disable the functionality that depends on this permission.
        else{ Toast.makeText( this, "permission denied", Toast.LENGTH_LONG ).show(); }
        return;
    }

    }//switch
}

protected synchronized void buildGoogleApiClient(){
    mGoogleApiClient = new GoogleApiClient.Builder( this )
            .addConnectionCallbacks( this )
            .addOnConnectionFailedListener( this )
            .addApi( LocationServices.API )
            .build();

    mGoogleApiClient.connect();
}

//
@Override public void onMapReady( GoogleMap googleMap ){
    //Initialize Google Play Services
    if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ){
        if ( ContextCompat.checkSelfPermission( this,
                                                Manifest.permission.ACCESS_FINE_LOCATION )
             != PackageManager.PERMISSION_GRANTED ){
            //Location Permission already granted
            checkLocationPermission();
            return;  //Request Location Permission
        }

    }

    mGoogleMap = googleMap;
    mGoogleMap.setMapType( GoogleMap.MAP_TYPE_NORMAL );

    mGoogleMap.setOnMapLongClickListener( this );
    mGoogleMap.setOnMarkerClickListener(this);

    mMarkerOptions = new MarkerOptions()
            .title( "Tap this marker again to register your location" )
            .icon( BitmapDescriptorFactory.defaultMarker( BitmapDescriptorFactory.HUE_MAGENTA) );
}



private void checkLocationPermission(){
    if ( ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION )
         != PackageManager.PERMISSION_GRANTED ){

        // Should we show an explanation?
        if ( ActivityCompat.shouldShowRequestPermissionRationale( this,
                                                                  Manifest.permission.ACCESS_FINE_LOCATION ) ){

// Show an explanation to the user *asynchronously* -- don't block this thread waiting for the user's response!
// After the user sees the explanation, try again to request the permission.
            new AlertDialog.Builder( this )
                    .setTitle( "Location Permission Needed" )
                    .setMessage(
                            "This app needs the Location permission, please accept to use location functionality" )
                    .setPositiveButton( "OK", new DialogInterface.OnClickListener(){
                        @Override public void onClick( DialogInterface dialogInterface, int i ){
                            //Prompt the user once explanation has been shown
                            ActivityCompat.requestPermissions( actGeocoder.this,
                                                               new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
                                                               MY_PERMISSIONS_REQUEST_LOCATION );
                        }
                    } )
                    .create()
                    .show();        }
        else{ // No explanation needed, we can request the permission.
            ActivityCompat.requestPermissions( this,
                                               new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
                                               MY_PERMISSIONS_REQUEST_LOCATION );
        }
    }
}

@Override public void onConnected( Bundle bundle ){
    mLocationRequest = new LocationRequest()
            .setInterval( 1000 )
            .setFastestInterval( 1000 )
            .setPriority( LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY );

    if ( ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION )
         == PackageManager.PERMISSION_GRANTED ){
                    LocationServices.FusedLocationApi.
                     requestLocationUpdates( mGoogleApiClient, mLocationRequest, this );
    }
}


private final static float ZOOM = 18;
@Override public void onLocationChanged( Location location ){//this is called only once on startup.
    //stop location updates since only current location is needed
    LocationServices.FusedLocationApi
            .removeLocationUpdates( mGoogleApiClient, this );

    LatLng latLng = new LatLng( location.getLatitude(), location.getLongitude() );
    mGoogleMap.moveCamera( CameraUpdateFactory.newLatLngZoom( latLng, ZOOM ) );

    onMapLongClick(latLng);
}


@Override public void onMapLongClick( final LatLng latLng ){
    startIntentService( latLng );

    if ( mMarker != null ) mMarker.remove();

    mMarkerOptions.position( latLng );
    mMarker = mGoogleMap.addMarker( mMarkerOptions );
}//onMapLongClick

@Override public boolean onMarkerClick( Marker marker) {
    startActivity(
            new Intent(this, actRegistration.class)
                    .putExtra( FetchAddressIntentService.LOCATION, marker.getSnippet() )
                    .putExtra( FetchAddressIntentService.LATLNG, marker.getPosition() )


                 );
    return true;
}//onMarkerClick


protected void startIntentService( final LatLng latLng ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.
    startService(
            new Intent( this, FetchAddressIntentService.class )
                    .putExtra( FetchAddressIntentService.RECEIVER, mResultReceiver )
                    .putExtra( FetchAddressIntentService.LATLNG, latLng )
                );
}//startIntentService()

protected void startRESTService( final pkRequest.pkURL aURL ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.

    startService(
            new Intent( this, RESTIntentService.class )
                    .putExtra( RESTIntentService.RECEIVER, mRESTResultReceiver )
                    .putExtra( RESTIntentService.REQUEST, aURL.name() )
                );
}//startRESTService()



//Receiver for data sent from FetchAddressIntentService.
class AddressResultReceiver extends ResultReceiver{
    public AddressResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from FetchAddressIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        mMarker.setSnippet( resultData.getString( FetchAddressIntentService.LOCATION ) );
        mMarker.showInfoWindow();
    }//onReceiveResult
}//class AddressResultReceiver

//Receiver for data sent from RESTIntentService.
class RESTResultReceiver extends ResultReceiver{
    public RESTResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from RESTIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        String snippet = resultData.getString( RESTIntentService.JSONResult );
        mLog.debug( "RESTResultReceiver:\t" + snippet );
    }//onReceiveResult
}//class RESTResultReceiver


@Override public void onConnectionSuspended( int i ){ mLog.info("onConnectionSuspended: " + i  );}
@Override public void onConnectionFailed( ConnectionResult connectionResult ){
    mLog.error( R.string.GoogleApiClientConnFailed + ":\t" + connectionResult.getErrorMessage() );
    Toast.makeText(this, R.string.GoogleApiClientConnFailed, Toast.LENGTH_LONG).show();
}
}//class actGeocoder