大家知道Android對UI線程的反應時間要求很高,超過5秒鐘直接ANR掉,根本不給你機會多等。
而Android應用與後端系統的交互是最基本的需求之一,如何實現高效的Asynchronous HTTPClient,確保UI線程在啓動任務後交由後端異步處理與服務器端的通信,尤爲關鍵。
Google過幾個方案,要麼太複雜要麼不符合要求,基本都淘汰了,最後發現這一版本的實現不錯,就拿來用了。
鏈接:Android Asynchronous HTTPClient tutorial
後來發現了幾個嚴重的問題,羅列如下:
1. 啓用單獨的線程後,簡直如脫繮的野馬,難以駕馭。
現象是:在調試的時候經常發現某個線程死掉(比如在服務器down掉的時候,由於線程無法連接而掛掉)
後果是:只能關掉模擬器,甚至還要重啓eclipse,否者兩者通信出現問題,再也不能繼續聯機調試
2. 異常的處理非常弱,Activity層難以捕捉並加以處理。
這個問題跟實現的機制有一定的關係,此實現根本就沒提供好的異常處理機制,以便捕捉、反饋、處理合理的可預見性的異常,諸如:
1)UnknownHostException – 誰能確保手機的網絡連接一直正常,信號一直滿格?
2)HttpResponseException – 後端500的錯誤,說不定就蹦出來了
3)SocketTimeoutException – 超時也是太正常不過了,如果人家在荒山野嶺(no 3G)擺弄超大的通信請求
4)諸如此類吧
所以改造就再說難免了。下面我貼出相關代碼(import就省了吧這裏),並加以簡單註釋說明,方面大家的理解。
首先定義AsyncHttpClient.java。這裏的重點是超時的設置。另外我加了個cancelRequest,用以在切換Activity後取消掉原有Activity發出的所有的異步請求,因爲一般情況下,切換了Activity後是不能再更新那個UI了,否則會拋出異常,直接導致應用crash掉,不過話說回來,這個cancel我發現好像不是那麼給力(any feedback?)。
public class AsyncHttpClient {
private static DefaultHttpClient httpClient;
public static int CONNECTION_TIMEOUT = 2*60*1000;
public static int SOCKET_TIMEOUT = 2*60*1000;
private static ConcurrentHashMap<Activity,AsyncHttpSender> tasks = new ConcurrentHashMap<Activity,AsyncHttpSender>();
public static void sendRequest(
final Activity currentActitity,
final HttpRequest request,
AsyncResponseListener callback) {
sendRequest(currentActitity, request, callback, CONNECTION_TIMEOUT, SOCKET_TIMEOUT);
}
public static void sendRequest(
final Activity currentActitity,
final HttpRequest request,
AsyncResponseListener callback,
int timeoutConnection,
int timeoutSocket) {
InputHolder input = new InputHolder(request, callback);
AsyncHttpSender sender = new AsyncHttpSender();
sender.execute(input);
tasks.put(currentActitity, sender);
}
public static void cancelRequest(final Activity currentActitity){
if(tasks==null || tasks.size()==0) return;
for (Activity key : tasks.keySet()) {
if(currentActitity == key){
AsyncTask<?,?,?> task = tasks.get(key);
if(task.getStatus()!=null && task.getStatus()!=AsyncTask.Status.FINISHED){
Log.i(TAG, "AsyncTask of " + task + " cancelled.");
task.cancel(true);
}
tasks.remove(key);
}
}
}
public static synchronized HttpClient getClient() {
if (httpClient == null){
//use following code to solve Adapter is detached error
//refer: http://stackoverflow.com/questions/5317882/android-handling-back-button-during-asynctask
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
// Set the timeout in milliseconds until a connection is established.
HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
// Set the default socket timeout (SO_TIMEOUT)
// in milliseconds which is the timeout for waiting for data.
HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
httpClient = new DefaultHttpClient(cm, params);
}
return httpClient;
}
}
然後是AsyncHttpSender。這裏我用了InputHolder和OutputHolder來進行對象傳遞,簡單包裝了下:
/**
* AsyncHttpSender is the AsyncTask implementation
*
* @author bright_zheng
*
*/
public class AsyncHttpSender extends AsyncTask<InputHolder, Void, OutputHolder> {
@Override
protected OutputHolder doInBackground(InputHolder... params) {
HttpEntity entity = null;
InputHolder input = params[0];
try {
HttpResponse response = AsyncHttpClient.getClient().execute((HttpUriRequest) input.getRequest());
StatusLine status = response.getStatusLine();
if(status.getStatusCode() >= 300) {
return new OutputHolder(
new HttpResponseException(status.getStatusCode(), status.getReasonPhrase()),
input.getResponseListener());
}
entity = response.getEntity();
Log.i(TAG, "isChunked:" + entity.isChunked());
if(entity != null) {
try{
entity = new BufferedHttpEntity(entity);
}catch(Exception e){
Log.e(TAG, e.getMessage(), e);
//ignore?
}
}
} catch (ClientProtocolException e) {
Log.e(TAG, e.getMessage(), e);
return new OutputHolder(e, input.getResponseListener());
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
return new OutputHolder(e, input.getResponseListener());
}
return new OutputHolder(entity, input.getResponseListener());
}
@Override
protected void onPreExecute(){
Log.i(TAG, "AsyncHttpSender.onPreExecute()");
super.onPreExecute();
}
@Override
protected void onPostExecute(OutputHolder result) {
Log.i(TAG, "AsyncHttpSender.onPostExecute()");
super.onPostExecute(result);
if(isCancelled()){
Log.i(TAG, "AsyncHttpSender.onPostExecute(): isCancelled() is true");
return; //Canceled, do nothing
}
AsyncResponseListener listener = result.getResponseListener();
HttpEntity response = result.getResponse();
Throwable exception = result.getException();
if(response!=null){
Log.i(TAG, "AsyncHttpSender.onResponseReceived(response)");
listener.onResponseReceived(response);
}else{
Log.i(TAG, "AsyncHttpSender.onResponseReceived(exception)");
listener.onResponseReceived(exception);
}
}
@Override
protected void onCancelled(){
Log.i(TAG, "AsyncHttpSender.onCancelled()");
super.onCancelled();
//this.isCancelled = true;
}
}
/**
* Input holder
*
* @author bright_zheng
*
*/
public class InputHolder{
private HttpRequest request;
private AsyncResponseListener responseListener;
public InputHolder(HttpRequest request, AsyncResponseListener responseListener){
this.request = request;
this.responseListener = responseListener;
}
public HttpRequest getRequest() {
return request;
}
public AsyncResponseListener getResponseListener() {
return responseListener;
}
}
public class OutputHolder{
private HttpEntity response;
private Throwable exception;
private AsyncResponseListener responseListener;
public OutputHolder(HttpEntity response, AsyncResponseListener responseListener){
this.response = response;
this.responseListener = responseListener;
}
public OutputHolder(Throwable exception, AsyncResponseListener responseListener){
this.exception = exception;
this.responseListener = responseListener;
}
public HttpEntity getResponse() {
return response;
}
public Throwable getException() {
return exception;
}
public AsyncResponseListener getResponseListener() {
return responseListener;
}
}
再來看看我們的Call back接口定義, AsyncResponseListener.java:
/**
* The call back interface for
*
* @author bright_zheng
*
*/
public interface AsyncResponseListener {
/** Handle successful response */
public void onResponseReceived(HttpEntity response);
/** Handle exception */
public void onResponseReceived(Throwable response);
}
以及抽象Call back的實現,AbstractAsyncResponseListener.java:
/**
* Abstract Async Response Listener implementation
*
* Subclass should implement at lease two methods.
* 1. onSuccess() to handle the corresponding successful response object
* 2. onFailure() to handle the exception if any
*
* @author bright_zheng
*
*/
public abstract class AbstractAsyncResponseListener implements AsyncResponseListener{
public static final int RESPONSE_TYPE_STRING = 1;
public static final int RESPONSE_TYPE_JSON_ARRAY = 2;
public static final int RESPONSE_TYPE_JSON_OBJECT = 3;
public static final int RESPONSE_TYPE_STREAM = 4;
private int responseType;
public AbstractAsyncResponseListener(){
this.responseType = RESPONSE_TYPE_STRING; // default type
}
public AbstractAsyncResponseListener(int responseType){
this.responseType = responseType;
}
public void onResponseReceived(HttpEntity response){
try {
switch(this.responseType){
case RESPONSE_TYPE_JSON_ARRAY:{
String responseBody = EntityUtils.toString(response);
Log.i(TAG, "Return JSON String: " + responseBody);
JSONArray json = null;
if(responseBody!=null && responseBody.trim().length()>0){
json = (JSONArray) new JSONTokener(responseBody).nextValue();
}
onSuccess(json);
break;
}
case RESPONSE_TYPE_JSON_OBJECT:{
String responseBody = EntityUtils.toString(response);
Log.i(TAG, "Return JSON String: " + responseBody);
JSONObject json = null;
if(responseBody!=null && responseBody.trim().length()>0){
json = (JSONObject) new JSONTokener(responseBody).nextValue();
}
onSuccess(json);
break;
}
case RESPONSE_TYPE_STREAM:{
onSuccess(response.getContent());
break;
}
default:{
String responseBody = EntityUtils.toString(response);
onSuccess(responseBody);
}
}
} catch(IOException e) {
onFailure(e);
} catch (JSONException e) {
onFailure(e);
}
}
public void onResponseReceived(Throwable response){
onFailure(response);
}
protected void onSuccess(JSONArray response){}
protected void onSuccess(JSONObject response){}
protected void onSuccess(InputStream response){}
protected void onSuccess(String response) {}
protected void onFailure(Throwable e) {}
}
這樣我們使用起來就非常清晰、簡單了。
下面貼個簡單的客戶端用法代碼片段:
1、這個是把服務器端響應當stream用的,用以諸如文件、圖片下載之類的場景:
AsyncHttpClient.sendRequest(this, request,
new AbstractAsyncResponseListener(AbstractAsyncResponseListener.RESPONSE_TYPE_STREAM){
@Override
protected void onSuccess(InputStream response){
Bitmap bmp = null;
try {
//bmp = decodeFile(response, _facial.getWidth());
bmp = BitmapFactory.decodeStream(response);
//resize to fit screen
bmp = resizeImage(bmp, _facial.getWidth(), true);
candidateCache.put(candidate_id, bmp);
((ImageView) v).setImageBitmap(bmp);
dialog.dismiss();
} catch (Exception e) {
onFailure(e);
}
}
@Override
protected void onFailure(Throwable e) {
Log.i(TAG, "Error: " + e.getMessage(), e);
updateErrorMessage(e);
dialog.dismiss();
}
});
2、這個是把服務器端響應當JSON用的,用以諸如獲取基本文本信息之類的場景:
// Async mode to get hit result
AsyncHttpClient.sendRequest(this, request,
new AbstractAsyncResponseListener(AbstractAsyncResponseListener.RESPONSE_TYPE_JSON_ARRAY){
@Override
protected void onSuccess(JSONArray response){
Log.i(TAG, "UploadAndMatch.onSuccess()...");
candidates = response;
if(candidates!=null && candidates.length()>0){
hit_count = candidates.length();
Log.i(TAG, "HIT: " + hit_count);
updateStatus(String.format(context.getString(R.string.msg_got_hit), hit_count));
//update UI
refreshCurrentUI(1);
}else{
Log.i(TAG, "No HIT!");
updateStatus(context.getString(R.string.msg_no_hit));
//update UI
refreshCurrentUI(0);
}
}
@Override
protected void onFailure(Throwable e) {
Log.e(TAG, "UploadAndMatch.onFailure(), error: " + e.getMessage(), e);
updateErrorMessage(e);
//update UI
refreshCurrentUI(-1);
}
});
歡迎拍磚,謝謝!