이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2017-06-21
기본적으로 안드로이드 앱을 종료하는 법은 finish()이다. 구글에서 가장 권장하는 방법이기도 하고. 하지만 스택 관리가 잘 안되거나 다른 버그가 발생하면 분명히 finish() 해도 다른 액티비티가 "뿅!" 하고 나오는 일이 잦다.
물론 finish 외에도 android.os.Process.killProcess(android.os.Process.myPid())
나 System.exit(0)
등이 있기야 하지만 다소 안드로이드에 안맞긴 한다. 결과적으로 위 두개 다 다른 액티비티가 나오는 등의 버그는 일어난다.
그래서 API 16부터 추가된게 Activity.finishAffinity()
이다. 어느 액티비티에서나 상위 스택에 쌓여진 액티비티를 종료할 수 있다. API 16보다 아래의 버전을 지원하는 경우 ActivityCompat.finishAffinity()
를 사용하면 된다.
finishAffinity에는 또 다른 사용법이 있는데 바로 앱을 재부팅 하는 것이다. 서비스등만 따로 종료하면 문제 없이 작동한다. 아래는 개발중인 RichUtils 라이브러리에 있는 코틀린 버전의 재부팅 메소드이다. (원본 코드: RichUtilsKt/RReboot.kt) CLEAR_TOP 로 홈 액티비티나 지정한 액티비티로 이동하고 finishAffinity()로 부모 액티비티를 날려버리는 구조이다.
@file:JvmName("Utils")
@file:JvmMultifileClass
package pyxis.uzuki.live.richutilskt
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
@JvmOverloads
fun Context.reboot(restartIntent: Intent = this.packageManager.getLaunchIntentForPackage(this.packageName)) {
restartIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
if (this is Activity) {
this.startActivity(restartIntent)
finishAffinity(this)
} else {
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(restartIntent)
}
}
private fun finishAffinity(activity: Activity) {
activity.setResult(Activity.RESULT_CANCELED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.finishAffinity()
} else if (Build.VERSION.SDK_INT >= Build.VERSION\_CODES.JELLY_BEAN) {
activity.runOnUiThread { activity.finishAffinity() }
}
}
다만 이 finishAffinity 또한 앱 종료를 위한 최적의 방법은 아니다. 안드로이드엔 앱 종료라는 개념이 존재하지 않기 때문이다. 생명주기만 존재할 뿐이다. 단지 모든 부모 액티비티를 종료하기에 안드로이드 VM이 메모리에서 제거하는 뿐이다.
또, 서비스는 따로 종료해야 하기에 정확히는 종료한다고 보긴 어렵다.
API 21부터 추가된 Activity.finishAndRemoveTask()
는 종료함과 동시에 테스크 바(최근 내역)에서 지운다. 검색해보니 크로니움 코드의 ApiCompatibilityUtils 에 호환 코드 비슷한게 있다.
public static void finishAndRemoveTask(Activity activity) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
activity.finishAndRemoveTask();
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
// crbug.com/395772 : Fallback for Activity.finishAndRemoveTask() failing.
new FinishAndRemoveTaskWithRetry(activity).run();
} else {
activity.finish();
}
}
private static class FinishAndRemoveTaskWithRetry implements Runnable {
private static final long RETRY_DELAY_MS = 500;
private static final long MAX_TRY_COUNT = 3;
private final Activity mActivity;
private int mTryCount;
FinishAndRemoveTaskWithRetry(Activity activity) {
mActivity = activity;
}
@Override
public void run() {
mActivity.finishAndRemoveTask();
mTryCount++;
if (!mActivity.isFinishing()) {
if (mTryCount < MAX_TRY_COUNT) {
ThreadUtils.postOnUiThreadDelayed(this, RETRY_DELAY_MS);
} else {
mActivity.finish();
}
}
}
}
다만 이거도 문제가 있는게, 다시 호출하면 어째선지 Application의 onCreate()가 불리지 않아 사용을 포기해야만 했었다.
그닥 신경쓰지 않아도 되는 코드면 아래와 같이 해도 무방하긴 하다.
System.runFinalizersOnExit(true);
System.exit(0);
사실 안드로이드 앱 개발자에게 '어떻게 앱을 종료시킬 것인가' 는 오래전부터 연구된 문제긴 하다. killProcess, exit, runFinalizersOnExit 등... 안드로이드의 컨셉 자체가 위에서도 말했듯이 앱 종료라는 개념이 없다. 이 때 개발자가 할 수 있는건 좀 더 빨리 heap memory 에서 사라지게 하는 것 뿐인거 같다.
물론, finish() 로도 종료할 수 있게 앱의 액티비티 스택을 잘 관리하는게 Best지만..
어느정도 시행착오 해본 결과 finishAffinity 가 그나마 안드로이드 환경에 맞게 '사실상'으로 앱을 종료하는 것 같다. 서비스는 따로 종료시켜야 되면서도 이제까지는 잘 되었기 때문이다. 앞으로 더 나은 메소드가 나와 한방에 정리되었으면 하는 바람이 있다.