編輯:關於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>());
clientHeaderMap = new HashMap();
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> exceptionWhitelist = new HashSet>();
private final static HashSet> exceptionBlacklist = new HashSet>();
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> 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封裝起來了,然後處理消息之後,通過抽象方法或者是接口將最後的結果回調出來,讓實現類去處理具體的業務邏輯了。
總結
最後我畫一張圖來簡單的描述一下網絡請求的一個基本原理,用一個非常生動的工廠的例子來說明一下。
編程能力)比較強,對於制衣服的工具(httpclient)了解的比較透徹,這樣子就可以教工人更好的調試制衣服的工具。因為善於協調各個部門的工作關系以及善於給工人洗腦嘛(也制代碼寫的好),所以最後做出來的衣服的質量和數量就比別人更厲害的,同樣的每個人都是一個小老板,你也可以自己慢慢的嘗試的去管理一個工廠(寫一個自己的框架),雖然前期的產出的質量和數量不怎麼好,但是後面你慢慢看多了別人的經驗(代碼)之後,你也慢慢的更會總結經驗了。這裡我就用一個最淺顯的比喻來描述了一下網絡請求的框架。
如何使用ES文件浏覽器的遠程管理。ES文件浏覽器是在android手機上常見的手機文件管理器,在基本的文件功能之上,它還支持開啟其他設備通過WiFi遠程連接
打開/dev/graphics/fb0節點的過程:打開/dev/graphics/fb0這個設備的調用過程如下:1.在HWComposer中,加載moduleHWComp
本示例以Servlet為例,演示Android與Servlet的通信。眾所周知,Android與服務器通信通常采用HTTP通信方式和Socket通信方式,而HTTP通信方
可以使用該套 SDK開發適用於Android系統移動設備的地圖應用,通過調用地圖SDK接口,您可以輕松訪問百度地圖服務和數據,構建功能豐富、交互性強的LBS(地圖類)應用