編輯:關於Android編程
現在市場的Http框架很多,比如我們熟知的NoHttp、Retrofit、Volley、android-async-http等上層框架,HttpURLConnection、OkHttp、HttpClient等底層框架,今天不說孰好孰壞,今天我帶大家來分析NoHttp的源碼,教大家如何來看NoHttp的源碼。
今天我們由淺到深,深入淺出的分析第一個模塊:請求模塊。
支持作者可以去Github上關注NoHttp源代碼:https://github.com/yanzhenjie/NoHttp,。
想要看一個框架的源碼,首先就要學會怎麼用,最起碼基礎用法要會,也不必深入或者精通。那麼我們先來看下NoHttp的異步請求和同步請求的簡單用法。
作為一個優秀的網絡框架,必定時提供了異步請求、同步請求兩種方式給開發者的。
NoHttp的異步,我們先來看一個最簡單實踐:
RequestQueue queue = NoHttp.newRequestQueue(); RequestsReq = new StringRequest(url, POST); ... queue.add(what sReq, new OnResponseListener() { public void onStart(int what){} public void onFinish(int what){} public void onSucceed(int what, Response response){} public void onFailed(int what, Response response){} });
來做個簡單的分析,首先是new了一個隊列,然後創建了一個String的Request,之後把這個請求添加到隊列,等待請求網絡後響應,完事兒,就是這麼簡單。
NoHttp同步請求,再來看一個最簡單實例:
Requestrequest = new StringRequest(url, GET); Response response = NoHttp.startRequestSync(request);
這個更簡單了,創建了一個請求,然後調用NoHttp同步請求的方法拿到請求結果。
看了異步請求和同步請求後我們來分析一下,應該從哪裡開始看NoHttp的源碼。
從上面的代碼中看到,異步請求和同步請求最大的區別是:異步請求需要用隊列發送,用Listener接受請求結果,而同步請求是直接請求到結果,那麼最大的可能就是異步請求是建立在同步請求這個基礎上的。所以我們直接看異步請求,在開始之前我已經畫好了幾個圖。
NoHttp流程圖
這個圖是NoHttp異步請求時,從主線程調用網絡到拿到結果的流程圖,這裡非常有必要對這些類做個說明。
名稱
類型
默認實現類
說明
RequestQueue
類
-
請求隊列
RequestDispatcher
類
-
輪詢隊列的線程
IRestParser
接口
RestParser
解析網絡結果,針對IParserRequest
IRestProtocol
接口
RestProtocol
按照Http協議請求網絡處理請求結果,針對IProtocolRequest
BasicConnection
抽象類
-
提供基礎的訪問網絡能力,針對IBaseRequst,更改底層為OkHttp時只需要改它
IBaseRequst
接口
BaseRequest
提供通用的網絡設置、參數設置、Cookie、重試機制,下載和請求模塊通用
IProtocolRequest
接口
ProtocolRequest
請求模塊的協議,例如緩存機制
IParserRequest
接口
ParserRequest
泛型,提供解析請求結果ByteArray為泛型
Request
接口
RestRequest
提供異步請求的參數記錄功能,如what、Listener等
注意:其中
BasicConnection是網絡訪問的基礎類,所以它提供了請求網絡、發送數據、上傳文件、拿到服務器響應頭、拿到服務器響應body、拿到服務器的流等基礎網絡功能。
NoHttp動態流程圖
如果上面的圖還是不夠顯然,我們可以結合下面的圖再看一邊動態流程圖就更加顯而易懂了:
<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxjb2RlPjxjb2RlPrTTyc/D5rXEwb3Vxc28v8nS1L+0tb2jrMjOus7Su7j2x+vH87a8yse009b3z9+zzL+qyry3osbwo6zPyLDRUmVxdWVzdMztvNO1vTxjb2RlPlJlcXVlc3RRdWV1ZaOottPB0KOp1tCjrLbTwdC0y8qxu+HG9Lav19PP37PMo6zU2dPJPGNvZGU+UmV1cWVzdERpc3BhdGNoZXKjqMfrx/O31rei1d+jqbDRttPB0NbQx+vH87C01dXSu7aotcTLs9Dyt9a3orj419PP37PMyKXWtNDQzfjC57LZ1/ejrDxjb2RlPlJldXFlc3REaXNwYXRjaGVyxMO1vb3hufu686Os08M8Y29kZT5IYW5kbGVyt6LLzbW91vfP37PMo6zV4rj2uf2zzL7NysdOb0h0dHC1xNX7uPbS7LK9x+vH87XEuf2zzKGjPC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9wPg0KPGgxIGlkPQ=="撸源碼">撸源碼
接著上面的分析,我們現在開始撸源碼,那麼先入手的入手的就是
RequestQueue了。
RequestQueue
我們先來看看NoHttp怎麼創建
RequestQueue:
public static RequestQueue newRequestQueue() {
return newRequestQueue(DEFAULT_REQUEST_THREAD_SIZE);
}
public static RequestQueue newRequestQueue(int threadPoolSize) {
return newRequestQueue(DiskCacheStore.INSTANCE, threadPoolSize);
}
public static RequestQueue newRequestQueue(Cache cache, int threadPoolSize) {
return newRequestQueue(RestProtocol.getInstance(cache), threadPoolSize);
}
public static RequestQueue newRequestQueue(IRestProtocol iRestProtocol, int threadPoolSize) {
return newRequestQueue(RestParser.getInstance(iRestProtocol), threadPoolSize);
}
public static RequestQueue newRequestQueue(IRestParser implRestParser, int threadPoolSize) {
RequestQueue requestQueue = new RequestQueue(implRestParser, threadPoolSize);
requestQueue.start();
return requestQueue;
}
這裡有5個方法可以創建
ReuqestQueue,但是每個方法最終都會來到最後方法,最後一個方法需要兩個參數,第一個參數是
IRestParser,也就是響應解析者,第二個參數是
threadPoolSize,也就是隊列中啟動的線程的數量,創建好
ReuqestQueue後,調用
start()方法啟動隊列,這時就可以往隊列中添加請求了。
既然到這裡我們就必須去看看
ReuqestQueue的源碼了,我已經把原文中的英文注釋改為中文了,方便大家理解:
public class RequestQueue {
/**
* 保存沒有完成的請求,包括正在執行的請求。
*/
private final BlockingQueue> mUnFinishQueue;
/**
* 保存沒有執行的請求,不包括正在執行的請求。
*/
private final BlockingQueue> mRequestQueue;
/**
* Http 請求結果解析器。
*/
private final IRestParser mImplRestParser;
/**
* 線程池。
*/
private RequestDispatcher[] mDispatchers;
public RequestQueue(IRestParser implRestParser, int threadPoolSize) {
mImplRestParser = implRestParser;
mDispatchers = new RequestDispatcher[threadPoolSize];
}
/**
* 啟動線程池,輪詢請求隊列。
*/
public void start() {
stop();
for (int i = 0; i < mDispatchers.length; i++) {
RequestDispatcher networkDispatcher = new RequestDispatcher(..., mImplRestParser);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
/**
* 1. 添加一個請求到隊列,如果隊列中請求數沒有滿,則會立即執行,否則等待前面的請求執行完成後再執行。
* 2. 在真正添加到隊列前檢查當前要添加的請求是否在隊列中,如果重復添加則無任何操作。
*/
public void add(int what, Request request, OnResponseListener responseListener) {
...
}
/**
* 沒有開始執行的請求數量。
*/
public int unStartSize() {}
/**
* 沒有完成的請求數量,包括正在執行的請求。
*/
public int unFinishSize() {}
/**
* 停止隊列,使三個線程停止,不進行輪詢隊列。
*/
public void stop() {}
/**
* 根絕sign取消所有用sign打標的請求。
*/
public void cancelBySign(Object sign) {}
/**
* 取消隊列中所有請求。
*/
public void cancelAll() {}
}
結合上文所說的,這裡貼出了所有方法和最重要的實現。在構造方法中,創建了一個
threadPoolSize大小的
RequestDispatcher數組,調用
start()的時候為數組的每一個index賦值一個真正的
RequestDispatcher線程,並啟動這個線程去輪詢
Queue,同時在創建
RequestDispatcher的時候把我們創建隊列的時候的
IRestParser穿進去了,說明真正的調用網絡還是在
RequestDispatcher這個子線程中,具體怎麼調用並處理接著看下文。關於隊列和線程的知識點這裡不講太多,只講NoHttp在這裡的設計
RequestDispatcher
我們上面說到
RequestDispatcher是一個線程,而且它在輪詢隊列裡面的請求,根絕開始的流程圖它也負責發送請求結果到主線程,所以它應該NoHttp異步請求中最重要的類之一了,我們不能放過它:
public class RequestDispatcher extends Thread {
// 線程輪詢是否停止。
private boolean mQuit = false;
// 停止線程輪詢。
public void stop() {
mQuit = true;
}
...
@Override
public void run() {
while (!mQuit) { // 線程輪詢沒有停止則無限循環。
final Request request;
try {
// 輪詢請求,如果隊列中請求為空則阻塞。
request = mRequestQueue.take();
} catch (InterruptedException e) {
continue;
}
if (request.isCanceled()) {// 如果請求已經被取消則進行下一個請求。
continue;
}
// 記錄下what和listener,等待結果回調。
final int what = request.what();
final OnResponseListener responseListener = request.responseListener();
request.start(); // 標志請求開始。
// 發送請求開始回調到主線程。
final ThreadPoster startThread = new ThreadPoster(what, responseListener);
startThread.onStart();
PosterHandler.getInstance().post(startThread);
// 用IRestParser發送請求,並解析結果,這就是上文中說過的。
Response response = mIRestParser.parserRequest(request);
// 執行完請求,從隊列移除。
mUnFinishQueue.remove(request);
// 發送請求完成回調到主線程。
final ThreadPoster finishThread = new ThreadPoster(what, responseListener);
finishThread.onFinished();
PosterHandler.getInstance().post(finishThread);
request.finish();// 標志網絡請求已經完成。
// 發送請求結果到主線程,如果請求在請求過程中被取消,則不發送。
if (request.isCanceled())
Logger.d(request.url() + " finish, but it's canceled.");
else {
final ThreadPoster responseThread = new ThreadPoster(what, responseListener);
responseThread.onResponse(response);
PosterHandler.getInstance().post(responseThread);
}
}
}
private class ThreadPoster implements Runnable {
public static final int COMMAND_START = 0;
public static final int COMMAND_RESPONSE = 1;
public static final int COMMAND_FINISH = 2;
private final int what;
private final OnResponseListener responseListener;
private int command;
private Response response;
public ThreadPoster(int what, OnResponseListener responseListener) {
this.what = what;
this.responseListener = responseListener;
}
public void onStart() {
this.command = COMMAND_START;
}
public void onResponse(Response response) {
this.command = COMMAND_RESPONSE;
this.response = response;
}
public void onFinished() {
this.command = COMMAND_FINISH;
}
@Override
public void run() {
if (responseListener != null) {
if (command == COMMAND_START) // 開始回調。
responseListener.onStart(what);
else if (command == COMMAND_FINISH) // 結束回調。
responseListener.onFinish(what);
else if (command == COMMAND_RESPONSE) {// 請求結果回調。
if (response.isSucceed()) {// 如果成功,回調成功方法。
responseListener.onSucceed(what, response);
} else { // 如果失敗,回調失敗方法。
responseListener.onFailed(what, response);
}
}
}
}
}
}
如果你認真看了我的注釋結合我的解釋也許就會明白很多了。
RequestDispatcher在線程沒有被標志停止的情況下會一直循環調用
Queue.take()輪詢隊列中的請求,如果線程中沒有請求,由於
Queue.take()的特性,這個子線程會處於阻塞狀態,當然這不會使APP卡頓,因為它在子線程。當它每拿到一個
Request先會判斷請求是否被取消,如果是取消了的則去輪詢下一個請求,如果沒有取消會利用
Handler發送一個
Runnable回調
Listener.onStart()方法通知主線程請求開始了,接著去執行
Request,其中最重要的一句就是調用
IRestParse的地方,這裡正好印證了我們最開始講的,在子線程中利用
IRestParse去發送請求並解析結果成泛型:
// 用IRestParser發送請求,並解析結果,這就是上文中說過的。
Response response = mIRestParser.parserRequest(request);
執行完請求後再次利用
Handler發送一個
Runnable回調
Listener.onFinish()方法通知主線程網絡請求執行完了:
// 發送請求完成回調到主線程。
final ThreadPoster finishThread = new ThreadPoster(what, responseListener);
finishThread.onFinished();
PosterHandler.getInstance().post(finishThread);
request.finish();
// 發送請求結果到主線程,如果請求在請求過程中被取消,則不發送。
if (request.isCanceled())
Logger.d(request.url() + " finish, but it's canceled.");
else {
final ThreadPoster responseThread = new ThreadPoster(what, responseListener);
responseThread.onResponse(response);
PosterHandler.getInstance().post(responseThread);
}
可以看到如果回調了
onStart()則一定會回調
onFinish(),所以我們在
OnResponseListener的
onStart和
onFinish非常適合做一個
Dilaog的顯示和關閉。
因為請求網絡可能耗時比較長或者網絡不好超時等因素,用戶可能會取消這個請求,所以我們看到在回調了
onFinish()後在發送結果到主線程前NoHttp又做了一個請求是否被取消的判斷,綜上所述我們得出一系列結論:
我們可以在onStart()時顯示了一個Dialog,那麼我們可以在onFinish()關閉Dialog。 如果請求開始之前就取消了,那這個請求不會被執行。 如果請求已經開始了,但是被中途取消,onFinish()還是會被回調,所以在這裡關閉Dialog是非常合適的;同時請求若是被中途取消,那麼也一定不會回調onSucceed()和onFailed()了,這裡就涉及到取消請求了,用戶退出當前頁面會不會發生內存洩漏的問題,答案自然是不會。
IRestParser和它的實現類RestParser
看完了
RequestQueue和
RequestDispatcher後發現,裡面除了和主線程的交互外,就是和網絡的交互和結果如果解析了。
由上可以看到在創建隊列、啟動子線程到真正的執行請求,最終都需要響應解析者
IRestParser,我們來看下它的源代碼:
public interface IRestParser {
/**
* 請求網絡並且解析結果。
*/
Response parserRequest(IParserRequest request);
}
根據源碼和流程圖的結構來看,它負責針對
IRestRequest解析出數據並返回
Response,數據肯定是來自網絡,因此它也怎麼從網絡拿到數據並解析成我們想要的泛型結果就是重點了。但它只是一個接口,想知道它是如何實現的,就得看NoHttp為它提供的默認實現類怎麼請求網絡並返回數據了。
我們選中
IRestParser,鍵盤上按下
Ctrl + T(也許你的快捷鍵跟我不一樣)就看到了接口的實現類,同時NoHttp為它提供的默認實現在創建隊列的時候可以看到
RestProtocol
public static RequestQueue newRequestQueue(IRestProtocol iRestProtocol, int threadPoolSize) {
return newRequestQueue(RestParser.getInstance(iRestProtocol), threadPoolSize);
}
我們可以看到源碼是用
RestParser.getInstance(iRestProtocol)單例模式從
RestParser獲取
IRestParser的實例,因此下面接著就要撸
RestParser的源碼了:
public class RestParser implements IRestParser {
private static RestParser _INSTANCE;
private final IRestProtocol mIRestProtocol;
public static IRestParser getInstance(IRestProtocol implRestConnection) {
synchronized (RestParser.class) {
if (_INSTANCE == null)
_INSTANCE = new RestParser(implRestConnection);
return _INSTANCE;
}
}
private RestParser(IRestProtocol iRestProtocol) {
this.mIRestProtocol = iRestProtocol;
}
@Override
public Response parserRequest(IParserRequest request) {
long startTime = SystemClock.elapsedRealtime(); // 記錄請求開始的時間。
// 調用Http協議處理器IRestProtocol分析Request並完成網絡請求拿到Response結果。
ProtocolResult httpResponse = mIRestProtocol.requestNetwork(request);
boolean isFromCache = httpResponse.isFromCache(); // 是否來自緩存的結果。
Headers responseHeaders = httpResponse.responseHeaders(); // 服務器相應頭。
Exception exception = httpResponse.exception(); // 請求時是否發生異常。
T result = null;
byte[] responseBody = httpResponse.responseBody(); // 服務器響應內容。
if (exception == null) {
try {
// 反調用IParserRequest去解析泛型結果。
result = request.parseResponse(responseHeaders, responseBody);
} catch (Throwable e) {
exception = new ParseError("Parse data error: " + e.getMessage());
}
}
return new RestResponse(request, isFromCache, responseHeaders,
result, SystemClock.elapsedRealtime() - startTime, exception);
}
}
這裡的單例和構造方法不多說,如果這都看不懂的話你可能不太適合做編程,我們接著分析
parserRequest()方法。
parserRequest()中是真正的請求網絡,這裡可以理解為同步請求的開始,所以看到這裡不是有了靈感,NoHttp的同步請求一並看了,這也是NoHttp設計上的優點,同步請求更多的內容看本文最後。繼續說,
parserRequest()開始時記錄了請求開始時間,然後立刻調用網絡協議處理器
IRestProtocol請求網絡:
// 調用Http協議處理器IRestProtocol分析Request並完成網絡請求拿到Response結果。
ProtocolResult httpResponse = mIRestProtocol.requestNetwork(request);
完成網絡協議請求後拿到
ByteArray後調用
IParserRequest.parseResponse()解析泛型結果,所以具體解析成什麼結果是由
IParserRequest決定的(題外話:因此我們在自定義請求時,繼承RestRequest需要重寫parseResponse解析結果),解析完
ByteArray成泛型的結果後把異常、是否來自緩存和結果封裝成
RestResponse返回,剩下的事情就是上文中分析過的
RequestDispatcher處理了,如果你忘記了
RequestDispatcher的邏輯,可以回頭去看看。
其實到這裡這個過程就完了,但是大家肯定要吐槽我了:你特喵的扯什麼淡呢,連一點點和http相關的都沒看到。嗯這就對了,我們上面看到和Http相關的的網絡請求是通過
IRestProtoco這個接口發出的,so,接下來該撸
IRestProtocol這個接口了。
IRestProtocol和它的實現類RestProtocol
IRestProtocol文章開頭就說道了,它是一個接口,沒啥好分析的,直接打開看源碼:
public interface IRestProtocol {
/**
* 解析Http協議相關參數,完成網絡請求。
*/
ProtocolResult requestNetwork(IProtocolRequest request);
}
根據源碼和流程圖的結構來看,它負責針對
IProtocolRequest解析Http協議參數,並發起網絡請求返回請求的結果。因此我們需要關心的地方是它如何處理Http協議的參數,so我們接著看NoHttp為它提供的默認實現類怎麼請求網絡並返回數據了。
引入
IRestProtocol:
選中IRestProtocol,鍵盤上按下Ctrl + T(也許你的快捷鍵跟我不一樣)就看到了接口的實現類,或者回去看創建RequestQueue時,獲取IRestParser的單例時:
public static RequestQueue newRequestQueue(Cache cache, int threadPoolSize) {
return newRequestQueue(RestProtocol.getInstance(cache), threadPoolSize);
}
public static RequestQueue newRequestQueue(IRestProtocol iRestProtocol, int threadPoolSize) {
return newRequestQueue(RestParser.getInstance(iRestProtocol), threadPoolSize);
}
public static RequestQueue newRequestQueue(IRestParser implRestParser, int threadPoolSize) {
RequestQueue requestQueue = new RequestQueue(implRestParser, threadPoolSize);
requestQueue.start();
return requestQueue;
}
這裡可以看到最終創建隊列時需要一個
IRestParser,而在上一個方法獲取
IRestParser時需要一個
IRestProtocol接口作為參數,在最上面的方法中看到
IRestProtocol的是由
RestProtocol.getInstance(cache)生成的,所以NoHttp為
IRestProtocol提供的默認實現類是
RestProtocol。
ProtocolResult requestNetwork(IProtocolRequest request);
由於這個類代碼有點多,我們一點點的來分析,首先就是
IRestProtocol必須要實現的方法
requestNetwork():
public class RestProtocol extends BasicConnection implements IRestProtocol {
@Override
public ProtocolResult requestNetwork(IProtocolRequest request) {
// 處理Http緩存頭。
CacheMode cacheMode = request.getCacheMode();
String cacheKey = request.getCacheKey();
CacheEntity cEntity = mCache.get(cacheKey);
ProtocolResult result;
// 根據緩存模式處理。
switch (cacheMode) {
case ONLY_READ_CACHE:// 只讀緩存.
if (cEntity == null) {
return new ProtocolResult(null, null, true, new NotFoundCacheError("沒找到緩存"));
} else {
return new ProtocolResult(cEntity.getResponseHeaders(), cEntity.getData(), true, null);
}
case ONLY_REQUEST_NETWORK:// 僅僅請求網絡.
result = getHttpResponse(request);
break;
case NONE_CACHE_REQUEST_NETWORK:// 先讀緩存,沒緩存再請求網絡。
if (cEntity == null) {
result = getHttpResponse(request);// 請求網絡。
} else {
return new ProtocolResult(cEntity.getResponseHeaders(), cEntity.getData(), true, null);
}
break;
case REQUEST_NETWORK_FAILED_READ_CACHE:// 請求網絡失敗後讀緩存。
if (cEntity != null)
setRequestCacheHeader(request, cEntity); // 緩存存在時設置緩存頭。
result = getHttpResponse(request);// 請求網絡。
break;
default:// 按照Http標准協議,304。
if (cEntity != null) {
// 緩存沒失效直接返回。
if (cEntity.getLocalExpire() > System.currentTimeMillis()) {
return new ProtocolResult(cEntity.getResponseHeaders(), cEntity.getData(), true, null);
}
// 緩存失敗但存在,時設置緩存頭。
setRequestCacheHeader(request, cEntity);
}
result = getHttpResponse(request);// 請求網絡。
break;
}
return handleResponseCache(request, cEntity, result); // 處理響應數據,緩存協議等。
}
大家千萬不要忘記了看類的繼承關系,
RestProtocol實現了
IRestProtocol接口,並且繼承
BasicConnection這個基類。
要注意的兩點:
第一:NoHttp的幾種緩存模式:
只讀緩存 只請求緩存 先讀緩存,沒有緩存再請求網絡 先請求網絡,請求網絡失敗再讀取緩存 按照Http標准協議,重定向緩存機制
第二:此方法中出現的幾個未知的方法:
setRequestCacheHeader(request, cacheEntity); // 為請求設置緩存頭。 getHttpResponse(request); // 真正的請求網絡。 handleResponseCache(request, cEntity, result); // 處理響應結果、緩存等。
setRequestCacheHeader(request, cacheEntity);
這個方法是在請求之前為Request添加和緩存協議有關的請求頭,這是Http標准協議的內容,
private void setRequestCacheHeader(IProtocolRequest request, CacheEntity cacheEntity) {
if (cacheEntity == null) { // 如果緩存為空,移除緩存相關請求頭。
request.headers().remove("If-None-Match");
request.headers().remove("If-Modified-Since");
} else { // 緩存不為空則添加相關請求頭。
Headers headers = cacheEntity.getResponseHeaders();
String eTag = headers.getETag();
if (eTag != null) {
request.headers().set("If-None-Match", eTag);
}
long lastModified = headers.getLastModified();
if (lastModified > 0) {
request.headers().set("If-Modified-Since", HeaderUtil.formatMillisToGMT(lastModified));
}
}
}
這裡需要解釋一下Http的緩存協議了。如果服務器支持http標准的緩存,當我們第一次請求服務器的時候,服務器會在響應頭中添加
Last-Modified頭,這個頭的含義是服務器最後一次修改
響應body的時間,如果服務器支持設置緩存有效期,還會添加一個
E-Tag的頭,這個頭是可以理解為
響應body的一個
tag。客戶端接受到響應頭到,會把這兩個相應頭保存起來,在第二次請求的時候會首先檢查
響應body是否過期,如果沒有過期則直接使用上次的
響應body,也就是我們在
requestNetwork()方法中看到的:
default:// 按照Http標准協議,304。
if (cEntity != null) {
// 緩存沒失效直接返回。
if (cEntity.getLocalExpire() > System.currentTimeMillis()) {
return new ProtocolResult(cEntity.getResponseHeaders(), cEntity.getData(), true, null);
}
// 緩存失敗但存在,時設置緩存頭。
setRequestCacheHeader(request, cEntity);
}
result = getHttpResponse(request);// 請求網絡。
break;
如果
響應body過期,那麼客戶端應該重新請求服務器,並且在請求頭中添加兩個請求頭,第一個是
If-None-Match頭,這個頭的值應該是上次請求服務器時,服務器返回的
E-Tag,第二個要添加的請求頭是
If-Modified-Since,這個頭的值應該是上次服務器返回的
Last-Modified的值。
服務器接受到請求後會用
If-None-Match和
If-Modified-Since和服務器現在
E-Tag和
Last-Modified做對比,判斷客戶端上次的緩存是否過期。如果沒有過期則返回304響應碼,並且不會向
響應body中寫入內容,客戶端接受到這個響應後,判斷響應碼是304時應該從客戶端拿上次的緩存數據;如果過期則返回200段的響應碼,並且會向
響應body中寫入新的內容,客戶端接受到這個響應後,判斷響應碼是200段,應該重新從
響應body中讀取內容。
這就是Http標准協議中的緩存協議內容,NoHttp做到了完美的支持。
getHttpResponse(request);
/**
* 真正的請求網絡。
*/
private ProtocolResult getHttpResponse(IProtocolRequest request) {
byte[] responseBody = null;
Connection connection = getConnection(request); // 從BasicConnection拿到網絡連接和響應內容。
Headers responseHeaders = connection.responseHeaders();
Exception exception = connection.exception();
if (exception == null) {
// 判斷是否有響應內容,比如304響應碼就沒有body。
if (hasResponseBody(request.getRequestMethod(), responseHeaders.getResponseCode()))
try {
// 把服務器響應body轉為ByteArray。
responseBody = IOUtils.toByteArray(connection.serverStream());
} catch (IOException e) {// IOException.
exception = e;
}
}
IOUtils.closeQuietly(connection); // 關閉服務器流。
// 返回響應內容。
return new ProtocolResult(responseHeaders, responseBody, exception != null, exception);
}
其中
Connection connection = getConnection(request); 。裡面才是真正的建立網絡請求,發送數據、上傳文件等操作,我將會新開一篇博客專門來講
BasicConnection類。
handleResponseCache(request, cEntity, result);
private ProtocolResult handleResponseCache(IProtocolRequest request, CacheEntity localCacheEntity, ProtocolResult httpResponse) {
boolean isFromCache = false;
Headers responseHeaders = httpResponse.responseHeaders();
byte[] responseBody = httpResponse.responseBody();
Exception exception = httpResponse.exception();
CacheMode cacheMode = request.getCacheMode();
int responseCode = responseHeaders.getResponseCode();
if (exception == null) {// 沒有發生異常,請求成功。
if (responseCode == 304) {
isFromCache = true;
if (localCacheEntity == null) { // 服務器304,但本地沒有緩存,兼容服務器bug。
responseBody = new byte[0];
} else {
// 更新相應頭信息。
localCacheEntity.getResponseHeaders().setAll(responseHeaders);
responseHeaders = localCacheEntity.getResponseHeaders();
localCacheEntity.setLocalExpire(HeaderUtil.getLocalExpires(responseHeaders));
// 讀取本地緩存數據。
responseBody = localCacheEntity.getData();
}
} else if (responseBody != null) {// 響應碼非304,並且有響應內容。
if (localCacheEntity == null) { // 如果本地沒緩存,則根據http協議進行緩存。
// 解析服務器緩存頭。
localCacheEntity = HeaderUtil.parseCacheHeaders(...);
// 這裡解析出來也許為空,因為服務器可能不允許緩存:no-cache,no-store。
} else {
// 如果本地已經有緩存,則更新數據。
localCacheEntity.getResponseHeaders().setAll(responseHeaders);
localCacheEntity.setLocalExpire(HeaderUtil.getLocalExpires(responseHeaders));
localCacheEntity.setData(responseBody);
}
}
if (localCacheEntity != null) {
// 解析響應頭後服務區允許緩存或者已經有緩存,更新數據庫緩存數據。
mCache.replace(request.getCacheKey(), localCacheEntity);
}
} else if (cacheMode == CacheMode.REQUEST_NETWORK_FAILED_READ_CACHE
&& localCacheEntity != null) {
// 如果請求失敗,但是緩存模式是請求失敗後讀緩存,那麼讀緩存數據。
exception = null;
isFromCache = true;
responseHeaders = localCacheEntity.getResponseHeaders();
responseBody = localCacheEntity.getData();
}
return new ProtocolResult(responseHeaders, responseBody, isFromCache, exception);
}
這個類的作用是根據服務器響應碼、服務器響應頭、本地數據等處理和緩存相關的邏輯,大家可以認真看下源碼和邏輯。主要做的事情是,解析服務器響應碼、響應內容,根據標准的http協議來決定是否緩存數據,或者更新上次的緩存數據。
總結
今天我們的請求模塊到這裡就解析完了,看完文章的同學也許還會有不明白的地方,歡迎大家在博客下方留言。
還剩下BasicConnection,因為它的能力請求網絡、發送數據、上傳文件、拿到服務器響應頭、拿到服務器響應body、拿到服務器的流等內容較多,我會專門開一篇新的博客專門講這個類。
RequestQueue是請求隊列,在主線程執行,主線程可以添加
Request到請求隊列中,等待子線程輪詢讀取
Request去執行。
RequestDispatcher是請求分發者,它是一個子線程,作用是輪詢請求隊列,拿到請求後調用
IRestParser執行請求結果,負責發送開始請求、結束請求、把結果發送到主線程等重要任務。
2.1
RequestQueue、
RequestDispatcher可以接受的請求類是
Request接口,
Request接口繼承自
IParserRequest接口,
Request接口負責記錄響應監聽器Listener和監聽器的what對象。
IRestParser是請求結果解析者,它在子線程中運行,負責把從網絡請求回來的ByteArray解析成開發者想要的泛型對象。
3.1
IRestParser接口帶有泛型,可以接受到的請求類是
IParserRequest接口,
IParserRequest接口繼承自
IRestPtotocol接口,
IParserRequest接口負責具體實現解析成開發者想要的泛型對象,由
IRestParser具體調用。
IRestProtocol是協議處理器,它在子線程中運行,負責從網絡請求結果,包括Header、Body(ByteArray),並且在請求到結果前後處理緩存協議(其他協議待加)等。
4.1
IRestProtocol接口可以接受的請求類是
IProtocolRequest接口,
IProtocolRequest接口繼承自
IBasicRequest接口,
IProtocolRequest接口負責記錄客戶端自定義Http協議等,例如緩存模式,緩存Key等。
BasicConnection是基礎的網絡訪問類,它的能力:能力請求網絡、發送數據、上傳文件、拿到服務器響應頭、拿到服務器響應body、拿到服務器的流等,是下載模塊和請求模塊的通用網絡請求模塊。
5.1
BasicConnection可以接受的請求類是
IBasicRequest接口,
IBasicRequest接口記錄了基礎的請求屬性、通用的網絡設置、參數設置、Cookie、重試機制,下載和請求模塊通用。
之前寫過一篇屏幕適配的文章Android 屏幕適配最佳實踐,裡面提到了類似百分比布局的東西,但是該方法缺點很明顯,就會增加很多無用的數據,導致apk包變大。而谷歌的sup
之前網上看了下自定義消息欄,通知欄,了解到了Notification這個控件,發現UC浏覽器等都是這種類型,今天寫個demo實現下,如圖:其中每個按鈕都有不同的功能,代碼
說明本來很懶,但是還是會忍不住的寫下這有用既沒有用的所謂技術博客,希望會給你帶來有所啟發,因為這樣的功能,寫的人很多,也是為了自己能夠理解的夠透徹,也是為了大家也能更好的
1. 程序截圖 拖動紅色區域,可以顯示出清晰的汽車部分。拖動下面的滑塊,可以更改模糊程度。 2. 程序實現方法 實現思路,用FrameLayout搞了