안드로이드 애플리케이션을 사용하다 보면 애플리케이션이 비정상적으로 종료하는 경우를 볼 수 있다. 이런 경우에 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가 캐치하지 않은 예외상황을 처리하는 단계는 다음과 같다.
- 프로세스가 크래시 상태라는 것을 알려준다.
- 메세지에 스레드 이름과 프로세스 이름등을 로그로 전달한다.
- ActivityManagerNative를 구현하고 있는 클래스(결국 ActivityManager라고 보면 된다)에게 애플리케이션에서 크래시가 발생했으니 다이얼 로그를 보여주고 입력을 받도록 요청한다.
- 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과 같은 서비스도 제공할 수 있을 것으로 기대한다.
위에서 살펴본 예제는 다운로드할 수 있다.