월별 글 목록: 2015년 3월월

이클립스 업데이트(Eclipse Update) 실패 해결

이클립스(Eclipse)는 자체만으로도 훌륭한 툴이지만, 이 툴에 추가해서 사용하는 플러그인들이 이클립스를 더 훌륭하게 만들어 준다. 이 플러그인은 자체로 업데이트를 지원하는 구조이기에 이클립스 툴에서 종종 업데이트한다. 이클립스에서 업데이트를 하는 방법은 Help -> Check for Updates 를 선택해서 확인할 수 있다.

하지만 업데이트가 잘 되다가 아래와 같은 에러가 발생하기도 한다. 아래에서 확인한 이클립스 버전은 4.4(Luna)이다.

이 에러에 대한 해결책을 찾아보니, 업데이트 목록에 있는 주소(URL)에 슬래시(/)를 추가하면 된다고 한다. 그래서 업데이트 주소 목록(Window -> Preference -> Install/Update -> Available Software Sites)을 확인해 보면 아래와 같다.

위 주소 목록에서 마지막에 슬래시(/)가 없는 주소에 슬래시를 추가하고 다시 업데이트하면 정상적으로 업데이트되는 것을 확인할 수 있다.

* 레퍼런스
http://stackoverflow.com/questions/6470802/what-to-do-about-eclipses-no-repository-found-containing-error-messages

안드로이드 멀티스레딩

오늘 안드로이드 멀티스레딩 책을 선물 받았다. 이 책의 제목은 “Efficient Android Threading” 이다. 번역서에 영문 제목을 그대로 사용하고 있어서 책을 받아 든 순간 원서인가? 라는 느낌을 살짝 받았다. 이 책의 제목을 굳이 번역해 보자면 “효율적인 안드로이드 스레드”라고 할 수 있겠다. 이 책의 제목만으로 안드로이드에서 스레드를 효율적으로 사용하는 기법에 대한 내용을 살펴볼 수 있을 것으로 기대하게 된다. 이 책은 처음 원서로 접했을 때도 관심이 있었고, 더욱이 같이 일하는 동료가 번역해서 더 기대하고 읽어보게 되었다.

이 책의 저자는 안데스 예란손(트위터)이고, 구글링 해보면 외부 활동을 많이 하신 분은 아닌듯 하다. 이 분의 유투브 발표영상(https://www.youtube.com/watch?v=_q12gb7OwsA)도 참고하면 좋겠다.

이 책은 2개의 파트로 크게 구분하고 있다. 첫 번째 파트는 안드로이드 프로세스와 스레드에 대한 기본 지식에 대한 내용을 살펴볼 수 있다. 두 번째 파트는 본격적으로 안드로이드 스레드와 사용방법 그리고 비교적 효율적인 방법에 대한 내용을 살펴볼 수 있다.

파트의 구성인 개별 장의 내용에 대해서 간략하게 살펴보면 다음과 같다.

1장에서 안드로이드를 구성하는 구성요소(액티비티, 브로드캐스트 리시버, 서비스, 컨텐트 프로바이더)를 살펴본다.

파트 1 : 기초
– 2~6장까지 구성되어 있고, 개별 장은 다음의 내용으로 구성되어 있다.
2장에서는 안드로이드 스레드의 기본인 자바에서 사용하는 스레드에 대해서 살펴본다. 자바 스레드에 대한 내용이 좀 부실해서 다른 책을 읽어서 잘 알아둘 필요가 있다.
3장에서는 안드로이드 프레임웍을 사용하는 앱이 사용하는 리눅스 프로세스와 스레드를 살펴본다.
4장에서는 스레드 간의 통신하는 방법으로 파이프, 공유 메모리 그리고 핸들러를 살펴볼 수 있다. 이 장에서 핸들러는 안드로이드 내부에서 일반 스레드가 UI 스레드에 데이터를 전달하는 기본 방법으로 자세히 알아둘 필요가 있다.
5장에서는 안드로이드 앱의 프로세스 간 통신 방법을 알 수 있다. RPC를 사용하는 방법
6장에서는 메모리 누수에 대한 내용을 살펴본다. 결론으로 내부 클래스는 정적으로 사용한다. 그리고 안드로이드 메모리 누수도 자바의 일반적인 메모리 누수와 같아서 자바의 메모리 누수에 대한 내용을 살펴볼 필요가 있다. 그리고 안드로이드에서 많이 발생하는 Bitmap의 메모리 누수에 대한 내용이 없는 게 아쉽다.

파트 2 : 비동기 기법
– – 7~15장까지 구성되어 있고, 개별 장은 다음의 내용으로 구성되어 있다.
7장에서는 스레드 생명주기에 대해서 살펴보고 있고, 액티비티와 프래그먼트에서 사용되는 스레드를 살펴볼 수 있다.
8장에서는 핸들러 스레드(HandlerThread)를 사용하는 형태를 확인할 수 있다.
9장에서는 Executor 프레임웍을 살펴보고 있다. 이 프레임웍은 자바 5.0에서 추가된 전형적인 생산자/소비자(Producer/Consumer) 패턴 처리를 쉽게 지원한다. 자바언어를 기본으로 동작하는 많은 환경(JVM위에서 동작하는 스크립트 언어 포함)도 병행(Concurrent) 처리를 위해서 Executor 프레임웍을 사용하기 때문에 개인적으로는 안드로이드 스레드를 이해하는데 가장 중요한 기술적 배경을 살펴볼 수 있다.
10장에서는 AsyncTask에 대한 내용을 살펴보고 있다. 이 클래스는 사용하기 편리해서 안드로이드 앱이 비동기로 태스크를 처리하는 데 가장 많이 사용되고 있다. 가장 많이 사용하고 있는 이 클래스는 잘 알아둘 필요가 있다.
11장에서는 서비스.
12장에서는 인텐트 서비스.
13장에서는 AsyncQueryHandler.
14장에서는 로더(Loader)를 살펴보자. 로더는 허니콤 이후에 추가된 클래스로 기존에 사용하던 비동기 태스크를 액티비티나 프레그먼트의 생명주기와 같이 연동해서 사용하는 방법에 대해서 살펴볼 수 있다.
15장에서는 비동기 기술을 어떻게 선택할 것인지를 확인할 수 있다. 이 장의 내용은 좀 추상적인 면이 있어서 안드로이드에서 여러 가지 스레드 사용을 경험해 보고 상황에 맞는 스레드를 사용하면 되겠다. 개인적으로는 로더와 AsyncTask를 같이 사용해서 비동기 태스크를 처리하는 걸 추천한다.

이 책의 장단점을 살펴보자.

장점
이 책은 앱을 더 잘 만들 수 있는 안드로이드 스레드의 기본과 +알파를 제공한다.
많은 안드로이드 책들이 UI를 기준으로 쓰여 있어서 UX를 잘 만들기 위한 성능 개선에 대한 내용은 쉽게 접하기 힘든 부분이라서 이 책에 대한 내용이 매우 좋다.

단점
안드로이드 스레드를 사용하는 기본 내용에 충실하게 내용을 기술하고 있다.
제목에 걸맞는 “효율적인 안드로이드 스레드”에 대한 내용은 약간 부족하지 않나 싶다.
편집이 약간 아쉽다.

이 책은 처음부터 읽는 것을 추천하지만, 개인적으로는 11, 12, 13 그리고 15장은 해당 구성요소를 사용하거나 AsyncQueryHandler를 사용하는 경우에 읽어봐도 무방할 것으로 본다.

이 책을 읽으시는 초보 개발자분은 자바 스레드 책과 같이 읽어보면 좋을 것 같다.
그리고, 중/고급의 개발자에게는 다시 한번 안드로이드 앱이 동작하는 프로세스/스레드에 대한 기본 지식과 안드로이드 스레드를 잘 사용하기 위한 지식을 다시 한번 생각해 볼 수 있어서 좋을 것 같다.

이클립스 프로세스(eclipse.exe)와 자바 프로세스(javaw.exe) 합치기

이클립스를 실행하면 이클립스 프로세스하고 가상머신의 프로세스하고 동시에 2개가 뜨게 된다. 간혹 이클립스가 멈추는 경우에 이클립스를 종료하기 위해서 프로세스를 한꺼번에 2개 종료해야 하는 상황이 된다. 이제 안드로이드 스튜디오를 실행시키거나 자바 애플리케이션을 실행시킨 경우에 어떤 프로세스(javaw.exe)를 종료시켜야 하는지 찾는 게 불편하다. 프로세스 익스폴로러로 찾으면 쉽긴 하지만 태스크 매니저를 자주 사용하기 때문에 프로세스가 분리가 안되면 좋겠다.

찾아보니, 이클립스의 실행 옵션 파일(eclipse.ini)에서 vm 옵션을 사용해서 해결할 수 있다.

* eclipse.ini

-startup
plugins/org.eclipse.equinox.launcher_1.3.0.v20140415-2008.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20150204-1316
-product
org.eclipse.epp.package.standard.product
--launcher.defaultAction
openFile
--launcher.XXMaxPermSize
256M
-showsplash
org.eclipse.platform
--launcher.XXMaxPermSize
256m
--launcher.defaultAction
openFile
--launcher.appendVmargs
-vm
C:\Program Files\Java\jre7\bin\server\jvm.dll
-vmargs
-Dgrails.console.enable.interactive=false
-Dgrails.console.enable.terminal=false
-Djline.terminal=jline.UnsupportedTerminal
-Dgrails.console.class=grails.build.logging.GrailsEclipseConsole
-Dosgi.requiredJavaVersion=1.7
-Xms512m
-Xmx2048m

위 eclipse.ini 파일에서 -vm 옵션에서 vm을 지정하면 된다. 이제 eclipse.exe 프로세스만 신경쓰면 되겠다.

* 레퍼런스
https://www.bsiag.com/scout/eclipse-exe-windows-processes/

파일 타입을 좀 더 정확하게 확인하도록 도와주는 자바 라이브러리

파일을 처리하는 애플리케이션을 개발하면서 많은 경우에 파일 처리는 확장자를 기준으로 파일의 타입을 결정하고 처리하게 된다. 예를 들면, 확장자가 .docx 이면 마이크로소프트 워드 파일이라는 것을 알 수 있고, 이 파일의 타입에 적합하게 처리(오픈 등)한다. 이 블로그의 파일 타입별 확장자 분류.. 포스팅을 확인해 보면, 비교적 자주 사용되는 파일 확장자와 그 확장자를 가지고 있는 파일이 어떤 타입의 파일인지 확인할 수 있다.

파일의 포맷을 인식하는 기본 방법이 확장자를 기준으로 타입을 구분해서 처리하는 방법이지만, 종종 파일의 타입을 알 수 없는 경우가 발생한다. 이런 경우는 다음과 같다.
– 수동으로 파일의 확장자를 수정한다.
– 특정 폴더에 이미지의 썸네일을 확장자 없이 저장(안드로이드에서 종종 볼 수 있다)한다.
– 각종 다운로드로 인해서 발생한다.

일반적으로 위 경우에는 파일을 처리하지 못하거나, 제대로 열 수 없는 애플리케이션을 호출하게 된다. 따라서 좀 더 정확하게 파일을 처리하기 위해서는 파일의 헤더를 분석해서 파일의 타입을 확인하고, 그 타입에 맞는 처리가 필요하다. 그래서, 이 글에서는 자바로 파일의 타입을 정확하게 확인할 수 있는 라이브러리를 확인해 보겠다.

파일의 타입을 확인할 수 있는 자바 라이브러리를 찾아보면 아래의 목록을 확인할 수 있다. 그리고 이 라이브러리는 안드로이드에서도 사용할 수 있다.

  1. SimpleMagic
    1. https://github.com/j256/simplemagic
    2. 의존 : 없음
  2. Java Mime Magic Library
    1. https://github.com/arimus/jmimemagic
    2. 의존 : Apache Common Logging 라이브러리
  3.  Apache Tika
    1. http://tika.apache.org/
    2. 의존 : 많음

이 글에서는 Apache Tika는 제외하고, 1.번과 2.번의 라이브러리를 확인해 보겠다. Apache Tika를 제외한 이유는 의존하는 라이브러리가 너무 많아서이다. 대상은 아래에 있는 5개 타입에 대해서 확인해 봤다.

docx = new java.io.File(".").getCanonicalPath()     + "/files/docx_file";
jpg = new java.io.File(".").getCanonicalPath() + "/files/jpg_file";
odt = new java.io.File(".").getCanonicalPath() + "/files/open_office_odt_file";
pdf = new java.io.File(".").getCanonicalPath() + "/files/pdf_file";
exe = new java.io.File(".").getCanonicalPath() + "/files/prgrep_exe_file";

위에서 파일의 확장자를 수동으로 변경했고, 이 파일에 대해서 포맷을 정확하게 인식하는 것을 테스트 했고, 결과는 다음과 같다.

– SimpleMagic 결과

0 ContentType : word
0 MimeType : application/vnd.openxmlformats-officedocument.wordprocessingml.document
0 File Extension 0 : docx
1 ContentType : jpeg
1 MimeType : image/jpeg
1 File Extension 0 : jpeg
1 File Extension 1 : jpg
1 File Extension 2 : jpe
2 ContentType : opendocument-text
2 MimeType : application/vnd.oasis.opendocument.text
2 File Extension 0 : odt
3 ContentType : pdf
3 MimeType : application/pdf
3 File Extension 0 : pdf
4 Unknown content-type

– Java Mime Magic Library 결과

0 File Ext : zip
0 File Mimetype : application/zip
1 File Ext : jpg
1 File Mimetype : image/jpeg
2 File Ext : zip
2 File Mimetype : application/zip
3 File Ext : pdf
3 File Mimetype : application/pdf
4 File Ext : ???
4 File Mimetype : ???

위 개별 라이브러리의 파일 타입 인식 결과로, 자바를 사용해서 파일의 포맷을 더 정확하게 확인하고 사용하려면, SimpleMagic 라이브러를 사용해서 파일의 타입을 확인하는게 좋겠다. 이 테스트를 위해서 사용한 소스는 다음과 같다.

import java.io.File;
import java.io.IOException;

import net.sf.jmimemagic.Magic;
import net.sf.jmimemagic.MagicMatch;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;

public class FileFormatCheckTest {

	static String[] files = null;

	static String docx = "";
	static String jpg = "";
	static String odt = "";
	static String pdf = "";
	static String exe = "";

	static {
		try {
			docx = new java.io.File(".").getCanonicalPath()	+ "/files/docx_file";
			jpg = new java.io.File(".").getCanonicalPath() + "/files/jpg_file";
			odt = new java.io.File(".").getCanonicalPath() + "/files/open_office_odt_file";
			pdf = new java.io.File(".").getCanonicalPath() + "/files/pdf_file";
			exe = new java.io.File(".").getCanonicalPath() + "/files/prgrep_exe_file";

			files = new String[] { docx, jpg, odt, pdf, exe };
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void testA() {
		ContentInfoUtil util = new ContentInfoUtil();
		try {
			ContentInfo info;

			for (int i = 0; i < files.length; i++) {
				info = util.findMatch(files[i]);
				if (info == null) {
					System.out.println(i + " Unknown content-type");
					continue;
				}

				System.out.println(i + " ContentType : " + info.getName());
				System.out.println(i + " MimeType : " + info.getMimeType());

				String[] extensions = info.getFileExtensions();
				if (extensions != null && extensions.length > 0) {
					for (int j = 0; j < extensions.length; j++) {
						System.out.println(i + " File Extension " + j + " : "
								+ extensions[j]);
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void testB() {
		MagicMatch match = null;

		try {
			for (int i = 0; i < files.length; i++) {
				match = Magic.getMagicMatch(new File(files[i]), true, false);

				if (match != null) {
					System.out.println(i + " File Ext  " + " : "
							+ match.getExtension());
					System.out.println(i + " File Mimetype  " + " : "
							+ match.getMimeType());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		// testA();
		testB();
	}

}

프로젝트 다운로드 : FileFormatCheckTest.zip

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에 타임아웃 처리를 할 수 있다.