Android Asynchronous HTTPClientの実現と最適化


詳細
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 3 G)で超大きな通信要求をいじっていたら
4)などでしょう
 
 
 
だから改造は避けられない.以下、関連コード(importは省略しましょう.ここで)を貼り、簡単な注釈を加えて説明します.皆さんの理解について.
まず、AsyncHttpClientを定義します.java.ここでのポイントはタイムアウトの設定です.また、Activityを切り替えた後に元のActivityから発行されたすべての非同期リクエストをキャンセルするためにcancelRequestを追加しました.一般的には、Activityを切り替えた後はそのUIを更新できません.そうしないと、異常を投げ出して、直接アプリケーションcrashを落とすことになります.
 
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 tasks = new ConcurrentHashMap();
		
	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 {

	@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インタフェースの定義を見てみましょうAsyncResponseListenerjava:
 
/**
 * 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);
			}
			
		});

 
レンガを撮るのを歓迎します.ありがとうございます.