본문 바로가기

Book/안드로이드 프로그래밍 Next Step(일부 공개)

[안드로이드 프로그래밍 Next Step] Chapter 5. Activity

Chapter 5. Activity

Activity는 앱에서 화면의 기본 단위가 되고 가장 많이 쓰이는 컴포넌트이다.

Activity는 필요한 만큼만 유지


Activity는 다른 컴포넌트와 마찬가지로 AndroidManifest.xml에 쉽게 선언할 수 있지만 Activity 개수가 많아지면 유지하기도 어려우므로, 불필요하게 많이 만들지 말 것을 권장한다. 내부에 UI 액션이 많고 로직이 많다면 우선 액티비티를 고려하고, 다른 액티비티 위에 팝업 형식으로 뜬다면 custom layout dialog나 DialogFragment, PopupWindow로 대체하는 것이 좋다.

Question

'로딩 중'이라고 전체를 덮는 반투명 화면은 어떤게 좋을까?

Answer

이것도 Activity보다는 DialogFragment가 적절하다.

독립적인 화면이라면 Activity가 더 적합하고, 종속적인 화면으로 보인다면 다른 것을 쓸 수 있는지 생각해 보는게 낫다.

setContentView() 메서드를 쓰지 않는 경우도 있음


Activity에서는 setContentView() 메서드로 메인 뷰를 화면에 표시한다. 그러나 setContentView()를 실행하지 않는다면 UI가 없는 Activity이다.

example

Intent의 스킴(scheme)에 따라 다른 화면으로 전환하는 경우 AndroidManifest.xml에서 하나의 Activity에 intent-filter를 추가하고 scheme에 따라 다른 Activity 시작하고 자신은 종료한다.

1234567891011 
<activity android:name=".IntentFilterActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="doc" />
        <data android:scheme="xls" />
        <data android:scheme="ppt" />
    </intent-filter>
</activity>

android.intent.action.VIEW 액션과 함께 doc, xls, ppt 스킴이 전달되면 IntentFilterActivity에서 처리하겠다는 의미이다.

1234567891011121314151617181920212223242526272829303132333435
public class IntentFilterActivity extends Activity {

    private static final String WORD_SCHEME = "doc";
    private static final String EXCEL_SCHEME = "xls";
    private static final String POWERPOINT_SCHEME = "ppt";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent data = getIntent();
        if(data != null) {
            Uri uri = data.getData();
            if(uri == null || TextUtils.isEmpty(uri.getScheme())) {
                // Uri does not exist. 에러 메시지 보여줌
                Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_SHORT).show();
            } else {
                switch (uri.getScheme()) {
                    case WORD_SCHEME:
                        startActivity(new Intent(this, WordActivity.class));
                        break;
                    case EXCEL_SCHEME:
                        startActivity(new Intent(this, ExcelActivity.class));
                        break;
                    case POWERPOINT_SCHEME:
                        startActivity(new Intent(this, PowerPointActivity.class));
                        break;
                }
            }
        }
        finish();
    }
}

호출 코드

12345
Uri uri = Uri.parse("doc://microsoft/0000")
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);


생명주기(LifeCycle)


생명주기를 이해하지 못했을 때 리소스가 반납되지 않을 수도 있고, 필요한 데이터를 읽어들이지 못 할 수도 있다.


액티비티 생명주기 다이어그램


2-4-1-1.jpg

출처 : 액티비티 생명주기 · [최신] 예제로 배우는 안드로이드 ANDROID


다른 Activity에 가리는 경우

  1. 다른 Activity가 일부만 가리는 경우에는 onPause()까지 불리고, 전면에 있던 다른 Activity가 종료돼서 원래 Activity에 다시 돌아오면 onResume()이 불린다.
  1. 다른 Activity가 전체를 가리는 경우에는 onStop()까지 불리고, 전면에 있던 다른 Activity가 종료돼서 원래 Activity에 다시 돌아오면 onRestart()와 onStart()가 불린다.


우선순위가 더 높은 앱이 메모리를 확보

우선순위가 더 높은 앱이 메모리가 필요하다면 앱은 어제든지 종료될 수 있기 때문에 onStop(), onDestroy() 메서드는 반드시 실행된다는 보장이 없다. 리소스를 안전하게 정리하는게 필요할 때는 onStop()이나 onDestroy()에 안전 장치로 코드를 추가하기도 한다.

시스템에 의한 Activity 제거


onDestroy()까지 불리는 것은 finish()가 호출될 때만으로 이해하기 쉽다. 그런데 시스템에 의해 제거 할 때도 onDestroy()까지 불린다.
시스템에 의한 Activity가 제거 될 경우는 여러 Task를 사용하는 앱에서 메모리가 많이 사용될 때 발생한다. 앱에서 하나의 Task만 사용할 때는 메모리 사용이 많아지면 OutOfMemoryError가 발생하지만, 앱에서 여러 Task를 사용한다면 OutOfMemoryError가 발생하기 전에 메모리 문제 가능성을 줄이는 방법이 있다.
가용 메모리의 ¾이 넘을 때 foreground의 Task를 우선시하면서 메모리를 확보하기 위해 background의 Task에서 Activity를 종료하는 것이다.
위 방법이 시스템에 의한 Activity 제거 방식이다. 시스템에 의해서도 제거될 수 있기 때문에 Task의 Activity 목록이 유지된다고 가정해서는 안 된다. 따라서 Activity 개수나 Activity 목록을 메모리에 유지하는 방식은 가능하면 사용해선 안 되고, 사용하더라도 이런 내용을 이해하고서 주의해야 한다.

생명주기 메서드 호출 시점


  • 시작할 때
 onCreate() → onStart() → onResume()
  • 화면 회전할 때(가로/세로)
 onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume()
  • 다른 Activity가 위에 뜰 때, 전원 키로 화면 OFF할 때, 홈 키
 onPause() → onStop()
  • 백 키로 Activity 종료
 onPause() → onStop() → onDestroy()
  • 백 키로 기존 Activity에 돌아올 때, 홈 키로 나갔다가 돌아올 때
 onRestart() → onStart() → onResume()
  • 다이얼로그 테마 Activity나 투명 Activity가 위에 뜰 때
 onPause()

화면이 일부 보이긴 하지만 백그라운드 상태이면(다른 화면이 가린 상태) onPause()까지 실행되고, 화면이 보이지 않는 상태이면 onStop()까지 실행된다.

Activity Lifetime

basic-lifecycle.png?hl=ko

출처 : 액티비티 시작  |  Android Developers

3가지 라이프타임(lifetime)으로 구분한다.

  • 전체 lifetime : onCreate() ~ onDestroy()
  • visible lifetime : onStart() ~ onStop()
  • foreground lifetime : onResume() ~ onPause()

Activity는 onPause()이전까지가 foreground 상태이고, onPause()까지 실행된다면 일부가 보이면서 background 상태가 된다. 마찬가지로 onStop() 이전까지 보이기는 하지만 onStop()까지 실행된다면 더 이상 보이지 않는다.

Question

onCreate() 메서드에서 setContentView()에 전달된 레이아웃이 visible lifetime인 onStart()에서부터 화면에 보이는 걸까?

Answer

그렇지 않다. onCreate()부터 onResume()까지는 하나의 Message로 처리되므로 setContentView()의 결과 화면은 onResume() 이후에 보인다.

추가로 onCreate()에서 finish()를 호출하면 다른 생명주기 메서드를 거치지 않고 곧바로 onDestroy()를 실행한다. 그리고 onActivityResult()는 onResume()보다 먼저 실행된다. 이 실행 순서에 주의해야 한다.

onPostCreate()나 onPostResume() 같은 메서드는 앱에서 권장하지 않는다. 
이들 메서드는 시스템에서 초기화를 위해서 사용하는 것으로 앱에서 쓰는 것은 권장하지 않는다.


호출자(caller)와 피호출자(callee)

A → B 간 Activity 전환에서 A를 호출자(caller)로 부르고 B를 피호출자(callee)로 정의한다.


Activity를 시작하는 메서드

Activity를 시작하는 방법은 startActivity()와 startActivityForResult()메서드를 호출하는 것이다. startActivity()는 Context의 메서드이기 때문에 Activity뿐만 아니라 Service, BroadcastReceiver, Application, 컴포넌트가 아니더라도 Context가 전달된 곳이라면 어디서든 startActivity()를 실행할 수 있다. 반면 startActivityForResult()는 Activity 메서드이다.
Fragment에도 startActivityForResult() 메서드가 있으나 최종적으로는 FragmentHostCallback의 onStartActivityFromFragment() 메서드를 호출한다.

startActivity(Intent intent)는 callee에 데이터를 전달하기만 한다. caller에게 다시 데이터를 전달하는 액션은 하지 않는다. 따라서 callee에서 getCallingActivity()와 getCallingPackage() 메서드는 null을 return한다.

startActivityForResult(Intent intent, int requestCode)는 callee에서 getCallingActivity()와 getCallingPackage()가 caller의 정보를 return한다. requestCode 파라미터에는 0 이상인 값을 넣으면 된다. 동일한 Task에 있을 때만 결과를 받을 수 있다. 결과는 setResult(int resultCode, Intent data) 메서드로 caller에 전달하는데 이 메서드는 finish() 메서드 전에 호출해야 한다. resultCode는 RESULT_OK(상수 -1)와 RESULT_CANCELED(상수 0)를 주로 사용하지만 원하는 정수값을 임의로 전달해도 된다.

Intent.FLAG_ACTIVITY_FORWARD_RESULT 플래그

Question

ActivityA에서 startActivityForResult() 메서드로 ActivityB를 시작하고 ActivityB에서는 자신을 닫으면서 또 다른 ActivityC를 시작하는 경우 ActivityC에서 setResult()로 전달한 데이터는 ActivityA에 전달될까?

Answer

전혀 그렇지 않다. ActivityC가 닫히는 순간 ActivityA의 onActivityResult() 메서드는 불리지만, setResult() 메서드의 파라미터인 resultCode와 data는 ActivityA에 전달되지 않는다. resultCode가 RESULT_CANCELED면서 data는 null이 될 뿐이다.

solution

이 경우에는 값을 전달받기 위해서는, ActivityB에서 startActivity() 메서드로 ActivityC를 시작하면서 Intent에 Intent.FLAG_ACTIVITY_FORWARD_RESULT 플래그를 추가해야 한다.

Intent.FLAG_ACTIVITY_FORWARD_RESULT 플래그를 startActivityForResult()에서 쓰면 아래와 같은 예외를 발생시킨다.

123456
com.tistory.gpark.nextstep E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.tistory.gpark.nextstep, PID: 5052
    android.util.AndroidRuntimeException: FORWARD_RESULT_FLAG used while also requesting a result
    ...

따라서 아래와 같이 호출해야한다.


ActivityC → ActivityA로 결과 전달
12345678910111213141516
// ActivityA
startActivityForResult(new Intent(ActivityA.this, ActivityB.class), REQUEST_CODE);

// ActivityB
Intent intent = new Intent(ActivityB.this, ActivityC.class);
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
startActivity(intent);
finish(); // C → A로 전달하고 싶은 경우에만 호출

// ActivityC
Intent intent = new Intent();
intent.putExtra("value", "Android");
setResult(RESULT_OK, intent);
finish();


Activity 전환 시 생명주기 메서드 호출


Activity에서 다른 Activity를 시작할 때

ActivityA에서 ActivityB를 시작할 때, ActivityA는 onPause()와 onStop()을 실행하고 ActivityB는 onCreate(), onStart(), onResume()을 실행한다. 그런데 이때 실행순서가 ActivityA의 onStop()까지 실행하고 나서 ActivityB의 onCreate()부터 실행하는 것이 아니다. 순서는 다음과 같다.

  1. ActivityA는 onPause() 메서드를 실행한다(background로 이동).
  2. ActivityB는 onCreate(), onStart(), onResume() 메서드를 실행하고 포커스를 갖는다(foreground 이동).
  3. ActivityA는 onStop() 메서드를 실행한다(ActivityB가 ActivityA 전체를 덮는 상태). ActivityB가 투명하거나 화면을 일부만 덮는 경우에는 onStop()을 실행하지 않는다.

따라서 ActivityA에서 SharedPreference나 DB를 저장할 때 onStop() 메서드에서 값을 저장하면 안되고 onPause()에 저장해야 ActivityB의 onCreate()에서 정상적으로 사용할 수 있다.

foreground Activity가 닫힐 때

ActivityB를 닫으면서 ActivityA가 다시 보일 때의 생명주기 메서드 실행 순서는 다음과 같다.

  1. ActivityB는 onPause() 메서드를 실행한다(background 이동).
  2. ActivityA는 onRestart(), onStart(), onResume() 메서드를 실행한다(foreground 이동).
  3. ActivityB는 onStop(), onDestroy() 메서드를 실행한다(종료).

생명주기 메서드 사용 시 주의사항


리소스 생성/제거는 대칭으로 실행

onCreate()에서 리소스를 생성했다면 onDestroy()에서 제거,
onResume()에서 생성했다면 onPause()에서 제거한다.

example

onResume()에서 registerReceiver()를 실행하고, onPause()에서 unregisterReceiver()를 실행하는 것이다. 즉 foreground에 있을 때만 broadcast 이벤트를 처리한다는 것이다.

super.onXxx() 호출 순서

onCreate(), onStart(), onResume()에서는 super.onCreate(), super.onStart(), super.onResume()을 먼저 실행하고, onPause(), onStop(), onDestroy()에서는 super.onPause(), super.onStop(), super.onDestroy()를 나중에 실행하는 것이 좋다.

12345678910111213141516171819202122232425262728293031323334353637
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
}

@Override
protected void onStart() {
    super.onStart();
    ...
}

@Override
protected void onResume() {
    super.onResume();
    ...
}

@Override
protected void onPause() {
    ...
    super.onPause();
}

@Override
protected void onStop() {
    ...
    super.onStop();
}

@Override
protected void onDestroy() {
    ...
    super.onDestroy();
}

finish() 메서드 호출하고 바로 리턴 필요

finish() 호출 위치가 메서드의 끝일 때만 리턴이 필요 없는 것이지 메서드 중간이라면 리턴은 반드시 필요하다.

example

12345678910111213141516
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    address = getIntent().getParcelableExtra(EXTRA_DATA_ADDRESS);
    if(address == null) {
        Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_LONG).show();
        finish();
        return; // return을 안할 경우 아래 코드가 실행되어 tvAddress.setText(address.getAddress());에서 NullPointerException이 발생한다.
    }
    TextView tvAddress = findViewById(R.id.address);
    tvAddress.setText(address.getAddress());
    ...
}

onXxx() 메서드 직접 호출은 권장하지 않음

onCreate(), onResume(), onPause(), onDestroy() 메서드를 직접 호출하는 것은 피하는게 좋다. 이유는 원하는 결과를 얻지 못할 수도 있기 때문에 시스템이 알아서 호출하는 메서드는 함부로 엮지 말고, 로직을 위한 별도 메서드를 만들어서 호출하는 것이 좋다.

example

123456789101112131415161718192021
@Override
public void onCreate(SQliteDatabase db) {
    createTables(db);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS schedule");
    db.execSQL("DROP TABLE IF EXISTS member");
    createTables(db); // onCretae(db) 이렇게 호출하는 것을 권장하지 않는다.
}

private void createTables(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE schedule ...");
    db.execSQL("INSERT INTO schedule VALUES ...");
    ...
    db.execSQL("CREATE TABLE member ...");
    ...
}


구성 변경


구성(Configuration)은 컴포넌트에서 어떤 리소스를 사용할지 결정하는 조건이고, 이 조건 항목은 프레임워크에서 정해져 있다.

android.content.res.Configuration 멤버 변수
densityDpi, fontScale, hardKeyboardHidden, keyboard, keyboardHidden, locale, mcc, mnc, 
navigtaion, navigationHidden, orientation, screenHeightDp, screenWidthDp,
smallestScreenWidthDp, touchScreen, uiMode

여기서 fontScale과 locale은 단말의 환경 설정에서 정할 수 있는 사용자 옵션이고 나머지는 단말의 현재 상태이다.


리소스 반영


구성은 컴포넌트에서 사용하는 리소스를 결정하기 때문에, 구성이 변경되면 컴포넌트에서 사용하는 리소스도 변경된다.

question

한국어 → 영어(/res/values-ko/strings.xml → /res/values-en/strings.xml)로 구성 변경하면 어떤 방식으로 변경이 되는걸까?

answer

안드로이드에서 선택한 방법은 화면에서 하나씩 문자열을 찾아서 변경하는게 아니라 Activity를 재시작해서 변경된 리소스를 사용하는 방식이다.

화면 회전도 마찬가지로 화면 회전에 따라 /res/layout-port와 /res/layout-land 디렉터리의 레이아웃을 교체하려면 Activity를 재시작한다.

참고로 Activity 외에 다른 컴포넌트는 구성이 변경되어도 재시작하지 않는다.

구성 변경으로 인한 Activity 재시작


구성이 변경되어 Activity를 재시작하면 하나의 인스턴스를 가지고 새로 초기화해서 재사용하는 것이 아니다. 기존 인스턴스는 onDestroy()까지 실행하고 새 인스턴스가 onCreate()부터 실행하는 것이다.

메모리 누수 가능성

Activity가 재시작하면서 메모리 누수 문제가 생길 수 있다. Activity가 ondestroy()까지 불리었는데도 Activity에 대한 참조가 남아 있다면 이 Activity는 GC되지 않고 메모리를 계속 차지하게 되면서 나중에는 OutOfMemoryError가 발생하게 된다. 메모리 누수 상황은 아래와 같다.

  • Activity 목록 참조
    Activity 목록은 시스템이 알아서 관리하는 영역이기도 하고, Activity가 종료할 때 컬랙션(Collection)에서 제거해야 하는데 실수로 빠뜨릴 가능성도 많다.
  • Activity의 내부 클래스나 익명 클래스 인스턴스
    Activity의 내부 클래스나 익명 클래스의 인스턴스가 Activity에 대한 참조를 갖고 있다면, 이들 인스턴스를 외부에 리스너로 등록한 경우에 해제도 반드시 되어야 한다. 해제를 빠드리는 것이 메모리 누수의 주된 원인이다. 내부 클래스에서 SomeActivity.this를 쓸 수 있는 상황이면 Activity에 대한 참조를 갖고 있는 것이다. 이때 Activity 참조를 없애기 위해 단순 내부 클래스는 정적 내부 클래스를 만드는 것이 권장된다. Activity의 변수나 메서드에 꼭 접근할 일이 있다면 정적 내부 클래스 생성자에 WeakReference로 Activity를 전달하기도 한다.
  • Singleton에서 Activity 참조
    Singleton에 Context가 전달되어야 하는데 Activity 자신을 전달한 경우이다. Singleton에 Activity 참조가 남아서 문제를 일으킨다.

  • AsyncTask에서 Activity 참조
    Activity가 시작하면서 작업을 실행하기 위해서 onCreate()에서 AsyncTask를 시작한다고 하자. AsyncTask는 Activity에 대한 참조를 가지고 있기 때문에, 화면을 회장하고 onDestroy()까지 불러도 AsyncTask가 끝나기 전까지 Activity는 GC 대상이 되지 않는다. 게다가 onCreate()에서 AsyncTask를 실행하면 회전할 때마다 AsyncTask가 매번 실행된다. Activity 자체나 AsyncTask 작업이 메모리를 많이 차지하고, AsyncTask가 시간이 오래 걸린다면, 화면을 빈번하게 회전할 때 OutOfMemoryError가 발생할 가능성이 높아진다.

프레임워크 소스 확인


구성을 업데이트할 때 프레임워크에서 호출 스택은 다음과 같다.

API 27 기준

123456789101112
ActivityThread.handleConfigurationChanged(Configuration config, CompatibilityInfo compat);
ResourcesManager.applyConfigurationToResourcesLocked(@NonNull Configuration config,                 @Nullable CompatibilityInfo compat);
Resources.updateSystemConfiguration(Configuration config, DisplayMetrics metrics,                   CompatibilityInfo compat);
Resources.updateConfiguration(Configuration config, DisplayMetrics metrics,                         CompatibilityInfo compat);
ResourcesImpl.updateConfiguration(Configuration config, DisplayMetrics metrics,                     CompatibilityInfo compat);
AssetManager.setConfiguration(int mcc, int mnc, String locale,
            int orientation, int touchscreen, int density, int keyboard,
            int keyboardHidden, int navigation, int screenWidth, int screenHeight,
            int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp,
            int screenLayout, int uiMode, int colorMode, int majorVersion);

구성이 변경 되었을때 ActivityThread의 Handler인 H에 handleMessage로 변경된 Configuration의 Message가 전달되고 결과적으로 AssetManager의 네이티브 메서드인 setConfiguration()을 실행한다.

[참고] 책 기준
123456789101112
ActivityManagerService.updateConfigurationLocked(Configuration values, ActivityRecord starting, boolean persistent, boolean initLocale);
ActivityThread.applyConfigurationToResources(Configuration config);
// through binder IPC
ResourcesManager.applyConfigurationToResourcesLocked(Configuration config, CompatibilityInfo compat);
Resources.updateConfiguration(Configuration config, DisplayMetrics metrics, CompatibilityInfo compat);
AssetManager.setConfiguration(int mcc, int mnc, String locale,
            int orientation, int touchscreen, int density, int keyboard,
            int keyboardHidden, int navigation, int screenWidth, int screenHeight,
            int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp,
            int screenLayout, int uiMode, int majorVersion);


구성 한정자


구성 한정자 샘플 Configuration 필드
MCC 및 MNC mcc310, mcc310-mnc004 mcc, mnc
언어 및 지역 en, fr, en-rUS, fr-rFR locale
레이아웃 방향 ldrtl, ldltr locale
가장 짧은 너비 sw320dp, sw600dp, sw720dp smallestScreenWidthDp
이용 가능한 너비 w720dp, w1024dp screenWidthDp
이용 가능한 높이 h720dp, h1024dp screenHeightDp
화면 크기 small, normal, large, xlarge screenLayout
화면 비율 long, notlong screenLayout
화면 방향 port, land orientation
UI 모드 car, desk, television, appliance, watch uiMode
야간 모드 night, notnight uiMode
화면 픽셀 밀도(dpi) ldpi, mdpi, hdpi, xhdpi, xxdpi, xxxdpi, nodpi, tvdpi densityDpi
  • Configuration 멤버 변수에는 플랫폼 버전 값이 없다. 플랫폼 버전은 hidden 멤버 변수인 Build.VERSION.RESOURCES_SDK_INT에 상수로 되어 있다.
  • Configuration의 멤버 변수 가운데서 fontScale은 구성 한정자와 관련된 것이 없다. Activity를 재시작할 때 화면에서 sp 단위로 된 문자열의 크기를 변경할 뿐이다.
  • 언어 설정을 아랍어, 히브리어 또는 페르시아어로 변경하면 RTL(right-to-left)로 레이아웃 방향이 변경된다(supportsRtl = true/targetSdkVersion >= 17).

데이터 복구


구성 변경으로 Activity가 재시작되어도 사용자 경험상 기존에 보던 화면을 유지하는게 좋다. 이때 상태를 임시 저장하고 복구하는 메서드인 onSaveInstanceState()와 onRestoreInstanceState()를 사용하면 된다. onRestoreInstanceState() 메서드에 전달되는 Bundle savedinstanceState 파라미터는 onCreate() 메서드에도 전달되지만, 대칭을 위해서 onRestoreInstaceState() 메서드에서 복구하는 로직을 많이 사용한다. onSaveInstanceState() 메서드는 구성 변경으로 재시작할 때 뿐만 아니라, 메모리 문제로 시스템이 Activity를 강제 종료하는 경우(주로 Background에 있을 때 우선순위가 밀려서 발생)에도 호출된다.
onSaveInstaceState()는 생명주기 메서드처럼 항상 호출되는 것이 아니다. 구성이 변경되는 조건에서(예: 화면 회전) onSaveInstanceState()가 호출되고 Activity는 onCreate()부터 새로 시작한다.

targetSdkVersion에 따른 onSaveInstanceState()/onRestoreInstanceState() 호출 시점


  • onSaveInstanceState()
targetSdkVersion < 11 targetSdkVersion >= 11
onPause() 이전에 호출 onStop() 이전에 호출
  • onRestoreInstanceState()
    onCreate() 메서드 이후, onResume() 메서드 이전에 호출

Activity 전환 시에 onSaveInstanceState() 메서드 호출


question

ActivityA에서 ActivityB로 Activity를 전환하고서 화면을 회전하면 2개의 Activity에서 한꺼번에 onSaveInstanceState()가 호출될까?

answer

그렇지 않다. 화면을 회전하는 그 순간에는 ActivityB에서만 onSaveInstanceState()가 호출된다. ActivityA에서 ActivityB로 Activity를 전환하면 ActivityA는 onStop()이 호출되는데, onStop() 이전에 onSaveInstanceState()가 호출된다. 즉, ActivityB가 foreground에 있을 때는 ActivityA에서는 onSaveInstanceState()가 이미 호출된 상태이다.

question

이 상태에서 ActivityB를 회전하면 ActivityB에서는 onSaveInstanceState()가 호출되고서 재시작된다. 이때 화면 회전에 대응해서 ActivityA도 재시작 될까?

answer

그렇지 않다. 화면에 보이는 Activity가 아니면 재시작할 필요가 없다. 백 키로 ActivityB를 종료하면 onSaveInstanceState()가 이미 호출된 ActivityA는 이제서야 재시작한다.

question

ActivityB를 회전했다가 다시 원래대로 돌린 다음에 백 키로 돌아가면 어떨까?

answer

ActivityA로 보면 방향이 바뀌지 않는 것이다. 이때는 백 키로 돌아와도 ActivityA는 재시작하지 않는다. 즉, 화면이 덮이는 상황에서는 onSaveInstanceState()를 해놓지만, 다시 돌아왔을 때 구성이 바뀌지 않는다면 화면을 그대로 보여줄 뿐 복구할 내용이 없는 것이다.

question

ActivityB에서 홈 키를 누르면 어떻게 될까?

answer

이때도 ActivityB는 onSaveInstanceState()를 호출한다. ActivityB가 홈 키로 다시 foreground로 돌아올 때 화면 방향 등의 구성이 변경된다면 ActivityB는 재시작되고 그렇지 않으면 재시작되지 않는다. 전원 키로 화면을 OFF하는 경우도 동일하다.

question

ActivityA에서 DialogActivity로 Activity를 전환하는 경우는 어떨까?

answer

ActivityA와 DialogActivity의 onSaveInstanceState()가 호출되면서 둘 다 재시작된다.

android:configChanges 속성


구성이 변경되어도 Activity를 재시작하지 않는 옵션이 있다. 예를 들어 화면 회전인 경우 AndroidManifest.xml에 Activity 선언에 지정하는 속성이 2가지가 있다. android:screenOrientation 속성을 지정해서 화면 방향(portrait/landscape)을 아예 고정하는 방법과 android:configChanges 속성에 orientation 을 추가해서 화면 방향이 바뀌어도 재시작하지 않게 하는 방법이다.

android:configChanges 속성

android:configChanges에 값을 넣을 때는 '최대'보다는 '최소'를 원칙으로 하는게 좋다.

항목들은 비트 OR값(|)으로 구성된다.

 mcc, mnc, locale, touchscreen, keyboard, keyboardHidden, navigation, 
 orientation, screenLayout, uiMode, screenSize, smallestScreenSize, 
 layoutDirection, fontScale, colorMode, density


Configuration의 멤버 변수와 android:configChanges 항목 비교(동일한 항목 제외)

Configuration 클래스에서 비트 OR 연산을 하는 updateFrom() 메서드를 참고

Configuration android:configChanges
screenHeightDp, screenWidthDp screenSize
hardKeyboardHidden, navigationHidden keyboardHidden
smallestScreenWidthDp smallestScreenSize


onConfigurationChanged() 메서드에서 구성 변경 대응

android:configChanges에 항목을 넣는 것은 해당 항목의 구성이 변경될 때 onConfigurationChanged()를 오버라이드해서 직접 처리하겠다는 의미이다.

example 화면 회전 대응

AndroidManifest.xml
1234567
<!-- targetSdkVersion이 13이상이면 android:configChanges="orientation|screenSize"로 해야함 -->
<!-- keyboardHidden 속성이 추가된 이유는 특정 기기가 키보드에 의해 화면이 회전되는 현상이 있기 때문임 -->

<activity android:name=".MainActivity"
          android:configChanges="orientation|keyboardHidden" 
          android:label="@string/app_name" />

/res/values-port/dimen.xml
123456
<?xml version="1.0" encoding="utf-8"?>
<resource>
    <dimen name="left_width">50dp</dimen>
</resource>

/res/values-land/dimen.xml
123456
<?xml version="1.0" encoding="utf-8"?>
<resource>
    <dimen name="left_width">70dp</dimen>
</resource>

/res/layout/view_list.xml
1234567891011121314151617
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="150dp"
    android:orientation="horizontal">
    
    <View
        android:id="@+id/left"
        android:layout_width="@dimen/left_width"
        android:layout_height="match_parent"
        android:background="@color/green" />
    
    <View 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/blue" />

/src/java/MainActivity.java
123456789101112131415161718
private View mLeft;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.view_list);
    mLeft = findViewById(R.id.left);
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    ViewGroup.LayoutParams params = mLeft.getLayoutParmas();
    params.width = getResource().getDimesionPixelSize(R.dimen.left_width);
    mLeft.setLayoutParams(params);
}

question

onConfigurationChanged() 메서드 이후에 어차피 화면을 다시 그린다. 이때 변경된 Configuration에 맞는 resource를 반영해서 그리는 건 아닐까? 그렇다면 onConfigurationChanged()를 override하지 않아도 되지 않을까?

answer

이것은 View 생성자에서 해당 Configuration의 resource를 대입하는 구조 때문에 해줘야 한다. android:layout_width나 android:layout_height는 View 속성이라기보다 상위 ViewGroup의 속성이다. 이 속성은 setContentView()에서 내부적으로 사용하는 LayoutInflater의 inflate() 메서드에서 View 생성자에 View의 속성을 먼저 반영한다. 그리고 ViewGroup의 generateLayoutParams()를 실행해서 android:layout_width나 android:layout_height를 반영한다. 즉, LayoutInflater의 inflate()가 실행되는 순간에 이미 대입되어 있고, Configuration이 변경된다고 해서 다시 대입되지 않는다. 이 때문에 onConfigurationChanged()에서 변경된 값을 다시 대입할 필요가 있다.

onSaveInstatnceState() 메서드는 여전히 필요함

android:configChanges에 값을 넣어도 Activity가 전환될 때 호출자에서 여전히 onSaveInstanceState()는 불린다.

example

ActivityA, ActivityB, ActivityC 3개에 android:configChanges 속성을 'orientation|screenSize'로 설정하고 ActivityA → ActivityB → ActivityC로 전환하였을 때, ActivityA와 ActivityB에서는 onSaveInstanceState()가 불린다. 이때 ActivityC에서 화면을 회전하면 onSaveInstanceState()는 불리지 않고 onConfigurationChanged() 메서드가 불린다. 화면 회전 이외의 구성이 변경되었을 때 비로소 ActivityC의 onSaveInstanceState()가 불린다.
Activity가 전환되면서 곧바로 onSaveInstanceState()가 불린 ActivityA와 ActivityB는 백 키로 다시 화면에 돌아가면, 화면 방향 외에 다른 구성이 변경되었다면 바로 재시작한다.
따라서, 선언한 구성 변경 외에도 변경될 구성이 있으므로 중요한 정보를 어떤 상황에도 유지하기 위해서는 onSaveInstanceState()와 onRestoreInstanceState()도 함께 사용하는게 좋다.

question

ActivityA는 가로 고정이고, ActivityB는 따로 방향을 고정하지 않았을 때 방향을 바꾸게 되면 어떻게 될까?

answer

ActivityB는 재시작 되고, 백 키를 사용하여 ActivityA로 돌아갔을 때 ActivityA는 아무 반응을 하지 않는다.

question

ActivityA와 ActivityB를 방향을 따로 고정하지 않은 상태에서 세로 모드로 ActivityA → ActivityB로 전환하고 ActivityB를 가로 모드로 바꿨다가 세로 모드로 다시 변경하고 나서 백 키를 사용하여 ActivityA로 돌아갔을 때는 어떻게 될까?
(순서 : (세로 모드)ActivityA → ActivityB → 가로 회전 → 세로 회전 → 백 키 → ActivityA)

answer

화면 회전외에 변경된 사항이 없다면 아무 반응이 없다. 구성이 바뀌는 것은 foreground에 있는 Activity가 기준이기 때문이다. 화면 회전외에 변경된 사항이 있으면 ActivityA는 재시작된다.

태스크


태스크는 간단하게 Activity 작업 묶음 단위(또는 사용자가 실질적으로 "하나의 어플리케이션처럼" 느끼게하는 Activity들의 집합)라고 보면 된다. 앱과 태스크는 일대일 대응이 아니다. 여러 개의 앱이 하나의 태스크가 될 수도 있고, 필요하면 하나의 앱에서도 태스크를 여러 개 가질 수 있다.

Back Stack

Activity는 백 스택(back stack)이라 불리는 스택에 차례대로 쌓인다. 태스크와 백스택은 용어를 혼용해서 쓰기도 하는데 태스크는 Activity의 모임이고 백 스택은 그 모임이 저장된 방식을 의미한다. 백 스택은 LIFO(Last-In-First-Out) 방식으로 쌓이고 사라진다.

태스크 관리 필요

실제 앱에서는 다양한 경로로 Activity를 접근하기 때문에 내비게이션(화면 흐름)이 꼬이는 경우가 많다. 이 때문에 태스크 관리가 필요하다.

example

캘린더 앱은 달력 화면(A)에서 일정 상세 화면(B)으로 가고 다시 일정 수정 화면©으로 이동한다. 그러다가 홈 키를 눌러서 태스크를 Background로 보낸다. 홈 스크린에 일정 목록 앱 위젯이 있고 여기서 특정한 일정 상세 화면(B)으로 이동할 수 있다. 그렇다면 이 특정한 일정 상세 화면은 앞에 있던 A, B, C 위에 B'를 추가하면 될까? 아니면 C를 스택에서 없애고, B를 다시 로딩하게 할까? 정해진 답이 있는 것이 아니므로 규칙을 정할 필요가 있다.

태스크 상태


태스크는 화면에 포커스되어 있는 foreground 상태와, 화면에 보이지 않는 background 상태가 있다. foreground에 있는 것은 홈 키를 통해서 언제든 background로 이동할 수 있다. background에 있는 것도 언제든 foreground로 이동할 수 있다.

foreground에서 background로 태스크 이동

foreground에서 background로 상태를 변경하는 메서드가 있다. Activity의 moveTaskToBack(boolean nonRoot)를 사용하면 된다. nonRoot 파라미터에 true가 들어가면 어느 위치에서건 백그라운드로 이동할 수 있고, false인 경우에는 태스크 루트일 때만 background 이동이 가능하다.

12345678910111213141516171819202122
/**
     * Move the task containing this activity to the back of the activity
     * stack.  The activity's order within the task is unchanged.
     *
     * @param nonRoot If false then this only works if the activity is the root
     *                of a task; if true it will work for any activity in
     *                a task.
     *
     * @return If the task was moved (or it was already at the
     *         back) true is returned, else false.
     */
    public boolean moveTaskToBack(boolean nonRoot) {
        try {
            return ActivityManager.getService().moveActivityTaskToBack(
                    mToken, nonRoot);
        } catch (RemoteException e) {
            // Empty
        }
        return false;
    }

이 메서드를 사용할 일은 많지 않은데 반드시 필요할 때가 있다.

example

카카오톡이나 라인(Line) 같은 앱에서 암호 잠금(또는 비밀번호 잠금)을 설정했을 경우, 앱이 foreground에 올 때마다 원래 보여지는 Activity 위로 패스코드(숫자로만 정해짐) 입력 Activity가 전면에 뜬다. 패스코드가 맞게 입력되면 패스크드 Activity가 종료되면서 원래 Activity로 돌아가 한다. 그리고 패스코드가 맞게 입력되지 않으면 원래 액티비티로 돌아갈 수 있는 방법이 없어야 한다. 이러한 상황에서 moveTaskToBack()메서드를 사용하면 된다. 백 키를 누르면 원래 Activity로 돌아갈 수 있기 때문에 onBackPressed() 메서드를 오버라이드해서 moveTaskToBack(true)를 호출하면 태스크가 background로 이동한다.

123456
@Override
public void onBackPressed() {
    moveTaskToBack(true);
}

background에서 foreground로 태스크 이동

background에서 foreground 상태로 변경하려면 Activity의 메서드로는 안된다. 이때는 ActivityManager에서 moveTaskToFront(int taskId, int flags) 메서드를 사용하면 된다. 이 메서드는 허니콤부터 사용 가능하고 android.permission.REORDER_TASKS 퍼미션이 필요하다.

example

1234567891011
ActivityManager activityManager = (ActivityManager) context.getSystemService(Content.ACTIVITY_SERVICE);
List runningTaskInfos = activityManager.getRecentTasks(Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
for (ActivityManager.RecentTaskInfo recentTaskInfo : runningTaskInfos) {
    if (TextUtils.equals(recentTaskInfo.baseIntent.getComponent().getPackageName(), context.getPackageName)) {
        if (recentTaskInfo.id > -1) {
            activityManager.moveTaskToFront(recentTaskInfo.id, 0);
        }
    }
}

롤리팝에서 getRecentTasks() 메서드는 지원 중단되었다(deprecated). 대신 getAppTasks() 메서드를 사용하면 된다.
getAppTasks()에서는 List를 리턴하는데, AppTask에는 moveToFront() 메서드가 있으며 
ActivityManager의 moveTaskToFront() 메서드와 역할이 동일하다.


dumpsys 명령어로 태스크 확인


activities는 a로 줄여 쓸 수도 있다.

1234567
adb shell dumpsys activity activities 

또는 

adb shell 내에서 dumpsys activity activities

dumpsys 명령어로 포커스된 Activity 찾기
123
adb shell dumpsys activity a | grep mFocusedActivity


taskAffinity 속성


taskAffinity 속성은 바로 Activity가 '관련된' 태스크에 들어갈 때 참고하는 값이라고 보면 된다.

question

Activity를 시작하면 태스크에 들어가는 기준으로 taskAffinity 속성은 언제 사용될까?

answer

바로 AndroidManifest.xml의 Activity 설정에서 android:launchMode에 singleTask를 지정하거나, 액티비티를 시작하는 Intent에 FLAG_ACTIVITY_NEW_TASK 플래그를 전달하는 경우에 사용된다.
이 두 가지 경우에 Activity가 시작되면서 TaskRecord의 affinity가 Activity의 taskAffinity와 동일한 것을 찾아 그 태스크에 Activity가 속하게 된다.

Activity 외의 컴포넌트에서 Activity 시작

Activity에서 startActivity()를 실행하는게 일반적이지만, BroadcastReceiver나 Service에서 startActivity()를 실행하기도 한다.

question

이런 경우 Activity 외의 다른 컴포넌트에서 startActivity()를 실행하면 어느 태스크에 올라가야 할까?

answer

이 때문에 필요한 규칙이 있는데 Activity를 시작하는 Intent에 FLAG_ACTIVITY_NEW_TASK 플래그를 추가해야 한다.

123456
Intent intent = new Intent(context, ScheduleViewerActivity.class);
intent.puExtra(ScheduleViewerActivity.CALENDAR_ID, 20);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
content.startActivity(intent);

위와 같이 플래그를 추가하면 ScheduleViewerActivity의 taskAffinity와 동일한 게 있다면 그 태스크 위에 올라가고 그런 태스크가 없다면 ScheduleViewerActivity는 새로운 태스크의 BaseActivity가 된다.

taskAffinity 속성 지정

taskAffinity는 AndroidManifest.xml의 Activity 선언에 android:taskAffinity로 지정할 수 있고 속성이 없다면 default 값은 패키지명이다. 결국 android:taskAffinity 속성을 선언하지 않은 것끼리는 FLAG_ACTIVITY_NEW_TASK 속성을 쓰더라도 같은 태스크에 있게 된다. android:taskAffinity는 보통은 쓰지 않는 속성인데, default 값이 지정되기 때문에 새로 Activity를 시작할 때 FLAG_ACTIVITY_NEW_TASK 플래그를 써도 새로운 태스크가 생기지 않는다. 별도로 속성을 줄 때(예를 들어 AlarmAlert 화면)는 android:taskAffinity에 ':alarm'과 같이 콜론(:) 뒤에 구분자를 적는 것이 권장된다.

º AlarmAlert 화면이 최근 앱 목록에 보이는 것을 방지하기
    AndroidManifest.xml의 AlarmAlert 선언에 android:excludeFromRecents 속성을 true로 하면 된다.


태스크 속성 부여

Activity 선언에 android:lauchMode에 standard, singleTop, singleTask, singleInstance로 지정한다. standard와 singleTop은 여러 인스턴스가 존재할 수 있고, singleTask와 singleInstance는 1개의 인스턴스만 존재한다. singleTask와 singleInstance는 특별한 상화에서만 사용한다.

topActivity(스택의 맨 위), baseActivity(스택의 맨 하단)
standard

기본 값이다. 태스크의 topActivity에 매번 새로운 Activity 인스턴스를 생성해서 Intent를 전달한다. Activity의 onCreate() 메서드에서부터 getIntent() 메서드를 사용해서 전달된 값을 읽어들인다.

singleTop

호출하고자 하는 Activity가 이미 topActivity에 있다면 새로 생성하지 않고, onNewIntent() 메서드로 Intent를 전달한다. topActivity에 없을 때는 standard와 동일하게 새로 생성한다.

singleTask

태스크에 인스턴스는 1개뿐이다. Activity의 taskAffinity 값을 참고해서 들어가게 되는 태스크가 존재하고 여기에 동일한 Activity의 인스턴스가 이미 있다면 새로 생성하지 않고, onNewIntent() 메서드로 Intent를 전달한다.

example

ActivityA, ActivityB, ActivityC 3개의 Activity 중에 ActivityB를 singleTask로 정하고, ActivityA → ActivityB → ActivityC → ActivityB를 호출하게 되면 ActivityC 위에 ActivityB가 올라가지 않고 ActivityC가 스택에서 제거 되면서 ActivityB의 onNewIntent()가 불린다. 결과적으로 태스크에 [ActivityA, ActivityB]로 남는다.

singleInstance

singleTask와 마찬가지로 태스크에 해당 Activity 인스턴스가 1개뿐이며 태스크의 유일한 Activity이다. singleInstance로 지정된 Activity에서 다른 Activity를 시작하면 다른 태스크에 들어가게 되어, 새로운 태스크를 만드는 효과가 있다.

example

ActivityA, ActivityB, ActivityC 3개의 Activity 중에 ActivityB를 singleInstance로 정하고 ActivityA → ActivityB → ActivityC 순서대로 호출하게 되면 ActivityB는 당연히 별도의 태스크가 된다. 그런데 ActivityA와 ActivityC는 taskAffinity가 동일하기 때문에(singleInstance인 ActivityB도 taskAffinity가 돌일하긴 하지만) 돌일한 태스크로 다시 묶인다. 즉 결과로 [ActivityB], [ActivityA, ActivityC]와 같이 2개의 태스크가 되고 ActivityC에서 백 키를 누르면 ActivityB가 아니라 ActivityA로 이동한다. 한 번 더 백 키를 눌러야만 ActivityB를 볼 수 있다.
(최근 앱 목록에서는 따로 보이지 않는다. 여기서 최근 앱 목록은 taskAffinity 기준이라는 것을 알 수 있다)

callee 속성 부여는 Intent 플래그에 지정


Intent에 setFlags(int flags)나 addFlags(int flags) 메서드로 지정한다. flags 파라미터는 Intent 클래스의 int 상수인 FLAG_ACTIVITY_XXX 값이고, 비트 OR 연산(|)으로 여러 개를 전달할 수 있다.
Intent 플래그에 전달하는 값은 callee의 launchMode보다 우선해서 적용된다.

FLAG_ACTIVITY_SINGLE_TOP

singleTop launchMode와 동일한 효과를 갖는다.

FLAG_ACTIVITY_NEW_TASK

singleTask lauchMode와 동일한 효과를 갖는다.

FLAG_ACTIVITY_CLEAR_TOP

스택에서 callee보다 위에 있는 Activity를 종료시킨다. 스택에 [ActivityA, ActivityB, ActivityC]가 있다면 ActivityC에서 ActivityB를 시작할 때 이 플래그를 사용하면 ActivityC는 사라지고 [ActivityA, ActivityB]만 스택에 남는다. 보통 FLAG_ACTIVITY_SINGLE_TOP 플래그와 같이 사용한다. FLAG_ACTIVITY_CLEAR_TOP 플래그를 단독으로 쓰면 callee는 종료되고 onCreate()부터 새로 실행한다.
한계는 있다. [ActivityA, ActivityB, ActivityA, ActivityB]까지 스택에 있을 때, ActivityB에서 FLAG_ACTIVITY_CLEAR_TOP 플래그를 전달해서 ActivityA를 시작하면 ActivityA만 남았으면 좋겠지만 실제로는 그렇지 않다. 결과는 [ActivityA, ActivityB, ActivityA]가 스택에 남는다.

FLAG_ACTIVITY_CLEAR_TASK

허니콤부터 사용 가능하다. callee가 시작되기 전에 관련된 스택이 모두 제거되고, callee는 빈 태스크의 baseActivity가 된다. 이 플래그는 FLAG_ACTIVITY_NEW_TASK와 함께 사용되어야 한다.

FLAG_ACTIVITY_REORDER_TO_FRONT

스택에 동일한 액티비티가 이미 있으면 그 액티비티를 스택의 맨 위로 올린다. 주의사항이 2가지 있다.

  1. FLAG_ACTIVITY_CLEAR_TOP 플래그와 함께 사용하면 옵션이 무시된다.
  2. caller가 Activity일 때만 정상적으로 재배치(reorder)가 동작한다. Service, BroadcastReceiver, Application에서는 FLAG_ACTIVITY_REORDER_TO_FRONT 플래그가 동작하지 않는다.

<activity-alias> 선언


AndroidManifest.xml에는 activity-alias 엘리먼트가 잇어서 Activity의 별명을 지정할 수 있다.

제거된 Activity 대체

activity-alias는 기존에 있던 Activity가 소스에서 제거될 때 사용할 수 있다.

example

SplashPage가 맨 처음에 뜨는 화면이었는데 SplashPage를 제거하고 바로 MainActivity를 보여주기로 했다. 그런데 숏컷(shortcut)과 같이 SplashPage에 대한 링크가 기존 버전을 설치한 단말에 남아 있는 경우가 있다. 기존 숏컷은 이제 MainActivity를 바라보게 해야 하는데, 이때 쓰는 것이 바로 activity-alias이다.

12345
<activity-alias
    android:name=".SplashActivity"
    android:targetActivity=".MainActivity" />

FLAG_ACTIVITY_CLEAR_TOP 플래그의 한계 해결

FLAG_ACTIVITY_CLEAR_TOP 플래그의 한계([ActivityA, ActivityB, ActivityA]로 남았던 이슈)를 activity-alias를 사용해서 ActivityA에 별명을 지어주고 첫 번째 Activity가 시작될 떄는 별명으로 시작하는 것이다. 그러면 activity-alias 이름으로 스택의 맨 아래에 1개만 있게 된다.

12345
<activity-alias
    android:name=".FirstActivityA"
    android:targetActivity=".ActivityA" />

activity-alias에 지정한 이름은 실제 클래스가 아니므로 아래와 같이 Component 클래스에 별명을 전달해서 Activity를 시작한다.

123456
Intent intent = new Intent();
intent.setComponent(new Component(this, "com.tistory.gpark.nextstep.FirstActivityA"));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);

activity-alias를 쓰면서 제한 사항이 있다. android:targetActivity에 들어가는 Activity는 이전에 선언되어 있어야 한다.
activity-alias 엘리먼트에는 쓸 수 있는 속성이 많지 않다. Activity의 기본 속성은 android:targetActivity에 지정된 속성을 그대로 따르고
intent-filter는 별도로 쓸 수 있다.


Reference


Understand Tasks and Back Stack  |  Android Developers
커니의 안드로이드 이야기 :: 액티비티와 태스크(Task)
[Android/안드로이드] Manifest Activity 태그의 taskAffinity Attribute. :: 돼지왕 왕돼지 놀이터<https://aroundck.tistory.com/76>