태그 보관물: AsyncTask

안드로이드 7.0 AsyncTask 개선점

안드로이드 7.0 누가(NOUGAT)에서 AsyncTask의 개선 점을 살펴보자. 안드로이드 6.0 AsyncTask 소스와 7.0 소스의 차이를 확인(Diff)해 보면, 아래 화면에서 보는 2가지 정도의 차이가 있다. 차이를 살펴보면 다음과 같다.

1. 작업을 처리하는 스레드의 상수 수정.

코드를 살펴보면, CORE_POOL_SIZE와 KEEP_ALIVE의 값이 수정된 것을 확인할 수 있다. CORE_POOL_SIZE가 주석에서 보다시피 2~4개 유지하게 변경했고, 작업 처리 스레드의 종료시기를 늦추는 것을 알 수 있다.

2. ThreadPoolExecutor 수정.

ThreadPoolExecutor는 작업을 처리하는 여러 스레드를 유지하고, 작업을 큐로 유지하는 디스패처이다. 아래는 위에서 변경한 상수로 ThreadPoolExecutor를 생성하는 예제로, 작업이 추가되지 않는다면 30초 뒤에 스레드 풀의 스레드를 종료시킨다.

안드로이드 5.1 AsyncTask 개선점

안드로이드 5.1에서 AsyncTask의 소스 일부가 변경되었다. 변경된 소스를 살펴보기 전에 AsyncTask의 API 문서에서 필요한 내용을 살펴보자. API 문서는 많은 것들을 설명하고 있고, 이것은 그 만큼 복잡하기도 하고 사용 시에 주의할 필요가 있다는 것이다. 아래는 AsyncTask의 API 설명 중에 일부이고, AsyncTask를 문제없이 사용하기 위해서 꼭 알아둘 필요가 있는 내용이다. 아래 내용은 결국 AsyncTask는 UI 스레드에서 사용하라는 것이다.

Threading rules


There are a few threading rules that must be followed for this class to work properly:

안드로이드 5.1에서 변경된 내용을 살펴보자.


이 변경된 내용을 보면, 이전 버전(5.0)에서는 핸들러(Handler)인 InternalHandler를 미리 생성하고 외부(ActivityThread 클래스가 로딩)에서 초기화(init() 메서드 호출해서 미리 UI 스레드 루퍼를 사용하는)하는 구조인데 반해, 5.1 버전에서는 핸들러 생성을 레이지 로딩(Lazy Loading)하는 형태이고 싱글톤 패턴을 사용하는 것을 알 수 있다. 개인적으로 위에서 가이드로 UI 스레드에서 AsyncTask를 생성하고 실행하라고 설명하고 있기에 레이지 로딩하는 구조의 싱글톤의 형태가 더 좋겠다.


이 변경 코드는 5.1에서 사용하는 핸들러가 싱글톤의 형태를 가지고 있고, 이것을 사용하는 형태로 바뀌었다.

이상 5.1에서 이전에 비해서 변경된 내용을 살펴봤다. 변경된 내용이 성능에 영향을 미치는 부분이 없기 때문에 개발자나 사용자나 변경에 대한 사이드 이펙트는 없을 것으로 보인다.

AsyncTask 실행을 AsyncTaskCompat에 맡기자.

안드로이드 앱이 허니콤(3.0) 이전 버전의 OS를 지원하고, AsyncTask를 사용한다고 가정해보자. 이 경우에 허니콤 이전버전의 기본 메서드인 execute()를 실행하는 디스패처가 다중 스레드를 사용해서 처리하고, 허니콤 부터는 단일 스레드를 사용해서 처리한다. 그래서 OS별 AsyncTask의 기본 태스크 디스패처로 SERIAL_EXECUTOR 또는 THREAD_POOL_EXECUTOR를 사용하는지 알아야 한다. 종종 허니콤 이전에서 execute()를 사용해서 다중 스레드를 사용하고 있었는데, 허니콤 이후부터는 단일 스레드로 태스크가 처리가 되서 당황스러운 경우가 종종 발생한다. 그래서 다중 스레드로 태스크를 처리하는 좀 더 명확한 메서드를 Support v4 버전에서 제공하고 있다. 이 클래스가 AsyncTaskCompat 이다. 이 클래스가 제공하는 메서드는 다음과 같다.

public static AsyncTask<Params, Progress, Result> executeParallel (AsyncTask<Params, Progress, Result> task, Params... params)

이 팩토리 형태의 메서드를 사용해서 AsyncTask를 실행요청하면 요청한 AsyncTask 객체를 돌려주기 때문에 취소등의 요청을 위해서 AsyncTask 객체를 유지하기 위해서 좀 더 깔끔한(?) 코드의 모습을 보게된다. 그럼, 이 메서드의 구현을 살펴보자.

    /**
     * Executes the task with the specified parameters, allowing multiple tasks to run in parallel
     * on a pool of threads managed by {@link android.os.AsyncTask}.
     *
     * @param task The {@link android.os.AsyncTask} to execute.
     * @param params The parameters of the task.
     * @return the instance of AsyncTask.
     */
    public static <Params, Progress, Result> AsyncTask<Params, Progress, Result> executeParallel(
            AsyncTask<Params, Progress, Result> task,
            Params... params) {
        if (task == null) {
            throw new IllegalArgumentException("task can not be null");
        }

        if (Build.VERSION.SDK_INT >= 11) {
            // From API 11 onwards, we need to manually select the THREAD_POOL_EXECUTOR
            AsyncTaskCompatHoneycomb.executeParallel(task, params);
        } else {
            // Before API 11, all tasks were run in parallel
            task.execute(params);
        }

        return task;
    }

위 코드를 보면, 허니콤 이후의 버전과 이전의 버전에서 실행하는 모습이 다른것을 알 수 있다. 그리고 이 메서드를 사용해서 코드가 좀 더 간결해 질 수 있다.

안드로이드 멀티스레딩

오늘 안드로이드 멀티스레딩 책을 선물 받았다. 이 책의 제목은 “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를 사용하는 경우에 읽어봐도 무방할 것으로 본다.

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

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