那些年我使用Volley遇到的坑
使用Volery已经快整整一年了,下面我来总结一下,我使用Volley时踩到的坑
(一) Volley的二次封装
下面看看我是怎么对Volley的二次封装的:
protected <T> void doSimpleRequest(String url, HashMap<String, String> params, final Class<?> clazz, final SimpleCallback callback) {
String requestUrl = urlBuilder(url, params);
Logger.e("url", requestUrl);
Response.Listener<String> successListener = new Response.Listener<String>() {
@Override
public void onResponse(String response) {
try {
T bean = GsonUtils.getFromStr(clazz, response);
callback.onResponse(bean);
} catch (Exception e) {
callback.onError();
}
}
};
Response.ErrorListener errorListener = new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
callback.onError();
}
};
StringRequest requestRequest = new StringRequest(url, params, successListener, errorListener);
LeSportsApplication.getApplication().addHttpRequest(requestRequest);
}
请求的时候使用的StringRequest,请求成功后,会返回一个String类型,然后用Gson解析成JavaBean,我之前一直都这么使用,看似天衣无缝,其实你会发现JSon解析是在主线程中实现的,如果数据量大的话,很容易导致UI卡顿。优化方案就是在数据解析在子线程中进行
(1) 新建一个GsonRequest请求类,继承Request对象
(2) 重写parseNetworkResponse方法,这个方法其实是在NetworkDispatcher这个子线程中执行的
package com.lesports.common.volley.toolbox;
import android.os.Looper;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.lesports.common.volley.NetworkResponse;
import com.lesports.common.volley.ParseError;
import com.lesports.common.volley.Request;
import com.lesports.common.volley.Response;
import com.letv.core.log.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
/**
* Created by liuyu8 on 2015/9/15.
*/
public class MyGsonRequest<T> extends Request<T>{
private final Logger mLogger = new Logger("GsonRequest");
private final Gson gson = new Gson();
private final Class<T> clazz;
private Response.Listener<T> listener;
/**
* Make a GET request and return a parsed object from JSON. Assumes
* {@link Request.Method#GET}.
*
* @param url
* URL of the request to make
* @param clazz
* Relevant class object, for Gson's reflection
*/
public MyGsonRequest(String url,HashMap<String, String> params,Class<T> clazz, Response.Listener<T> listener, Response.ErrorListener errorListener) {
super(Request.Method.GET, url, params,errorListener);
this.clazz = clazz;
this.listener = listener;
}
/**
* Make a GET request and return a parsed object from JSON. Assumes
* {@link Request.Method#GET}.
*
* @param url
* URL of the request to make
* @param clazz
* Relevant class object, for Gson's reflection
*/
public MyGsonRequest(String url, Class<T> clazz, Response.Listener<T> listener, Response.ErrorListener errorListener) {
super(Request.Method.GET, url, errorListener);
this.clazz = clazz;
this.listener = listener;
}
/**
* Like the other, but allows you to specify which {@link Request.Method} you want.
*
* @param method
* @param url
* @param clazz
* @param listener
* @param errorListener
*/
public MyGsonRequest(int method, String url, Class<T> clazz, Response.Listener<T> listener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
this.clazz = clazz;
this.listener = listener;
}
@Override
protected void deliverResponse(T response) {
if(listener != null){
listener.onResponse(response);
}
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
if(Looper.myLooper() == Looper.getMainLooper()){
mLogger.e("数据是 ==>在主线程中解析的~");
}else{
mLogger.e("数据不是 ==>在主线程中解析的~");
}
String json = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
JSONObject jsonObject = new JSONObject(json);
if(null != jsonObject && jsonObject.has("code") && jsonObject.getInt("code") == 200){
return Response.success(gson.fromJson(json, clazz), HttpHeaderParser.parseCacheHeaders(response));
}else{
return Response.error(new ParseError());
}
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (JsonSyntaxException e) {
mLogger.e("JsonSyntaxException ==== ");
return Response.error(new ParseError(e));
} catch (JSONException e) {
return Response.error(new ParseError(e));
}
}
@Override
public void finish(final String tag) {
super.finish(tag);
listener = null;
}
}
(3) 然后进行二次封装
/**
* 优化:
* (1)避免在主线程中解析json数据
* (2)添加了取消请求方法
*
* @param tag
* @param url
* @param params
* @param clazz
* @param callback
* @param <T>
*/
protected <T> void doRequest(String tag, String url, HashMap<String, String> params, final Class<?> clazz, final HttpCallback callback) {
String requestUrl = urlBuilder(url, params);
Logger.e("BaseTVApi", requestUrl);
callback.onLoading();
Response.Listener<T> successListener = new Response.Listener<T>() {
@Override
public void onResponse(T bean) {
if (bean != null) {
callback.onResponse(bean);
Logger.e("BaseTVApi", "request-->onResponse");
} else {
callback.onEmptyData();
Logger.e("BaseTVApi", "request-->onEmptyData");
}
}
};
Response.ErrorListener errorListener = new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
callback.onError();
Logger.e("BaseTVApi", "异常信息-->" + error.getMessage());
}
};
GsonRequest requestRequest = new GsonRequest(url, params, clazz, successListener, errorListener);
requestRequest.setTag(tag);
LeSportsApplication.getApplication().addHttpRequest(requestRequest);
}
打印log你会发现,你会发现数据解析是在子线程中执行的。
(二) 使用Volley内存泄露
在用MAT做内存泄露检查的时候,发现由于Volley的回调没有干掉导致的泄露问题,解决方法就是保证在Activity退出时,cancel掉请求,Request方法有一个cancel和finish方法并在这2个方法中把listener置为空。
(1) 在Application中提供一个取消请求的方法,因为一般请求队列是在Application中初始化的。
/**
* 网络请求优化,取消请求
* @param tag
*/
public void cancelRequest(String tag){
try {
mRequestQueue.cancelAll(tag);
}catch (Exception e){
Logger.e("LeSportsApplication","tag =="+ tag + "的请求取消失败");
}
}
(2) 在onStop中调用cancelRequest方法,最终会调用request的finish方法
(3) 在GsonRequest中重写finish方法,并在该方法中把listener置为空
@Override
public void finish(final String tag) {
super.finish(tag);
listener = null;
}
(4) 在再看看GsonRequest 的父类Request的finish方法,我已经把mErrorListener干掉了。
public void finish(final String tag) {
if (mRequestQueue != null) {
mRequestQueue.finish(this);
}
if (MarkerLog.ENABLED) {
final long threadId = Thread.currentThread().getId();
if (Looper.myLooper() != Looper.getMainLooper()) {
// If we finish marking off of the main thread, we need to
// actually do it on the main thread to ensure correct ordering.
Handler mainThread = new Handler(Looper.getMainLooper());
mainThread.post(new Runnable() {
@Override
public void run() {
mEventLog.add(tag, threadId);
mEventLog.finish(this.toString());
}
});
return;
}
mEventLog.add(tag, threadId);
mEventLog.finish(this.toString());
} else {
long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
VolleyLog.d("%d ms: %s", requestTime, this.toString());
}
}
mErrorListener = null;
}
总结:请求取消最终都会调用Request的finish方法,我们只要在这个方法中,把2个回调给干掉,问题就解决了。
(三) 在onStop中取消请求
QA妹子给我报了一个奇葩的bug,具体是第一次打开应用的时候APP一直在loading,找了半天发现原来是由于是取消请求不当引起的。
原因是:我第一次进入应用的时候,立马弹出了一个升级对话框,这个时候刚好触发了onStop方法,自然请求就取消掉了,所以就一直在loading呗! 后来我就把该页面的请求放在onDestroy中cancel。
(四) 嵌套请求
一个页面中可能会有多个请求,有的请求要等到其它的请求完后才能进行。就像这样:
private void requestUserSubscribesGame() {
uid = LoginUtils.getUid();
if (StringUtils.isStringEmpty(uid)) {
return;
}
UserTVApi.getInstance().getUserSubscribeGameId(TAG, uid, new SimpleCallback<ApiBeans.SubScribeListModel>() {
@Override
public void onResponse(ApiBeans.SubScribeListModel bean) {
SubScribeModel subScribeModel = bean.data;
if (subScribeModel != null) {
scribeGameEntrys = subScribeModel.getEntities();
requestHallData();
}
}
@Override
public void onError() {
Logger.e(TAG, "获取订阅信息失败...");
}
});
}
我之前也很喜欢这样写,后来我查看log的时候发现,第一个请求会超时请求,导致会重试一次。我设置的超时时间是2秒,可能是由于第二个请求太慢,导致请求第一次回调的时候太长导致的吧,具体原因还需要去查。
解决方法:有嵌套请求的情况,建议第一个接口请求完后,用handler发送消息,然后第二个开始请求。