1. 程式人生 > >Volley+OkHttp+Gson自定義框架

Volley+OkHttp+Gson自定義框架

本篇部落格內容:

在前面兩篇部落格,已經記錄如何自定義專案需求的請求。這裡來講解如何修改Volley原始碼,自定義需求框架。

1.先來了解下Volley部分原始碼:

在Volley.java中,可以看到一些配置,例如聯網操作類(HttpURLConnection或者androids-http-clients),磁碟快取,執行緒池(實際上是4個網路執行緒,一個快取執行緒)。

/**
 * 用途:
 *  初始化Volley中網路配置,非同步執行緒配置,磁碟快取配置
 */
public class Volley {

    /** Default on-disk cache directory.   預設快取的資料夾名*/
private static final String DEFAULT_CACHE_DIR = "volley"; /** * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. * * @param context A {@link Context} to use for creating the cache dir. * @param stack An {@link HttpStack} to use for the network, or null for default. * @return
A started {@link RequestQueue} instance. * * 建立一個預設的工作池物件,且呼叫RequestQueue的start() * 引數Contentxt用於建立磁碟快取的資料夾 * 引數HttpStack用於網路工作,預設是nulll */
public static RequestQueue newRequestQueue(Context context, HttpStack stack) { //在手機記憶體中建立一個快取資料的資料夾 File cacheDir = new
File(context.getCacheDir(), DEFAULT_CACHE_DIR); String userAgent = "volley/0"; try { String packageName = context.getPackageName(); PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); userAgent = packageName + "/" + info.versionCode; } catch (NameNotFoundException e) { } if (stack == null) { //api版本不小於9,則使用java中HttpURLConnection作為聯網方式 if (Build.VERSION.SDK_INT >= 9) { stack = new HurlStack(); } else { // Prior to Gingerbread, HttpUrlConnection was unreliable. // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); } } //建立一個執行網路工作的操作類 Network network = new BasicNetwork(stack); //建立一個請求佇列,新增磁碟快取的操作類,執行網路工作的操作類 RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network); //開啟。 queue.start(); return queue; } /** * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. * * @param context A {@link Context} to use for creating the cache dir. * @return A started {@link RequestQueue} instance. * * 建立一個預設的工作池物件,且呼叫RequestQueue的start() * 引數Contentxt用於建立磁碟快取的資料夾 * */ public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, null); } }

從上面原始碼可知,噹噹前手機系統的api>=9 時候,volley才有HttpURLConnection來連線伺服器。磁碟快取,執行緒池這裡暫時省略不講解。接下來了解下,HurlStack這類。

HurlStack這個類包括了這幾個操作,設定請求的header和body,以及讀取響應資料。

/**
 * An {@link HttpStack} based on {@link HttpURLConnection}.
 *
 * 用途:
 *      用HttpURLConnection作為聯網通訊類。
 */
public class HurlStack implements HttpStack { 

    ...................//部分原始碼未貼出
       /**
     *  執行HttpURLConnection,返回HttpResponse
     *
     * @param request the request to perform
     * @param additionalHeaders additional headers to be sent together with
     *         {@link Request#getHeaders()}
     * @return
     * @throws IOException
     * @throws AuthFailureError
     */
    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) 
       throws IOException, AuthFailureError { 

       ...................//部分原始碼未貼出  
        String url = request.getUrl();
        //建立一個HttpUrlConnection或者其子類,進行網路連線。
        URL parsedUrl = new URL(url);
        HttpURLConnection connection = openConnection(parsedUrl, request);
        //新增Http的標頭
        for (String headerName : map.keySet()) {
            connection.addRequestProperty(headerName, map.get(headerName));
        }
        //根據volley中請求,來設定HttpUrlConnection的連線方式,和傳遞的內容
        setConnectionParametersForRequest(connection, request);  

         ...................//部分原始碼未貼出  
    }   


    /**
     * Opens an {@link HttpURLConnection} with parameters.
     * @param url
     * @return an open connection
     * @throws IOException
     *
     * 根據url中帶有的協議,來開啟一個帶有引數的HttpURLConnection,或者HttpsURLConnection
     */
    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
        HttpURLConnection connection = createConnection(url);

        int timeoutMs = request.getTimeoutMs();
        //設定連線時間
        connection.setConnectTimeout(timeoutMs);
        //設定讀取時間
        connection.setReadTimeout(timeoutMs);
        //不設定http快取
        connection.setUseCaches(false);
        connection.setDoInput(true);

        // use caller-provided custom SslSocketFactory, if any, for HTTPS
        // 若是HTTPS協議,則使用HttpsURLConnection進行連線,且新增自定義的SSLSocketFactory
        if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
            ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
        }
        return connection;
    }  

    /**
     * Create an {@link HttpURLConnection} for the specified {@code url}.
     * 通過URL開啟一個客戶端與url指向資源的間的網路通道。
     */
    protected HttpURLConnection createConnection(URL url) throws IOException {
        return (HttpURLConnection) url.openConnection();
    } 

    /**
     * 若是請求中存在Body(post傳遞的引數),則寫入body到流中。
     * @param connection
     * @param request
     * @throws IOException
     * @throws AuthFailureError
     */
    private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
            throws IOException, AuthFailureError {
        byte[] body = request.getBody();
        if (body != null) {
            //設定post請求方法,允許寫入客戶端傳遞的引數
            connection.setDoOutput(true);
            //設定標頭的Content-Type屬性
            connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
            DataOutputStream out = new DataOutputStream(connection.getOutputStream());
            //寫入post傳遞的引數
            out.write(body);
            out.close();
        }
    }
}

從上面原始碼可知:
1.建立HttpUrlConnection物件時通過createConnection(URL url)來實現的

2.新增請求的body會在addBodyIfExists()內呼叫request.getBody()來實現

3.新增請求的標頭Content-type是呼叫request.getBodyContentType()來實現的。

瞭解聯網操作類HttpUrlConnection如何建立,如何新增請求的Body和Header,便可以知道如何修改Volley原始碼。

2.使用OkHttp作為傳輸層:

OkHttp的描述:一個http+spdy的客戶端,可以用於android 和java運用程式。

OkHttp的優勢:

  • Http/2 支援允許全部(訪問同一主機的)請求共享一個socket.

  • 連線池減少請求延遲(若是Http/2不能使用)

  • 3.資料壓縮成GZIP格式,來縮小下載大小。

  • 4.響應快取可以避免重複(已經完成的網路操作的)請求

  • 5.OkHttp perseveres,正當網路是麻煩:
    它會默默地從常見連線問題池中恢復。
    若是你的伺服器有多個IP地址,當第一次連線失敗,OkHttp會嘗試使用備用地址。
    這是必需的,對於IP4+IP6和(沉珂資料中的)主機服務

  • OkHttp初始化新連線是通過現今TLS功能(SNI,ALPN),若是握手失敗,則回退到TLS1.0中
  • 使用Okhttp是容易的,它的請求/響應的API是被設計成流暢builder和immutability.
    它支援同步阻塞回調和非同步回撥。

OkHttp的版本變化:

  • OkHttp1.x 存在一些問題:
    問題: OkHttp changes the global SSL context, breaks other HTTP clients
    原因:OkHttp更改全域性的SSL context,打斷其他Http客戶端
    解決方式:建立自己的SSL context,不使用系統預設的。但是存在問題,自定義的SSLcontext會失去自定義的屬性,這可能打破一些特徵,例如證書固定。

    /**
     * 自定義一個SSLContext,而不使用預設的。
     *
     * @return
     */
    public static OkHttpClient createOkHttpClient(){
        OkHttpClient okHttpClient=new OkHttpClient();
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, null, null);
            okHttpClient.setSslSocketFactory( sslContext.getSocketFactory());
        //避免OkHttp與UrlHttpConnection混合,出現某些方法找不到
        //參考連結:https://github.com/square/okhttp/issues/673
            URL.setURLStreamHandlerFactory(okHttpClient);
          ;
        } catch (GeneralSecurityException e) {
            throw new AssertionError(); // The system has no TLS. Just give up.
        }
    
        return  okHttpClient;
    }

    獲取HttpUrlConnection物件的方式:okHttpClient.open(url)

  • OkHttp2.x 存在一些問題:版本2.x修復以上的問題,但是仍然存在些問題:
    問題:java.io.IOException: stream was reset: PROTOCOL_ERROR
    原因:導致原因: OkHttp 傳送標頭使用的格式:accept-encoding: gzip 。 這導致nginx伺服器報告一個protocol error
    解決方式:http://mailman.nginx.org/pipermail/nginx/2015-October/048978.html

    // 強迫使用HTTP1.1:
    OkHttpClient client = new OkHttpClient();
    // Disable HTTP/2 for interop with NGINX 1.9.5.
    client.setProtocols(Collections.singletonList(Protocol.HTTP_1_1));    

    獲取HttpUrlConnection物件的方式:OkUrlFactory(okHttpClient).open()

  • OkHttp3.x 版本已經比較成熟,實用起來也比較方便,通過構建者方式來建立物件。

原本打算都講解下如何讓OkHttp各個版本作為Volley的傳輸層,但是考慮到大多數人都是使用OkHttp3.x。故這裡就不在講解如何實現各個版本的OkHttp作為傳輸層。

先在Volley原始碼的Gradle中新增依賴庫:

dependencies {
    //OkHttp庫
    compile 'com.squareup.okhttp3:okhttp:3.5.0'
    //okhttp-urlconnection庫
    compile 'com.squareup.okhttp3:okhttp-urlconnection:3.5.0'
   //Gson庫
    compile 'com.google.code.gson:gson:2.2.4'
}

然後,建立一個OkHttpClientStatck,來作為傳輸層(若是有需要的,可以在原始碼下自定義一個包,在包下建立)
這裡寫圖片描述

思路:採用OkHttp作為傳輸層,替代原生的HttpUrlConnection。

實現方式:繼承HurlStack 類,複寫createConnection(URL url)返回OkHttpClient中的HttpURLConnection

OkHttpClientStatck.java完整程式碼如下:

/**
 * Created by ${新根} on 2016/11/3.
 * 部落格:http://blog.csdn.net/hexingen
 *
 * 採用OkHttp作為傳輸層,替代原生的HttpUrlConnection
 */
public class OkHttpClientStatck extends HurlStack {
    private OkHttpClient okHttpClient;

    /**
     * 採購構建者方式建立OkHttpClient
     * OkHttpClient可以自定義攔截器,快取,聯網時間和寫入時間等設定。
     * Volley預設有這些東西,這裡就不在設定。
     */
    public OkHttpClientStatck(){
        OkHttpClient.Builder builder=new OkHttpClient.Builder();
        okHttpClient=builder.build();
    }
    /**
     * 獲取到OkHttpClient();
     * @return
     */
    private  OkHttpClient getOkHttpClient(){
        return  okHttpClient;
    }
    /**
     * 這裡採用OkHttp框架中HttpURLConnection,而不使用原生的。
     *
     * OkHttpClient1.x:通過OkHttpClient.open(url)來獲取
     *
     * OkHttpClient2.x:可以通過OkUrlFactory.open(URL url)來獲取
     *
     * @param url
     * @return
     * @throws IOException
     */
    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        String protocol=  url.getProtocol();
        if (protocol.equals("http")) return new OkHttpURLConnection(url, getOkHttpClient(),null);

        if (protocol.equals("https")) return new OkHttpsURLConnection(url, getOkHttpClient(),null);
        throw new IllegalArgumentException("Unexpected protocol: " + protocol);
    }
}

然後將OkHttpClientStatck新增到Volley中,作為預設的傳輸層:
在Volley.java中修改如下:

public class Volley { 

   ............//部分原始碼未貼出

   /**
     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
     *
     * @param context A {@link Context} to use for creating the cache dir.
     * @return A started {@link RequestQueue} instance.
     *
     * 建立一個預設的工作池物件,且呼叫RequestQueue的start()
     * 引數Contentxt用於建立磁碟快取的資料夾
     *
     */
    public static RequestQueue newRequestQueue(Context context) {
        //修改原始碼:這裡採用OkHttp作為傳輸層
        return newRequestQueue(context, new OkHttpClientStatck());
    }
}

有人好奇為什麼不用系統預設的HttpUrlConnetion,而用OkHttp作為傳輸層?
單純的HttpUrlConnetion的使用起來存在一些問題,OkHttp不止針對其問題做了處理,還對連線伺服器過程中一些問題做了處理。

Volley與OkHttp的整合已經完成了,整合後的Volley框架使用起來和原本的一樣,自我鼓勵下。

3.新增自定義的Request:

既然改了Volley的傳輸層,那再改Request也是順手之勞。

  • 在框架原始碼中,新增 GsonRequest,這裡不再貼程式碼,感興趣的可以閱讀Gson解析的 GsonRequest

  • 在框架原始碼中,新增 檔案上傳的MultiPartRequest ,這裡有部分程式碼進行了修改,原本是上傳檔案的byte[],變成了上傳file。省略了轉化過程,使用起來更方便。

    完整程式碼程式碼如下:

/**
 * Created by 新根 on 2016/8/9.
 * 用途:
 * 各種資料上傳到伺服器的內容格式:
 * <p/>
 * 檔案上傳(內容格式):multipart/form-data
 * String字串傳送(內容格式):application/x-www-form-urlencoded
 * json傳遞(內容格式):application/json
 *
 * 部落格:http://blog.csdn.net/hexingen
 */
public class MultiPartRequest<T> extends Request<T> {
    private  static  final  String TAG=MultiPartRequest.class.getSimpleName();
    /**
     * 解析後的實體類
     */
    private final Class<T> clazz;

    private final Response.Listener<T> listener;

    /**
     * 自定義header:
     */
    private Map<String, String> headers;
    private final Gson gson = new Gson();
    /**
     * 字元編碼格式
     */
    private static final String PROTOCOL_CHARSET = "utf-8";

    private static final String BOUNDARY = "----------" + System.currentTimeMillis();
    /**
     * Content type for request.
     */
    private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data; boundary=" + BOUNDARY;

    /**
     * 檔案列表。引數1是檔名,引數2是檔案
     */
    private Map<String, File > fileList;
    /**
     * 多個檔案間的間隔
     */
    private static final String FILEINTERVAL = "\r\n";

    public MultiPartRequest(int method, String url,
                            Class<T> clazz,
                            Response.Listener<T> listener, Response.ErrorListener errorListenerr) {
        super(method, url, errorListenerr);
        this.clazz = clazz;
        this.listener = listener;
        headers = new HashMap<>();
        fileList = new HashMap<>();
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(
                    response.data,
                    "utf-8");
            T t = gson.fromJson(json, clazz);
            return Response.success(t, HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(T t) {
        listener.onResponse(t);
    }


    /**
     * 重寫getHeaders(),新增自定義的header
     *
     * @return
     * @throws AuthFailureError
     */
    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers;
    }

    /**
     * 設定請求的標頭
     * @param key
     * @param content
     * @return
     */
    public Map<String, String> setHeader(String key, String content) {
        if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(content)) {
            headers.put(key, content);
        }
        return headers;
    }

    /**
     * 新增檔名和檔案資料
     *
     * @param fileName
     * @param file
     */
    public void addFile(String fileName, File file) {
        if (!TextUtils.isEmpty(fileName) && file != null) {
            fileList.put(fileName, file);
        }
    }


    /**
     * 重寫Content-Type:設定為json
     */
    @Override
    public String getBodyContentType() {
        return PROTOCOL_CONTENT_TYPE;
    }

    /**
     * post引數型別
     */
    @Override
    public String getPostBodyContentType() {
        return getBodyContentType();
    }

    /**
     * post引數
     */
    @Override
    public byte[] getPostBody() throws AuthFailureError {

        return getBody();
    }

    /**
     * 將string編碼成byte
     *
     * @return
     * @throws AuthFailureError
     */
    @Override
    public byte[] getBody() throws AuthFailureError {
        byte[] body;
        ByteArrayOutputStream outputStream = null;
        try {
            outputStream = new ByteArrayOutputStream();
            Set<Map.Entry<String, File>> set = fileList.entrySet();
            int i=1;
            for (Map.Entry<String,File> entry : set) {
                //新增檔案的頭部格式
                writeByte(outputStream, getFileHead( entry.getKey()));
                //新增檔案資料
                writeByte(outputStream,fileTranstateToByte(entry.getValue()));
                //新增檔案間的間隔
                if (set.size() > 1&&i<set.size()) {
                    i++;
                    Log.i(TAG,"新增檔案間隔");
                    writeByte(outputStream, FILEINTERVAL.getBytes(PROTOCOL_CHARSET));
                }
            }
            writeByte(outputStream, getFileFoot());
            outputStream.flush();
            body = outputStream.toByteArray();
            return body == null ? null : body;
        } catch (Exception e) {
            return null;
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (Exception e) {

            }
        }
    }

    /**
     * 將file轉成byte[]資料
     */
    public byte[] fileTranstateToByte(File file){
        byte[] data=null;
        FileInputStream fileInputStream=null;
        ByteArrayOutputStream outputStream = null;
        try {
            fileInputStream=new FileInputStream(file);
            byte[] buffer=new byte[1024];
            int length=0;
            while ((length=fileInputStream.read(buffer))!=-1){
                outputStream.write(buffer,0,length);
            }
            outputStream.flush();
           data= outputStream.toByteArray();
        }catch (Exception e){
              data=null;
            e.printStackTrace();
        }finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
                if(fileInputStream!=null){
                    fileInputStream.close();
                }
            } catch (Exception e) {

            }
        }
        return data;
    }
    public void writeByte(ByteArrayOutputStream outputStream, byte[] bytes) {
        if(bytes!=null){
            outputStream.write(bytes, 0, bytes.length);
        }
    }


    /**
     * 獲取到檔案的head
     *
     * @return
     */
    public byte[] getFileHead(String fileName) {
        try {
            StringBuffer buffer = new StringBuffer();
            buffer.append("--");
            buffer.append(BOUNDARY);
            buffer.append("\r\n");
            buffer.append("Content-Disposition: form-data;name=\"media\";filename=\"");
            buffer.append(fileName);
            buffer.append("\"\r\n");
            buffer.append("Content-Type:application/octet-stream\r\n\r\n");
            String s = buffer.toString();
            return s.getBytes("utf-8");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 獲取檔案的foot
     *
     * @return
     */
    public byte[] getFileFoot() {
        try {
            StringBuffer buffer = new StringBuffer();
            buffer.append("\r\n--");
            buffer.append(BOUNDARY);
            buffer.append("--\r\n");
            String s = buffer.toString();
            return s.getBytes("utf-8");
        } catch (Exception e) {
            return null;
        }
    }

}

最終專案結構如下:

這裡寫圖片描述

總結:
1. Volley框架的設計是相當不錯的,具備1個快取執行緒,4個網路執行緒,可以避免相同的Url的請求併發訪問伺服器
2. Volley框架帶有圖片處理ImageLoader,防止在ImageView在listview,gridview,recycleview中錯亂。
3. Volley也帶有磁碟快取,可以自行配置記憶體快取。
4. OkHttp也是帶有磁碟快取(需配置),非同步或者同步執行,請求重試,攔截器等等。

這篇 Volley+OkHttp+Gson結合使用,較為適合普通專案中需求。例如加上斷點續傳,下載大資料的檔案(volley不具備的優勢),還可以繼續深入的修改結合使用。慢慢長征路,還需繼續走下去。