본문 바로가기
안드로이드

[Kotlin] 앱 비정상 종료 시, 대체 액티비티 띄우기 !

by 디지털노마더 2021. 3. 12.

개발자에게 있어서, 앱의 버그만큼 신경쓰이는 부분이 있습니다.

바로 앱 구동 중에, 비정상적으로 종료되버리는 현상입니다.

 

 

앱이 비정상 종료되는 원인은 다양합니다.

현재까지 대표적으로 겪은 사례로는 IndexOutOfException, Thread Exception 두 가지가 있었습니다.

 

* IndexOutOfException : ArrayList를 사용하던 중에, 빈 데이터에서 직접적으로 특정 인덱스를 접근하는 경우, 에러가 발생하는 경우.

 

ArrayList<Strng> articleList = new ArrayList<String>();

// 5번째 항목을 접근
String fifthValue = articleList.get(4); // IndexOutOfException 발생

 

* Thread Exception : 백그라운드 스레드에서는 UI 요소를 수정하거나 갱신하는 것은 불가능합니다. 그러나 직접적으로 접근하려고 시도하는 경우는 에러를 뿜으면서 앱이 종료되는 현상을 확인 가능.

 

 

Application 클래스에서 오류처리 Handler 설정

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        setCrashHandler()
    }

  private fun setCrashHandler() {
      val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
      Thread.setDefaultUncaughtExceptionHandler { _, _ ->
          // Crashlytics에서 기본 handler를 호출하기 때문에 이중으로 호출되는것을 막기위해 빈 handler로 설정
      }
      Fabric.with(this, Crashlytics())
      val fabricExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
      Thread.setDefaultUncaughtExceptionHandler(
          AppServiceExceptionHandler(
              this,
              defaultExceptionHandler,
              fabricExceptionHandler
          )
      )
  }  
}
  • defaultExceptionHandler: 안드로이드의 기본 오류 처리 Handler
  • fabricExceptionHandler: Crashlytics에서 사용되는 오류처리 Handler
  • HeyDealerExceptionHandler: 위의 2개를 가지고 있으며 오류처리 화면을 표시하기 위한 Handler

 

오류 메세지 발생하는 핸들러 정의

class AppServiceExceptionHandler(
    application: Application,
    private val defaultExceptionHandler: Thread.UncaughtExceptionHandler,
    private val fabricExceptionHandler: Thread.UncaughtExceptionHandler
) : Thread.UncaughtExceptionHandler {

    private var lastActivity : Activity? = null
    private var activityCount = 0

    init {
        application.registerActivityLifecycleCallbacks(
            object : SimpleActivityLifecycleCallbacks() {
                override fun onActivityStarted(activity: Activity) {
                    if (isSkipActivity(activity)) return

                    activityCount++
                    lastActivity = activity
                }

                override fun onActivityStopped(activity: Activity) {
                    if (isSkipActivity(activity)) return

                    activityCount--

                    if (activityCount < 0) lastActivity = null
                }

                override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                    if (isSkipActivity(activity)) return

                    lastActivity = activity
                }

                override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {

                }
            }
        )
    }

    private fun isSkipActivity(activity: Activity) = activity is ErrorActivity

    override fun uncaughtException(thread: Thread, throwable: Throwable) {
        fabricExceptionHandler.uncaughtException(thread, throwable)
        lastActivity?.run {
            val stringWriter = StringWriter()
            throwable.printStackTrace(PrintWriter(stringWriter))

            startErrorActivity(this, stringWriter.toString())
            overridePendingTransition(R.anim.anim_slide_in_left, R.anim.anim_slide_out_left)
        } ?: defaultExceptionHandler.uncaughtException(thread, throwable)

        Process.killProcess(Process.myPid())
        System.exit(-1)
    }

    private fun startErrorActivity(activity: Activity, errorText: String) = activity.run {
        val errorActivityIntent = Intent(this, ErrorActivity::class.java)
            .apply {
                putExtra(ErrorActivity.EXTRA_INTENT, intent)
                putExtra(ErrorActivity.EXTRA_ERROR_TEXT, errorText)
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
            }

        startActivity(errorActivityIntent)
        finish()
    }
}

 

- SimpleActivityLiSimpleActivityLifecycleCallbacks (추상 클래스) 내용

더보기
abstract class SimpleActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {

    override fun onActivityPaused(activity: Activity) {
        // no-op
    }

    override fun onActivityResumed(activity: Activity) {
        // no-op
    }

    override fun onActivityStarted(activity: Activity) {
        // no-op
    }

    override fun onActivityDestroyed(activity: Activity) {
        // no-op
    }   

    override fun onActivityStopped(activity: Activity) {
        // no-op
    }

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        // no-op
    }
}

 

오류 상세 메세지 안내 액티비티

class ErrorActivity : AppCompatActivity() {

    private lateinit var binding : ActivityErrorBinding

    private val lastActivityIntent by lazy { intent.getParcelableExtra<Intent>(EXTRA_INTENT)}
    private val errorText by lazy { intent.getStringExtra(EXTRA_ERROR_TEXT) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityErrorBinding.inflate(layoutInflater)
        setContentView(binding.root)

		// 에러메세지
        String message = errorText
        Log.d("TAG", "에러메세지 : $message")
        
        startActivity(lastActivityIntent)
        finish()        
    }

    companion object {
        const val EXTRA_INTENT = "EXTRA_INTENT"
        const val EXTRA_ERROR_TEXT = "EXTRA_ERROR_TEXT"
    }
}

 

본 포스팅은 '박상권님의 블로그' 내용을 참조한 글입니다.

 

댓글