天天看點

OkHttp3源碼詳解(一) Request類

每一次網絡請求都是一個Request,Request是對url,method,header,body的封裝,也是對Http協定中請求行,請求頭,實體内容的封裝

public final class Request {
   private final HttpUrl url;
   private final String method;
   private final Headers headers;
   private final RequestBody body;
   private final Object tag;
 
   private volatile CacheControl cacheControl; // Lazily initialized.           

1.HttpUrl

HttpUrl主要用來規範普通的url連接配接,并且解析url的組成部分

現通過下面的例子來示例httpUrl的使用

https://www.google.com/search?q=maplejaw

①使用parse解析url字元串:

1.  HttpUrl url =  HttpUrl.parse("https://www.google.com/search?q=maplejaw");           

②通過構造者模式來常見:

1.  HttpUrl url =  new  HttpUrl.Builder()
2.  .scheme("https")
3.  .host("www.google.com")
4.  .addPathSegment("search")
5.  .addQueryParameter("q",  "maplejaw")
6.  .build();           

2.Headers

Headers用于配置請求頭,對于請求頭配置大家一定不陌生吧,比如`Content-Type,`User-Agent和`Cache-Control等等。

```

`建立Headers也有兩種方式。如下:

(1)of()建立:傳入的數組必須是偶數對,否則會抛出異常。`

1.  Headers.of("name1","value1","name2","value2",.....);           

還可以使用它的重載方法of(Map map)方法來建立

(2)

建構者模式建立:

`

`

1.  Headers mHeaders=new  Headers.Builder()
2.  .set("name1","value1")//set表示name1是唯一的,會覆寫掉已經存在的
3.  .add("name2","value2")//add不會覆寫已經存在的頭,可以存在多個
4.  .build();           

我們來看一下Header的内部,源碼就不粘貼了很簡單,Headers内部是通過一個數組來儲存header private final String[] namesAndValues;大家可能會有這樣的疑問,為什麼不用Map而用數組呢?因為Map的Key是唯一的,而header要求不唯一

另外,數組友善取資料嗎?很友善,我們來看着三個方法

最後通過toString方法轉變成String,友善寫入請求頭,

1.  @Override
2.  public  String toString()  {
3.  StringBuilder result =  new  StringBuilder();
4.  , size = size(); i < size; i++)  {
5.  result.append(name(i)).append(": ").append(value(i)).append("\n");
6.  }
7.  return result.toString();
8.  }

10.  /** Returns the field at {@code position} or null if that is out of range. */
11.  public  String name(int index)  {
12.  ;
13.  || nameIndex >= namesAndValues.length)  {
14.  return  null;
15.  }
16.  return namesAndValues[nameIndex];
17.  }

19.  /** Returns the value at {@code index} or null if that is out of range. */
20.  public  String value(int index)  {
21.  +  ;
22.  || valueIndex >= namesAndValues.length)  {
23.  return  null;
24.  }
25.  return namesAndValues[valueIndex];
26.  }           

3.RequestBody

requestBody也就是請求實體内容,我們先來看一下如何來建構一個RequestBody

(1)Request.create()方法建立

1.  public  static  final  MediaType TEXT =  MediaType.parse("text/plain; charset=utf-8");
2.  public  static  final  MediaType STREAM =  MediaType.parse("application/octet-stream");
3.  public  static  final  MediaType JSON =  MediaType.parse("application/json; charset=utf-8");

5.  //建構字元串請求體
6.  RequestBody body1 =  RequestBody.create(TEXT,  string);
7.  //建構位元組請求體
8.  RequestBody body2 =  RequestBody.create(STREAM,  byte);
9.  //建構檔案請求體
10.  RequestBody body3 =  RequestBody.create(STREAM, file);
11.  //post上傳json
12.  RequestBody body4 =  RequestBody.create(JSON, json);//json為String類型的

14.  //将請求體設定給請求方法内
15.  Request request =  new  Request.Builder()
16.  .url(url)
17.  .post(xx)// xx表示body1,body2,body3,body4中的某一個
18.  .build();           

(2)建構表單請求體,送出鍵值對(OkHttp3沒有FormEncodingBuilder這個類,替代它的是功能更加強大的FormBody:)

1.  //建構表單RequestBody
2.  RequestBody formBody=new  FormBody.Builder()
3.  .add("name","maplejaw")
4.  .add(")
5.  ...
6.  .build();           

(3)建構分塊表單請求體:(OkHttp3取消了MultipartBuilder,取而代之的是MultipartBody.Builder()

既可以添加表單,又可以也可以添加檔案等二進制資料)           
1.  public  static  final  MediaType STREAM =  MediaType.parse("application/octet-stream");
2.  //建構表單RequestBody
3.  RequestBody multipartBody=new  MultipartBody.Builder()
4.  .setType(MultipartBody.FORM)//指明為 multipart/form-data 類型
5.  .addFormDataPart(") //添加表單資料
6.  .addFormDataPart("avatar","111.jpg",RequestBody.create(STREAM,file)) //添加檔案,其中avatar為表單名,111.jpg為檔案名。
7.  .addPart(..)//該方法用于添加RequestBody,Headers和添加自定義Part,一般來說以上已經夠用
8.  .build();           

知道了RequestBody的建立,我們來看一下源碼

RequestBody也就是請求實體内容,對于一個Get請求時沒有實體内容的,Post送出才有,而且浏覽器與伺服器通信時基本上隻有表單上傳才會用到POST送出,是以RequestBody其實也就是封裝了浏覽器表單上傳時對應的實體内容,對于實體内容是什麼樣還不清楚的可以去看一下我的一篇部落格Android的Http協定的通信詳解

OkHttp3中RequestBody有三種建立方式

①方式一:

1.  public  static  RequestBody create(MediaType contentType,  String content)  {
2.  Charset charset =  Util.UTF_8;
3.  if  (contentType !=  null)  {
4.  charset = contentType.charset();//MediaType的為請求頭中的ContentType建立方式:public static final MediaType TEXT =
5.  //MediaType.parse("text/plain; charset=utf-8")
6.  if  (charset ==  null)  {
7.  charset =  Util.UTF_8;<span style="font-family:Microsoft YaHei;">//如果contentType中沒有指定charset,預設使用UTF-8</span>
8.  contentType =  MediaType.parse(contentType +  "; charset=utf-8");
9.  }
10.  }
11.  byte[] bytes = content.getBytes(charset);
12.  return create(contentType, bytes);
13.  }           

②方式二:FormBody表單建立,我們來看一下

FormBody用于普通post表單上傳鍵值對,我們先來看一下建立的方法,再看源碼

1.  RequestBody formBody=new  FormBody.Builder()
2.  .add("name","maplejaw")
3.  .add(")
4.  ...
5.  .build();           

FormBody源碼

1.  public  final  class  FormBody  extends  RequestBody  {
2.  private  static  final  MediaType CONTENT_TYPE =
3.  MediaType.parse("application/x-www-form-urlencoded");

5.  private  final  List<String> encodedNames;
6.  private  final  List<String> encodedValues;

8.  private  FormBody(List<String> encodedNames,  List<String> encodedValues)  {
9.  this.encodedNames =  Util.immutableList(encodedNames);
10.  this.encodedValues =  Util.immutableList(encodedValues);
11.  }

13.  /** The number of key-value pairs in this form-encoded body. */
14.  public  int size()  {
15.  return encodedNames.size();
16.  }

18.  public  String encodedName(int index)  {
19.  return encodedNames.get(index);
20.  }

22.  public  String name(int index)  {
23.  return percentDecode(encodedName(index),  true);
24.  }

26.  public  String encodedValue(int index)  {
27.  return encodedValues.get(index);
28.  }

30.  public  String value(int index)  {
31.  return percentDecode(encodedValue(index),  true);
32.  }

34.  @Override  public  MediaType contentType()  {
35.  return CONTENT_TYPE;
36.  }

38.  @Override  public  long contentLength()  {
39.  return writeOrCountBytes(null,  true);
40.  }

42.  @Override  public  void writeTo(BufferedSink sink)  throws  IOException  {
43.  writeOrCountBytes(sink,  false);
44.  }

46.  /**
47.  * Either writes this request to {@code sink} or measures its content length. We have one method
48.  * do double-duty to make sure the counting and content are consistent, particularly when it comes
49.  * to awkward operations like measuring the encoded length of header strings, or the
50.  * length-in-digits of an encoded integer.
51.  */
52.  private  long writeOrCountBytes(BufferedSink sink,  boolean countBytes)  {
53.  long byteCount =  0L;

55.  Buffer buffer;
56.  if  (countBytes)  {
57.  buffer =  new  Buffer();
58.  }  else  {
59.  buffer = sink.buffer();
60.  }

62.  , size = encodedNames.size(); i < size; i++)  {
63.  ) buffer.writeByte('&');
64.  buffer.writeUtf8(encodedNames.get(i));
65.  buffer.writeByte('=');
66.  buffer.writeUtf8(encodedValues.get(i));
67.  }

69.  if  (countBytes)  {
70.  byteCount = buffer.size();
71.  buffer.clear();
72.  }

74.  return byteCount;
75.  }

77.  public  static  final  class  Builder  {
78.  private  final  List<String> names =  new  ArrayList<>();
79.  private  final  List<String> values =  new  ArrayList<>();

81.  public  Builder add(String name,  String value)  {
82.  names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET,  false,  false,  true,  true));
83.  values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET,  false,  false,  true,  true));
84.  return  this;
85.  }

87.  public  Builder addEncoded(String name,  String value)  {
88.  names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET,  true,  false,  true,  true));
89.  values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET,  true,  false,  true,  true));
90.  return  this;
91.  }

93.  public  FormBody build()  {
94.  return  new  FormBody(names, values);
95.  }
96.  }
97.  }           

我們主要來看一下方法

writeOrCountBytes

`

`,通過

writeOrCountBytes來計算請求體大小和将請求體寫入BufferedSink。

至于BufferSink和Buffer類,這兩個類是Okio中的類,Buffer相當于一個緩存區,BufferedSink相當于OutputStream,它擴充了

OutputStream的功能,Okio的完整源碼我後續也會寫部落格

③方式三:MultipartBody分塊表單建立

`MultipartBody

, 既可以添加表單,又可以也可以添加檔案等二進制資料,我們就看幾個重要的方法`

1.  public  static  Part createFormData(String name,  String filename,  RequestBody body)  {
2.  if  (name ==  null)  {
3.  throw  new  NullPointerException("name == null");
4.  }
5.  StringBuilder disposition =  new  StringBuilder("form-data; name=");
6.  appendQuotedString(disposition, name);

8.  if  (filename !=  null)  {
9.  disposition.append("; filename=");
10.  appendQuotedString(disposition, filename);
11.  }

13.  return create(Headers.of("Content-Disposition", disposition.toString()), body);
14.  }           

我們來看這個方法,我們是addPart還是addFormDataPart最終都走到了這個方法,封裝成一個Part對象,也就是實體内容中

的Content-Disposition跟檔案二進制流或者鍵值對的值

MultipartBody和FormBody大體上相同,主要差別在于`writeOrCountBytes方法,分塊表單主要是将每個塊的大小進行累加來求出請求體大小,如果其中有一個塊沒有指定大小,就會傳回-1。是以分塊表單中如果包含檔案,預設是無法計算出大小的,除非你自己給檔案的RequestBody指定contentLength。

1.  private  long writeOrCountBytes(BufferedSink sink,  boolean countBytes)  throws  IOException  {
2.  long byteCount =  0L;

4.  Buffer byteCountBuffer =  null;
5.  if  (countBytes)  {
6.  //如果是計算大小的話,就new個
7.  sink = byteCountBuffer =  new  Buffer();
8.  }
9.  //循環塊
10.  , partCount = parts.size(); p < partCount; p++)  {
11.  Part part = parts.get(p);
12.  //擷取每個塊的頭
13.  Headers headers = part.headers;
14.  //擷取每個塊的請求體
15.  RequestBody body = part.body;

17.  //寫 --xxxxxxxxxx 邊界
18.  sink.write(DASHDASH);
19.  sink.write(boundary);
20.  sink.write(CRLF);

22.  //寫塊的頭
23.  if  (headers !=  null)  {
24.  , headerCount = headers.size(); h < headerCount; h++)  {
25.  sink.writeUtf8(headers.name(h))
26.  .write(COLONSPACE)
27.  .writeUtf8(headers.value(h))
28.  .write(CRLF);
29.  }
30.  }

32.  //寫塊的Content_Type
33.  MediaType contentType = body.contentType();
34.  if  (contentType !=  null)  {
35.  sink.writeUtf8("Content-Type: ")
36.  .writeUtf8(contentType.toString())
37.  .write(CRLF);
38.  }

40.  //寫塊的大小
41.  long contentLength = body.contentLength();
42.  )  {
43.  sink.writeUtf8("Content-Length: ")
44.  .writeDecimalLong(contentLength)
45.  .write(CRLF);
46.  }  else  if  (countBytes)  {
47.  // We can't measure the body's size without the sizes of its components.
48.  //如果有個塊沒有這名大小,就傳回-1.
49.  byteCountBuffer.clear();
50.  return  -1L;
51.  }

53.  sink.write(CRLF);

55.  //如果是計算大小就累加,否則寫入BufferedSink
56.  if  (countBytes)  {
57.  byteCount += contentLength;
58.  }  else  {
59.  body.writeTo(sink);
60.  }

62.  sink.write(CRLF);
63.  }

65.  //寫 --xxxxxxxxxx-- 結束邊界
66.  sink.write(DASHDASH);
67.  sink.write(boundary);
68.  sink.write(DASHDASH);
69.  sink.write(CRLF);

71.  if  (countBytes)  {
72.  byteCount += byteCountBuffer.size();
73.  byteCountBuffer.clear();
74.  }

76.  return byteCount;
77.  }           

4.CacheControl

( 1) Cache-Control:

Cache-Control指定請求和響應遵循的緩存機制。在請求消息或響應消息中設定Cache-Control并不會修改另一個消息處理過程中的緩存處理過程。請求時的緩存指令有下幾種:

  1. Public:所有内容都将被緩存(用戶端和代理伺服器都可緩存)。
  2. Private:内容隻緩存到私有緩存中(僅用戶端可以緩存,代理伺服器不可緩存)
  3. no-cache:請求或者響應消息不能緩存
  4. no-store:不使用緩存,也不存儲緩存
  5. max-age:緩存的内容将在指定時間(秒)後失效, 這個選項隻在HTTP 1.1可用, 并如果和Last-Modified一起使用時, 優先級較高
  6. 在 xxx 秒後,浏覽器重新發送請求到伺服器,指定時間(秒)内,用戶端會直接傳回cache而不會發起網絡請求,若過期會自動發起網絡請求
  7. min-fresh:訓示用戶端可以接收響應時間小于目前時間加上指定時間的響應。
  8. max-stale:訓示用戶端可以接收超出逾時期間的響應消息。如果指定max-stale消息的值,那麼客戶機可以接收超出逾時期指定值之内的響應消息。

(2)CacheControl類

①常用的函數

1.  final  CacheControl.Builder builder =  new  CacheControl.Builder();
2.  builder.noCache();//不使用緩存,全部走網絡
3.  builder.noStore();//不使用緩存,也不存儲緩存
4.  builder.onlyIfCached();//隻使用緩存
5.  builder.noTransform();//禁止轉碼
6.  builder.maxAge(,  TimeUnit.MILLISECONDS);//訓示客戶機可以接收生存期不大于指定時間的響應。
7.  builder.maxStale(,  TimeUnit.SECONDS);//訓示客戶機可以接收超出逾時期間的響應消息
8.  builder.minFresh(,  TimeUnit.SECONDS);//訓示客戶機可以接收響應時間小于目前時間加上指定時間的響應。
9.  CacheControl cache = builder.build();//cacheControl           

②CacheControl的兩個常量:

1.  public  static  final  CacheControl FORCE_NETWORK =  new  Builder().noCache().build();//不使用緩存
2.  public  static  final  CacheControl FORCE_CACHE =  new  Builder()
3.  .onlyIfCached()
4.  .maxStale(Integer.MAX_VALUE,  TimeUnit.SECONDS)
5.  .build();//隻使用緩存           

③請求時如何使用:

1.  final  CacheControl.Builder builder =  new  CacheControl.Builder();
2.  builder.maxAge(,  TimeUnit.MILLISECONDS);
3.  CacheControl cache = builder.build();
4.  final  Request request =  new  Request.Builder().cacheControl(cache).url(requestUrl).build();
5.  final  Call call = mOkHttpClient.newCall(request);//
6.  call.enqueue(new  Callback()  {
7.  @Override
8.  public  void onFailure(Call call,  IOException e)  {
9.  failedCallBack("通路失敗", callBack);
10.  Log.e(TAG, e.toString());
11.  }

13.  @Override
14.  public  void onResponse(Call call,  Response response)  throws  IOException  {
15.  if  (response.isSuccessful())  {
16.  String  string  = response.body().string();
17.  Log.e(TAG,  "response ----->"  +  string);
18.  successCallBack((T)  string, callBack);
19.  }  else  {
20.  failedCallBack("伺服器錯誤", callBack);
21.  }
22.  }
23.  });
24.  return call;
25.  }  catch  (Exception e)  {
26.  Log.e(TAG, e.toString());
27.  }           

以上如果Cache沒有過期會直接傳回cache而不會去發起網絡請求,若過期自動發起網絡請求,注意:如果您使用FORCE_CACHE和網絡的響應需求,OkHttp則會傳回一個504提示,告訴你不可滿足請求響應,是以我們加一個判斷在沒有網絡的情況下使用

1.  //判斷網絡是否連接配接
2.  boolean connected =  NetworkUtil.isConnected(context);
3.  if  (!connected)  {
4.  request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
5.  }           

原文連結

https://www.bbsmax.com/A/MAzArw9OJ9/