AsyncTask에 타임아웃(Timeout) 추가하기

안드로이드 애플리케이션에서 비동기 태스크를 수행하는데 AsyncTask를 자주 사용하게 된다. 이 클래스를 사용해서 작업을 처리하면 내부에서는 일반 스레드에서 실행한 결과 데이터를 UI 스레드에게 전달하고 UI 스레드에서는 콜백 메서드인 onXXX를 호출해서 개발자가 구현한 AsyncTask가 생애를 마감하게 된다.

AsyncTask는 doInBackground 메서드 부분이 일반스레드로 동작하게 된다. 그래서 이 메서드에서 실행시간이 오래 걸리는 I/O 작업을 처리하게 된다. 그리고, 이 작업이 언제 끝날지는 아무도(?) 모르는 상황이다.

네트워크 I/O의 경우에는 보통 소켓(Socket)에 타임아웃을 설정(HTTP의 경우에도 동일)해서 전송/수신 등의 경우에 오래 걸리는 작업으로 인해서 스레드 자원이 고갈되지 않도록 한다. 하지만 소켓의 타임아웃을 설정하는 경우에는 여러 상황(3G/4G)을 고려해야 하기에 일반적으로 넉넉하게 설정해야 한다.

네트워크 I/O가 아닌 경우에, AsyncTask는 doInBackground 의 일반 스레드가 종료하거나 예외가 발생하지 않는 경우에 계속 동작하게 된다. 또는 극단적이지만  일반 스레드 로직이 무한루프에 빠지거나 교착상태가 될 수도 있다.

그래서, 위의 상황을 해결하기 위해서 AsyncTask에 타임아웃을 추가하는 방법을 살펴보자.

  1. AsyncTask를 생성한다.
  2. 생성된 AsyncTask 객체를 모니터링하는 클래스(AsyncTaskCancelTimerTask)에 타임아웃과 인터벌 그리고 interrupt 여부를 추가한다.
  3. AsyncTask 객체를 실행하고, 모니터링을 시작한다.

* AsyncTaskCancelTimerTask 클래스
이 클래스는 AsyncTask를 모니터링 하는 클래스이다.

	static class AsyncTaskCancelTimerTask extends CountDownTimer {
		private AsyncTask asyncTask;
		private boolean interrupt;
		
		private AsyncTaskCancelTimerTask(AsyncTask asyncTask, long startTime, long interval) {
			super(startTime, interval);
			this.asyncTask = asyncTask;
		}
		
		private AsyncTaskCancelTimerTask(AsyncTask asyncTask, long startTime, long interval, boolean interrupt) {
			super(startTime, interval);
			this.asyncTask = asyncTask;
			this.interrupt = interrupt;
		}
		
		@Override
		public void onTick(long millisUntilFinished) {
			Log.d(TAG, "onTick..");
			
			if(asyncTask == null) {
				this.cancel();
				return;
			}
						
			if(asyncTask.isCancelled())
				this.cancel();
			
			if(asyncTask.getStatus() == AsyncTask.Status.FINISHED)
			    this.cancel();
		}

		@Override
		public void onFinish() {
			Log.d(TAG, "onTick..");
			
			if(asyncTask == null || asyncTask.isCancelled() )
				return;
			
			try {
				if(asyncTask.getStatus() == AsyncTask.Status.FINISHED)
					return;
				
				if(asyncTask.getStatus() == AsyncTask.Status.PENDING || 
						asyncTask.getStatus() == AsyncTask.Status.RUNNING ) {
					
					asyncTask.cancel(interrupt);
				}
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
	}

이 클래스 android.os.CountDownTimer 클래스를 구현한 클래스로, 생성자에서 AsyncTask와 AsyncTask의 interrupt 여부를 추가했다. onTick() 메서드에서 인터벌이 되면 호출이 돼서 AsyncTask의 상태를 확인하고 모니터링을 중단할지 확인한다. onFinish() 메서드에서는 모니터링을 종료하고 AsyncTask 상태에 따라서 실행을 취소시킨다.

* MainActivity 클래스
이 클래스는 안드로이드 앱의 메인 클래스이고, 전체 소스를 확인할 수 있다.

package net.sjava.test.asynctaskcancel;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {
	static final String TAG = MainActivity.class.getSimpleName();
	
	private LoadingTask task;
	private ProgressDialog pd;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		pd = new ProgressDialog(this);
		pd.setTitle("");		
		pd.setMessage("Loading...");
		pd.setCancelable(true);
		pd.setOnDismissListener(new OnDismissListener() {
			@Override
			public void onDismiss(DialogInterface dialog) {
				Log.d(TAG, "onDismissed() ");
				
				//task.cancel(true);
				//task.cancel(false);
			}
		});
		
		Button btn = (Button)findViewById(R.id.button1);
		btn.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				task = new LoadingTask(pd);
				new AsyncTaskCancelTimerTask(task, 10000, 1000, true).start();
				task.execute("");
			}
		});
	}
	
	static class AsyncTaskCancelTimerTask extends CountDownTimer {
		private AsyncTask asyncTask;
		private boolean interrupt;
		
		private AsyncTaskCancelTimerTask(AsyncTask asyncTask, long startTime, long interval) {
			super(startTime, interval);
			this.asyncTask = asyncTask;
		}
		
		private AsyncTaskCancelTimerTask(AsyncTask asyncTask, long startTime, long interval, boolean interrupt) {
			super(startTime, interval);
			this.asyncTask = asyncTask;
			this.interrupt = interrupt;
		}
		
		@Override
		public void onTick(long millisUntilFinished) {
			Log.d(TAG, "onTick..");
			
			if(asyncTask == null) {
				this.cancel();
				return;
			}
						
			if(asyncTask.isCancelled())
				this.cancel();
			
			if(asyncTask.getStatus() == AsyncTask.Status.FINISHED)
			    this.cancel();
		}

		@Override
		public void onFinish() {
			Log.d(TAG, "onTick..");
			
			if(asyncTask == null || asyncTask.isCancelled() )
				return;
			
			try {
				if(asyncTask.getStatus() == AsyncTask.Status.FINISHED)
					return;
				
				if(asyncTask.getStatus() == AsyncTask.Status.PENDING || 
						asyncTask.getStatus() == AsyncTask.Status.RUNNING ) {
					
					asyncTask.cancel(interrupt);
				}
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	static class LoadingTask extends AsyncTask<String, Integer, Boolean> {
		private ProgressDialog pd = null;
				
		public LoadingTask(ProgressDialog pd) {
			this.pd = pd;
		}
		
		@Override
		protected void onPreExecute() {	
			super.onPreExecute();
			if(pd != null)
				pd.show();
			
			Log.d(TAG, "onPreExecute..");
		}
		
		@Override
		protected Boolean doInBackground(String... params) {			
			try {
				Thread.sleep(1000 * 20);
			} catch(InterruptedException e) {
				Log.d(TAG, "Exception : " + e.getLocalizedMessage());
			}
			
			return Boolean.TRUE;
		}

		@Override
		protected void onCancelled(Boolean result) {
			Log.d(TAG, "onCancelled : " + result);
			
			if(pd != null)
				pd.dismiss();
		}
		
		@Override
		protected void onPostExecute(Boolean result) {
			Log.d(TAG, "onPostExecute : " + result);
			
			if(pd != null)
				pd.dismiss();
		}
	}
}

* 결과..

03-06 10:36:31.689: D/MainActivity(23768): onPreExecute.. <- AsyncTask가 시작되었다고 가정
03-06 10:36:31.689: D/MainActivity(23768): onTick..
03-06 10:36:32.689: D/MainActivity(23768): onTick..
03-06 10:36:33.689: D/MainActivity(23768): onTick..
03-06 10:36:34.689: D/MainActivity(23768): onTick..
03-06 10:36:35.689: D/MainActivity(23768): onTick..
03-06 10:36:36.689: D/MainActivity(23768): onTick..
03-06 10:36:37.689: D/MainActivity(23768): onTick..
03-06 10:36:38.689: D/MainActivity(23768): onTick..
03-06 10:36:39.699: D/MainActivity(23768): onTick..
03-06 10:36:41.669: D/MainActivity(23768): onTick..
03-06 10:36:41.669: D/MainActivity(23768): Exception : null
03-06 10:36:41.679: D/MainActivity(23768): onCancelled : true <- AsyncTask가 취소됨
03-06 10:36:41.699: D/MainActivity(23768): onDismissed()

위 결과 로그는 MainActivity클래스에서 AsyncTask를 실행한 뒤에 10초의 타임아웃을 가진 모니터링 클래스를 실행한 결과이다. 모니터링 클래스는 1초씩 확인을 하면서 10초 뒤에 AsyncTask를 종료시킨 것을 알 수 있다. 이제 위의 모니터링 클래스를 사용해서 쉽게 AsyncTask에 타임아웃 처리를 할 수 있다.

AsyncTask에 타임아웃(Timeout) 추가하기”에 대한 2개의 생각

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.