Activity에 대한 이야기

게시일:

OverView

안드로이드 4대 컴포넌트 요소이며 내가 생각하기에 가장 기본이 되는 내용이다. Google Developer Docs를 보면 액티비티는 한 앱이 다른 앱을 호출할 때 앱 자체를 호출하는게 아니라 특정 행동을 호출하기 때문에 Activity라는 표현을 사용한다고 한다. 예로 facebook을 하다가 내부에서 email앱을 호출하는 것과 비슷한 경우를 들 수 있다.

액티비티가 다른 컴포넌트와 다른 점은 UI를 표시한다는 것이다. 즉 사용자와 상호작용을 위해 사용되는 구성요소라고 할 수 있다. 사용자 인터페이스를 포함한 화면 하나 하나가 한 개의 Activity가 되는데 이런 특징 때문에 특정 앱의 Activity를 호출하는 행위가 가능해진다. 액티비티는 여러 화면이 합쳐진 형태를 띄기도 하는데 Fragment 등을 활용하여 화면을 행위별로 구분지어 분리한 경우가 이에 해당한다.

Life Cycle

위와 같이 각각의 액티비티가 한 화면을 이루기 때문에 화면의 전환이 잦아지면 속도의 저하 및 메모리 이슈가 발생할 수 있다. 효율적으로 자원을 관리하기 위해서 Activity 인스턴스는 생명 주기인 Life Cycle에 따라 동작하며 화면 생성, 일시 정지 등의 상태에 대한 콜백을 제공하여 Activity의 상태 변화에 따른 변화를 효율적으로 관리한다.

예제를 들어보자면 웹툰을 보고있는 도중에 친구로부터 전화가 걸려오는 경우 전화앱으로 전환이 이루어지게 되는데 이 때 웹툰에 대한 Life Cycle이 잘못구현되어 있을 경우 비정상 종료를 할 수 있다. 또한 여러가지 앱이 켜져있을 경우 메모리 관리가 제대로 이루어지지 않을 경우, 특정 앱이 원할하게 작동하지 않을 수 있다. 즉, 데이터의 손실이 이루어져 취약해지거나 크래시가 발생할 수 있다. 이런 문제를 해결하기 위해서 LifeCycle을 정의하여 모든 Activity의 서브 클래스는 해당 행위를 따라 작동한다.

life_cycle

Activity는 생명 주기로 6가지 상태가 제공되는데 onCreate, onStart, onResume, onPause, onStop, onDestroy가 이에 해당된다. Activity가 처음 생성되는 순간부터 행위를 끝마쳐 종료되는 시점까지의 과정에서 앞의 6가지 생명 주기를 모두 거치게 된다. 위의 공식 문서상의 다이어그램을 보면 상태에 따라 변경되는 대략적인 모습을 확인 가능한데 백그라운드로 전환되거나 다른 앱의 메모리가 부족하여 가비지 컬렉션이 진행된 앱이 재 실행되는 경우와 같은 여러 상황들에 대한 과정이 그려져있다. Activity는 특수한 경우를 제외하고는 Background가 아닌 Foreground에서 작동한다. 그렇기 때문에 여러 Activity가 켜져있을 경우 Android는 그 상태를 확인하여 메모리에서 제거 등의 행위를 할 때 대상으로 선정하게 된다.

onCreate

Activity가 최초로 생성될 때 호출이 되며 콜백은 필수적으로 구현되어야 된다. 또한 생명 주기 동안 맨 처음 딱 한번만 생성되기 때문에 한 번만 수행해야 되는 코드들에 해당 부분에 위치하여야 한다. 대표적으로 Layout 데이터들을 변수로 초기화하거나 Listener를 설정하는 행위들이 이에 해당된다.

public class LifeCycle extends Activity {
    private static final String VALUE = "";
    Button btn;
    String value;

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

        if(savedInstanceState != null){
            value = savedInstanceState.getString(VALUE);
        }

        setContentView(R.layout.activity_lifecycle);

        btn = (Button) findViewById(R.id.button);
    }

    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        btn.setText(savedInstanceState.getString(VALUE));
        super.onRestoreInstanceState(savedInstanceState);
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        outState.putString(VALUE, value);
        super.onSaveInstanceState(outState);
    }
}

위으 코드는 xml로 정의된 레이아웃 사용자 인터페이스를 설정하고 멤버 변수를 설정하는 방법을 보여주는 예제이다.

layout

개발자 문서의 레이아웃 부분을 보면 레이아웃은 앱에서 사용자 인터페이스를 위한 구조를 정의하는 것으로 모든 요소들은 View와 ViewGroup 객체의 계층을 사용하여 빌드된다고 한다. View는 사용자가 보고 상호작용할 수 있는 Button, TextView 등을 의미하고 ViewGroup은 레이아웃이라고도 불리는 다양한 구조를 제공하는 LinearLayout, ConstraintLayout 등을 의미한다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:name="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:text="LifeCycle" />

</RelativeLayout>

onCreate에서 호출되는 setContentView의 파라미터인 R.layout.activity_lifecycle가 위의 xml 파일을 일컫는데 RelativeLayout ViewGroup의 내부에 Button이라는 View 객체를 가지고 있음을 알 수 있다. 앱이 컴파일을 진행할 때 XML 레이아웃 파일이 View 리소스 안에 컴파일 되게 되는데 onCreate 콜백에서 앱 코드로부터 레이아웃 리소스를 로드해야 된다. setContentView가 호출이 되면 레이아웃 리소스에 대한 참조가 전달되어 액티비티에 로드되게 된다.

findViewById는 이렇게 지정된 레이아웃의 View를 찾아서 연결해줄 때 사용되는데 Button 객체의 name이 button이기 때문에 findViewById(R.id.button) 의 형태로 접근하여 Button 객체에 초기화를 수행해주는 것이다. 초기화 부분 역시 해당 Activity가 실행되는 동안 단 한번만 수행하면 되기 때문에 onCreate부분에서 수행해주는 것이 좋다.

onCreate의 파라미터로는 Bundle 타입의 savedInstanceState 매개변수가 넘어오는데 Activity의 이전 저장 상태가 포함되어 있는 객체이다. 이는 onPause, onStop 등의 상태에서 메모리가 부족한 상황등에 의하여 정리될 경우에 앱이 다시 실행되면 onCreate로 돌아오게 되는데, 이 때 onSaveInstanceState를 호출하여 저장이 필요한 값들에 대한 백업을 진행한다. onRestoreInstanceState는 값을 복구하는 단계에서 호출이 되며 메모리에서 정리된 값들을 백업한 값들을 토대로 다시 복구해준다.

Activity가 처음 시작하는 순간에는 백업할 데이터가 없기 때문에 특수한 경우를 제외하고는 null 값이 넘어오게 된다.

onStart

Activity가 시작되는 상태로 사용자에게 보이게 되는 순간이다. 이 부분에 도달하게 되면 Activity를 포그라운드에 배치하여 사용자와 상호작용을 할 수 있도록 준비하게 된다. ON_START 이벤트를 수신하게 되면 그 즉시 실행되고 사용자와 상호작용을 하지는 않고 있는 상태라고 볼 수 있다.

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

onCreate와 달리 onStop 상태에서 onRestart에 의해 다시 실행될 수 있기 때문에 onStop 이후 화면을 표시해줄 때 변경되어야 할 부분이 있다면 해당 부분을 정의할 수 있다.

onResume

바로 이 상태가 사용자와 상호작용을 시작하는 그 순간이다. 특정 이벤트가 발생하여서 앱에서 Activity에 대한 포커스가 사라지는 그 순간까지 이 상태에 머물러있게 된다. 즉, 전화가 오거나 다른 앱이 실행되거나 또는 화면이 꺼지는 경우 등이 발생하기 전까지는 onResume 상태에서 지속적으로 사용자와 상호작용을 하고 있다는 의미이다.

만약 이 상태에서 시그널 또는 이벤트가 발생하게 되면 Activity는 일시정지 상태로 들어가게 되어 onPause 상태가 된다. 해당 Activity위로 팝업창 등이 떠있어서 사용자의 포커스가 팝업창으로 이동한 상태의 경우가 이에 속하는데 팝업 창을 지우고 다시 Activity 화면으로 돌아오면 onResume이 재호출 되고 홈 버튼을 눌러서 화면에서 Activity가 사라지게 되면 onStop 상태가 된다.

여기서 조금 모호한 점이 있었는데 onStart와 onResume의 차이를 짚고 넘어가는 것이 좋겠다. 둘 다 사용자가 화면을 볼 수는 있지만 실질적으로 사용자가 이벤트를 발생시킬 수 있는 단계는 onResume 단계이다.

좀 더 세부적인 차이로 들어가자면 멀티 윈도우를 예로 들 수 있다. Developer Docs에 따르면 멀티 윈도우 환경일 경우 topmost Activity 만이 onResume 상태를 가지고 있고 나머지는 onStart 상태를 가지고 있다고 한다. 예를 들어서 사용자가 문서를 작성하는 동안에도 옆의 창에서는 youtube 영상이 재생되고 있을 확률이 있다. 이런 경우 youtube가 topmost 앱이 아니기 때문에 그 상태는 onstart로 남아서 사용자와 상호작용을 하지는 않지만 영상을 눈으로 볼 수 있게 되는 것이다. 이렇기에 기본적인 life cycle에 따라 onPause가 호출되었을 때 동영상을 정지하게 되면 onStart로 전환된 topmost가 아닌 youtube와 같은 앱들에 대한 동작이 정상적이지 않을 수 있다. 그렇기에 onStop 부분에 정지와 같은 처리를 하는 것을 권고한다고 한다.

onPause

앞에서 설명했듯이 Activity가 포그라운드에 존재하지 않는 경우 onPause가 호출이 된다. (멀티 윈도우 모드에서는 보일 수 있음) onPause는 잠시 후 다시 해당 Activity를 실행할 가능성이 높기 때문에 작업을 일시정지하는 코드들이 주로 배치된다.

하지만 onPause는 정말 잠깐 실행되기 때문에 저장 작업을 수행하여서는 안된다. 즉, onPause 부분에서 데이터를 저장하거나, 네트워크 호출을 하거나, DB 작업을 하면 메서드의 실행이 끝나기 전에 완료되지 못할 수 있기 때문에 부하가 큰 작업은 onStop 상태일 때 실행해야 된다. onPause는 사용자에게 완전히 보이지 않을 때까지 머무르며 이 상태에서 다시 Activity가 실행되면 onResume이 호출되게 된다. 만약 화면이 전환되거나 화면에서 Activity가 완전히 보이지 않게 되면 onStop이 호출된다.

onStop

Activity 화면이 노출되어 있지 않으면 onStop 상태에 진입하며 다른 Activity가 화면 전체를 차지하고 있음을 의미한다. 해당 부분에서는 앱이 사용자에게 보이지 않는 동안 필요하지 않은 리소스를 해제하거나 조정하게 되는데 애니메이션을 정지시키거나 위치에 대한 정보를 바꾸는 행위들이 이에 해당된다. 이 또한 멀티 윈도우 환경에서는 사용자에게 UI 작업이 변경되는 것이 확인 된다. onPause와 다르게 onStop에서는 CPU를 많이 사용하는 작업들에 대한 종료를 수행해야 되는데 DB에 저장하는 행위들을 하기에 적합하다.

만약 Activity가 다시 실행되면 onRestart가 호출이 되며 메모리가 부족하여 killed된 경우에는 onCreate를 호출하게 된다. 메모리 정리에도 우선순위가 존재하는데 프로세스의 상태와 Activity 상태에 따라 우선순위가 주어진다.

종료될 가능성이 가장 높은 경우는 onStop/onDestroy 상황이며 그 다음으로는 onPause 상태가 되며 나머지 상태는 적은 가능성을 가진다.

onDestroy

Activity가 종료되는 시기에 호출이 되는데 2가지 경우를 통해 호출이 된다. 첫 경우는 코드 상에서 finish가 호출되거나 Activity를 닫는 경우이며, 다른 한 경우는 기기의 방향이 회전되어 뷰를 다시 구성해야 되는 경우 등으로 인하여 Android에 의해 일시적으로 소멸되는 경우이다.

LifeCycleObserver / LifeCycleOwner

눈으로 직접 행위를 확인하기 위해서 Observer를 사용한다. 위에서 언급했듯이 각각의 생명 주기에 따라 event가 발생하기 떄문에 이를 캐치해서 로그를 찍어보도록 하자.


public class CustomLifeCycleObserver implements LifecycleObserver {

    private final static String TAG = "LifecycleObserver";

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    public void onCreate(){
        Log.e(TAG, "onCreate");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void onStart(){
        Log.e(TAG, "onStart");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void onResume(){
        Log.e(TAG, "onResume");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void onPause(){
        Log.e(TAG, "onPause");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void onStop(){
        Log.e(TAG, "onStop");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy(){
        Log.e(TAG, "onDestroy");
    }
}

각각의 상태에 대한 Event가 발생하면 Log가 찍히도록 하였다.

public class LifeCycleActivity extends Activity implements LifecycleOwner {

    private static final String VALUE = "";
    Button btn;
    String value;

    private LifecycleRegistry lifecycleRegistry;

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

        if(savedInstanceState != null){
            value = savedInstanceState.getString(VALUE);
        }

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.addObserver(new CustomLifeCycleObserver());
        lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);

        setContentView(R.layout.activity_lifecycle);

        btn = (Button) findViewById(R.id.button);
    }

    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        btn.setText(savedInstanceState.getString(VALUE));
        super.onRestoreInstanceState(savedInstanceState);
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        outState.putString(VALUE, value);
        super.onSaveInstanceState(outState);
    }

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

        lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
    }

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

        lifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
    }

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

        lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
    }

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

        lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
    }

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

        lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
    }

    @Override
    protected void onRestart() {
        super.onRestart();

        lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

LifeCycle 액티비티에서 생명주기를 몽땅 오버라이딩 하여 Observer에 state 변경을 알리는 코드를 삽입하였다. 여러 작업을 수행하고 나서 아래와 같은 log 기록을 확인할 수 있었다.

2020-06-12 21:01:39.035 24485-24485/wizley.android.playground E/LifecycleObserver: onCreate
2020-06-12 21:01:39.054 24485-24485/wizley.android.playground E/LifecycleObserver: onStart
2020-06-12 21:01:39.059 24485-24485/wizley.android.playground E/LifecycleObserver: onResume
2020-06-12 21:01:42.265 24485-24485/wizley.android.playground E/LifecycleObserver: onPause
2020-06-12 21:01:44.009 24485-24485/wizley.android.playground E/LifecycleObserver: onResume
2020-06-12 21:01:47.265 24485-24485/wizley.android.playground E/LifecycleObserver: onPause
2020-06-12 21:01:47.941 24485-24485/wizley.android.playground E/LifecycleObserver: onStop
2020-06-12 21:01:52.610 24485-24485/wizley.android.playground E/LifecycleObserver: onStart
2020-06-12 21:01:52.611 24485-24485/wizley.android.playground E/LifecycleObserver: onResume

복습하는 의미에서 생각을 해보면 onCreate를 통해서 최초로 Activity가 생성이 되고 레이아웃 및 초기화 작업이 수행된다. 그 후 onStart로 진입하는데 사용자에게 화면이 보여지지만 상호작용은 하지 않고 있다. onResume이 호출되는 그 순간 사용자와 상호작용을 시작하고 Activity가 최상단에 위치하게 된다. 그러다 팝업이 뜨게 되면 onPause 상태가 되고 이는 포커스를 잃었음을 의미한다. 그 상태에서 팝업을 없애고 다시 Activity가 최상단에 위치하게 되면 onResume 상태로 돌아가게 된다.

여기서 홈 버튼을 누름으로써 onPause에서 onStop이 호출되게 되고 이는 Activity가 화면에 보이지 않는 상태임을 의미한다. 다시 해당 Activity로 돌아오게 될 때 RAM 상태에 따라 Activity가 정리된 경우에는 onCreate부터 다시 호출이 되지만 해당 실습에서는 onRestart를 걸쳐 onStart가 호출이 된다. 그 후 다시 상호작용을 위한 단계인 OnResume이 호출된 것을 확인할 수 있다.

Activity 호출

Activity 내부에서는 다른 Activity를 호출하는 것이 가능하다. 이 경우 Intent 객체가 호출되는데 이 부분에 대한 내용은 추후에 다룰 예정이므로 지금은 수행하고자 하는 작업을 설명하는 명세서와 같다고 말하고 넘어간다.

호출에는 2가지 형태가 존재한다. startActivity를 통한 새로운 액티비티 호출과 startActivityForResult를 통한 반환값을 결과로 돌려받는 호출이다. Result의 경우 다른 앱에게 요청을 하거나 특정 데이터가 해당 액티비티의 호출 결과에 의해 변경되는 것과 같은 경우에 주로 사용된다. 카메라나 갤러리에서 사진을 가져오는 행위도 결국에 이미지에 대한 정보를 가져와야 하기 때문에 result가 필요한 경우라고 할 수 있다.

public class NavigateFromActivity extends Activity implements View.OnClickListener {
    private static final int NEW_INTENT_REQUEST = 200;
    private static final int NEW_INTENT_RESPONSE = 300;

    private static final String TAG = "NavigateFromActivity";

    private Button start_activity;
    private Button start_activity_for_result;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_navigatefrom);

        start_activity = (Button) findViewById(R.id.start_btn);
        start_activity_for_result = (Button) findViewById(R.id.start_for_result_btn);

        start_activity.setOnClickListener(this);
        start_activity_for_result.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.start_btn:
                Log.e(TAG, "startActivity");
                Intent start_intent = new Intent(this, NavigateToActivity.class);
                start_intent.putExtra("status", 1);
                startActivity(start_intent);
                break;
            case R.id.start_for_result_btn:
                Log.e(TAG, "startActivityForResult");
                Intent start_for_result_intent = new Intent(this, NavigateToActivity.class);
                start_for_result_intent.putExtra("status", 2);
                startActivityForResult(start_for_result_intent, NEW_INTENT_REQUEST);
                break;
            default:
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == NEW_INTENT_REQUEST) {
            if (resultCode == NEW_INTENT_RESPONSE) {
                Log.e(TAG, "Intent was successful");
            } else {
                Log.e(TAG, "Intent was not successful");
            }
        }
    }
}

예제는 2개의 액티비티를 다루며 NavigateFromActivity에서 NavigateToActivity가 호출이 된다. OnClickListener를 달아주어서 두 개의 버튼에 대한 작업을 onClick에 정의하였고 하나는 startActivity를 다른 하나는 startActivityForResult를 호출한다.

startActivityForResult 함수는 Activity 실행에 대한 결과가 필요한 경우에 호출이 되는데 startActivityForResult의 두 번째 파라미터가 requestCode가 되고 돌아오는 결과가 resultCode가 된다. 이를 바탕으로 비교를 하여 정상적으로 수행된 경우에는 Intent was successful이라는 로그가 찍히도록 세팅하였다.

public class NavigateToActivity extends Activity implements View.OnClickListener {
    private static final int NEW_INTENT_RESPONSE = 300;

    private static final String TAG = "NavigateToActivity";
    private int status = -1;

    private Button ok;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_navigateto);

        status = getIntent().getIntExtra("status", -1);

        if(status == -1){
            finish();
        }

        ok = (Button) findViewById(R.id.ok);

        ok.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.ok:
                Intent intent = new Intent();
                if(status == 2){
                    setResult(NEW_INTENT_RESPONSE, intent);
                }
                Log.e(TAG, "RETURN TO NavigateFromActivity");
                finish();
                break;
            default:
                break;
        }
    }
}

호출되는 Activity도 비슷한 형식으로 생성하되 status라는 변수를 통해 result가 필요한 상태인지에 대한 여부를 판단한다. 만약 startActivityForResult로 인하여 호출되었다면 2라는 값을 가지게 되고 setResult를 통해 NEW_INTENT_RESPONSE가 수행결과로 돌아가게 된다. finish 함수가 호출이 되면 onDestroy가 호출되어서 Activity가 생명을 다하게 되고 다시 호출지점으로 돌아가게 된다.

onActivityResult는 NavigateToActivity로부터 setResult의 결과인 NEW_INTENT_RESPONSE을 정상적으로 받아온 경우, Intent was successful을 호출해주는 부분의 루틴을 수행하게 되는 것이다. 이 부분은 결과를 제대로 가져온 경우 변경사항에 대한 코드가 존재할 것이다.

이 경우에도 두 액티비티간의 life cycle을 생각해보면 다음과 같다. 사실 이렇게 호출이 이루어진 경우 호출을 한 activity와 호출을 당한 activity가 동시에 실행되는 구간이 존재한다. 즉 실행되는 시간이 일정기간 겹친다는 뜻이다. NavigateFromActivity이 onResume 상태에서 Click 이벤트가 발생하게 되면 onPause 상태로 변하게 되며 NavigateToActivity의 onCreate가 호출이 된다. 그 후 onStart를 거쳐 onResume 상태가 되게 되면 focus를 가져가게 되므로 해당 지점에서 NavigateFromActivity는 화면에 표시가 되지 않으면 onStop 상태가 된다. 이렇게 명확한 순서가 존재하기 때문에 여러 데이터에 대한 관리가 제대로 이루어질 수 있는 것이기도 하다.

2020-06-13 00:16:32.648 25589-25589/wizley.android.playground E/NavigateFromActivity: startActivity
2020-06-13 00:16:34.096 25589-25589/wizley.android.playground E/NavigateToActivity: RETURN TO NavigateFromActivity
2020-06-13 00:16:36.454 25589-25589/wizley.android.playground E/NavigateFromActivity: startActivityForResult
2020-06-13 00:16:37.721 25589-25589/wizley.android.playground E/NavigateToActivity: RETURN TO NavigateFromActivity
2020-06-13 00:16:37.762 25589-25589/wizley.android.playground E/NavigateFromActivity: Intent was successful

log를 확인해보면 정상적으로 이루어지는 것을 확인할 수가 있다.

BackStack

시스템 해킹 공부를 하면 수도 없이 하는 스택의 개념이 안드로이드 Activity에도 존재한다. 예를 들면 사용자가 리사이클러뷰로 뿌려진 ViewHolder 중 하나를 클릭하여 새로운 Activity로 이동한 경우 그 Acitivity는 Activity Stack에 Push된다. 그리고 만약 뒤로가기 버튼을 클릭하여 호출이 되었었던 리사이클러뷰가 있는 화면으로 돌아가게 된다면 Stack에서 Pop이 된다.

activity_stack

Google Developer Docs 부분의 figure에도 보이듯이 Activity는 LIFO 구조로 push되고 pop되는 것을 알 수 있다. 그리고 만약 사용자가 뒤로 버튼을 계속 누르게 되면 결국 모든 Activity가 pop되고 홈 화면으로 돌아가게 될 것이다. 또한 Stack이기 때문에 특정 Activity를 top으로 임의로 올리는 행위를 하지 않으며 이로 인하여 특정 Activity가 Stack에 여러개 push 되어 있을 수도 있다. 하지만 여러가지 문제가 발생함으로써 특정 Activity를 Stack에 하나만 가지고 있어야 할 경우가 존재하는데 이럴 경우를 대비해 activity 속성이 존재한다. 바로 FLAG_ACTIVITY_SINGLE_TOP과 같은 flag들과 launchMode, allowTaskReparenting과 같은 속성들인데 manifest 또는 Intent의 flag로 설정할 수 있다. (intent의 flag가 manifest보다 우선시 됨)

launchMode

Manifest에서는 launchMode 속성으로 실행 방식을 지정할 수 있는데 4가지 모드로 standard, singleTop, singleTask, singleInstance가 있다.

standard는 default 값으로 작업에 대한 새 인스턴스를 생성하며 이는 여러번 생성될 수 있다. 예를 들어 A-B-C의 상태로 push가 이루어진 경우, C Activity에 대한 호출이 들어오게 되면 A-B-C-C의 상태가 되어 C의 Instance가 Stack에 여러개 존재한다는 의미이다.

2020-06-15 16:03:00.281 26971-26971/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 16:03:00.283 26971-26971/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:03:00.284 26971-26971/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:03:03.049 26971-26971/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 16:03:03.087 26971-26971/wizley.android.playground E/StandardActivity: onCreate
2020-06-15 16:03:03.090 26971-26971/wizley.android.playground E/StandardActivity: onStart
2020-06-15 16:03:03.090 26971-26971/wizley.android.playground E/StandardActivity: onResume
2020-06-15 16:03:04.571 26971-26971/wizley.android.playground E/StandardActivity: onPause
2020-06-15 16:03:04.609 26971-26971/wizley.android.playground E/StandardActivity: onCreate
2020-06-15 16:03:04.612 26971-26971/wizley.android.playground E/StandardActivity: onStart
2020-06-15 16:03:04.613 26971-26971/wizley.android.playground E/StandardActivity: onResume
2020-06-15 16:03:05.113 26971-26971/wizley.android.playground E/StandardActivity: onStop
2020-06-15 16:03:07.794 26971-26971/wizley.android.playground E/StandardActivity: onPause
2020-06-15 16:03:07.803 26971-26971/wizley.android.playground E/StandardActivity: onStart
2020-06-15 16:03:07.804 26971-26971/wizley.android.playground E/StandardActivity: onResume
2020-06-15 16:03:08.331 26971-26971/wizley.android.playground E/StandardActivity: onStop
2020-06-15 16:03:08.332 26971-26971/wizley.android.playground E/StandardActivity: onDestroy
2020-06-15 16:03:08.860 26971-26971/wizley.android.playground E/StandardActivity: onPause
2020-06-15 16:03:08.869 26971-26971/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:03:08.870 26971-26971/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:03:09.360 26971-26971/wizley.android.playground E/StandardActivity: onStop
2020-06-15 16:03:09.361 26971-26971/wizley.android.playground E/StandardActivity: onDestroy

log를 찍어본 결과 처음 ProtoActivity에서 StandardActivity가 호출이 되면 onPause 상태가 된 뒤 onCreate -> onStart -> onResume이 호출된다. 다시 StandardActivity를 호출하면 onCreate -> onStart -> onResume 순서로 다시 호출을 하면서 onPause -> onStop으로 변경되는 것을 알 수 있다.

dumpsys activity activities -a

참고로 위의 명령어로 adb 상에서 확인하는 것이 가능하다.

singleTop은 해당 Activity가 맨 위에 위치하고 있을 경우 Instance를 생성하지 않고 onNewIntent()를 호출하여 기존 Instance로 라우팅해주는데 Top에 있지 않은 이상 같은 Activity의 Instance가 여러개 존재할 수 있다. A-B-C의 순으로 Task가 Stack에 push되면 top의 값은 C이다. 만약 C에 대하여 singleTop이 걸려있지 않는다면 A-B-C-C로 스택 상태가 변경될 수 있지만 flag로 인하여 onNewIntent가 호출이 되면 A-B-C로 유지되게 된다. 하지만 B가 singleTop인 상태라고 하더라도 C가 top에 위치하고 있기 때문에 A-B-C-B와 같이 Instance가 여러개 존재하는 경우가 발생할 수 있다.

2020-06-15 16:52:51.387 27825-27825/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 16:52:51.393 27825-27825/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:52:51.404 27825-27825/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:52:54.291 27825-27825/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 16:52:54.308 27825-27825/wizley.android.playground E/SingleTopActivity: onCreate
2020-06-15 16:52:54.310 27825-27825/wizley.android.playground E/SingleTopActivity: onStart
2020-06-15 16:52:54.311 27825-27825/wizley.android.playground E/SingleTopActivity: onResume
2020-06-15 16:52:58.463 27825-27825/wizley.android.playground E/SingleTopActivity: onPause
2020-06-15 16:52:58.463 27825-27825/wizley.android.playground E/SingleTopActivity: onNewIntent
2020-06-15 16:52:58.463 27825-27825/wizley.android.playground E/SingleTopActivity: onResume

위의 상황이 A -> B -> B 이 순서로 호출한 상황인데, 이미 top에 SingleTopActivity가 존재하기 때문에 onPause가 실행된 후에 onNewIntent -> onResume의 순서로 다시 실행된 것을 확인할 수 있다.

2020-06-15 16:54:11.623 27910-27910/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 16:54:11.625 27910-27910/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:54:11.626 27910-27910/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:54:26.624 27910-27910/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 16:54:26.652 27910-27910/wizley.android.playground E/SingleTopActivity: onCreate
2020-06-15 16:54:26.656 27910-27910/wizley.android.playground E/SingleTopActivity: onStart
2020-06-15 16:54:26.656 27910-27910/wizley.android.playground E/SingleTopActivity: onResume
2020-06-15 16:54:28.068 27910-27910/wizley.android.playground E/SingleTopActivity: onPause
2020-06-15 16:54:28.089 27910-27910/wizley.android.playground E/StandardActivity: onCreate
2020-06-15 16:54:28.091 27910-27910/wizley.android.playground E/StandardActivity: onStart
2020-06-15 16:54:28.091 27910-27910/wizley.android.playground E/StandardActivity: onResume
2020-06-15 16:54:28.680 27910-27910/wizley.android.playground E/SingleTopActivity: onStop
2020-06-15 16:54:30.231 27910-27910/wizley.android.playground E/StandardActivity: onPause
2020-06-15 16:54:30.254 27910-27910/wizley.android.playground E/SingleTopActivity: onCreate
2020-06-15 16:54:30.259 27910-27910/wizley.android.playground E/SingleTopActivity: onStart
2020-06-15 16:54:30.260 27910-27910/wizley.android.playground E/SingleTopActivity: onResume
2020-06-15 16:54:30.797 27910-27910/wizley.android.playground E/StandardActivity: onStop

이번에는 A -> B -> C -> B의 순서로 호출된 결과이다. SingleTopActivity(B) 가 이미 task의 stack에 존재하지만 top은 C이므로 다시 onCreate -> onStart -> onResume의 순서로 호출되어 stack에 Instance가 2개 존재하는 것을 알 수 있다.

singleTask은 stack에 같은 Activity를 하나만 두기 때문에 stack에 있는 Activity가 호출이 되면 onNewIntent를 호출하여 라우팅한다.

2020-06-15 16:58:39.575 28370-28370/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 16:58:39.577 28370-28370/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:58:39.582 28370-28370/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:58:59.030 28370-28370/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 16:58:59.058 28370-28370/wizley.android.playground E/SingleTaskActivity: onCreate
2020-06-15 16:58:59.060 28370-28370/wizley.android.playground E/SingleTaskActivity: onStart
2020-06-15 16:58:59.060 28370-28370/wizley.android.playground E/SingleTaskActivity: onResume
2020-06-15 16:59:04.133 28370-28370/wizley.android.playground E/SingleTaskActivity: onPause
2020-06-15 16:59:04.134 28370-28370/wizley.android.playground E/SingleTaskActivity: onNewIntent
2020-06-15 16:59:04.134 28370-28370/wizley.android.playground E/SingleTaskActivity: onResume

A -> B -> B의 순서로 호출한 결과인데 SingleTask의 경우 Instance를 하나만 보유해야 하기 때문에 onNewIntent를 호출한 것을 확인할 수 있다. 만약 back버튼을 누르게 되면 다음의 동작이 추가된다.

2020-06-15 16:59:30.726 28370-28370/wizley.android.playground E/SingleTaskActivity: onPause
2020-06-15 16:59:30.760 28370-28370/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 16:59:30.761 28370-28370/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 16:59:31.278 28370-28370/wizley.android.playground E/SingleTaskActivity: onStop
2020-06-15 16:59:31.279 28370-28370/wizley.android.playground E/SingleTaskActivity: onDestroy

stack에서 pop된 SingleTaskActivity가 Pause상태가 되면서 ProtoActivity에 대하여 onStart -> onResume 작업이 수행되며 그 작업이 마무리 되면 화면에서 보이지 않기에 onStop 상태가 된 뒤 back을 통한 onDestroy가 호출이 된다.

2020-06-15 17:01:20.687 28599-28599/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 17:01:20.690 28599-28599/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 17:01:20.690 28599-28599/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 17:01:25.657 28599-28599/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 17:01:25.686 28599-28599/wizley.android.playground E/SingleTaskActivity: onCreate
2020-06-15 17:01:25.690 28599-28599/wizley.android.playground E/SingleTaskActivity: onStart
2020-06-15 17:01:25.690 28599-28599/wizley.android.playground E/SingleTaskActivity: onResume
2020-06-15 17:01:27.636 28599-28599/wizley.android.playground E/SingleTaskActivity: onPause
2020-06-15 17:01:27.662 28599-28599/wizley.android.playground E/StandardActivity: onCreate
2020-06-15 17:01:27.665 28599-28599/wizley.android.playground E/StandardActivity: onStart
2020-06-15 17:01:27.665 28599-28599/wizley.android.playground E/StandardActivity: onResume
2020-06-15 17:01:28.179 28599-28599/wizley.android.playground E/SingleTaskActivity: onStop
2020-06-15 17:01:29.044 28599-28599/wizley.android.playground E/StandardActivity: onPause
2020-06-15 17:01:29.060 28599-28599/wizley.android.playground E/SingleTaskActivity: onStart
2020-06-15 17:01:29.060 28599-28599/wizley.android.playground E/SingleTaskActivity: onNewIntent
2020-06-15 17:01:29.060 28599-28599/wizley.android.playground E/SingleTaskActivity: onResume
2020-06-15 17:01:29.561 28599-28599/wizley.android.playground E/StandardActivity: onStop
2020-06-15 17:01:29.562 28599-28599/wizley.android.playground E/StandardActivity: onDestroy

이번에는 A -> B -> C -> B의 순서로 호출을 해보았다. singleTop의 경우 새로운 Instance가 생성되었지만 singleTask의 경우 onStart -> onNewIntent -> onResume의 순서로 호출이 되는 것을 확인할 수 있다. 그리고 StandardActivity가 샌드위치 위치이기 때문에 onDestroy를 통해 제거되었다. 이제 뒤로가기 버튼을 눌러보자.

2020-06-15 17:01:45.951 28599-28599/wizley.android.playground E/SingleTaskActivity: onPause
2020-06-15 17:01:45.971 28599-28599/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 17:01:45.972 28599-28599/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 17:01:46.498 28599-28599/wizley.android.playground E/SingleTaskActivity: onStop
2020-06-15 17:01:46.499 28599-28599/wizley.android.playground E/SingleTaskActivity: onDestroy
2020-06-15 17:01:50.016 28599-28599/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 17:01:50.763 28599-28599/wizley.android.playground E/ProtoActivity: onDestroy

SingleTask가 onDestroy로 향하면서 스택에는 ProtoActivity만이 남게 되었다.

singleInstance는 singleTask와 비슷한데 Task에는 딱 그 한 Activity가 root task이자 top task임을 보장하기 위한 모드이다. task는 안드로이드에서 여러 컴포넌트들을 하나의 그룹으로 묶어서 여러 패키지를 하나의 프로그램인것마냥 동작하게 하기 위해 나누는데 사용하는데 대체 이 flag는 언제 사용하는가에 대한 궁금증이 들어서 찾아보았다. 그러나 대부분의 질문에서 거의 사용하지 않는다는 대답만 하고 예제에 대한 글을 찾기는 어려웠는데, 한 사람의 답변에 의하면 home화면의 luncher 같은 경우 딱 하나만 존재하며 한 번만 실행됨이 보장되어야 하기 때문에 이런 특수한 상황을 제외하고는 사용하지 않는다고 한다.

2020-06-15 17:05:29.833 28814-28814/wizley.android.playground E/ProtoActivity: onCreate
2020-06-15 17:05:29.835 28814-28814/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 17:05:29.836 28814-28814/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 17:05:34.377 28814-28814/wizley.android.playground E/ProtoActivity: onPause
2020-06-15 17:05:34.432 28814-28814/wizley.android.playground E/SingleInstanceActivity: onCreate
2020-06-15 17:05:34.436 28814-28814/wizley.android.playground E/SingleInstanceActivity: onStart
2020-06-15 17:05:34.436 28814-28814/wizley.android.playground E/SingleInstanceActivity: onResume
2020-06-15 17:05:39.185 28814-28814/wizley.android.playground E/SingleInstanceActivity: onPause
2020-06-15 17:05:39.201 28814-28814/wizley.android.playground E/ProtoActivity: onStart
2020-06-15 17:05:39.201 28814-28814/wizley.android.playground E/ProtoActivity: onResume
2020-06-15 17:05:39.931 28814-28814/wizley.android.playground E/SingleInstanceActivity: onStop
2020-06-15 17:05:39.931 28814-28814/wizley.android.playground E/SingleInstanceActivity: onDestroy

A -> B -> back의 순서로 호출을 해보았는데, B가 생성되는 순간 new Task에 SingleInstanceActivity가 독립적으로 push되어 top이 되며 back을 누르면 해당 task의 모든 작업이 종료되며 기존의 root task였던 ProtoActivity 부분의 onStart -> onResume이 실행된다.

Activity Flags

launchmode를 Manifest에서 지정하지 않아도 Intent로 Activity를 부르는 순간 flag를 설정할 수 있다. 이렇게 설정된 flag는 Manifest의 flag보다 우선시된다.

생각보다 많은 Flag가 존재하는데 짧게나마 살펴보고자 한다.

FLAG_ACTIVITY_BROUGHT_TO_FRONT : Developer Docs의 launchmode 부분을 보면 singleTask의 경우 system에 의해 설정되는 값이라고 한다. 만약 instance가 stack상에 이미 존재한다면 onNewIntent를 호출하는 과정에서 이 flag가 추가된 Intent가 호출된다고 한다. 만약 stack의 top에 위치하는 경우에는 해당 flag가 비활성화 된 Intent가 호출된다.

FLAG_ACTIVITY_CLEAR_TASK : 해당 Activity와 관련된 모든 task가 실행전에 정리된다고 한다. 그로 인하여 텅텅 비게된 task의 root task가 되게 된다. 그렇기 때문에 FLAG_ACTIVITY_NEW_TASK와 같이 사용해야만 한다.

FLAG_ACTIVITY_CLEAR_TOP : stack 상에서 호출되는 Activity가 이미 존재한다면 해당 Activity 상위에 존재하는 Activity 들을 정리하고 top에 위치시키게 한다. 예로 A -> B -> C -> D의 순서로 push가 되었는데 해당 플래그를 토대로 B가 호출이 되면 D -> C가 정리되고 A -> B만이 남게 된다. 여기서 많은 글들에 의하면 FLAG_ACTIVITY_SINGLE_TOP과 함께 사용하길 조언하는데 약간의 차이가 있다. SINGLE_TOP과 함께 사용되면 OnNewIntent가 호출이 되지만 CLEAR_TOP만 사용할 경우 OnNewIntent가 아닌 Destroyed 후 OnCreate로 새로 생성하게 된다.

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS : 최근 실행된 액티비티 목록에 저장되지 않도록 한다.

FLAG_ACTIVITY_FORWARD_RESULT : 결과를 전달하기위해서 사용하는데 예로 들어 이런 경우이다. A -> B -> C의 상황에서 A->B를 호출할 때 StartActivityForResult로 호출을 하면 B의 결과값이 A로 돌아가게 된다. 그런데 만약에 B->C의 결과를 A가 받고 싶을 경우에 B->C를 startActivity로 호출하는 과정에서 Intent에 해당 flag를 추가하면 setResult의 결과를 A가 가져올 수 있게 된다.

FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY : system에 의해서 추가되는 flag로 history에서 실행이 된 경우를 의미한다.

FLAG_ACTIVITY_LAUNCH_ADJACENT : 멀티 윈도우 환경에서 사용되는데 새로운 Activity가 옆에서 표시될 때 사용된다.

FLAG_ACTIVITY_MATCH_EXTERNAL : intent에 대한 처리가 불가능할 때 instant app을 통해 해결하기를 요청한다.

FLAG_ACTIVITY_NEW_TASK : task에 새로 추가하는데 affinity를 따른다. 만약 MULTI_TASK와 같이 사용된다면 무조건 새로운 Activity가 생성이 된다.

FLAG_ACTIVITY_NO_ANIMATION : Activity 전환 애니메이션을 보이지 않도록 한다.

FLAG_ACTIVITY_NO_HISTORY : IntroActivity와 같이 다시 실행할 필요가 없는 Activity를 history에 남지 않도록 한다.

FLAG_ACTIVITY_NO_USER_ACTION : onUserLeaveHint()는 사용자가 home키를 누르거나 하는 이벤트가 발생하면 background로 전환되게 되는데 이를 알리기 위해 onPause직전에 호출되는 callback인데 이를 비활성화 할 때 사용된다. 어떤 경우에 사용되는지 궁금해서 찾아보았지만 적절한 예제나 설명이 없었지만 다만 사용자에 의해 화면이 전환되는 것이 아님을 명시적으로 알리기 위해 사용되는 것 같다.

FLAG_ACTIVITY_PREVIOUS_IS_TOP : 해당 Activity를 top으로 만들지 않음으로써 Activity를 top으로 가져오는 행위를 막아준다. 대부분 이 Activity가 금방 finish될 때 사용된다.

FLAG_ACTIVITY_REORDER_TO_FRONT : A->B->C->D의 상황에서 B가 호출이 되면 A->C->D->B로 변환된다. CLEAR_TOP과 같이 명시되면 무시된다.

FLAG_ACTIVITY_SINGLE_TOP : singleTop과 똑같다.