深入淺出的理解Android網路請求框架
以前在寫網路請求的時候,我們經常乾的一件事情就是首先開啟一個執行緒,然後建立一個Handler將網路請求的操作放到一個執行緒中去執行。因為大家都知道網路請求是一個耗時的操作,不能放在主執行緒中去執行,但是我們發現我們用框架去處理網路請求的時候,程式碼量確實非常的少同時還不用寫那些繁瑣的Handler去處理資料,只需要一個回撥介面就可以處理我們的一些業務邏輯就可以了。現在就來分析一下這其中的來龍去脈,因為我平時用的比較多的請求框架是 async-http,這裡我就對這個框架進行分析一下: https://github.com/loopj/android-async-http
根據它官方的例子,我們介紹一下它最基本的大概的用法:
AsyncHttpClient httpClient = new AsyncHttpClient(); httpClient.get("http://www.baidu.com", new AsyncHttpResponseHandler() { @Override public void onSuccess(int code, Header[] headers, byte[] responseBody) { //請求成功的回撥處理 } @Override public void onFailure(int code, Header[] headers, byte[] responseBody, Throwable error){ //請求失敗的回撥處理 } });
我們平時在使用網路請求框架的時候見到最多的形式就是這種模式,一個回撥介面,然後一個請求的url地址,在福州一些的話,可能會有一些什麼請求頭之類的等等,而且為什麼人家就不需要使用繁瑣的Handler呢?而且還可以在回撥方法中更新UI介面的。下面就進入程式碼中看看。
1. 首先建立一個AsyncHttpClient物件
public AsyncHttpClient() { this(false, 80, 443); } public AsyncHttpClient(int httpPort) { this(false, httpPort, 443); } public AsyncHttpClient(int httpPort, int httpsPort) { this(false, httpPort, httpsPort); } public AsyncHttpClient(boolean fixNoHttpResponseException, int httpPort, int httpsPort) { this(getDefaultSchemeRegistry(fixNoHttpResponseException, httpPort, httpsPort)); } public AsyncHttpClient(SchemeRegistry schemeRegistry) { BasicHttpParams httpParams = new BasicHttpParams(); ConnManagerParams.setTimeout(httpParams, connectTimeout); ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(maxConnections)); ConnManagerParams.setMaxTotalConnections(httpParams, DEFAULT_MAX_CONNECTIONS); HttpConnectionParams.setSoTimeout(httpParams, responseTimeout); HttpConnectionParams.setConnectionTimeout(httpParams, connectTimeout); HttpConnectionParams.setTcpNoDelay(httpParams, true); HttpConnectionParams.setSocketBufferSize(httpParams, DEFAULT_SOCKET_BUFFER_SIZE); HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1); ClientConnectionManager cm = createConnectionManager(schemeRegistry, httpParams); threadPool = getDefaultThreadPool(); requestMap = Collections.synchronizedMap(new WeakHashMap<Context, List<RequestHandle>>()); clientHeaderMap = new HashMap<String, String>(); httpContext = new SyncBasicHttpContext(new BasicHttpContext()); httpClient = new DefaultHttpClient(cm, httpParams); httpClient.addRequestInterceptor(new HttpRequestInterceptor() { @Override public void process(HttpRequest request, HttpContext context) { ...... } }); httpClient.addResponseInterceptor(new HttpResponseInterceptor() { @Override public void process(HttpResponse response, HttpContext context) { ...... } }); httpClient.addRequestInterceptor(new HttpRequestInterceptor() { @Override public void process(final HttpRequest request, final HttpContext context) { ...... } }, 0); httpClient.setHttpRequestRetryHandler(new RetryHandler(DEFAULT_MAX_RETRIES, DEFAULT_RETRY_SLEEP_TIME_MILLIS)); }
構造方法中主要做了以下的事情:
1、四個構造方法分別對http和https的埠設定,同時也對http和https註冊Scheme
2、建立httpclient、httpContext物件,同時設定httpclient的讀取資料超時的時間、連線超時的時間、http協議的版本號、socket快取大小
3、對httpclient設定重試次數以及重試的時間間隔,同時為HttpClient新增Request 攔截器以及Response 攔截器,建立Cached ThreadPool執行緒池.
4、對httpclient設定重試的處理方式 setHttpRequestRetryHandler(),我們這裡自定義了一個重試處理的類RetryHandler;
2.接下來就是為get 方法請求網路做準備工作
public RequestHandle get(String url, RequestParams params, ResponseHandlerInterface responseHandler) {
return get(null, url, params, responseHandler);
}
/**
* Perform a HTTP GET request and track the Android Context which initiated the request.
*
* @param context the Android Context which initiated the request.
* @param url the URL to send the request to.
* @param params additional GET parameters to send with the request.
* @param responseHandler the response handler instance that should handle the response.
* @return RequestHandle of future request process
*/
public RequestHandle get(Context context, String url, RequestParams params,
ResponseHandlerInterface responseHandler) {
return sendRequest(httpClient, httpContext,
new HttpGet(getUrlWithQueryString(isUrlEncodingEnabled, url, params)),
null, responseHandler, context);
}
1、首先是將請求的引數進行拆分然後轉化成一個字串
2、將url進行decode,然後按照指定的規則再將請求引數拼接到url的後面
public static String getUrlWithQueryString(boolean shouldEncodeUrl, String url, RequestParams params) {
if (url == null)
return null;
if (shouldEncodeUrl) {//這裡預設是需要開啟 url encode 功能的。
try {
String decodedURL = URLDecoder.decode(url, "UTF-8");
URL _url = new URL(decodedURL);
URI _uri = new URI(_url.getProtocol(), _url.getUserInfo(), _url.getHost(), _url.getPort(), _url.getPath(), _url.getQuery(), _url.getRef());
url = _uri.toASCIIString();
} catch (Exception ex) {
log.e(LOG_TAG, "getUrlWithQueryString encoding URL", ex);
}
}
if (params != null) {
//首先我們之前設定的請求進行分解,然後將這些請求資訊拼接成一個字串。
String paramString = params.getParamString().trim();
//我們都知道平時瀏覽器中的請求地址後面是url + ? + params,這裡也這麼處理的
如果有 ? 存在的話表示之前就跟有請求引數了,如果還沒有問題,就新增問號並且新增請求引數
if (!paramString.equals("") && !paramString.equals("?")) {
url += url.contains("?") ? "&" : "?";
url += paramString;
}
}
return url;
}
3.正式的請求網路操作
protected RequestHandle sendRequest(DefaultHttpClient client, HttpContext httpContext,
HttpUriRequest uriRequest, String contentType,
ResponseHandlerInterface responseHandler, Context context) {
//1、首先一上來就對uriRequest, responseHandler引數為空檢查
......
//2、然後接著就是判斷Content-Type型別,設定到請求頭中去
if (contentType != null) {
if (uriRequest instanceof HttpEntityEnclosingRequestBase
&& ((HttpEntityEnclosingRequestBase) uriRequest).getEntity() != null
&& uriRequest.containsHeader(HEADER_CONTENT_TYPE)) {
log.w(LOG_TAG, "Passed contentType will be ignored because HttpEntity sets content type");
} else {
uriRequest.setHeader(HEADER_CONTENT_TYPE, contentType);
}
}
//3、接著就是將請求引數和請求的url地址儲存到responseHandler
responseHandler.setRequestHeaders(uriRequest.getAllHeaders());
responseHandler.setRequestURI(uriRequest.getURI());
4、接著就是建立一個請求任務了,也就是HttpRequest.
AsyncHttpRequest request = newAsyncHttpRequest(client, httpContext, uriRequest, contentType, responseHandler, context);
5、然後將任務扔到之前建立的中的執行緒池中去執行.
threadPool.submit(request);
RequestHandle requestHandle = new RequestHandle(request);
.......
return requestHandle;
}
通過上面的程式碼中,我們還是非常的懵懂的不清楚具體的請求網路是在哪裡,我們知道了 AsyncHttpRequest 任務是線上程池中在執行了,那麼我們就有一點是非常的肯定的認為該類基本上是實現了Runnable介面的,所以請求的操作肯定是在 AsyncHttpRequest 中處理的。
@Override
public void run() {
//首先是檢查是否是取消
if (isCancelled()) {
return;
}
......
//這裡這個 responseHandler 只是一個回撥介面,別被這個給迷惑了
responseHandler.sendStartMessage();
try {
makeRequestWithRetries();
} catch (IOException e) {
if (!isCancelled()) {
//這裡是處理請求失敗的回撥結果
responseHandler.sendFailureMessage(0, null, null, e);
} else {
AsyncHttpClient.log.e("AsyncHttpRequest", "makeRequestWithRetries returned error", e);
}
}
//執行完畢之後的回撥介面
responseHandler.sendFinishMessage();
......
}
private void makeRequestWithRetries() throws IOException {
boolean retry = true;
IOException cause = null;
HttpRequestRetryHandler retryHandler = client.getHttpRequestRetryHandler();
try {
while (retry) {
try {
makeRequest();
return;
} catch (UnknownHostException e) {
// switching between WI-FI and mobile data networks can cause a retry which then results in an UnknownHostException
// while the WI-FI is initialising. The retry logic will be invoked here, if this is NOT the first retry
// (to assist in genuine cases of unknown host) which seems better than outright failure
cause = new IOException("UnknownHostException exception: " + e.getMessage(), e);
retry = (executionCount > 0) && retryHandler.retryRequest(e, ++executionCount, context);
} catch (NullPointerException e) {
// there's a bug in HttpClient 4.0.x that on some occasions causes
// DefaultRequestExecutor to throw an NPE, see
// https://code.google.com/p/android/issues/detail?id=5255
cause = new IOException("NPE in HttpClient: " + e.getMessage());
retry = retryHandler.retryRequest(cause, ++executionCount, context);
} catch (IOException e) {
if (isCancelled()) {
// Eating exception, as the request was cancelled
return;
}
cause = e;
retry = retryHandler.retryRequest(cause, ++executionCount, context);
}
if (retry) {
responseHandler.sendRetryMessage(executionCount);
}
}
} catch (Exception e) {
// catch anything else to ensure failure message is propagated
AsyncHttpClient.log.e("AsyncHttpRequest", "Unhandled exception origin cause", e);
cause = new IOException("Unhandled exception: " + e.getMessage(), cause);
}
// cleaned up to throw IOException
throw (cause);
}
從上面的程式碼中我們可以看出只要makeRequest函式不丟擲異常的話就直接返回也就不走迴圈了,要是在請求的過程中出現了問題的話,那麼捕獲異常並且檢查重試的次數,要是次數達到了指定的重試的次數之後就跳出整個迴圈,並且丟擲一個異常讓呼叫者去處理就可以了。接下來我們來看看人家是如何處理重試的問題的,當然這個重試的問題我也可以自己利用一個迴圈去處理的,但是既然Httpclient已經提供瞭解決方案給我們,我也就可以直接繼承人家的方案就可以了。
class RetryHandler implements HttpRequestRetryHandler {
private final static HashSet<Class<?>> exceptionWhitelist = new HashSet<Class<?>>();
private final static HashSet<Class<?>> exceptionBlacklist = new HashSet<Class<?>>();
static {
// Retry if the server dropped connection on us
exceptionWhitelist.add(NoHttpResponseException.class);
// retry-this, since it may happens as part of a Wi-Fi to 3G failover
exceptionWhitelist.add(UnknownHostException.class);
// retry-this, since it may happens as part of a Wi-Fi to 3G failover
exceptionWhitelist.add(SocketException.class);
// never retry timeouts
exceptionBlacklist.add(InterruptedIOException.class);
// never retry SSL handshake failures
exceptionBlacklist.add(SSLException.class);
}
private final int maxRetries;
private final int retrySleepTimeMS;
public RetryHandler(int maxRetries, int retrySleepTimeMS) {
this.maxRetries = maxRetries;
this.retrySleepTimeMS = retrySleepTimeMS;
}
.....
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
boolean retry = true;
if (executionCount > maxRetries) {
// Do not retry if over max retry count
retry = false;
} else if (isInList(exceptionWhitelist, exception)) {
// immediately retry if error is whitelisted
retry = true;
} else if (isInList(exceptionBlacklist, exception)) {
// immediately cancel retry if the error is blacklisted
retry = false;
} else if (!sent) {
// for most other errors, retry only if request hasn't been fully sent yet
retry = true;
}
......
if (retry) {
SystemClock.sleep(retrySleepTimeMS);
} else {
exception.printStackTrace();
}
return retry;
}
protected boolean isInList(HashSet<Class<?>> list, Throwable error) {
for (Class<?> aList : list) {
if (aList.isInstance(error)) {
return true;
}
}
return false;
}
}
首先在構造方法中就有了兩個set集合用於存放哪些白名單異常和黑名單異常,所謂的白名單的異常是指一些網路切換時出現的問題,伺服器端的異常,這個時候出現異常的話我們就需要重試請求;所謂的黑名單異常就是比如說https的驗證出現了問題了,還有就是中斷異常,這個時候出現這個異常的時候我們就不需要進行重試了,因為你重試也沒用處的,會一直的失敗,還不如節省時間。我們在呼叫 retryRequest()方法的時候,會傳入一個異常物件,還有就是次數,然後就是httpContext,傳入的次數主要是用於跟我們之前設定的的最大重試次數比較。然後傳入的異常看看該異常是否是白名單還是黑名單的來決定是否重試的,最後我們還看到了一個重試的間隔時間,SystemClock.sleep() 函式最後其實還是呼叫的Thread.sleep(),只是對這個進行了封裝一下而已。
在研究完重試機制之後我們接著剛才的網路請求來看看,因為我們只看到了處理開始的訊息和結束的訊息,但是我們並沒有看到真正的網路請求的資訊。
private void makeRequest() throws IOException {
......
// Fixes #115
if (request.getURI().getScheme() == null) {
// subclass of IOException so processed in the caller
throw new MalformedURLException("No valid URI scheme was provided");
}
if (responseHandler instanceof RangeFileAsyncHttpResponseHandler) {
((RangeFileAsyncHttpResponseHandler) responseHandler).updateRequestHeaders(request);
}
HttpResponse response = client.execute(request, context);
......
// The response is ready, handle it.
responseHandler.sendResponseMessage(response);
.....
// Carry out post-processing for this response.
responseHandler.onPostProcessResponse(responseHandler, response);
}
從上面的程式碼中是不是可以很簡單的看出 client.execute()才是真正的執行網路請求的,然後拿到HttpResponse,緊接著就用介面回撥函式將該介面扔出去了,然後實現ResponseHandlerInterface 介面的類去處理介面了,然後我們再回到我們我們最開始的例子中,我們在請求網路的時候是傳入了AsyncHttpResponseHandler 型別的。下面我們就來看看這個類做了什麼處理的。
private Handler handler;
public abstract class AsyncHttpResponseHandler implements ResponseHandlerInterface {
public AsyncHttpResponseHandler() {
this(null);
}
public AsyncHttpResponseHandler(Looper looper) {
// Do not use the pool's thread to fire callbacks by default.
this(looper == null ? Looper.myLooper() : looper, false);
}
private AsyncHttpResponseHandler(Looper looper, boolean usePoolThread) {
if (!usePoolThread) {
this.looper = looper;
// Create a handler on current thread to submit tasks
this.handler = new ResponderHandler(this, looper);
} else {
// If pool thread is to be used, there's no point in keeping a reference
// to the looper and handler.
this.looper = null;
this.handler = null;
}
this.usePoolThread = usePoolThread;
}
}
從上面的構造方法中,我們發現這個類基本上是非常的簡單的,因為 AsyncHttpResponseHandler 是在主執行緒中建立的,因此Looper.myLooper()也是MainLooper的,所以我們知道這裡的Handler是在主執行緒中定義的。下面我們來看看之前請求完網路的之後,然後就呼叫了介面的回撥結果:sendResponseMessage(),所以我們就尋找一下類中的這個方法。
@Override
public void sendResponseMessage(HttpResponse response) throws IOException {
// do not process if request has been cancelled
if (!Thread.currentThread().isInterrupted()) {
StatusLine status = response.getStatusLine();
byte[] responseBody;
responseBody = getResponseData(response.getEntity());
// additional cancellation check as getResponseData() can take non-zero time to process
if (!Thread.currentThread().isInterrupted()) {
if (status.getStatusCode() >= 300) {
sendFailureMessage(status.getStatusCode(), response.getAllHeaders(),
responseBody, new HttpResponseException(status.getStatusCode(), status.getReasonPhrase()));
} else {
sendSuccessMessage(status.getStatusCode(), response.getAllHeaders(), responseBody);
}
}
}
}
/**
* Returns byte array of response HttpEntity contents
*
* @param entity can be null
* @return response entity body or null
* @throws java.io.IOException if reading entity or creating byte array failed
*/
byte[] getResponseData(HttpEntity entity) throws IOException {
byte[] responseBody = null;
if (entity != null) {
InputStream instream = entity.getContent();
if (instream != null) {
long contentLength = entity.getContentLength();
/**
*獲取位元組流的長度,如果位元組流的長度大於整數的最大值的話就直接丟擲異常,
*對於一般的網路資料的話沒有這麼大的,除非是下載的,作者在這裡就直接做了處理
*/
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalArgumentException("HTTP entity too large to be buffered in memory");
}
int buffersize = (contentLength <= 0) ? BUFFER_SIZE : (int) contentLength;
try {
ByteArrayBuffer buffer = new ByteArrayBuffer(buffersize);
try {
byte[] tmp = new byte[BUFFER_SIZE];
long count = 0;
int l;
// do not send messages if request has been cancelled
while ((l = instream.read(tmp)) != -1 && !Thread.currentThread().isInterrupted()) {
count += l;
buffer.append(tmp, 0, l);
sendProgressMessage(count, (contentLength <= 0 ? 1 : contentLength));
}
} finally {
AsyncHttpClient.silentCloseInputStream(instream);
AsyncHttpClient.endEntityViaReflection(entity);
}
responseBody = buffer.toByteArray();
} catch (OutOfMemoryError e) {
System.gc();
throw new IOException("File too large to fit into available memory");
}
}
}
return responseBody;
}
這裡就比較簡單了也就是我們平時所說的對於一個位元組輸入流的處理了,只不過是坐著這裡利用了ByteArrayBuffer,中間利用了一個位元組陣列作為過渡,這樣子就可以大大的提高讀取的速度,最後在轉換成一個位元組陣列返回出去,回到sendResponseMessage 方法中我們對statusCode進行判斷,如果返回的狀態碼 >= 300的話那麼我們就可以斷定這個請求是失敗的了,具體的問題大家去看看http協議就清楚了;接下來看看傳送訊息函式是如何處理的。
final public void sendSuccessMessage(int statusCode, Header[] headers, byte[] responseBytes) {
sendMessage(obtainMessage(SUCCESS_MESSAGE, new Object[]{statusCode, headers, responseBytes}));
}
protected Message obtainMessage(int responseMessageId, Object responseMessageData) {
return Message.obtain(handler, responseMessageId, responseMessageData);
}
protected void sendMessage(Message msg) {
if (getUseSynchronousMode() || handler == null) {
handleMessage(msg);
} else if (!Thread.currentThread().isInterrupted()) {
// do not send messages if request has been cancelled
Utils.asserts(handler != null, "handler should not be null!");
handler.sendMessage(msg);
}
}
protected void handleMessage(Message message) {
Object[] response;
try
{
switch (message.what)
{
case SUCCESS_MESSAGE:
response = (Object[]) message.obj;
if (response != null && response.length >= 3) {
onSuccess((Integer) response[0], (Header[]) response[1], (byte[]) response[2]);
} else {
log.e(LOG_TAG, "SUCCESS_MESSAGE didn't got enough params");
}
break;
case FAILURE_MESSAGE:
response = (Object[]) message.obj;
if (response != null && response.length >= 4) {
onFailure((Integer) response[0], (Header[]) response[1], (byte[]) response[2], (Throwable) response[3]);
} else {
log.e(LOG_TAG, "FAILURE_MESSAGE didn't got enough params");
}
break;
case START_MESSAGE:
onStart();
break;
case FINISH_MESSAGE:
onFinish();
break;
case PROGRESS_MESSAGE:
response = (Object[]) message.obj;
if (response != null && response.length >= 2) {
try {
onProgress((Long) response[0], (Long) response[1]);
} catch (Throwable t) {
log.e(LOG_TAG, "custom onProgress contains an error", t);
}
} else {
log.e(LOG_TAG, "PROGRESS_MESSAGE didn't got enough params");
}
break;
case RETRY_MESSAGE:
response = (Object[]) message.obj;
if (response != null && response.length == 1) {
onRetry((Integer) response[0]);
} else {
log.e(LOG_TAG, "RETRY_MESSAGE didn't get enough params");
}
break;
case CANCEL_MESSAGE:
onCancel();
break;
}
} catch (Throwable error) {
onUserException(error);
}
}
從這裡我們就可以一目瞭然的看到了原來所有的討論都是這樣子的,首先封裝一個Message訊息體,然後訊息中包裝了了一個Object陣列,將我們剛才獲取到的資訊全部放到陣列中的,最後使用handler.sendMessage()發出去,然後這些都是我們平時的一些很平常的程式碼了,最後在呼叫抽象方法 onSuccess() 方法了。這個就是我們例子中的重寫onSuccess 方法,並且能在這個方法中更新UI介面的原因,現在我們知道了為什麼第三方的框架為啥可以在回撥介面中進行 UI 更新了吧:其實人家最後還是用了Handler去處理主執行緒與子執行緒的這種資料互動,只是人家內部把這個Handler封裝起來了,然後處理訊息之後,通過抽象方法或者是介面將最後的結果回調出來,讓實現類去處理具體的業務邏輯了。
總結
最後我畫一張圖來簡單的描述一下網路請求的一個基本原理,用一個非常生動的工廠的例子來說明一下。
根據上面的圖我們來做一個比喻:
- 執行緒池 ————————> 廠長
- 工人 —————————> 任務(實現Runnable介面的任務)
- 製衣服的工具 ——-———> 比作網路請求的基本工具(也就是程式碼中 HttpClient等等一系列吧)
- 客戶 —————————> 比作 主執行緒中的要做的事情
- 銷售人員 ———————> 比作 Handler 也就是傳達事情的信使
- 工程師 ————————> 比作 老闆,這個時候我們就協調好各個的環節,同時我們還要告訴使用工具的一些細節,畢竟工具太高階了嘛,所以需要告訴工人如何正確的使用工具才能更快更好的制好衣服的嘛。
我們編碼的過程的其實也就是一個高階的搬磚的活,我們要協調各個部門的關係,同時我們還要教工人們如何正確的使用工具,該如何配置工具(網路請求的最基本的工具 httpClient或者是HttpUrlConnection)的哪些引數,同時還要把握各個製衣服和銷售衣服的各個環節。廠長就好比執行緒池一樣來管理工人(任務),將工人最大的做事的空間發揮出來,比如說看到某個工人空閒而且有任務來的時候,這個時候廠長就開始分配任務給工人了;當工人們將衣服做完之後(也就是任務結束之後)我們就需要一個專門的人去將這個衣服銷售出去,這個銷售的過程又有專門的人去做了,我們肯定不能讓工人去賣衣服的,因為這個是不安全的,萬一哪個工廠來挖牆腳了就不好了,所以這裡的銷售人員就相當於是(Handler)來分發最後的結果的,與外界打交道的。
Ps:其實市面上的大多數的框架基本上的原理就是這樣子的,只是有些人的指揮能力(程式設計能力)比較強,對於製衣服的工具(httpclient)瞭解的比較透徹,這樣子就可以教工人更好的調試製衣服的工具。因為善於協調各個部門的工作關係以及善於給工人洗腦嘛(也制程式碼寫的好),所以最後做出來的衣服的質量和數量就比別人更厲害的,同樣的每個人都是一個小老闆,你也可以自己慢慢的嘗試的去管理一個工廠(寫一個自己的框架),雖然前期的產出的質量和數量不怎麼好,但是後面你慢慢看多了別人的經驗(程式碼)之後,你也慢慢的更會總結經驗了。這裡我就用一個最淺顯的比喻來描述了一下網路請求的框架。