월별 글 목록: 2015년 4월월

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

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

안드로이드 빌드 툴과 Support 라이브러리 갱신시 빌드 스크립트(build.gradle)에 반영하기

안드로이드 개발 환경은 빈번하게 갱신되는 편이다. 안드로이드 개발환경을 갱신하는 데는 안드로이드 SDK Manager라는 툴을 실행해서 개발환경을 갱신할 수 있다. 아래의 화면은 SDK Manager를 실행해서 갱신을 완료한 화면이다. 이 화면에서 오늘까지 사용하고 있던 빌드 툴 버전과 Support 라이브러리가 갱신되었다.

필자가 사용하는 안드로이드 스튜디오에서 사용했던 빌드 툴 버전과 Support 라이브러리 버전은 build.gradle 파일에 아래와 같이 선언되어 있다. 위 이미지를 보면 빌드 툴과 Support 라이브러리가 22버전으로 올라간 것을 확인할 수 있다.

compileSdkVersion 21
buildToolsVersion "21.1.2"

compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.android.support:recyclerview-v7:21.0.3'
compile 'com.android.support:recyclerview-v7:21.0.3'

그래서 위에서 사용했던 build.gradle을 최신 버전으로 변경하면 다음과 같다.

compileSdkVersion 22
buildToolsVersion "22.0.1"

compile 'com.android.support:appcompat-v7:22.0.0'
compile 'com.android.support:recyclerview-v7:22.0.0'
compile 'com.android.support:recyclerview-v7:22.0.0'

위에서 수정된 build.gradle에서 빌드 툴과 Support 라이브러리의 최신 버전은 위 이미지의 SDK Manager에서 확인할 수 있다. 이 정보와 더불어 안드로이드 SDK는 화면에서 볼 수 있는 Support 라이브러리를 로컬에 다운로드 받기에 그 위치에서도 정확한 버전 정보를 확인할 수 있다. 아래는 필자의 PC에서 확인한 정보이다.

위 정보로 간단하게 빌드 스크립트(build.gradle)를 갱신해서 최신 버전의 툴을 사용해서 최신 버전의 Support 라이브러리를 사용할 수 있다.

안드로이드 애플리케이션에서 스레드의 UncaughtExceptionHandler 인터페이스를 사용해보자

안드로이드 애플리케이션을 사용하다 보면 애플리케이션이 비정상적으로 종료하는 경우를 볼 수 있다. 이런 경우에 http://www.crashlytics.com과 같은 서비스에 가입해서 크래시에 대한 정보를 확인할 수도 있다. 필자도 이 서비스를 사용해서 크래시 리포팅을 받고 이 정보를 기준으로 버그를 수정하고 다시 배포한다.

여기에서 크래시에 대한 정보는 어떻게 수집을 할까?에 대한 간단한 답을 해보고 한다. “자바로 개발하는 애플리케이션은 모두 스레드다”라고 할 정도로 자바는 스레드 위에서 동작하게 된다. 물론 안드로이드도 스레드 기반에서 동작한다. 스레드 클래스(Thread)를 보면 UncaughtException을 처리할 수 있는 핸들러를 등록할 수 있도록 인터페이스를 제공한다. 10년 자바를 개발했는데 이제서야 봤다. ㅠ.ㅠ

이 인터페이스를 사용해서 안드로이드 애플리케이션이 UncaughtException을 캐치하는 형태를 살펴보자. 안드로이드 애플리케이션에서 예외를 캐치하지 않아서 크래시가 발생하면 화면에 다음과 같은 다이얼로그가 뜨게 된다.

그럼 안드로이드 애플리케이션에서 위 화면과 같은 다이얼로그가 어떻게 뜨게 되는지 살펴보자.
안드로이드에서 애플리케이션에서 캐치하지 않은 예외가 발생하면 처리하는 기본 ExceptionHandler는 다음과 같다.

이 화면은 디버거로 안드로이드 기본 UncaughExceptionHandler의 구현체를 확인한 화면이다. 이 구현체는 com.android.internal.os.RuntimInit 클래스에 있다는 것을 알 수 있다.

위에서 확인한 RuntimInit 클래스의 기본 UncaughtExceptionHandler의 구현체는 다음과 같다.

    /**
     * Use this to log a message when a thread exits due to an uncaught
     * exception.  The framework catches these for the main threads, so
     * this should only matter for threads created by applications.
     */
    private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
            try {
                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                if (mCrashing) return;
                mCrashing = true;

                if (mApplicationObject == null) {
                    Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
                } else {
                    StringBuilder message = new StringBuilder();
                    message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                    final String processName = ActivityThread.currentProcessName();
                    if (processName != null) {
                        message.append("Process: ").append(processName).append(", ");
                    }
                    message.append("PID: ").append(Process.myPid());
                    Clog_e(TAG, message.toString(), e);
                }

                // Bring up crash dialog, wait for it to be dismissed
                ActivityManagerNative.getDefault().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
            } catch (Throwable t2) {
                try {
                    Clog_e(TAG, "Error reporting crash", t2);
                } catch (Throwable t3) {
                    // Even Clog_e() fails!  Oh well.
                }
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }
    }

위 UncaughtExceptionHandler가 캐치하지 않은 예외상황을 처리하는 단계는 다음과 같다.

  1. 프로세스가 크래시 상태라는 것을 알려준다.
  2. 메세지에 스레드 이름과 프로세스 이름등을 로그로 전달한다.
  3. ActivityManagerNative를 구현하고 있는 클래스(결국 ActivityManager라고 보면 된다)에게 애플리케이션에서 크래시가 발생했으니 다이얼 로그를 보여주고 입력을 받도록 요청한다.
  4. finally 부분에서 애플리케이션의 프로세스를 종료시킨다.

이상 안드로이드가 기본으로 UncaughtExceptionHandler를 살펴봤다. 다음으로 UncaughtExceptionHandler를 사용해서 crashlytics과 같은 처리를 하기 위한 과정을 살펴보자. 여기에서는 Application과 2개의 Activity를 사용한다.

* MainApplication 클래스
– 이 클래스에서 UncaughtExceptionHandler을 구현한다. 이 UncaughtExceptionHandler 클래스에서는 크래시에 대한 다이얼로그를 보여주지 않고 바로 애플리케이션을 종료시킨다.

package net.sjava.ex.threaduncatchexception;

import android.app.Application;
import android.util.Log;

public class MainApplication extends Application {
	static String CNAME = MainApplication.class.getSimpleName();
	
	private Thread.UncaughtExceptionHandler androidDefaultUEH;
	private UncaughtExceptionHandler unCatchExceptionHandler;
	
	public UncaughtExceptionHandler getUncaughtExceptionHandler() {
		return unCatchExceptionHandler;
	}
	
	@Override
	public void onCreate() {
		androidDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
		unCatchExceptionHandler = new UncaughtExceptionHandler();
		
		Thread.setDefaultUncaughtExceptionHandler(unCatchExceptionHandler);
	}
	
	public class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
		@Override
		public void uncaughtException(Thread thread, Throwable ex) {
			// 이곳에서 로그를 남기는 작업을 하면 된다.
			Log.e(CNAME, "error -----------------> ");
			
			// 
			android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(10);
            
			//androidDefaultUEH.uncaughtException(thread, ex);
		}
	}
}

* BaseActivity 클래스
– 이 클래스는 애플리케이션에서 사용하는 Activity 클래스의 부모 클래스이다. 여기에서 스레드의 기본 UncatchExceptionHandler로 MainApplication에서 구현한 UncaughtExceptionHandler를 사용하도록 설정한다.

package net.sjava.ex.threaduncatchexception;

import android.app.Activity;
import android.os.Bundle;

public class BaseActivity extends Activity {
	protected String CNAME = MainActivity.class.getSimpleName();
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		CNAME = this.getClass().getSimpleName();
		
		Thread.setDefaultUncaughtExceptionHandler(((MainApplication)getApplication()).getUncaughtExceptionHandler());
	}
}

* MainActivity 클래스
– 이 Activity 클래스가 애플리케이션의 시작 클래스이다. 그리고 위의 BaseActivity를 상속하고 있다. 테스트를 위해서 2개의 버튼을 사용하고 있다. 첫 번째 버튼은 UI 스레드에서 발생하는 예외 상황을 태스트할 수 있고, 두 번째 버튼은 AsyncTask를 사용해서 일반 스레드에서 발생하는 에러를 태스트 할 수 있다.

package net.sjava.ex.threaduncatchexception;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class MainActivity extends BaseActivity {
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		super.setContentView(R.layout.activity_main);
	}

	public void OnClick(View v) {
		if(v.getId() == R.id.button1) {
			
			String temp = "aaaaaaaaaaaa";
			String[] tempArray = temp.split(" ");
			
			Log.d(CNAME, tempArray[1]);
		}
		
		if(v.getId() == R.id.button2) {
			new TestAsyncTask().execute("");
		}
	}
	
	public class TestAsyncTask extends AsyncTask<String, Integer, String> {
		@Override
		protected String doInBackground(String... params) {
			String temp = "aaaaaaaaaaaa";
			String[] tempArray = temp.split(" ");
			
			return tempArray[1].toLowerCase();
		}
	}

}

이상 Thread 클래스가 선언하고 있는 UncaughtExceptionHandler를 사용해서 캐치되지 않은 예외 상황을 애플리케이션에서 처리할 수 있는 기회를 가질 수 있는 것을 확인해 봤다. UncaughtExceptionHandler 인터페이스를 이용해서 간단한 crashlytics과 같은 서비스도 제공할 수 있을 것으로 기대한다.

위에서 살펴본 예제는 다운로드할 수 있다.