使用Volley+OkHttp+Gson加速Android网络开发

来源:互联网 发布:淘宝做什么项目好 编辑:程序博客网 时间:2024/06/04 18:08
在Android应用开发中,我们不可避免的需要网络操作,尤其是在与服务器频繁的交互过程中,更是需要大量的重复编码:HttpURLConnection,Thread, AsyncTask,  Service等,十分复杂又容易出错。在2013年Google I/O大会上推出了网络通信框架——Volley。它可以极大简化HTTP通信,加载网络图片的操作,并且在小数据量的频繁网络交互中性能表现良好。不过,它不适合进行大文件的上传下载。


基于上述原因,在最近的项目中我学习使用了Volley网络库,然而Volley在使用中也存在一些问题:


比如,Volley自带的JsonRequest只支持JSONObject,而如果想要使用Gson,还需要自己定制Request。


另外,默认情况下,Volley在Froyo使用Apache Http stack作为其传输层,而在Gingerbread及之后的版本上使用HttpURLConnection stack作为传输层。很自然我们想到,能不能将口碑十分好的OkHttp替换为Volley的传输层呢。


最后,很多时候我们需要一些小文件的上传操作(如用户头像),能不能使用Volley完成这个任务呢。


所以,本文描述一种结合使用Volley+OkHttp+Gson进行快速网络开发的方法,并给出一些网络接口封装的例子。


首先,要将OkHttp替换为Volley的传输层,需要实现一个OkHttpStack类,然后在实例化Volley的RequestQueue时将之替换即可:

RequestQueue mQueue = Volley.newRequestQueue(this, new OkHttpStack(new OkHttpClient()));


OkHttpStack类的代码如下(来自GitHub,详见文后参考资料部分,这里为了方便网络不佳的读者贴出来):

/** * The MIT License (MIT) * * Copyright (c) 2015 Circle Internet Financial * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */import com.android.volley.AuthFailureError;import com.android.volley.Request;import com.android.volley.toolbox.HttpStack;import com.squareup.okhttp.Call;import com.squareup.okhttp.Headers;import com.squareup.okhttp.MediaType;import com.squareup.okhttp.OkHttpClient;import com.squareup.okhttp.Protocol;import com.squareup.okhttp.RequestBody;import com.squareup.okhttp.Response;import com.squareup.okhttp.ResponseBody;import org.apache.http.HttpEntity;import org.apache.http.HttpResponse;import org.apache.http.ProtocolVersion;import org.apache.http.StatusLine;import org.apache.http.entity.BasicHttpEntity;import org.apache.http.message.BasicHeader;import org.apache.http.message.BasicHttpResponse;import org.apache.http.message.BasicStatusLine;import java.io.IOException;import java.util.Map;import java.util.concurrent.TimeUnit;/** * OkHttp backed {@link com.android.volley.toolbox.HttpStack HttpStack} that does not * use okhttp-urlconnection */public class OkHttpStack implements HttpStack {    private final OkHttpClient mClient;    public OkHttpStack(OkHttpClient client) {        this.mClient = client;    }    @Override    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)            throws IOException, AuthFailureError {        OkHttpClient client = mClient.clone();        int timeoutMs = request.getTimeoutMs();        client.setConnectTimeout(timeoutMs, TimeUnit.MILLISECONDS);        client.setReadTimeout(timeoutMs, TimeUnit.MILLISECONDS);        client.setWriteTimeout(timeoutMs, TimeUnit.MILLISECONDS);        com.squareup.okhttp.Request.Builder okHttpRequestBuilder = new com.squareup.okhttp.Request.Builder();        okHttpRequestBuilder.url(request.getUrl());        Map<String, String> headers = request.getHeaders();        for (final String name : headers.keySet()) {            okHttpRequestBuilder.addHeader(name, headers.get(name));        }        for (final String name : additionalHeaders.keySet()) {            okHttpRequestBuilder.addHeader(name, additionalHeaders.get(name));        }        setConnectionParametersForRequest(okHttpRequestBuilder, request);        com.squareup.okhttp.Request okHttpRequest = okHttpRequestBuilder.build();        Call okHttpCall = client.newCall(okHttpRequest);        Response okHttpResponse = okHttpCall.execute();        StatusLine responseStatus = new BasicStatusLine(parseProtocol(okHttpResponse.protocol()), okHttpResponse.code(), okHttpResponse.message());        BasicHttpResponse response = new BasicHttpResponse(responseStatus);        response.setEntity(entityFromOkHttpResponse(okHttpResponse));        Headers responseHeaders = okHttpResponse.headers();        for (int i = 0, len = responseHeaders.size(); i < len; i++) {            final String name = responseHeaders.name(i), value = responseHeaders.value(i);            if (name != null) {                response.addHeader(new BasicHeader(name, value));            }        }        return response;    }    private static HttpEntity entityFromOkHttpResponse(Response r) throws IOException {        BasicHttpEntity entity = new BasicHttpEntity();        ResponseBody body = r.body();        entity.setContent(body.byteStream());        entity.setContentLength(body.contentLength());        entity.setContentEncoding(r.header("Content-Encoding"));        if (body.contentType() != null) {            entity.setContentType(body.contentType().type());        }        return entity;    }    @SuppressWarnings("deprecation")    private static void setConnectionParametersForRequest(com.squareup.okhttp.Request.Builder builder, Request<?> request)            throws IOException, AuthFailureError {        switch (request.getMethod()) {            case Request.Method.DEPRECATED_GET_OR_POST:                // Ensure backwards compatibility.  Volley assumes a request with a null body is a GET.                byte[] postBody = request.getPostBody();                if (postBody != null) {                    builder.post(RequestBody.create(MediaType.parse(request.getPostBodyContentType()), postBody));                }                break;            case Request.Method.GET:                builder.get();                break;            case Request.Method.DELETE:                builder.delete();                break;            case Request.Method.POST:                builder.post(createRequestBody(request));                break;            case Request.Method.PUT:                builder.put(createRequestBody(request));                break;            case Request.Method.HEAD:                builder.head();                break;            case Request.Method.OPTIONS:                builder.method("OPTIONS", null);                break;            case Request.Method.TRACE:                builder.method("TRACE", null);                break;            case Request.Method.PATCH:                builder.patch(createRequestBody(request));                break;            default:                throw new IllegalStateException("Unknown method type.");        }    }    private static ProtocolVersion parseProtocol(final Protocol p) {        switch (p) {            case HTTP_1_0:                return new ProtocolVersion("HTTP", 1, 0);            case HTTP_1_1:                return new ProtocolVersion("HTTP", 1, 1);            case SPDY_3:                return new ProtocolVersion("SPDY", 3, 1);            case HTTP_2:                return new ProtocolVersion("HTTP", 2, 0);        }        throw new IllegalAccessError("Unkwown protocol");    }    private static RequestBody createRequestBody(Request r) throws AuthFailureError {        final byte[] body = r.getBody();        if (body == null) return null;        return RequestBody.create(MediaType.parse(r.getBodyContentType()), body);    }}


然后,我们需要实现使用Gson的Request类:

import com.android.volley.AuthFailureError;import com.android.volley.NetworkResponse;import com.android.volley.ParseError;import com.android.volley.Request;import com.android.volley.Response;import com.android.volley.toolbox.HttpHeaderParser;import com.google.gson.Gson;import java.io.UnsupportedEncodingException;import java.lang.reflect.Type;import java.util.Map;import me.zq.youjoin.utils.LogUtils;import me.zq.youjoin.utils.StringUtils;public class PostObjectRequest<T> extends Request<T> {    private ResponseListener listener;    private Gson gson;    private Type clazz;    private Map<String, String> params;    public PostObjectRequest(String url, Map<String, String> params, Type type,                             ResponseListener listener){        super(Method.POST, url, listener);        this.listener = listener;        this.clazz = type;        this.params = params;        this.gson = new Gson();    }    @Override    protected Response<T> parseNetworkResponse(NetworkResponse response){        try{            T result;            String jsonString =                    new String(response.data, HttpHeaderParser.parseCharset(response.headers));            LogUtils.d("hehe", jsonString);            result = gson.fromJson(StringUtils.FixJsonString(jsonString), clazz);            return Response.success(result,                    HttpHeaderParser.parseCacheHeaders(response));        }catch (UnsupportedEncodingException e) {            return Response.error(new ParseError(e));        }    }    @Override    protected void deliverResponse(T response){        listener.onResponse(response);    }    @Override    protected Map<String, String> getParams() throws AuthFailureError{        return params;    }}



最后,我们来实现图片上传部分。由于我们希望在上传图片的时候添加一些参数,例如用户id等,导致这部分的实现比较复杂。虽然我们可以将这些信息存储在图片文件名中实现传递,但这总归是取巧的办法。比较正规一点的方法还是研究网页表单提交时的Post请求的格式,然后自己构造一个合法的Post请求。我们可以使用Chorme的开发者工具来抓取Post请求,研究其格式,下图为我抓取的一个带图片和参数的表单Post请求:


从图中可以看出,我们只需要重写getBody()方法,逐行仿照Post格式写入我们自己的图片和参数,就可实现参数+图片的上传。多张图片的上传原理相同。另外,图片要注意在上传之前进行压缩处理。另外,为了方便服务器解析多张图片,将图片参数设置为uploadedfile[index]格式。代码如下:

import android.util.Log;import com.android.volley.AuthFailureError;import com.android.volley.DefaultRetryPolicy;import com.android.volley.NetworkResponse;import com.android.volley.ParseError;import com.android.volley.Request;import com.android.volley.Response;import com.android.volley.toolbox.HttpHeaderParser;import com.google.gson.Gson;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.lang.reflect.Type;import java.util.List;import java.util.Map;import me.zq.youjoin.model.ImageInfo;import me.zq.youjoin.utils.StringUtils;public class PostUploadRequest<T> extends Request<T> {    /**     * 正确数据的时候回掉用     */    private ResponseListener mListener ;    /*请求 数据通过参数的形式传入*/    private List<ImageInfo> mListItem ;    private static final String BOUNDARY = "--------------520-13-14"; //数据分隔线    private static final String MULTIPART_FORM_DATA = "multipart/form-data";//使用表单数据方法提交    private static final String TAG = "hehe_upload_request";    private static final String PARAM = "uploadedfile";//图片的参数,<span style="font-family: Arial, Helvetica, sans-serif;">为了上传多张图片,在下面的封装中使用uploadedfile[index]格式作为图片参数</span>    private Map<String, String> params;    private Gson gson;    private Type clazz;    public PostUploadRequest(String url, List<ImageInfo> listItem,                             Map<String, String> params,                             Type type, ResponseListener listener) {        super(Method.POST, url, listener);        this.mListener = listener ;        this.params = params;        this.gson = new Gson();        this.clazz = type;        setShouldCache(false);        mListItem = listItem ;        //设置请求的响应事件,因为文件上传需要较长的时间,所以在这里加大了,设为5秒        setRetryPolicy(new DefaultRetryPolicy(5000,DefaultRetryPolicy.DEFAULT_MAX_RETRIES,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));    }    /**     * 这里开始解析数据     * @param response Response from the network     * @return     */    @Override    protected Response<T> parseNetworkResponse(NetworkResponse response) {        try {            String mString =                    new String(response.data, HttpHeaderParser.parseCharset(response.headers));            Log.v(TAG, "mString = " + mString);            T result = gson.fromJson(StringUtils.FixJsonString(mString), clazz);            return Response.success(result,                    HttpHeaderParser.parseCacheHeaders(response));        } catch (UnsupportedEncodingException e) {            return Response.error(new ParseError(e));        }    }    /**     * 回调正确的数据     * @param response The parsed response returned by     */    @Override    protected void deliverResponse(T response) {        mListener.onResponse(response);    }//重写getBody()方法,封装Post包    @Override    public byte[] getBody() throws AuthFailureError {        if (mListItem == null||mListItem.size() == 0){            return super.getBody() ;        }        ByteArrayOutputStream bos = new ByteArrayOutputStream() ;        if(!params.isEmpty()) {            StringBuilder sbParams = new StringBuilder();            for (Map.Entry<String, String> o : params.entrySet()) {                Map.Entry entry = (Map.Entry) o;                /*第一行*/                //`"--" + BOUNDARY + "\r\n"`                sbParams.append("--" + BOUNDARY);                sbParams.append("\r\n");                /*第二行*/                //Content-Disposition: form-data; name="参数的名称"; + 参数 + "\r\n"                sbParams.append("Content-Disposition: form-data;");                sbParams.append(" name=\"");                sbParams.append((String) entry.getKey());                sbParams.append("\"");                sbParams.append("\r\n");                sbParams.append("\r\n");                sbParams.append((String) entry.getValue());                sbParams.append("\r\n");            }            try {                bos.write(sbParams.toString().getBytes("utf-8"));//                bos.write("\r\n".getBytes("utf-8"));            } catch (IOException e) {                e.printStackTrace();            }        }        int N = mListItem.size() ;        ImageInfo imageInfo ;        for (int i = 0; i < N ;i++){            imageInfo = mListItem.get(i) ;            StringBuilder sb= new StringBuilder() ;            /*第一行*/            //`"--" + BOUNDARY + "\r\n"`            sb.append("--"+BOUNDARY);            sb.append("\r\n") ;            /*第二行*/            //Content-Disposition: form-data; name="参数的名称"; filename="上传的文件名" + "\r\n"            sb.append("Content-Disposition: form-data;");            sb.append(" name=\"");            sb.append(PARAM + "[" + Integer.toString(i) + "]"); //为了上传多张图片,使用uploadedfile[index]格式作为图片参数            sb.append("\"") ;            sb.append("; filename=\"") ;            sb.append(imageInfo.getFileName()) ;            sb.append("\"");            sb.append("\r\n") ;            /*第三行*/            //Content-Type: 文件的 mime 类型 + "\r\n"            sb.append("Content-Type: ");            sb.append(imageInfo.getMime()) ;            sb.append("\r\n") ;            /*第四行*/            //"\r\n"            sb.append("\r\n") ;            try {                bos.write(sb.toString().getBytes("utf-8"));                /*第五行*/                //文件的二进制数据 + "\r\n"                bos.write(imageInfo.getValue());                bos.write("\r\n".getBytes("utf-8"));            } catch (IOException e) {                e.printStackTrace();            }        }        /*结尾行*/        //`"--" + BOUNDARY + "--" + "\r\n"`        String endLine = "--" + BOUNDARY + "--" + "\r\n" ;        try {            bos.write(endLine.getBytes("utf-8"));        } catch (IOException e) {            e.printStackTrace();        }       // Log.v(TAG,"imageInfo =\n"+bos.toString()) ;        return bos.toByteArray();    }    //Content-Type: multipart/form-data; boundary=----------8888888888888    @Override    public String getBodyContentType() {        return MULTIPART_FORM_DATA+"; boundary="+BOUNDARY;    }//    @Override //注意一旦重写getBody()方法,则使用此方法添加参数无效。//    protected Map<String, String> getParams() throws AuthFailureError{//        return params;//    }}

为了方便调用,封装Response接口如下:

import com.android.volley.Response;public interface ResponseListener<T> extends Response.ErrorListener,Response.Listener<T> {}

至此,对Volley+OkHttp+Gson的封装就完成了。在使用时,我们可以在自定义Application中初始化Queue,再封装一个专门的NetworkManager类来提供所有的网络访问操作,比如,在我最近的项目中是这样用的:


/** * 网络管理类,封装网络操作接口 */public class NetworkManager {    /**     * 网络接口相关常量     */    public static final String USER_USERNAME = "user_name";    public static final String USER_PASSWORD = "user_password";    public static final String USER_EMAIL = "user_email";    public static final String USER_ID = "user_id";    public static final String USER_WORK = "user_work";    public static final String USER_BIRTH = "user_birth";    public static final String USER_SIGN = "user_sign";    public static final String USER_LOCATION = "user_location";    public static final String USER_SEX = "user_sex";    public static final String TWEETS_CONTNET = "tweets_content";    public static final String FRIEND_ID = "friend_id";    public static final String SEND_USERID = "send_userid";    public static final String RECEIVE_USERID = "receive_userid";    public static final String MESSAGE_CONTENT = "message_content";    /**     * 服务器接口URL     */    public static final String BASE_API_URL = "http://192.168.0.103:8088/youjoin-server/controllers/";    public static final String API_SIGN_IN = BASE_API_URL + "signin.php";    public static final String API_SIGN_UP = BASE_API_URL + "signup.php";    public static final String API_UPDATE_USERINFO = BASE_API_URL + "update_userinfo.php";    public static final String API_SEND_TWEET = BASE_API_URL + "send_tweet.php";    public static final String API_REQUEST_USERINFO = BASE_API_URL + "request_userinfo.php";    public static final String API_ADD_FRIEND = BASE_API_URL + "add_friend.php";    public static final String API_SEND_MESSAGE = BASE_API_URL + "send_message.php";    public static final String API_RECEIVE_MESSAGE = BASE_API_URL + "receive_message.php";    public static final String API_COMMENT_TWEET = BASE_API_URL + "comment_tweet.php";    public static final String API_UPVOTE_TWEET = BASE_API_URL + "upvote_tweet.php";    private static RequestQueue mRequestQueue ;    /**     * 发送动态接口     * @param listener ResponseListener     */    public static void postSendTweet(String content, List<ImageInfo> images,                                     ResponseListener listener){        Map<String, String> params = new HashMap<>();        params.put(USER_ID, YouJoinApplication.getCurrUser().getId());        params.put(TWEETS_CONTNET, content);        Request request = new PostUploadRequest(API_SEND_TWEET, images, params,                new TypeToken<UpdateUserInfoResult>(){}.getType(), listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 发送私信接口     * @param receiveUserId 接收方用户id     * @param content 私信内容     * @param listener ResponseListener     */    public static void postSendMessage(String receiveUserId, String content,                                       ResponseListener listener){        Map<String, String> params = new HashMap<>();        params.put(SEND_USERID, YouJoinApplication.getCurrUser().getId());        params.put(RECEIVE_USERID, receiveUserId);        params.put(MESSAGE_CONTENT, content);        Request request = new PostObjectRequest(                API_SEND_MESSAGE,                params, new TypeToken<ResultInfo>(){}.getType(),                listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 添加好友接口     * @param friendUserId 要添加的好友id     * @param listener ResponseListener     */    public static void postAddFriend(String friendUserId,                                     ResponseListener listener){        Map<String, String> params = new HashMap<>();        params.put(USER_ID, YouJoinApplication.getCurrUser().getId());        params.put(FRIEND_ID, friendUserId);        Request request = new PostObjectRequest(                API_ADD_FRIEND,                params, new TypeToken<ResultInfo>(){}.getType(),                listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 个人资料请求(下载)接口     * @param userId   要获取的用户id     * @param listener ResponseListener     */    public static void postRequestUserInfo(String userId, ResponseListener listener){        Map<String, String> params = new HashMap<>();        params.put(USER_ID, userId);        Request request = new PostObjectRequest(                API_REQUEST_USERINFO,                params,new TypeToken<UserInfo>(){}.getType(),                listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 个人资料更新(上传)接口     * @param userInfo 用户实体类     * @param picPath 头像的本地路径     * @param listener ResponseListener     */    public static void postUpdateUserInfo(UserInfo userInfo, String picPath, ResponseListener listener){        if(userInfo.getId() == null) return;        List<ImageInfo> imageList = new ArrayList<>();        imageList.add(new ImageInfo(picPath));        Map<String, String> params = new HashMap<>();        params.put(USER_ID, userInfo.getId());        params.put(USER_WORK, userInfo.getWork());        params.put(USER_LOCATION, userInfo.getLocation());        params.put(USER_SEX, userInfo.getSex());        params.put(USER_BIRTH, userInfo.getBirth());        params.put(USER_SIGN, userInfo.getUsersign());        Request request = new PostUploadRequest(API_UPDATE_USERINFO, imageList, params,                new TypeToken<UpdateUserInfoResult>(){}.getType(), listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 登陆接口     * @param username 登录用户名     * @param password 登陆密码     * @param listener ResponseListener     */    public static void postSignIn(String username, String password,                                  ResponseListener listener){        Map<String, String> param = new HashMap<>();        param.put(USER_USERNAME, username);        param.put(USER_PASSWORD, Md5Utils.getMd5(password));        Request request = new PostObjectRequest(                API_SIGN_IN,                param,                new TypeToken<UserInfo>(){}.getType(),                listener);        NetworkManager.getRequestQueue().add(request);    }    /**     * 注册接口     * @param username 注册用户名     * @param password 注册密码     * @param email 注册邮箱     * @param listener ResponseListener     */    public static void postSignUp(String username, String password, String email,                                  ResponseListener listener){        Map<String, String> param = new HashMap<>();        param.put(USER_USERNAME, username);        param.put(USER_PASSWORD, Md5Utils.getMd5(password));        param.put(USER_EMAIL, email);        Request request = new PostObjectRequest(                API_SIGN_UP,                param,                new TypeToken<UserInfo>(){}.getType(),                listener);        NetworkManager.getRequestQueue().add(request);    }    /**初始化Volley 使用OkHttpStack     * @param context 用作初始化Volley RequestQueue     */    public static synchronized void initialize(Context context){        if (mRequestQueue == null){            synchronized (NetworkManager.class){                if (mRequestQueue == null){                    mRequestQueue =                            Volley.newRequestQueue(context, new OkHttpStack(new OkHttpClient()));                }            }        }        mRequestQueue.start();    }    /**获取RequestQueue实例     * @return 返回RequestQueue实例     */    public static RequestQueue getRequestQueue(){        if (mRequestQueue == null)            throw new RuntimeException("请先初始化mRequestQueue") ;        return mRequestQueue ;    }}

import android.app.Application;import android.content.Context;import me.zq.youjoin.model.UserInfo;import me.zq.youjoin.network.NetworkManager;public class YouJoinApplication extends Application {    public static float sScale;    public static int sHeightPix;    private static Context context;    private static UserInfo currUser;    @Override    public void onCreate(){        super.onCreate();        context = getApplicationContext();        NetworkManager.initialize(context);        sScale = getResources().getDisplayMetrics().density;        sHeightPix = getResources().getDisplayMetrics().heightPixels;    }    public static Context getAppContext(){        return context;    }    public static UserInfo getCurrUser() {        return currUser;    }    public static void setCurrUser(UserInfo currUser) {        YouJoinApplication.currUser = currUser;    }}


参考资料:


OkHttpStack.java -- from GitHub
Android Networking I: OkHttp, Volley and Gson
Android Volley完全解析(三),定制自己的Request
Android volley 解析(三)之文件上传篇
Android OkHttp完全解析 是时候来了解OkHttp了
2 0
原创粉丝点击