태그 보관물: Android AsyncTask

안드로이드 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;
    }

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

AsyncTask와 같이 사용하는 ProgressDialog를 사용해서 태스크를 바로 종료시키기

안드로이드 애플리케이션이 데이터를 로딩하는데 AsyncTask를 사용하고, 사용자에게 데이터 로딩을 알려주려고 ProgressDialog를 같이 사용하는 경우를 종종 볼 수 있다. 아래의 그림이 이 예제이다.

위 그림의 상황에서, 사용자가 다른 화면을 보길 원하거나 더는 로딩하기 원하지 않는 경우에도 ProgressDialog 클래스의 cancelable 변수를 false로 설정해서 백 키를 눌러도 데이터 로딩창을 종료하지 않고 계속 보이는 경우를 볼 수 있다. UX의 관점에서 보면, 이런 경우는 좋지 않다.

같은 상황에서 iOS 애플리케이션들의 UX를 보니, 많은 경우 화면을 전환하기 전에 데이터를 로딩하고 로딩을 완료하면 화면을 전환한다. 이 UX로 인해서 사용자는 로딩을 취소하고 쉽게 다른 화면으로 전환할 수 있다. 물론 꼭 그렇지는 않지만, 필자의 경우에는 비교적 iOS의 UX가 안드로이드보다 좋게 느껴진다. 그래서 안드로이드 애플리케이션도 ProgressDialog 창을 종료시키고, 데이터 로딩과 같은 비동기 요청(AsyncTask를 사용하는 경우)을 바로 취소하는 방법을 사용해서 비교적 UX를 개선할 수 있다. 사용자에게 조금 더 좋은 UX를 제공할 수 있는 이 방법을 살펴보자.

1. 백 키를 사용해서 종료하는 ProgressDialog 객체 생성

		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);
			}
		});

이 코드는 ProgressDialog 객체를 생성하면서 백 키로 창을 종료하면서 Dismiss 이벤트를 받아서 비동기 데이터를 로딩하는 태스크인 task를 취소한다.

2. 비동기로 데이터를 로딩하는 AsyncTask 클래스

	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();
		}
		
		@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();
		}
	}

이 클래스는 비동기로 데이터를 로딩하는 형태로 비동기의 태스크 로직 메서드에서 20초를 Sleep하고 있다. 그리고 이 태스크의 취소에 대한 콜백 메서드인 onCancelled(Boolean result) 메서드를 확인할 수 있다.

이제 위에서 살펴본 코드를 사용해서 사용자가 백 키를 누른 경우, ProgressDialog를 종료하고 LoadingTask도 종료시킬 수 있다. AsyncTask를 종료하는 방법으로 cancel(boolean mayInterruptIfRunning) 메서드를 호출한다. 이 메서드를 호출하면 내부에서 cancelled 변수를 true로 설정한다. 그리고 mayInterruptIfRunning를 true로 호출하면 바로 종료시키기 위한 인터럽트를 던지게 된다. 그래서 AsyncTask가 IO를 looping하면서 읽어들이지 않는 경우에는 cancel(true)로 바로 종료시킬 수 있다.

다음으로 ProgressDialog가 종료하면서 AsyncTask를 취소하는 메서드의 호출 결과를 살펴보자.
– 아래는 cancel(false)을 호출한 결과이다.

01-30 05:56:05.720: D/MainActivity(1416): onDismissed() 
01-30 05:56:24.136: D/MainActivity(1416): onCancelled : true

이 결과로 cancel(false)을 호출하면, doInBackground()가 종료된 후에 onCancelled가 호출된 것을 알 수 있다.

– 아래는 cancel(true)을 호출한 결과이다.

01-30 05:58:33.038: D/MainActivity(1584): onDismissed()
01-30 05:58:33.038: D/MainActivity(1584): Exception : null
01-30 05:58:33.038: D/MainActivity(1584): onCancelled : true

cancel(true)을 호출 결과로 AsyncTask의 doInBackground() 메서드에서 InterruptedException이 발생하게 되고, 이것으로 바로 태스크 로직을 빠져나오게 한다.

이상 살펴본 형태로 AsyncTask와 ProgressDialog를 결합해서 사용하는 경우에 사용자의 반응에 즉시 반응할 수 있는 형태로 개발해서 UX를 개선할 수 있다.

Android 4.4 AsyncTask 개선점..

안드로이드에서 제일 많이 사용하는 쓰레드 형태가 AsyncTask를 상속하는 구조이고, AsyncTask의 개선은 바로 전체적인 성능개선으로 이어질 가능성이 높다.

AsyncTask의 최신(4.4 KitKat) 버전과 이전(4.3 Jelly Bean)을 비교해 보자.

위 그림에서 최신버전의 180, 181줄의 코드를 확인해 보면, 디바이스의 CPU의 개수에 따른 최적화가 눈에 띈다.

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;

하지만, 기존의 코드에서는 아래의 코드로 하드코드되어 있어서 CPU가 2개인 디바이스에서는 오히려 성능을 저하시킬 수도 있다.

private static final int CORE_POOL_SIZE = 5;

위의 코드가 간단해 보일지 모르겠지만, 자바에서 Concurrent 쓰레드 프로그래밍에서는 매우 중요한 포인트이다. 이 작은 포인트가 디바이스에 최적화된 코드중의 일부라고 보면 되겠다.

Android java.util.concurrent.RejectedExecutionExecution 핸들링(1/2)..

안드로이드는 기본적으로 프로그래밍 모델이 랜더링과 별도로 데이터를 가져오는 쓰레드를 AsyncTask를 상속받아서 Asynchronous하게 처리하게 끔 가이드를 하고 있습니다.. 참고로, 아래내용은 안드로이드 2.2 기반에서의 경험입니다. ^^

그래고, 많은 데이터를 가지고 오기 위해서 페이징을 합니다. 안드로이드는 하단에 spinner와 Loading과 같은 메세지가 표준처럼 사용되고 있죠.

사진에 대한 Thumbnail이 매우 많고, 30개씩 가져온다고 가정을 합니다.. 보통 ArrayAdapter나 ListAdapter를 상속받아서 Adapter 객체를 만들고, Adapter 객체의 getView()에서 랜더링을 위한 작업을 구현하게 됩니다.

@Override
public View getView(int position, View cView, ViewGroup parent) {
}

Thumbnail을 가져와야 하니, 보통 위 메소드 안에서 Thumbnail을 가져오기 위해, AsyncTask를 상속받은 쓰레드로 처리를 하는 것이 일반적입니다. 이 상황은 쓰레드를 많이 생성하게 됩니다..

위의 경우에 RejectedExecutionException 이 발생할 수 있습니다.. 안드로이드는 내부적으로 java.util.concurrent 패키지의 ExecutorService를 Thread 디스패칭에 사용하고 있습니다. 따라서, 요 문제는 아래의 ExecutorService가 처리해야 하는 AsyncTask의 POOL_SIZE가 순간적으로 넘어서고, 이 경우에 AsyncTask가 풀에 들어갈 수 없어서 발생하는 익셉션입니다. 위 문제는 빈번하게 발생하지 않을 것 같지만, 게임이라던지 쓰레드가 많이 필요한 작업을 하다 보면 발생할 수 있습니다..

아래는 Android 플랫폼에서 기본적으로 가지고 있는 AsyncTask의 정책값입니다.

private static final int CORE_POOL_SIZE = 1;
private static final int MAXIMUM_POOL_SIZE = 10;
private static final int KEEP_ALIVE = 10;

따라서, 위 문제는 하나의 쓰레드로 30개의 이미지를 가져오는 형태로 살짝 바꾸면 해결이 됩니다. 당근 성능과 안전성의 트레이드 오프라고 할 수 있겠습니다..

위에서 간단하게 RejectedExecutionException을 피해갈 수 있는 방안에 대해서 살펴보았습니다..
위에서 고려한 사진의 Thumbnail에 대한 예로, 한번 더 고민을 해 보면, 만약에 30개를 가져오는 쓰레드를 getView()외에서 처리를 한다면, 지금 보고 있는 화면에서 사진이 안 보일 겁니다.. 왜냐하면, getView()에서 처리를 하지 않았기 때문에 빈 사진만 보이겠죠.

그래서, 위 가정대로, 사진이 많아서 페이징을 해야하고 Thumbnail을 보여준다면, 사진을 가져오는 첫 페이지에서는 getView()에서 AsyncTask를 불러서 사진을 가져오고, 두 번째 페이지 부터는 데이터를 가져와서 Adapter에 바인딩하는 쓰레드가 30개의 사진을 처리하는 쓰레드를 다시 부르는 형태로 구현을 하는 것이 좋을 것 같습니다..