HTTP是一个属于应用层的面向对象的协议,因为其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,通过几年的使用与发展,获得不断地完善和扩展。php
http://host[":"port][abs_path]
http表示要经过HTTP协议来定位网络资源;
host表示合法的Internet主机域名或者IP地址;
port指定一个端口号,为空则使用默认端口80;
abs_path指定请求资源的URI(Web上任意的可用资源)。 html
HTTP有两种报文分别是请求报文和响应报文,让咱们先来看看请求报文。java
先来看看请求报文的通常格式:android
一般来讲一个HTTP请求报文由请求行、请求报头、空行、**请求数据**4个部分组成。nginx
请求行由请求方法、URL字段、HTTP协议的版本组成,格式以下:git
Method Request-URI HTTP-Version CRLF
Method表示请求方法;
Request-URI是一个统一资源标识符;
HTTP-Version表示请求的HTTP协议版本;
CRLF表示回车和换行(除了做为结尾的CRLF外,不容许出现单独的CR或LF字符)。github
HTTP请求方法有8种,分别是GET、POST、DELETE、PUT、HEAD、TRACE、CONNECT 、OPTIONS。
其中PUT、DELETE、POST、GET分别对应着增删改查,对于移动开发最经常使用的就是POST和GET了。web
例如我去访问个人CSDN博客地址请求行是:算法
GET http://blog.csdn.net/itachi85 HTTP/1.1
在请求行以后会有0个或者多个请求报头,每一个请求报头都包含一个名字和一个值,它们之间用“:”分割。
关于请求报头,会在后面的消息报头一节作统一的解释。sql
请求头部会以一个空行,发送回车符和换行符,通知服务器如下不会有请求头。
请求数据不在GET方法中使用,而是在POST方法中使用。
POST方法适用于须要客户填写表单的场合,与请求数据相关的最经常使用的请求头是Content-Type和Content-Length。
先来看看响应报文的通常格式:
HTTP的响应报文由状态行、消息报头、空行、响应正文组成。响应报头后面会讲到,响应正文是服务器返回的资源的内容,先来看看状态行。
状态行格式以下:
HTTP-Version Status-Code Reason-Phrase CRLF
HTTP-Version表示服务器HTTP协议的版本;
Status-Code表示服务器发回的响应状态代码;
Reason-Phrase表示状态代码的文本描述。
状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:
常见的状态码以下:
例如访问个人CSDN博客地址响应的状态行是:
HTTP/1.1 200 OK
消息报头分为通用报头、请求报头、响应报头、实体报头等。
消息头由键值对组成,每行一对,关键字和值用英文冒号“:”分隔。
既能够出如今请求报头,也能够出如今响应报头中
请求报头通知服务器关于客户端请求的信息,典型的请求头有:
用于服务器传递自身信息的响应,常见的响应报头:
实体报头用来定义被传送资源的信息,既能够用于请求也可用于响应。
请求和响应消息均可以传送一个实体,常见的实体报头为:
要想查看网页或者手机请求网络的请求报文和响应报文有不少种方法,这里推荐采用Fiddler,在Android利用Fiddler进行网络数据抓包这篇文章中详尽介绍了如何使用Fiddler,在这里就不赘述了。
打开Fiddler,而后用浏览器访问个人CSDN博客网站:
点击红色画笔的区域就能够看到请求报文和响应报文了
请求报文:
GET http://blog.csdn.net/itachi85 HTTP/1.1 //请求行 Host: blog.csdn.net //请求报头 Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 QQBrowser/9.3.6872.400 Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8 Cookie: bdshare_firstime=1443768140949; uuid_tt_dd=5028529250430960147_20151002; ...省略
很容易看出访问的是个人博客地址http://blog.csdn.net/itachi85,请求的方法是GET,由于是GET方法因此并无请求数据。
响应报文:
HTTP/1.1 200 OK //状态行 Server: openresty //响应报头 Date: Sun, 27 Mar 2016 08:26:54 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Keep-Alive: timeout=20 Vary: Accept-Encoding Cache-Control: private X-Powered-By: PHP 5.4.28 Content-Encoding: gzip //不能省略的空格 28b5 }ysI 1ߡFsgl n- ]{^_ { 'z! C , m# 0 !l ` 4x ly .ݪ* ڴzAt_Xl * 9'O ɬ ' ק 3 ^1a ...省略
响应报文没什么可说的,接下来咱们配置好手机网络代理,访问一个应用的界面
请求报文:
POST http://patientapi.shoujikanbing.com/api/common/getVersion HTTP/1.1 //请求行 Content-Length: 226 //请求报头 Content-Type: application/x-www-form-urlencoded Host: patientapi.shoujikanbing.com Connection: Keep-Alive User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; MI NOTE LTE Build/KTU84P) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 Accept-Encoding: gzip //不能省略的空格,下面是请求数据 clientversion=2_2.0.0&time=1459069342&appId=android&channel=hjwang&sessionId=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9&token=b191944d680145b5ed97f2f4ccf03058&deviceId=869436020220717&type=2&version=2.0.0
从请求报文的请求行来看,请求的方法是POST,请求地址为http://patientapi.shoujikanbing.com/api/common/getVersion,很显然是获取版本信息的接口。
响应报文:
HTTP/1.1 200 OK //状态行 Server: nginx //响应报头 Date: Sun, 27 Mar 2016 09:02:20 GMT Content-Type: text/html;charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Set-Cookie: sessionId=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9; expires=Mon, 28-Mar-2016 09:02:20 GMT; Max-Age=86400; path=/; domain=.shoujikanbing.com Set-Cookie: PHPSESSID=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9; path=/; domain=.shoujikanbing.com Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Content-Encoding: gzip //不能省略的空格 17f //实体报文编码格式为gzip因此显示在这里的响应数据是乱码 mP N @ "E ? n m 1 w ( HL (1^ P nK E ѷ93'3gNLH 7P $c \ T 4a6 L:+ 1dY%$g h H + ...省略
响应报文的实体采用的编码格式为为gzip,因此在Fiddler软件中显示的是乱码。
一般咱们进行HTTP链接网络的时候咱们会进行TCP的三次握手,而后传输数据,而后再释放链接。
TCP三次握手的过程为:
第一次握手:创建链接。客户端发送链接请求报文段,将SYN位置为1,Sequence Number为x;而后,客户端进入SYN_SEND状态,等待服务器的确认;
第二次握手:服务器收到客户端的SYN报文段,须要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,本身本身还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述全部信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK报文段。而后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕之后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
当客户端和服务器经过三次握手创建了TCP链接之后,当数据传送完毕,断开链接就须要进行TCP四次分手:
第一次分手:主机1(可使客户端,也能够是服务器端),设置Sequence Number和Acknowledgment
Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence
第三次分手:主机2向主机1发送FIN报文段,请求关闭链接,同时主机2进入LAST_ACK状态;
第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,而后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段之后,就关闭链接;此时,主机1等待2MSL后依然没有收到回复,则证实Server端已正常关闭,那好,主机1也能够关闭链接了。
来看下面的图增强下理解:
固然大量的链接每次链接关闭都要三次握手四次分手的很显然会形成性能低下,所以http有一种叫作keepalive connections的机制,它能够在传输数据后仍然保持链接,当客户端须要再次获取数据时,直接使用刚刚空闲下来的链接而不须要再次握手。
Apache的HttpClient和Java的HttpURLConnection,这两种都是咱们日常请求网络会用到的。不管咱们是本身封装的网络请求类仍是第三方的网络请求框架都离不开这两个类库。
Android SDK中包含了HttpClient,在Android6.0版本直接删除了HttpClient类库,若是仍想使用则解决方法是:
android { useLibrary 'org.apache.http.legacy' }
首先咱们来用DefaultHttpClient类来实例化一个HttpClient,并配置好默认的请求参数:
//建立HttpClient private HttpClient createHttpClient() { HttpParams mDefaultHttpParams = new BasicHttpParams(); //设置链接超时 HttpConnectionParams.setConnectionTimeout(mDefaultHttpParams, 15000); //设置请求超时 HttpConnectionParams.setSoTimeout(mDefaultHttpParams, 15000); HttpConnectionParams.setTcpNoDelay(mDefaultHttpParams, true); HttpProtocolParams.setVersion(mDefaultHttpParams, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(mDefaultHttpParams, HTTP.UTF_8); //持续握手 HttpProtocolParams.setUseExpectContinue(mDefaultHttpParams, true); HttpClient mHttpClient = new DefaultHttpClient(mDefaultHttpParams); return mHttpClient; }
接下来建立HttpGet和HttpClient,请求网络并获得HttpResponse,并对HttpResponse进行处理:
private void useHttpClientGet(String url) { HttpGet mHttpGet = new HttpGet(url);//建立HttpGet mHttpGet.addHeader("Connection", "Keep-Alive"); try { HttpClient mHttpClient = createHttpClient();//建立HttpClient HttpResponse mHttpResponse = mHttpClient.execute(mHttpGet);//请求网络并获得HttpResponse //处理HttpResponse HttpEntity mHttpEntity = mHttpResponse.getEntity(); int code = mHttpResponse.getStatusLine().getStatusCode(); if (null != mHttpEntity) { InputStream mInputStream = mHttpEntity.getContent(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "请求状态码:" + code + "\n请求结果:\n" + respose); mInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
converStreamToString方法将请求结果转换成String类型:
private String converStreamToString(InputStream is) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuffer sb = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } String respose = sb.toString(); return respose; }
最后咱们开启线程访问百度:
new Thread(new Runnable() { @Override public void run() { useHttpClientGet("http://www.baidu.com"); } }).start();
请求的返回结果,请求状态码为200,结果就是个html页,这里只截取了部分html代码:
GET请求的参数暴露在URL中,这有些不大稳当,并且URL的长度也有限制:长度在2048字符以内,在HTTP 1.1后URL长度才没有限制。通常状况下POST能够替代GET,接下来咱们来看看HttpClient的POST请求。
post请求和get相似就是须要配置要传递的参数:
private void useHttpClientPost(String url) { HttpPost mHttpPost = new HttpPost(url); mHttpPost.addHeader("Connection", "Keep-Alive"); try { HttpClient mHttpClient = createHttpClient(); List<NameValuePair> postParams = new ArrayList<>(); //要传递的参数 postParams.add(new BasicNameValuePair("username", "moon")); postParams.add(new BasicNameValuePair("password", "123")); mHttpPost.setEntity(new UrlEncodedFormEntity(postParams)); HttpResponse mHttpResponse = mHttpClient.execute(mHttpPost); HttpEntity mHttpEntity = mHttpResponse.getEntity(); int code = mHttpResponse.getStatusLine().getStatusCode(); if (null != mHttpEntity) { InputStream mInputStream = mHttpEntity.getContent(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "请求状态码:" + code + "\n请求结果:\n" + respose); mInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
Android 2.2版本以前,HttpURLConnection一直存在着一些使人厌烦的bug。好比说对一个可读的InputStream调用close()方法时,就有可能会致使链接池失效了。那么咱们一般的解决办法就是直接禁用掉链接池的功能:
private void disableConnectionReuseIfNecessary() { // 这是一个2.2版本以前的bug if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) { System.setProperty("http.keepAlive", "false"); } }
因此在Android 2.2版本以及以前的版本使用HttpClient是较好的选择,而在Android 2.3版本及之后,HttpURLConnection则是最佳的选择,它的API简单,体积较小,于是很是适用于Android项目。
压缩和缓存机制能够有效地减小网络访问的流量,在提高速度和省电方面也起到了较大的做用。
另外在Android 6.0版本中,HttpClient库被移除了,HttpURLConnection则是之后咱们惟一的选择。
由于会了HttpURLConnection的POST请求那GET请求也就会了,因此我这里只举出POST的例子
首先咱们建立一个UrlConnManager类,而后里面提供getHttpURLConnection()方法用于配置默认的参数并返回HttpURLConnection:
public static HttpURLConnection getHttpURLConnection(String url){ HttpURLConnection mHttpURLConnection=null; try { URL mUrl=new URL(url); mHttpURLConnection=(HttpURLConnection)mUrl.openConnection(); //设置连接超时时间 mHttpURLConnection.setConnectTimeout(15000); //设置读取超时时间 mHttpURLConnection.setReadTimeout(15000); //设置请求参数 mHttpURLConnection.setRequestMethod("POST"); //添加Header mHttpURLConnection.setRequestProperty("Connection","Keep-Alive"); //接收输入流 mHttpURLConnection.setDoInput(true); //传递参数时须要开启 mHttpURLConnection.setDoOutput(true); } catch (IOException e) { e.printStackTrace(); } return mHttpURLConnection ; }
由于咱们要发送POST请求,因此在UrlConnManager类中再写一个postParams()方法用来组织一下请求参数并将请求参数写入到输出流中:
public static void postParams(OutputStream output,List<NameValuePair>paramsList) throws IOException{ StringBuilder mStringBuilder=new StringBuilder(); for (NameValuePair pair:paramsList){ if(!TextUtils.isEmpty(mStringBuilder)){ mStringBuilder.append("&"); } mStringBuilder.append(URLEncoder.encode(pair.getName(),"UTF-8")); mStringBuilder.append("="); mStringBuilder.append(URLEncoder.encode(pair.getValue(),"UTF-8")); } BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(output,"UTF-8")); writer.write(mStringBuilder.toString()); writer.flush(); writer.close(); }
接下来咱们添加请求参数,调用postParams()方法将请求的参数组织好传给HttpURLConnection的输出流,请求链接并处理返回的结果:
private void useHttpUrlConnectionPost(String url) { InputStream mInputStream = null; HttpURLConnection mHttpURLConnection = UrlConnManager.getHttpURLConnection(url); try { List<NameValuePair> postParams = new ArrayList<>(); //要传递的参数 postParams.add(new BasicNameValuePair("username", "moon")); postParams.add(new BasicNameValuePair("password", "123")); //HttpURLConnection.getOutputStream输出流 请求 UrlConnManager.postParams(mHttpURLConnection.getOutputStream(), postParams); //HttpURLConnection.connect 链接 mHttpURLConnection.connect(); ////HttpURLConnection.getInputStream输入流 响应 mInputStream = mHttpURLConnection.getInputStream(); int code = mHttpURLConnection.getResponseCode(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "请求状态码:" + code + "\n请求结果:\n" + respose); mInputStream.close(); } catch (IOException e) { e.printStackTrace(); } }
最后开启线程请求网络:
private void useHttpUrlConnectionGetThread() { new Thread(new Runnable() { @Override public void run() { useHttpUrlConnectionPost("http://www.baidu.com"); } }).start(); }
这里咱们仍旧请求百度,看看会发生什么?
mInputStream = mHttpURLConnection.getInputStream() 这句代码报错了,找不到文件。打开Fiddler来分析一下,不了解Fiddler和HTTP协议原理的请查看Android网络编程(一)HTTP协议原理这篇文章。
咱们的请求报文:
看来请求报文没有问题,再来看看响应报文:
报504错误,读取响应的数据报错,对于咱们此次请求服务端不能返回完整的响应,返回的数据为0 bytes,因此mHttpURLConnection.getInputStream() 也读不到服务端响应的输入流。固然此次错误是正常的,百度没理由处理咱们的此次POST请求。
在2013年Google I/O大会上推出了一个新的网络通讯框架Volley。
Volley既能够访问网络取得数据,也能够加载图片,而且在性能方面也进行了大幅度的调整。
它的设计目标就是很是适合去进行数据量不大,但通讯频繁的网络操做,
而对于大数据量的网络操做,好比说下载文件等,Volley的表现就会很是糟糕。
在使用Volley前请下载Volley库并放在libs目录下并add到工程中。 下载Volley请点击这
Volley请求网络都是基于请求队列的,开发者只要把请求放在请求队列中就能够了,请求队列会依次进行请求,通常状况下,一个应用程序若是网络请求没有特别频繁则彻底能够只有一个请求队列(对应Application),若是很是多或其余状况,则能够是一个Activity对应一个网络请求队列,这就要看具体状况了,首先建立队列Volley.newRequestQueue(context)
:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
StringRequest返回的数据是String类型的,咱们查看下StringRequest的源码:
public class StringRequest extends Request<String> { private final Listener<String> mListener; public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) { super(method, url, errorListener); this.mListener = listener; } public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { this(0, url, listener, errorListener); } ... }
有两个构造函数,其中第一个比第二个多了一个请求的方法,若是采用第二个则默认是GET请求。好了,咱们试着用GET方法来请求百度:
//建立请求队列 RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); //StringRequest StringRequest mStringRequest = new StringRequest(Request.Method.GET, "http://www.baidu.com", new Response.Listener<String>() { @Override public void onResponse(String response) { Log.i("wangshu", response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } }); //将请求添加在请求队列中 mQueue.add(mStringRequest);
固然别忘了添加网络访问权限:
<uses-permission android:name="android.permission.INTERNET"/>
请求结果不用说是百度界面的html文件:
和StringRequest相似,咱们直接上代码:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//JsonRequest JsonObjectRequest mJsonObjectRequest = new JsonObjectRequest(Request.Method.POST,"http://api.1-blog.com/biz/bizserver/article/list.do", new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Log.d("wangshu", response.toString()); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } } ); mQueue.add(mJsonObjectRequest);
为了解析这些Json数据,咱们用Gson来解析Json数据。
点击这里下载Gson将jar包放在libs目录下并add进工程中。
咱们开始写article类用于存储数据:
public class Article { private String desc; private String status; private List<detail> detail = new ArrayList<detail>(); public List<Article.detail> getDetail() { return detail; } public void setDetail(List<Article.detail> detail) { this.detail = detail; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public class detail { private String title; private String article_url; private String my_abstract; private String article_type; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getArticle_url() { return article_url; } public void setArticle_url(String article_url) { this.article_url = article_url; } public String getMy_abstract() { return my_abstract; } public void setMy_abstract(String my_abstract) { this.my_abstract = my_abstract; } public String getArticle_type() { return article_type; } public void setArticle_type(String article_type) { this.article_type = article_type; } } }
最后咱们改写JsonRequest的请求回调:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
JsonObjectRequest mJsonObjectRequest = new JsonObjectRequest(Request.Method.POST,"http://api.1-blog.com/biz/bizserver/article/list.do", new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { //new Gson().fromJson 把json转成bean Article mArticle=new Gson().fromJson(response.toString(), Article.class); List<Article.detail>mList=mArticle.getDetail(); String title=mList.get(0).getTitle(); Log.d("wangshu", title); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } } ); mQueue.add(mJsonObjectRequest);
来看看打印结果:
ImageRequest已是过期的方法了,和前面两种的用法相似:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//ImageRequest ImageRequest imageRequest = new ImageRequest( "http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { iv_image.setImageBitmap(response); } }, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { iv_image.setImageResource(R.drawable.ico_default); } }); mQueue.add(imageRequest);
查看ImageRequest的源码发现它能够设置你想要的图片的最大宽度和高度,在加载图片时若是图片超过时望的最大宽度和高度则会进行压缩:
public ImageRequest(String url, Listener<Bitmap> listener, int maxWidth, int maxHeight, ScaleType scaleType, Config decodeConfig, ErrorListener errorListener) { super(0, url, errorListener); this.setRetryPolicy(new DefaultRetryPolicy(1000, 2, 2.0F)); this.mListener = listener; this.mDecodeConfig = decodeConfig; this.mMaxWidth = maxWidth; this.mMaxHeight = maxHeight; this.mScaleType = scaleType; } //压缩图片 private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; Options decodeOptions = new Options(); Bitmap bitmap = null; if(this.mMaxWidth == 0 && this.mMaxHeight == 0) { decodeOptions.inPreferredConfig = this.mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; int desiredWidth = getResizedDimension(this.mMaxWidth, this.mMaxHeight, actualWidth, actualHeight, this.mScaleType); int desiredHeight = getResizedDimension(this.mMaxHeight, this.mMaxWidth, actualHeight, actualWidth, this.mScaleType); decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); if(tempBitmap == null || tempBitmap.getWidth() <= desiredWidth && tempBitmap.getHeight() <= desiredHeight) { bitmap = tempBitmap; } else { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } } return bitmap == null?Response.error(new ParseError(response)):Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ScaleType scaleType) { if(maxPrimary == 0 && maxSecondary == 0) { return actualPrimary; } else if(scaleType == ScaleType.FIT_XY) { return maxPrimary == 0?actualPrimary:maxPrimary; } else { double ratio; if(maxPrimary == 0) { ratio = (double)maxSecondary / (double)actualSecondary; return (int)((double)actualPrimary * ratio); } else if(maxSecondary == 0) { return maxPrimary; } else { ratio = (double)actualSecondary / (double)actualPrimary; int resized = maxPrimary; if(scaleType == ScaleType.CENTER_CROP) { if((double)maxPrimary * ratio < (double)maxSecondary) { resized = (int)((double)maxSecondary / ratio); } return resized; } else { if((double)maxPrimary * ratio > (double)maxSecondary) { resized = (int)((double)maxSecondary / ratio); } return resized; } } } } static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { double wr = (double)actualWidth / (double)desiredWidth; double hr = (double)actualHeight / (double)desiredHeight; double ratio = Math.min(wr, hr); float n; for(n = 1.0F; (double)(n * 2.0F) <= ratio; n *= 2.0F) { ; } return (int)n; }
ImageLoader的内部使用ImageRequest来实现,它的构造器能够传入一个ImageCache缓存形参,实现了图片缓存的功能,同时还能够过滤重复连接,避免重复发送请求。
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//ImageLoader ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); ImageLoader.ImageListener listener = ImageLoader.getImageListener(iv_image,R.drawable.ico_default, R.drawable.ico_default); imageLoader.get("http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", listener);
与ImageRequest实现效果不一样的是,ImageLoader加载图片会先显示默认的图片,等待图片加载完成才会显示在ImageView上。
固然ImageLoader也提供了设置最大宽度和高度的方法:
public ImageLoader.ImageContainer get(String requestUrl, ImageLoader.ImageListener imageListener, int maxWidth, int maxHeight) { return this.get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); }
NetworkImageView是一个自定义控件,继承自ImageView,封装了请求网络加载图片的功能。
先在布局中引用:
<com.android.volley.toolbox.NetworkImageView android:id="@+id/nv_image" android:layout_width="200dp" android:layout_height="200dp" android:layout_centerHorizontal="true" android:layout_below="@id/iv_image" android:layout_marginTop="20dp" ></com.android.volley.toolbox.NetworkImageView>
代码中调用,和ImageLoader用法相似:
iv_image = (ImageView) this.findViewById(R.id.iv_image); RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); nv_image.setDefaultImageResId(R.drawable.ico_default); nv_image.setErrorImageResId(R.drawable.ico_default); nv_image.setImageUrl("http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", imageLoader);
NetworkImageView并无提供设置最大宽度和高度的方法,根据咱们设置控件的宽和高结合网络图片的宽和高内部会自动去实现压缩,若是咱们不想要压缩能够设置NetworkImageView控件的宽和高都为wrap_content。
从上图能够看到Volley分为三个线程,分别是主线程、缓存调度线程、和网络调度线程。
首先请求会加入缓存队列,若是发现能够找到相应的缓存结果就直接读取缓存并解析,而后回调给主线程;
若是在缓存中没有找到结果,则将这条请求加入到网络队列中,而后发送HTTP请求,解析响应并写入缓存,并回调给主线程。
咱们都知道使用Volley以前首先要建立RequestQueue:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
这也是volley运做的入口,看看newRequestQueue:
public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, (HttpStack)null); } public static RequestQueue newRequestQueue(Context context, HttpStack stack) { return newRequestQueue(context, stack, -1); }
连续调用了两个重载函数,最终调用的是:
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) { File cacheDir = new File(context.getCacheDir(), "volley"); String userAgent = "volley/0"; try { String network = context.getPackageName(); PackageInfo queue = context.getPackageManager().getPackageInfo(network, 0); userAgent = network + "/" + queue.versionCode; } catch (NameNotFoundException var7) { ; } if(stack == null) { if(VERSION.SDK_INT >= 9) { stack = new HurlStack(); } else { stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); } } BasicNetwork network1 = new BasicNetwork((HttpStack)stack); RequestQueue queue1; if(maxDiskCacheBytes <= -1) { queue1 = new RequestQueue(new DiskBasedCache(cacheDir), network1); } else { queue1 = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network1); } queue1.start(); return queue1; }
能够看到若是android版本大于等于2.3则调用基于HttpURLConnection的HurlStack,不然就调用基于HttpClient的HttpClientStack。
并建立了RequestQueue,调用了start()方法:
public void start() { this.stop(); this.mCacheDispatcher = new CacheDispatcher(this.mCacheQueue, this.mNetworkQueue, this.mCache, this.mDelivery); this.mCacheDispatcher.start(); for(int i = 0; i < this.mDispatchers.length; ++i) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(this.mNetworkQueue, this.mNetwork, this.mCache, this.mDelivery); this.mDispatchers[i] = networkDispatcher; networkDispatcher.start(); } }
CacheDispatcher是缓存调度线程,并调用了start()方法,
在循环中调用了NetworkDispatcher的start()方法,NetworkDispatcher是网络调度线程,默认状况下mDispatchers.length为4,默认开启了4个网络调度线程,也就是说有5个线程在后台运行并等待请求的到来。
接下来咱们建立各类的Request,并调用RequestQueue的add()方法:
public Request add(Request request) { request.setRequestQueue(this); Set var2 = this.mCurrentRequests; synchronized(this.mCurrentRequests) { this.mCurrentRequests.add(request); } request.setSequence(this.getSequenceNumber()); request.addMarker("add-to-queue"); //若是不能缓存,则将请求添加到网络请求队列中 if(!request.shouldCache()) { this.mNetworkQueue.add(request); return request; } else { Map var8 = this.mWaitingRequests; synchronized(this.mWaitingRequests) { String cacheKey = request.getCacheKey(); //以前是否有执行相同的请求且尚未返回结果的,若是有的话将此请求加入mWaitingRequests队列,再也不重复请求 if(this.mWaitingRequests.containsKey(cacheKey)) { Object stagedRequests = (Queue)this.mWaitingRequests.get(cacheKey); if(stagedRequests == null) { stagedRequests = new LinkedList(); } ((Queue)stagedRequests).add(request); this.mWaitingRequests.put(cacheKey, stagedRequests); if(VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", new Object[]{cacheKey}); } } else { //没有的话就将请求加入缓存队列mCacheQueue,同时加入mWaitingRequests中用来作下次一样请求来时的重复判断依据 this.mWaitingRequests.put(cacheKey, (Object)null); this.mCacheQueue.add(request); } return request; } } }
经过判断request.shouldCache(),来判断是否能够缓存,默认是能够缓存的,
若是不能缓存,则将请求添加到网络请求队列中,
若是能缓存,就判断以前是否有执行相同的请求且尚未返回结果的,
若是有的话将此请求加入mWaitingRequests队列,再也不重复请求;
没有的话就将请求加入缓存队列mCacheQueue,同时加入mWaitingRequests中用来作下次一样请求来时的重复判断依据。
从上面能够看出RequestQueue的add()方法并无作什么请求网络或者对缓存进行操做。
当将请求添加到网络请求队列或者缓存队列时,这时在后台的网络调度线程和缓存调度线程轮询各自的请求队列发现有请求任务则开始执行,咱们先看看缓存调度线程。
CacheDispatcher的run()方法:
public void run() { if(DEBUG) { VolleyLog.v("start new dispatcher", new Object[0]); } //线程优先级设置为最高级别 Process.setThreadPriority(10); this.mCache.initialize(); while(true) { while(true) { while(true) { while(true) { try { //获取缓存队列中的一个请求 final Request e = (Request)this.mCacheQueue.take(); e.addMarker("cache-queue-take"); //若是请求取消了则将请求中止掉 if(e.isCanceled()) { e.finish("cache-discard-canceled"); } else { //查看是否有缓存的响应 Entry entry = this.mCache.get(e.getCacheKey()); //若是缓存响应为空,则将请求加入网络请求队列 if(entry == null) { e.addMarker("cache-miss"); this.mNetworkQueue.put(e); //判断缓存响应是否过时 } else if(!entry.isExpired()) { e.addMarker("cache-hit"); //对数据进行解析并回调给主线程 Response response = e.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders)); e.addMarker("cache-hit-parsed"); if(!entry.refreshNeeded()) { this.mDelivery.postResponse(e, response); } else { e.addMarker("cache-hit-refresh-needed"); e.setCacheEntry(entry); response.intermediate = true; this.mDelivery.postResponse(e, response, new Runnable() { public void run() { try { CacheDispatcher.this.mNetworkQueue.put(e); } catch (InterruptedException var2) { ; } } }); } } else { e.addMarker("cache-hit-expired"); e.setCacheEntry(entry); this.mNetworkQueue.put(e); } } } catch (InterruptedException var4) { if(this.mQuit) { return; } } } } } } } static { DEBUG = VolleyLog.DEBUG; }
看到四个while循环有些晕吧,让咱们挑重点的说:
首先从缓存队列取出请求,判断请求是否被取消了,
若是没有,则判断该请求是否有缓存的响应,
若是有而且没有过时则对缓存响应进行解析并回调给主线程,
不然,将请求加入网络请求队列。
接下来看看网络调度线程。
NetworkDispatcher的run()方法:
public void run() { Process.setThreadPriority(10); while(true) { long startTimeMs; Request request; while(true) { startTimeMs = SystemClock.elapsedRealtime(); try { //从队列中取出请求 request = (Request)this.mQueue.take(); break; } catch (InterruptedException var6) { if(this.mQuit) { return; } } } try { request.addMarker("network-queue-take"); if(request.isCanceled()) { request.finish("network-discard-cancelled"); } else { this.addTrafficStatsTag(request); //请求网络 NetworkResponse e = this.mNetwork.performRequest(request); request.addMarker("network-http-complete"); if(e.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); } else { Response volleyError1 = request.parseNetworkResponse(e); request.addMarker("network-parse-complete"); if(request.shouldCache() && volleyError1.cacheEntry != null) { //将响应结果存入缓存 this.mCache.put(request.getCacheKey(), volleyError1.cacheEntry); request.addMarker("network-cache-written"); } request.markDelivered(); this.mDelivery.postResponse(request, volleyError1); } } } catch (VolleyError var7) { var7.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); this.parseAndDeliverNetworkError(request, var7); } catch (Exception var8) { VolleyLog.e(var8, "Unhandled exception %s", new Object[]{var8.toString()}); VolleyError volleyError = new VolleyError(var8); volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); this.mDelivery.postError(request, volleyError); } } }
网络调度线程也是从队列中取出请求而且判断是否被取消了,
若是没取消,就去请求网络获得响应并回调给主线程。
请求网络时调用this.mNetwork.performRequest(request)
,这个mNetwork是一个接口,实现它的类是BasicNetwork,咱们来看看BasicNetwork的performRequest()方法:
public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while(true) { HttpResponse httpResponse = null; Object responseContents = null; Map responseHeaders = Collections.emptyMap(); try { HashMap e = new HashMap(); this.addCacheHeaders(e, request.getCacheEntry()); //调用HttpStack的performRequest()方法请求网络 httpResponse = this.mHttpStack.performRequest(request, e); StatusLine statusCode1 = httpResponse.getStatusLine(); int networkResponse1 = statusCode1.getStatusCode(); responseHeaders = convertHeaders(httpResponse.getAllHeaders()); //根据不一样的响应状态码来返回不一样的NetworkResponse if(networkResponse1 == 304) { Entry requestLifetime2 = request.getCacheEntry(); if(requestLifetime2 == null) { return new NetworkResponse(304, (byte[])null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } requestLifetime2.responseHeaders.putAll(responseHeaders); return new NetworkResponse(304, requestLifetime2.data, requestLifetime2.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } ...
从上面能够看到在12行调用的是HttpStack的performRequest()方法请求网络,接下来根据不一样的响应状态码来返回不一样的NetworkResponse。
另外HttpStack也是一个接口,实现它的两个类咱们在前面已经提到了就是HurlStack和HttpClientStack。
让咱们再回到NetworkDispatcher,请求网络后,会将响应结果存在缓存中,若是响应结果成功则调用this.mDelivery.postResponse(request, volleyError1)来回调给主线程。
来看看Delivery的postResponse()方法:
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) { request.markDelivered(); request.addMarker("post-response"); this.mResponsePoster.execute(new ExecutorDelivery.ResponseDeliveryRunnable(request, response, runnable)); }
来看看ResponseDeliveryRunnable里面作了什么:
private class ResponseDeliveryRunnable implements Runnable { private final Request mRequest; private final Response mResponse; private final Runnable mRunnable; public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { this.mRequest = request; this.mResponse = response; this.mRunnable = runnable; } public void run() { if(this.mRequest.isCanceled()) { this.mRequest.finish("canceled-at-delivery"); } else { if(this.mResponse.isSuccess()) { //deliverResponse this.mRequest.deliverResponse(this.mResponse.result); } else { this.mRequest.deliverError(this.mResponse.error); } if(this.mResponse.intermediate) { this.mRequest.addMarker("intermediate-response"); } else { this.mRequest.finish("done"); } if(this.mRunnable != null) { this.mRunnable.run(); } } } }
第17行调用了this.mRequest.deliverResponse(this.mResponse.result)。deliverResponse
就是实现Request抽象类必需要实现的方法,咱们来看看StringRequest的源码:
public class StringRequest extends Request { private final Listener mListener; public StringRequest(int method, String url, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); this.mListener = listener; } public StringRequest(String url, Listener listener, ErrorListener errorListener) { this(0, url, listener, errorListener); } protected void deliverResponse(String response) { //最终将response回调给了Response.Listener的onResponse()方法 this.mListener.onResponse(response); } ... }
在deliverResponse方法中调用了this.mListener.onResponse(response),最终将response回调给了Response.Listener的onResponse()方法。
咱们用StringRequest请求网络的写法是这样的:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
StringRequest mStringRequest = new StringRequest(Request.Method.GET, "http://www.baidu.com", new Response.Listener() { @Override public void onResponse(String response) { Log.i("wangshu", response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } }); //将请求添加在请求队列中 mQueue.add(mStringRequest);
咱们接下来看看目前比较火的网络框架OkHttp, 它处理了不少网络疑难杂症:会从不少经常使用的链接问题中自动恢复。
若是您的服务器配置了多个IP地址,当第一个IP链接失败的时候,OkHttp会自动尝试下一个IP,此外OkHttp还处理了代理服务器问题和SSL握手失败问题。
eclipse引入jar包地址:
okhttp-2.7.5.jar
okio-1.7.0.jar
Android Studio 配置gradle:
compile 'com.squareup.okhttp:okhttp:2.7.5' compile 'com.squareup.okio:okio:1.7.0'
基本的步骤很简单:
- ① 建立OkHttpClient
OkHttpClient mOkHttpClient = new OkHttpClient();
final Request request = new Request.Builder() .url("http://www.baidu.com") .build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { } });
可是每次这么写确定是很麻烦,确定是要进行封装的。须要注意的是onResponse回调并非在UI线程。
最简单的get请求,老规矩请求百度:
private void getAsynHttp() { //① 建立okHttpClient对象 OkHttpClient mOkHttpClient = new OkHttpClient(); //② 建立Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); //③ 建立Call Call call = mOkHttpClient.newCall(request); //④ 调用Call的enqueue 进行异步请求 call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { //onResponse回调并非在UI String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplication(), "请求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
运行程序log打印出来的是百度首页的html文件。
同步Get请求和异步调用区别就是调用了call的execute()方法。
private String getSyncHttp() throws IOException{ //① 建立okHttpClient对象 OkHttpClient mOkHttpClient = new OkHttpClient(); //② 建立Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); //③ 建立Call Call call = mOkHttpClient.newCall(request); //④ 调用Call的execute 进行同步请求 Response mResponse=call.execute(); if (mResponse.isSuccessful()) { return mResponse.body().string(); } else { throw new IOException("Unexpected code " + mResponse); } }
post与get不一样的就是要建立RequestBody = new FormEncodingBuilder
().add(“size”, “10”).build(); 并传进Request中,一样onResponse回调不是在UI线程。
private void postAsynHttp() { OkHttpClient mOkHttpClient = new OkHttpClient(); //建立RequestBody RequestBody formBody = new FormEncodingBuilder() .add("size", "10") .build(); Request request = new Request.Builder() .url("http://api.1-blog.com/biz/bizserver/article/list.do") .post(formBody)//RequestBody传进Request中 .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(Response response) throws IOException { String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
首先咱们设置缓存路径和大小并设置给OkHttpClient: mOkHttpClient.setCache
mOkHttpClient = new OkHttpClient(); File sdcache = getExternalCacheDir(); int cacheSize = 10 * 1024 * 1024; mOkHttpClient.setCache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
接下来异步GET请求baidu:
private void getAsynHttp() { //建立请求Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { response.body().string(); String str=response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
第一次请求会请求网络获得数据,第二次以及后面的请求则会从缓存中取出数据:
固然也有种状况是有的请求每次都须要最新的数据,则在建立Request,来设置cacheControl
为CacheControl.FORCE_NETWORK
,用来表示请求会一直请求网络获得数据:
final Request request = new Request.Builder() .url("http://www.baidu.com") .cacheControl(CacheControl.FORCE_NETWORK) .build();
运行程序结果为:
另外咱们也须要设置超时的时间用来处理各类网络超时的状况,超时的缘由多是网络问题也多是服务器响应慢等问题,OkHttp固然不会忽略这一点,它支持链接、读取和写入超时的时间设置:mOkHttpClient.setConnectTimeout
/.setWriteTimeout
/.setReadTimeout
mOkHttpClient = new OkHttpClient(); mOkHttpClient.setConnectTimeout(15, TimeUnit.SECONDS); mOkHttpClient.setWriteTimeout(20, TimeUnit.SECONDS); mOkHttpClient.setReadTimeout(20, TimeUnit.SECONDS);
使用call.cancel()
能够当即中止掉一个正在执行的call。
若是一个线程正在写请求或者读响应,将会引起IOException。
当用户离开一个应用时或者跳到其余界面时,使用Call.cancel()能够节约网络资源,另外无论同步仍是异步的call均可以取消。
也能够经过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)
来分配一个标签。以后你就能够用OkHttpClient.cancel(tag)
来取消全部带有这个tag的call。
为了模拟这个场景咱们首先建立一个定时的线程池:
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
接下来的代码为:
private void cancel(){ final Request request = new Request.Builder() .url("http://www.baidu.com") .cacheControl(CacheControl.FORCE_NETWORK) .build(); Call call=null; call = mOkHttpClient.newCall(request); final Call finalCall = call; //100毫秒后取消call executor.schedule(new Runnable() { @Override public void run() { finalCall.cancel(); } }, 100, TimeUnit.MILLISECONDS); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { try { response.body().string(); } catch (IOException e) { Log.i("wangshu", "IOException"); e.printStackTrace(); } String str = response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } } }); Log.i("wangshu", "是否取消成功"+call.isCanceled()); }
100毫秒后调用call.cancel(),为了能让请求耗时,咱们设置每次请求都要请求网络,运行程序而且不断的快速点击发送请求按钮:
很明显每次cancel()都失败了,仍旧成功的访问了网络,在cancel()时已经有读写操做了因此会报IOException。每隔100毫秒来调用call.cancel()显然时间间隔太长,咱们设置为1毫秒并不断的快速的点击发送请求按钮:
没有请求网络的log,几乎每次都取消成功了。
若是每次请求网络都须要写重复的代码绝对是使人头疼的,网上也有不少对OkHttp封装的优秀开源项目,功能也很是强大,封装的意义就在于更加方便的使用,具备拓展性,可是对OkHttp封装最须要解决的是如下的两点:
根据以上两点,咱们也简单封装一下,在此只是举个例子,若是想要使用OkHttp封装的开源库,推荐使用OkHttpFinal。
首先呢咱们写一个抽象类用于请求回调:
public abstract class ResultCallback<T> { public abstract void onError(Request request, Exception e); public abstract void onResponse(Response response); }
接下来封装OkHttp,并实现了异步GET请求:
public class OkHttpEngine { private static OkHttpEngine mInstance; private OkHttpClient mOkHttpClient; private Handler mHandler; public static OkHttpEngine getInstance() { if (mInstance == null) { synchronized (OkHttpEngine.class) { if (mInstance == null) { mInstance = new OkHttpEngine(); } } } return mInstance; } private OkHttpEngine() { mOkHttpClient = new OkHttpClient(); mOkHttpClient.setConnectTimeout(15, TimeUnit.SECONDS); mOkHttpClient.setWriteTimeout(20, TimeUnit.SECONDS); mOkHttpClient.setReadTimeout(20, TimeUnit.SECONDS); mHandler = new Handler(); } public OkHttpEngine setCache(Context mContext) { File sdcache = mContext.getExternalCacheDir(); int cacheSize = 10 * 1024 * 1024; mOkHttpClient.setCache(new Cache(sdcache.getAbsoluteFile(), cacheSize)); return mInstance; } /* 异步get请求 * @param url * @param callback */ public void getAsynHttp(String url, ResultCallback callback) { final Request request = new Request.Builder() .url(url) .build(); Call call = mOkHttpClient.newCall(request); dealResult(call, callback); } private void dealResult(Call call, final ResultCallback callback) { call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { sendFailedCallback(request, e, callback); } @Override public void onResponse(final Response response) throws IOException { sendSuccessCallback(response, callback); } private void sendSuccessCallback(final Response object, final ResultCallback callback) { mHandler.post(new Runnable() { @Override public void run() { if (callback != null) { callback.onResponse(object); } } }); } private void sendFailedCallback(final Request request, final Exception e, final ResultCallback callback) { mHandler.post(new Runnable() { @Override public void run() { if (callback != null) callback.onError(request, e); } }); } }); } }
原理很简单就是,写一个双重检查模式的单例,不了解双重检查模式的请查看设计模式之单例模式的七种写法这篇文章。在开始建立的时候配置好OkHttpClient,在请求网络的时候用Handler将请求的结果回调给UI线程。
最后调用这个OkHttpEngine的getAsynHttp()方法:
OkHttpEngine.getInstance().getAsynHttp("http://www.baidu.com", new ResultCallback() { @Override public void onError(Request request, Exception e) { } @Override public void onResponse(Response response) { String str = response.networkResponse().toString(); Log.i("wangshu", str); Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show(); } });
使用起来简单多了,并且请求结果回调是在UI线程的。
Android Studio 配置gradle:
compile 'com.squareup.okhttp3:okhttp:3.2.0' compile 'com.squareup.okio:okio:1.7.0'
添加网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
与2.x版本并无什么不一样,比较郁闷的是回调仍然不在UI线程。
惯例,请求百度:
private void getAsynHttp() { mOkHttpClient=new OkHttpClient(); Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com"); requestBuilder.method("GET",null);//能够省略,默认是GET请求 Request request = requestBuilder.build(); Call mcall= mOkHttpClient.newCall(request); mcall.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { response.body().string(); String str = response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
OkHttp3异步POST请求和OkHttp2.x有一些差异就是没有FormEncodingBuilder这个类,替代它的是功能更增强大的FormBody:
private void postAsynHttp() { mOkHttpClient=new OkHttpClient(); RequestBody formBody = new FormBody.Builder() .add("size", "10") .build(); Request request = new Request.Builder() .url("http://api.1-blog.com/biz/bizserver/article/list.do") .post(formBody) .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
上传文件自己也是一个POST请求,上一篇没有讲,这里咱们补上。首先定义上传文件类型:
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
将sdcard根目录的wangshu.txt文件上传到服务器上:
private void postAsynFile() { mOkHttpClient=new OkHttpClient(); File file = new File("/sdcard/wangshu.txt"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))//上传文件RequestBody.create .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.i("wangshu",response.body().string()); } }); }
固然若是想要改成同步的上传文件只要调用 mOkHttpClient.newCall(request).execute()就能够了。
在wangshu.txt文件中有一行字“Android网络编程(六)OkHttp3用法全解析”咱们运行程序点击发送文件按钮,最终请求网络返回的结果就是咱们txt文件中的内容 :
固然不要忘了添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下载文件一样在上一篇没有讲到,实现起来比较简单,在这里下载一张图片,咱们获得Response后将流写进咱们指定的图片文件中就能够了。
private void downAsynFile() { mOkHttpClient = new OkHttpClient(); String url = "http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg"; Request request = new Request.Builder().url(url).build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) { //获得Response后将流写进咱们指定的图片文件 InputStream inputStream = response.body().byteStream(); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(new File("/sdcard/wangshu.jpg")); byte[] buffer = new byte[2048]; int len = 0; while ((len = inputStream.read(buffer)) != -1) { fileOutputStream.write(buffer, 0, len); } fileOutputStream.flush(); } catch (IOException e) { Log.i("wangshu", "IOException"); e.printStackTrace(); } Log.d("wangshu", "文件下载成功"); } }); }
关键类MultipartBody
这种场景很经常使用,咱们有时会上传文件同时还须要传其余类型的字段,OkHttp3实现起来很简单,须要注意的是没有服务器接收我这个Multipart文件,因此这里只是举个例子,具体的应用还要结合实际工做中对应的服务器。
首先定义上传文件类型:
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private void sendMultipart(){ mOkHttpClient = new OkHttpClient(); //关键类MultipartBody RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "wangshu") .addFormDataPart("image", "wangshu.jpg", RequestBody.create(MEDIA_TYPE_PNG, new File("/sdcard/wangshu.jpg"))) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + "...") .url("https://api.imgur.com/3/image") .post(requestBody) .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.i("wangshu", response.body().string()); } }); }
和OkHttp2.x有区别的是不能经过OkHttpClient直接设置超时时间和缓存了,而是经过OkHttpClient.Builder
来设置,经过builder配置好OkHttpClient后用builder.build()来返回OkHttpClient,因此咱们一般不会调用new OkHttpClient()来获得OkHttpClient,而是经过builder.build():
File sdcache = getExternalCacheDir();
int cacheSize = 10 * 1024 * 1024; OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize)); OkHttpClient mOkHttpClient=builder.build();
取消请求仍旧能够调用call.cancel(),这个没有变化,不明白的能够查看上一篇文章Android网络编程(五)OkHttp2.x用法全解析,这里就不赘述了,封装上一篇也讲过仍旧推荐OkHttpFinal,它目前是基于OkHttp3来进行封装的。
当咱们要请求网络的时候咱们须要用OkHttpClient.newCall(request)进行execute或者enqueue操做,当咱们调用newCall时:
@Override public Call newCall(Request request) { return new RealCall(this, request); }
实际返回的是一个RealCall类,咱们调用enqueue异步请求网络其实是调用了RealCall的enqueue方法:
void enqueue(Callback responseCallback, boolean forWebSocket) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket)); }
能够看到最终的请求是dispatcher来完成的。
Dispatcher主要用于控制并发的请求,它主要维护了如下变量:
/* 最大并发请求数*/ private int maxRequests = 64; /* 每一个主机最大请求数*/ private int maxRequestsPerHost = 5; /* 消费者线程池 */ private ExecutorService executorService; /* 将要运行的异步请求队列 */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /* 正在运行的异步请求队列 */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /* 正在运行的同步请求队列 */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) { this.executorService = executorService; } public Dispatcher() { } public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }
Dispatcher有两个构造函数,可使用本身设定线程池,若是没有设定线程池则会在请求网络前本身建立线程池,这个线程池相似于CachedThreadPool比较适合执行大量的耗时比较少的任务。不了解线程池的同窗能够查看Android多线程(一)线程池这篇文章。其中用到了SynchronousQueue,不了解它的同窗能够查看Java并发编程(六)阻塞队列这篇文章。
synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } }
当正在运行的异步请求队列中的数量小于64而且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,不然就再入到readyAsyncCalls中进行缓存等待。
线程池中传进来的参数就是AsyncCall它是RealCall的内部类,内部也实现了execute方法:
@Override protected void execute() { boolean signalledCallback = false; try { Response response = getResponseWithInterceptorChain(forWebSocket); if (canceled) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } catch (IOException e) { if (signalledCallback) { // Do not signal the callback twice! logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e); } else { responseCallback.onFailure(RealCall.this, e); } } finally { client.dispatcher().finished(this); } }
首先咱们来看看最后一行, 不管这个请求的结果如何都会执行client.dispatcher().finished(this);
synchronized void finished(AsyncCall call) { if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!"); promoteCalls(); }
finished方法将这次请求从runningAsyncCalls移除后还执行了promoteCalls方法:
private void promoteCalls() { if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity. if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote. for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall call = i.next(); if (runningCallsForHost(call) < maxRequestsPerHost) { i.remove(); runningAsyncCalls.add(call); executorService().execute(call); } if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity. } }
能够看到最关键的点就是会从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。好了让咱们再回到上面的AsyncCall的execute方法,咱们会发getResponseWithInterceptorChain方法返回了Response,很明显这是在请求网络。
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket); return chain.proceed(originalRequest); }
getResponseWithInterceptorChain方法,建立了ApplicationInterceptorChain,它是一个拦截器链,这个类也是RealCall的内部类,接下来执行了它的proceed方法:
@Override
public Response proceed(Request request) throws IOException { // If there's another interceptor in the chain, call that. if (index < client.interceptors().size()) { Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket); //从拦截器列表取出拦截器 Interceptor interceptor = client.interceptors().get(index); Response interceptedResponse = interceptor.intercept(chain); if (interceptedResponse == null) { throw new NullPointerException("application interceptor " + interceptor + " returned null"); } return interceptedResponse; } // No more interceptors. Do HTTP. return getResponse(request, forWebSocket); }
proceed方法每次从拦截器列表中取出拦截器,当存在多个拦截器时都会在第七行阻塞,并等待下一个拦截器的调用返回。下面分别以 拦截器链中有1个、2个拦截器的场景加以模拟:
拦截器主要用来观察,修改以及可能短路的请求输出和响应的回来。一般状况下拦截器用来添加,移除或者转换请求或者响应的头部信息。好比将域名替换为ip地址,将请求头中添加host属性,也能够添加咱们应用中的一些公共参数,好比设备id、版本号等等。 不了解拦截器的能够查看Okhttp-wiki 之 Interceptors 拦截器这篇文章。
回到代码上来,咱们看最后一行 return getResponse(request, forWebSocket),若是没有更多的拦截器的话,就会执行网络请求,来看看getResponse方法作了些什么(RealCall.java):
Response getResponse(Request request, boolean forWebSocket) throws IOException { ...省略 // Create the initial HTTP engine. Retries and redirects need new engine for each attempt. engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null); int followUpCount = 0; while (true) { if (canceled) { engine.releaseStreamAllocation(); throw new IOException("Canceled"); } boolean releaseConnection = true; try { engine.sendRequest(); engine.readResponse(); releaseConnection = false; } catch (RequestException e) { // The attempt to interpret the request failed. Give up. throw e.getCause(); } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. ...省略 } }
getResponse方法比较长我省略了一些代码,能够看到建立了HttpEngine类而且调用HttpEngine的sendRequest方法和readResponse方法。
咱们先来看看sendRequest方法:
public void sendRequest() throws RequestException, RouteException, IOException { if (cacheStrategy != null) return; // Already sent. if (httpStream != null) throw new IllegalStateException(); //请求头部添加 Request request = networkRequest(userRequest); //获取client中的Cache,同时Cache在初始化的时候会去读取缓存目录中关于曾经请求过的全部信息。 InternalCache responseCache = Internal.instance.internalCache(client); //cacheCandidate为上次与服务器交互缓存的Response Response cacheCandidate = responseCache != null ? responseCache.get(request) : null; long now = System.currentTimeMillis(); //建立CacheStrategy.Factory对象,进行缓存配置 cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get(); //网络请求 networkRequest = cacheStrategy.networkRequest; //缓存的响应 cacheResponse = cacheStrategy.cacheResponse; if (responseCache != null) { //记录当前请求是网络发起仍是缓存发起 responseCache.trackResponse(cacheStrategy); } if (cacheCandidate != null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } //不进行网络请求而且缓存不存在或者过时则返回504错误 if (networkRequest == null && cacheResponse == null) { userResponse = new Response.Builder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(EMPTY_BODY) .build(); return; } // 不进行网络请求,并且缓存可使用,直接返回缓存 if (networkRequest == null) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); userResponse = unzip(userResponse); return; } //须要访问网络时 boolean success = false; try { httpStream = connect(); httpStream.setHttpEngine(this); if (writeRequestHeadersEagerly()) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. httpStream.writeRequestHeaders(networkRequest); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request headers until the // entire body is ready; otherwise we can't set the Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { httpStream.writeRequestHeaders(networkRequest); requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength); } } success = true; } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (!success && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } }
上面的代码显然是在发送请求,可是最主要的是作了缓存的策略。cacheCandidate是上次与服务器交互缓存的Response,这里的缓存都是基于Map,key是请求中url的md5,value是在文件中查询到的缓存,页面置换基于LRU算法,咱们如今只须要知道它是一个能够读取缓存Header的Response便可。根据cacheStrategy的处理获得了networkRequest和cacheResponse这两个值,根据这两个值的数据是否为null来进行进一步的处理,当networkRequest和cacheResponse都为null的状况也就是不进行网络请求而且缓存不存在或者过时,这时候则返回504错误;当networkRequest 为null时也就是不进行网络请求,并且缓存可使用时则直接返回缓存;其余的状况则请求网络。
接下来咱们查看readResponse方法:
public void readResponse() throws IOException { ...省略 else{ //读取网络响应 networkResponse = readNetworkResponse(); } //将响应头部存入Cookie中 receiveHeaders(networkResponse.headers()); // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { //检查缓存是否可用,若是可用。那么就用当前缓存的Response,关闭网络链接,释放链接。 if (validate(cacheResponse, networkResponse)) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .headers(combine(cacheResponse.headers(), networkResponse.headers())) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); releaseStreamAllocation(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). InternalCache responseCache = Internal.instance.internalCache(client); responseCache.trackConditionalCacheHit(); // 更新缓存 responseCache.update(cacheResponse, stripBody(userResponse)); userResponse = unzip(userResponse); return; } else { closeQuietly(cacheResponse.body()); } } userResponse = networkResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (hasBody(userResponse)) { maybeCache(); userResponse = unzip(cacheWritingResponse(storeRequest, userResponse)); } }
这个方法发起刷新请求头部和请求体,解析HTTP响应头部。若是有缓存而且可用则用缓存的数据并更新缓存,不然就用网络请求返回的数据。
咱们再来看看validate(cacheResponse, networkResponse)方法是如何判断缓存是否可用的:
private static boolean validate(Response cached, Response network) { //若是服务器返回304则缓存有效 if (network.code() == HTTP_NOT_MODIFIED) { return true; } //经过缓存和网络请求响应中的Last-Modified来计算是不是最新数据,若是是则缓存有效 Date lastModified = cached.headers().getDate("Last-Modified"); if (lastModified != null) { Date networkLastModified = network.headers().getDate("Last-Modified"); if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) { return true; } } return false; }
如缓存果过时或者强制放弃缓存,在此状况下,缓存策略所有交给服务器判断,客户端只用发送条件get请求便可,若是缓存是有效的,则返回304 Not Modifiled,不然直接返回body。条件get请求有两种方式一种是Last-Modified-Date,一种是 ETag。这里采用了Last-Modified-Date,经过缓存和网络请求响应中的Last-Modified来计算是不是最新数据,若是是则缓存有效。
最后咱们再回到RealCall的getResponse方法:
Response getResponse(Request request, boolean forWebSocket) throws IOException { ...省略 boolean releaseConnection = true; try { engine.sendRequest(); engine.readResponse(); releaseConnection = false; } catch (RequestException e) { // The attempt to interpret the request failed. Give up. throw e.getCause(); } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null); if (retryEngine != null) { releaseConnection = false; engine = retryEngine; continue; } // Give up; recovery is not possible. throw e.getLastConnectException(); } catch (IOException e) { // An attempt to communicate with a server failed. The request may have been sent. HttpEngine retryEngine = engine.recover(e, null); if (retryEngine != null) { releaseConnection = false; engine = retryEngine; continue; } // Give up; recovery is not possible. throw e; } finally { // We're throwing an unchecked exception. Release any resources. if (releaseConnection) { StreamAllocation streamAllocation = engine.close(); streamAllocation.release(); } } ...省略 engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null, response); } }
查看代码第11行和21行当发生IOException或者RouteException时会执行HttpEngine的recover方法:
public HttpEngine recover(IOException e, Sink requestBodyOut) { if (!streamAllocation.recover(e, requestBodyOut)) { return null; } if (!client.retryOnConnectionFailure()) { return null; } StreamAllocation streamAllocation = close(); // For failure recovery, use the same route selector with a new connection. return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody, forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse); }
最后一行能够看到就是从新建立了HttpEngine并返回,用来完成重连。
到这里OkHttp请求网络的流程基本上讲完了,下面是关于OKHttp的请求流程图:
参考资料:
http://www.jianshu.com/p/aad5aacd79bf
http://www.jianshu.com/p/64e256c1dbbf
http://www.cnblogs.com/LuLei1990/p/5534791.html
http://frodoking.github.io/2015/03/12/android-okhttp/
固然大量的链接每次链接关闭都要三次握手四次分手的很显然会形成性能低下,所以http有一种叫作keepalive connections的机制,它能够在传输数据后仍然保持链接,当客户端须要再次获取数据时,直接使用刚刚空闲下来的链接而不须要再次握手。
Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。
在okhttp中,在高层代码的调用中,使用了相似于引用计数的方式跟踪Socket流的调用,这里的计数对象是StreamAllocation,它被反复执行aquire与release操做,这两个函数实际上是在改变RealConnection中的List<Reference<StreamAllocation>>
的大小。(StreamAllocation.java)
public void acquire(RealConnection connection) { connection.allocations.add(new WeakReference<>(this)); }
private void release(RealConnection connection) { for (int i = 0, size = connection.allocations.size(); i < size; i++) { Reference<StreamAllocation> reference = connection.allocations.get(i); if (reference.get() == this) { connection.allocations.remove(i); return; } } throw new IllegalStateException(); }
RealConnection是socket物理链接的包装,它里面维护了List<Reference<StreamAllocation>>
的引用。List中StreamAllocation的数量也就是socket被引用的计数,若是计数为0的话,说明此链接没有被使用就是空闲的,须要经过下文的算法实现回收;若是计数不为0,则表示上层代码仍然引用,就不须要关闭链接。
链接池的类位于okhttp3.ConnectionPool:
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */, Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true)); /** The maximum number of idle connections for each address. */ //空闲的socket最大链接数 private final int maxIdleConnections; //socket的keepAlive时间 private final long keepAliveDurationNs; // 双向队列 private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning;
主要的变量有必要说明一下:
Deque<RealConnection>
,双向队列,双端队列同时具备队列和栈性质,常常在缓存中被使用,里面维护了RealConnection也就是socket物理链接的包装。public ConnectionPool() { //默认空闲的socket最大链接数为5个,socket的keepAlive时间为5秒 this(5, 5, TimeUnit.MINUTES); } public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) { this.maxIdleConnections = maxIdleConnections; this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration); // Put a floor on the keep alive duration, otherwise cleanup will spin loop. if (keepAliveDuration <= 0) { throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration); } }
经过构造函数能够看出ConnectionPool默认的空闲的socket最大链接数为5个,socket的keepAlive时间为5秒。
ConnectionPool实例化是在OkHttpClient实例化时进行的:
public OkHttpClient() { this(new Builder()); }
在OkHttpClient的构造函数中调用了new Builder():
public Builder() {
dispatcher = new Dispatcher();
...省略 connectionPool = new ConnectionPool(); ...省略 }
ConnectionPool提供对Deque<RealConnection>
进行操做的方法分别为put、get、connectionBecameIdle和evictAll几个操做。分别对应放入链接、获取链接、移除链接和移除全部链接操做,这里咱们举例put和get操做。
put操做
void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); }
在添加到Deque<RealConnection>
以前首先要清理空闲的线程,这个后面会讲到。
get操做
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address) && !connection.noNewStreams) { streamAllocation.acquire(connection); return connection; } } return null; }
遍历connections缓存列表,当某个链接计数的次数小于限制的大小而且request的地址和缓存列表中此链接的地址彻底匹配。则直接复用缓存列表中的connection做为request的链接。
okhttp是根据StreamAllocation引用计数是否为0来实现自动回收链接的。咱们在put操做前首先要调用executor.execute(cleanupRunnable)
来清理闲置的线程。咱们来看看cleanupRunnable到底作了什么:
private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } };
线程不断的调用cleanup来进行清理,并返回下次须要清理的间隔时间,而后调用wait进行等待以释放锁与时间片,当等待时间到了后,再次进行清理,并返回下次要清理的间隔时间,如此循环下去,接下来看看cleanup方法:
long cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { //遍历链接 for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); //查询此链接的StreamAllocation的引用数量,若是大于0则inUseConnectionCount数量加1,不然idleConnectionCount加1 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //若是空闲链接keepAlive时间超过5分钟,或者空闲链接数超过5个,则从Deque中移除此链接 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside // of the synchronized block). connections.remove(longestIdleConnection); //若是空闲链接大于0,则返回此链接即将到期的时间 } else if (idleConnectionCount > 0) { // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; //若是没有空闲链接,而且活跃链接大于0则返回5分钟 } else if (inUseConnectionCount > 0) { // All connections are in use. It'll be at least the keep alive duration 'til we run again. return keepAliveDurationNs; } else { //若是没有任何链接则跳出循环 cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }
cleanup所作的简单总结就是根据链接中的引用计数来计算空闲链接数和活跃链接数,而后标记出空闲的链接,若是空闲链接keepAlive时间超过5分钟,或者空闲链接数超过5个,则从Deque中移除此链接。接下来根据空闲链接或者活跃链接来返回下次须要清理的时间数:若是空闲链接大于0则返回此链接即将到期的时间,若是都是活跃链接而且大于0则返回默认的keepAlive时间5分钟,若是没有任何链接则跳出循环并返回-1。在上述代码中的第13行,经过pruneAndGetAllocationCount方法来判断链接是否闲置的,若是pruneAndGetAllocationCount方法返回值大于0则是空闲链接,不然就是活跃链接,让咱们来看看pruneAndGetAllocationCount方法:
private int pruneAndGetAllocationCount(RealConnection connection, long now) { List<Reference<StreamAllocation>> references = connection.allocations; //遍历弱引用列表 for (int i = 0; i < references.size(); ) { Reference<StreamAllocation> reference = references.get(i); //若StreamAllocation被使用则接着循环 if (reference.get() != null) { i++; continue; } // We've discovered a leaked allocation. This is an application bug. Internal.logger.warning("A connection to " + connection.route().address().url() + " was leaked. Did you forget to close a response body?"); //若StreamAllocation未被使用则移除引用 references.remove(i); connection.noNewStreams = true; // If this was the last allocation, the connection is eligible for immediate eviction. //若是列表为空则说明此链接没有被引用了,则返回0,表示此链接是空闲链接 if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; } } //不然返回非0的数,表示此链接是活跃链接 return references.size(); }
pruneAndGetAllocationCount方法首先遍历传进来的RealConnection的StreamAllocation列表,若是StreamAllocation被使用则接着遍历下一个StreamAllocation,若是StreamAllocation未被使用则从列表中移除。若是列表为空则说明此链接没有引用了,则返回0,表示此链接是空闲链接,不然就返回非0的数表示此链接是活跃链接。
能够看出链接池复用的核心就是用Deque<RealConnection>
来存储链接,经过put、get、connectionBecameIdle和evictAll几个操做来对Deque进行操做,另外经过判断链接中的计数对象StreamAllocation来进行自动回收链接。
参考资料
okhttp3源码
简析TCP的三次握手与四次分手
TCP三次握手过程
短链接、长链接与keep-alive
OkHttp3源码分析[复用链接池]
okhttp链接池复用机制
Retrofit是Square公司开发的一款针对Android网络请求的框架,Retrofit2**底层基于OkHttp**实现的,而OkHttp如今已经获得Google官方承认。
老生长谈,先配置build.gradle:
dependencies {
...
compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:converter-scalars:2.1.0'//ConverterFactory的String依赖包 }
固然别忘了在manifest加入访问网络的权限:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
此次咱们访问的网站产生了变化,咱们用淘宝IP地址库,里面有访问接口的说明:
1. 请求接口(GET):
/service/getIpInfo.php?ip=[ip地址字串]
2. 响应信息:
(json格式的)国家 、省(自治区或直辖市)、市(县)、运营商
3. 返回数据格式:
{ "code": 0, "data": { "ip": "210.75.225.254", "country": "\u4e2d\u56fd", "area": "\u534e\u5317", "region": "\u5317\u4eac\u5e02", "city": "\u5317\u4eac\u5e02", "county": "", "isp": "\u7535\u4fe1", "country_id": "86", "area_id": "100000", "region_id": "110000", "city_id": "110000", "county_id": "-1", "isp_id": "100017" } }
其中code的值的含义为,0:成功,1:失败。
咱们能够用JSON字符串转换成Java实体类(POJO)这个网站将Json转为实体类,通过修改的实体类以下:
IpModel.java:
public class IpModel { private int code; private IpData data; public void setCode(int code) { this.code = code; } public int getCode() { return this.code; } public void setData(IpData data) { this.data = data; } public IpData getData() { return this.data; } }
IpData.java:
public class IpData { private String country; private String country_id; private String area; private String area_id; private String region; private String region_id; private String city; private String city_id; private String county; private String county_id; private String isp; private String isp_id; private String ip; public void setCountry(String country) { this.country = country; } public String getCountry() { return this.country; } public void setCountry_id(String country_id) { this.country_id = country_id; } ... }
上文已知,请求接口(GET方式)为:/service/getIpInfo.php?ip=[ip地址字串]
public interface IpService{ @GET("/service/getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
Retrofit提供的请求方式注解有@GET和@POST等,分别表明GET请求和POST请求,咱们在这里访问的界面是“getIpInfo.php”。
参数注解有@PATH和@Query等,@Query就是咱们的请求的键值对的设置,在这里@Query**(“ip”)表明键,**String ip则表明值。
String url = "http://ip.taobao.com"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) //增长返回值为String的支持 .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();
这里的baseUrl加上以前@GET(“/service/getIpInfo.php”)定义的参数造成完整的请求地址http://ip.taobao.com/service/getIpInfo.php
; addConverterFactory
用于指定返回的参数数据类型,这里咱们支持String和Gson类型。
IpService ipService = retrofit.create(IpService.class); Call<IpModel>call=ipService.getIpMsg(ip);
用retrofit建立咱们以前定义的IpService接口对象,并调用该接口定义的getIpMsg方法获得Call对象。
call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
这里是异步请求网络,回调的Callback是运行在主线程的。获得返回的Response后将返回数据的country字段用Toast显示出来。
若是想同步请求网络请使用 call.execute()。
若是想中断网络请求则可使用 call.cancel()。
完整的代码以下:
public class MainActivity extends AppCompatActivity { private Button bt_request; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bt_request = (Button) findViewById(R.id.bt_request); bt_request.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getIpInformation("59.108.54.37"); } }); } private void getIpInformation(String ip) { String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) //增长返回值为String的支持 .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); IpService ipService = retrofit.create(IpService.class); Call<IpModel>call=ipService.getIpMsg(ip); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } }); }
上文讲了Retrofit访问网络的基本方法,接下来咱们来了解下Retrofit经常使用的请求参数。
请求方法除了上文讲到的@GET,还有@POST、@PUT、@DELETE、@HEAD、@OPTIONS、@PATCH、@HTTP。
其中@HTTP用来替换以上7个,其余的分别对应着不一样的请求方法。
前面的例子就用了Query用来查询参数。
public interface IpService{ @GET("getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
若是Query**参数比较多,那么能够经过@QueryMap方式将全部的参数集成在一个Map统一传递**。
public interface BlueService { @GET("book/search") Call<BookSearchResponse> getSearchBooks(@QueryMap Map<String, String> options); }
@Path用来替换路径。
public interface ApiStores { @GET("adat/sk/{cityId}.html") Call<ResponseBody> getWeather(@Path("cityId") String cityId); }
@Body与@POST注解一块儿使用,提供查询主体内容,其中ApiInfo是一个bean类。
public interface ApiStores { @POST("client/shipper/getCarType") Call<ResponseBody> getCarType(@Body ApiInfo apiInfo); }
interface SomeService {
@GET("some/endpoint") @Headers("Accept-Encoding: application/json") Call<ResponseBody> getCarType(); }
@Headers用来添加头部信息,上面用的是固定头部,也能够采用动态头部:
interface SomeService {
@GET("some/endpoint") Call<SomeResponse> someEndpoint(@Header("Location") String location); }
@Multipart用来上传文件
public interface FileUploadService { @Multipart @POST("upload") Call<ResponseBody> upload(@Part("description") RequestBody description, @Part MultipartBody.Part file); }
github源码下载
参考资料
Retrofit 2.0文件上传
RxJava 与 Retrofit 结合的最佳实践
Retrofit2使用初探
android 介绍Retrofit的简单使用
Retrofit框架使用笔记
Retrofit 解析 JSON 数据
用 Retrofit 2 简化 HTTP 请求
Android Retrofit 2.0使用
Retrofit提供了不少的请求参数注解,使得请求网路时更加便捷。在这里咱们仍旧访问淘宝IP地址库。
其中,@Path用来动态的配置URL地址。请求网络接口代码以下所示。
public interface IpServiceForPath { @GET("{path}/getIpInfo.php?ip=59.108.54.37") Call<IpModel> getIpMsg(@Path("path") String path); }
在GET注解中包含了{path},它对应着@Path注解中的”path”,而用来替换{path}的正是须要传入的”String path”的值。
接下来请求网络的代码以下所示。
String url = "http://ip.taobao.com/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPath ipService = retrofit.create(IpServiceForPath.class); Call<IpModel>call=ipService.getIpMsg("service");//1 call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
在注释1处,传入”service”来替换 @GET注解中的{path}的值。
在上一篇中咱们用@Query来动态的替换ip地址为了能更方便的获得该ip所对应的地理信息:
public interface IpServiceForQuery{ @GET("getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
可是在网络请求中通常为了更精确的查找到咱们所须要的数据,须要传入不少的查询参数,若是用@Query会比较麻烦,这时咱们能够采用@QueryMap,将全部的参数集成在一个Map统一传递:
public interface IpServiceForQueryMap { @GET("getIpInfo.php") Call<IpModel> getIpMsg(@QueryMap Map<String, String> options); }
传输数据类型为键值对,这是咱们最经常使用的POST请求数据类型,淘宝ip库支持数据类型为键值对的POST请求:
public interface IpServiceForPost { @FormUrlEncoded @POST("getIpInfo.php") Call<IpModel> getIpMsg(@Field("ip") String first); }
首先用到@FormUrlEncoded
注解来标明这是一个表单请求,
而后在getIpMsg方法中使用@Field
注解来标示所对应的String类型数据的键,从而组成一组键值对进行传递。
接下来请求网络的代码以下所示。
String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPost ipService = retrofit.create(IpServiceForPost.class); Call<IpModel>call=ipService.getIpMsg("59.108.54.37"); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
咱们也能够用POST方式将Json字符串做为请求体发送到服务器,请求网络接口代码为:
public interface IpServiceForPostBody { @POST("getIpInfo.php") Call<IpModel> getIpMsg(@Body Ip ip); }
用@Body
这个注解标识参数对象便可,retrofit会将Ip对象转换为字符串。
public class Ip { private String ip; public Ip(String ip) { this.ip = ip; } }
请求网络的代码基本上都是一致的:
String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPostBody ipService = retrofit.create(IpServiceForPostBody.class); Call<IpModel>call=ipService.getIpMsg(new Ip(ip)); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
运行程序用Fiddler抓包,以下图所示。
能够看到请求数据是一个Json字符串,由于淘宝ip库并不支持此类型因此不会返回咱们须要的地理信息数据。
public interface UploadFileForPart { @Multipart @POST("user/photo") Call<User> updateUser(@Part MultipartBody.Part photo, @Part("description") RequestBody description); }
@Multipart
注解表示容许多个@Part,updateUser方法第一个参数是准备上传的图片文件,使用了MultipartBody.Part类型,另外一个参数是RequestBody类型,它用来传递简单的键值对。请求网络代码以下所示。
...
File file = new File(Environment.getExternalStorageDirectory(), "wangshu.png"); RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file); MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "wangshu.png", photoRequestBody); UploadFileForPart uploadFile = retrofit.create(UploadFileForPart.class); Call<User> call = uploadFile.updateUser(photo, RequestBody.create(null, "wangshu")); ...
@Multipart @POST("user/photo") Call<User> updateUser(@PartMap Map<String, RequestBody> photos, @Part("description") RequestBody description);
和单文件上传是相似的,只是使用Map封装了上传的文件,并用@PartMap注解来标示起来。其余的都同样,这里就不赘述了。
Http请求中,为了防止攻击或是过滤掉不安全的访问或是添加特殊加密的访问等等,用来减轻服务器的压力和保证请求的安全,一般都会在消息报头中携带一些特殊的消息头处理。
Retrofit也提供了@Header来添加消息报头。
添加消息报头有两种方式,一种是静态的,另外一种是动态的,
先来看静态方式,以下所示
interface SomeService {
@GET("some/endpoint") @Headers("Accept-Encoding: application/json") Call<ResponseBody> getCarType(); }
使用@Headers注解添加消息报头,若是想要添加多个消息报头,则可使用{}包含起来:
interface SomeService {
@GET("some/endpoint") @Headers({ "Accept-Encoding: application/json", "User-Agent: MoonRetrofit" }) Call<ResponseBody> getCarType(); }
动态方式添加消息报头以下所示。
interface SomeService {
@GET("some/endpoint") Call<ResponseBody> getCarType( @Header("Location") String location); }
使用@Header注解,能够经过调用getCarType方法来动态的添加消息报头。
前言
最近博客的产出确实不多,由于博主我正在写一本Android进阶书籍,两头很难兼顾,可是每月也得至少发一篇博客。上一篇咱们介绍了Retrofit的使用方法,这一篇咱们照例来学习Retrofit的源码。
当咱们使用Retrofit请求网络时,首先要写请求接口:
public interface IpService { @GET("getIpInfo.php?ip=59.108.54.37") Call<IpModel> getIpMsg();
接着咱们经过调用以下代码来建立Retrofit:
Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build();
Retrofit 是经过建造者模式构建出来的,接下来查看Builder方法作了什么:
public Builder() { this(Platform.get()); }
很简短,查看Platform的get方法,以下所示。
private static final Platform PLATFORM = findPlatform(); static Platform get() { return PLATFORM; } private static Platform findPlatform() { try { Class.forName("android.os.Build"); if (Build.VERSION.SDK_INT != 0) { return new Android(); } } catch (ClassNotFoundException ignored) { } try { Class.forName("java.util.Optional"); return new Java8(); } catch (ClassNotFoundException ignored) { } try { Class.forName("org.robovm.apple.foundation.NSObject"); return new IOS(); } catch (ClassNotFoundException ignored) { } return new Platform(); }
Platform的get方法最终调用的是findPlatform方法,根据不一样的运行平台来提供不一样的线程池。接下来查看build方法,代码以下所示。
public Retrofit build() { if (baseUrl == null) {//1 throw new IllegalStateException("Base URL required."); } okhttp3.Call.Factory callFactory = this.callFactory;//2 if (callFactory == null) { callFactory = new OkHttpClient();//3 } Executor callbackExecutor = this.callbackExecutor; if (callbackExecutor == null) { callbackExecutor = platform.defaultCallbackExecutor();//4 } List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);//5 adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor)); List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);//6 return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories, callbackExecutor, validateEagerly); }
从注释1处能够看出baseUrl 是必须指定的。注释2处callFactory默认为this.callFactory,this.callFactory就是咱们在构建Retrofit时调用callFactory方法所传进来的,以下所示。
public Builder callFactory(okhttp3.Call.Factory factory) { this.callFactory = checkNotNull(factory, "factory == null"); return this; }
所以,若是须要对OkHttpClient进行设置,则能够构建OkHttpClient对象,而后调用callFactory方法将设置好的OkHttpClient传进去。注释3处,若是没有设置callFactory则直接建立OkHttpClient。注释4的callbackExecutor用来将回调传递到UI线程。注释5的adapterFactories主要用于存储对Call进行转化的对象,后面在Call的建立过程会再次提到它。注释6处的converterFactories主要用于存储转化数据对象,后面也会说起到。此前在例子中调用的addConverterFactory(GsonConverterFactory.create()),就是设置返回的数据支持转换为Gson对象。最终会返回配置好的Retrofit类。
紧接着咱们建立Retrofit实例并调用以下代码来生成接口的动态代理对象:
IpService ipService = retrofit.create(IpService.class);
接下来看Retrofit的create方法作了什么,代码以下所示。
public <T> T create(final Class<T> service) { Utils.validateServiceInterface(service); if (validateEagerly) { eagerlyValidateMethods(service); } return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { private final Platform platform = Platform.get(); @Override public Object invoke(Object proxy, Method method, Object... args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } if (platform.isDefaultMethod(method)) { return platform.invokeDefaultMethod(method, service, proxy, args); } ServiceMethod serviceMethod = loadServiceMethod(method);//1 OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); }
能够看到create方法返回了一个Proxy.newProxyInstance动态代理对象,当咱们调用IpService的getIpMsg方法最终会调用InvocationHandler的invoke 方法,它有3个参数,第一个是代理对象,第二个是调用的方法,第三个是方法的参数。注释1处的loadServiceMethod(method)中的method就是咱们定义的getIpMsg方法。接下来查看loadServiceMethod方法里作了什么:
private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>(); ServiceMethod loadServiceMethod(Method method) { ServiceMethod result; synchronized (serviceMethodCache) { result = serviceMethodCache.get(method); if (result == null) { result = new ServiceMethod.Builder(this, method).build(); serviceMethodCache.put(method, result); } } return result; }
首先会从serviceMethodCache查询传入的方法是否有缓存,若是有就用缓存的ServiceMethod,若是没有就建立一个,并加入serviceMethodCache缓存起来。接下来看ServiceMethod是如何构建的,代码以下所示。
public ServiceMethod build() {
callAdapter = createCallAdapter();//1 responseType = callAdapter.responseType();//2 if (responseType == Response.class || responseType == okhttp3.Response.class) { throw methodError("'" + Utils.getRawType(responseType).getName() + "' is not a valid response body type. Did you mean ResponseBody?"); } responseConverter = createResponseConverter();//3 for (Annotation annotation : methodAnnotations) { parseMethodAnnotation(annotation);//4 } ... int parameterCount = parameterAnnotationsArray.length; parameterHandlers = new ParameterHandler<?>[parameterCount]; for (int p = 0; p < parameterCount; p++) { Type parameterType = parameterTypes[p]; if (Utils.hasUnresolvableType(parameterType)) { throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", parameterType); } Annotation[] parameterAnnotations = parameterAnnotationsArray[p];//5 if (parameterAnnotations == null) { throw parameterError(p, "No Retrofit annotation found."); } parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); } ... return new ServiceMethod<>(this); }
注释1处调用了createCallAdapter方法,它最终会获得咱们在构建Retrofit调用build方法时adapterFactories添加的对象的get方法,Retrofit的build方法部分代码:
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories); adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
adapterFactories列表默认会添加defaultCallAdapterFactory,defaultCallAdapterFactory指的是ExecutorCallAdapterFactory,ExecutorCallAdapterFactory的get方法以下所示。
public CallAdapter<Call<?>> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { if (getRawType(returnType) != Call.class) { return null; } final Type responseType = Utils.getCallResponseType(returnType); return new CallAdapter<Call<?>>() { @Override public Type responseType() { return responseType; } @Override public <R> Call<R> adapt(Call<R> call) { return new ExecutorCallbackCall<>(callbackExecutor, call); } }; }
get方法会获得CallAdapter对象,它的responseType方法会返回数据的真实类型,好比 Call<IpModel>
,它就会返回IpModel。adapt方法会建立ExecutorCallbackCall,它会将call的回调转发至UI线程。
接着回到ServiceMethod的 build方法,注释2处调用CallAdapter的responseType获得的是返回数据的真实类型。
注释3处调用createResponseConverter方法来遍历converterFactories列表中存储的Converter.Factory,并返回一个合适的Converter用来转换对象。此前咱们在构建Retrofit 调用了addConverterFactory(GsonConverterFactory.create())将GsonConverterFactory(Converter.Factory的子类)添加到converterFactories列表中,表示返回的数据支持转换为Json对象。
注释4处遍历parseMethodAnnotation方法来对请求方式(好比GET、POST)和请求地址进行解析。注释5处对方法中的参数注解进行解析(好比@Query、@Part)。最后建立ServiceMethod类并返回。
接下来回过头来查看Retrofit的create方法,在调用了loadServiceMethod方法后会建立OkHttpCall,OkHttpCall的构造函数只是进行了赋值操做。紧接着调用serviceMethod.callAdapter.adapt(okHttpCall)
,callAdapter的adapt方法前面讲过,它会建立ExecutorCallbackCall,ExecutorCallbackCall的部分代码以下所示。
ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
this.callbackExecutor = callbackExecutor; this.delegate = delegate; } @Override public void enqueue(final Callback<T> callback) { if (callback == null) throw new NullPointerException("callback == null"); delegate.enqueue(new Callback<T>() {//1 @Override public void onResponse(Call<T> call, final Response<T> response) { callbackExecutor.execute(new Runnable() { @Override public void run() { if (delegate.isCanceled()) { callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled")); } else { callback.onResponse(ExecutorCallbackCall.this, response); } } }); } @Override public void onFailure(Call<T> call, final Throwable t) { callbackExecutor.execute(new Runnable() { @Override public void run() { callback.onFailure(ExecutorCallbackCall.this, t); } }); } }); }
能够看出ExecutorCallbackCall是对Call的封装,它主要添加了经过callbackExecutor将请求回调到UI线程。
当咱们获得Call对象后会调用它的enqueue方法,其实调用的是ExecutorCallbackCall的enqueue方法,而从注释1处能够看出ExecutorCallbackCall的enqueue方法最终调用的是delegate的enqueue方法。delegate从Retrofit的create方法的代码中咱们知道它其实就是OkHttpCall。
接下来咱们就来查看OkHttpCall的enqueue方法,代码以下所示。
public void enqueue(final Callback<T> callback) { if (callback == null) throw new NullPointerException("callback == null"); okhttp3.Call call; ... call.enqueue(new okhttp3.Callback() {//1 @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) throws IOException { Response<T> response; try { response = parseResponse(rawResponse);//2 } catch (Throwable e) { callFailure(e); return; } callSuccess(response); } ... }
注释1处调用了okhttp3.Call的enqueue方法。注释2处调用parseResponse方法:
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException { ResponseBody rawBody = rawResponse.body(); ... int code = rawResponse.code(); if (code < 200 || code >= 300) { try { ResponseBody bufferedBody = Utils.buffer(rawBody); return Response.error(bufferedBody, rawResponse); } finally { rawBody.close(); } } if (code == 204 || code == 205) { return Response.success(null, rawResponse); } ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody); try { T body = serviceMethod.toResponse(catchingBody);//2 return Response.success(body, rawResponse); } catch (RuntimeException e) { catchingBody.throwIfCaught(); throw e; } }
根据返回的不一样的状态码code值来作不一样的操做,若是顺利则会调用注释2处的代码,接下来看toResponse方法里作了什么:
T toResponse(ResponseBody body) throws IOException { return responseConverter.convert(body); }
这个responseConverter就是此前讲过在ServiceMethod的build方法调用createResponseConverter方法返回的Converter,在此前的例子中咱们传入的是GsonConverterFactory,所以能够查看GsonConverterFactory的代码,以下所示。
public final class GsonConverterFactory extends Converter.Factory { ... @Override public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); return new GsonResponseBodyConverter<>(gson, adapter); } ... }
在GsonConverterFactory 中有一个方法responseBodyConverter,它最终会建立GsonResponseBodyConverter:
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { private final Gson gson; private final TypeAdapter<T> adapter; GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) { this.gson = gson; this.adapter = adapter; } @Override public T convert(ResponseBody value) throws IOException { JsonReader jsonReader = gson.newJsonReader(value.charStream()); try { return adapter.read(jsonReader); } finally { value.close(); } } }
在GsonResponseBodyConverter的convert方法里会将回调的数据转换为Json格式。所以咱们也知道了此前调用responseConverter.convert
是为了转换为特定的数据格式。
Call的enqueue方法主要作的就是用OKHttp来请求网络并将返回的Response进行数据转换并回调给UI线程。
至此,Retrofit的源码就讲到这里。
网络请求开源库是一个将 网络请求的相关功能封装好的类库
没有网络请求框架以前
App想与服务器进行网络请求交互是一件很痛苦的事:由于Android的主线程不能进行网络请求,需另开1个线程请求、考虑到线程池,缓存等一堆问题
使用网络请求库后
实现网络请求的需求同时不须要考虑:
网络请求库的本质 = 封装了 网络请求 + 异步 + 数据处理功能的库
其中,网络请求功能则是采用Android
网络请求的原生方法(HttpClient
或HttpURLConnection
)
具体以下图
现在Android
中主流的网络请求框架有:
Android-Async-Http
Volley
OkHttp
Retrofit
下面是简单介绍:
Github地址
引用:
★★★Android网络编程(一)HTTP协议原理
★★★Android网络编程(二)HttpClient与HttpURLConnection
★★★Android网络编程(三)Volley用法全解析
★★★Android网络编程(四)从源码解析volley
★★★Android网络编程(五)OkHttp2.x用法全解析
★★★Android网络编程(六)OkHttp3用法全解析
★★★Android网络编程(七)源码解析OkHttp前篇-请求网络
★★★Android网络编程(八)源码解析OkHttp后篇-复用链接池
★★★Android网络编程(九)Retrofit2前篇-基本使用
★★★Android网络编程(十)Retrofit2后篇-注解
★★★Android网络编程(十一)源码解析Retrofit
★★★Android:主流网络请求开源库的对比(Android-Async-Http、Volley、OkHttp、Retrofit)