본문 바로가기

내일배움캠프 안드로이드 3기

[TIL] 24.04.17 알고리즘, Activity와 Fragment 전환 시 생명주기 차이

1. 알고리즘 문제 해결

 

퍼즐(백준 1525):

 BFS 문제이긴 한데, 학교 알고리즘 수업을 들을 때 비슷한 문제를 A* 알고리즘을 적용해 풀었던 기억이 있어서 A* 알고리즘으로 풀어보았다. A* 알고리즘 f(x) = g(x) + h(x)라고 하고, h(x)를 휴리스틱 함수라고 할 때, g(x)는 현재까지의 이동 횟수로, h(x)는 완성된 퍼즐과의 일치 정도로 설정했다.

 메모리 제한이 32MB로 꽤나 빠듯하기 때문에, 탐색 과정에서 퍼즐정보를 2차원 정수 배열로 담았다간 우선순위 큐에 원소가 쌓이며 메모리 초과가 나기 십상일 것이다. 그래서 어떡할까 하다가 문자열을 이용하기로 했다. 9글자짜리 문자열은 비교적 적은 메모리를 차지할 것이라고 생각했기 때문이다. 만약 그럼에도 초과가 난다면 Int형 하나만 써서 퍼즐정보를 나란히 나열할 생각이었는데(9자리 정수로 치환할 수 있으므로), 그럴 필요까지는 없었다.

 이후 로직은 A* 알고리즘을 이용해 그대로 구현했다. 우선순위 큐를 이용했으며, 방문 상태처리를 위해서 해시셋 자료구조인 unordered_set을 이용했다. 퍼즐정보를 문자열로 나타내고 있기 때문에 해시셋 자료구조를 이용하면 방문여부를 빠르게 조회할 수 있을 것 같아서다.

 g(x)와 h(x)는 위에서 설명한 것처럼 구현했고, 문자열만 이용해 편하게 구현하려고 다음 노드 탐색이나 우선순위 큐에 들어가는 값 등을 약간 트릭키하게 쓰긴 했지만 로직 자체는 심플하게 짰고 통과할 수 있었다.

 

#include <iostream>
#include <queue>
#include <unordered_set>

using namespace std;

int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 3, 0, -3};

int h(const string* s) {
    int score = 0;
    for(int i=0; i<8; i++) {
        if((*s)[i]=='1'+i) {
            score++;
        }
    }
    if((*s)[8]=='0') {
        score++;
    }

    return score;
}

int main() {
    string matrix;
    for(int i=0; i<9; i++) {
        char c;
        cin >> c;
        matrix.push_back(c);
    }

    priority_queue<pair<int, string> > pq;
    unordered_set<string> isVisited;
    pq.push({-h(&matrix), matrix});

    while(!pq.empty()) {
        int hScore = h(&pq.top().second);
        int gScore = -pq.top().first-hScore;
        string curMatrix = pq.top().second;
        pq.pop();

        if(isVisited.find(curMatrix)!=isVisited.end()) continue;

        if(hScore==9) {
            cout << gScore;
            return 0;
        }
        isVisited.insert(curMatrix);
        
        int zeroPosition = -1;
        for(int i=0; i<9; i++) {
            if(curMatrix[i]=='0') {
                zeroPosition = i;
                break;
            }
        }

        for(int i=0; i<4; i++) {
            int newPosition = zeroPosition+dy[i]+dx[i];
            if((dy[i]!=0&&newPosition>=0&&newPosition<9) || (dy[i]==0&&newPosition!=-1&&newPosition/3==zeroPosition/3)) {
                string newStr = curMatrix;
                newStr[zeroPosition] = curMatrix[newPosition];
                newStr[newPosition] = curMatrix[zeroPosition];
                if(isVisited.find(newStr)==isVisited.end()) {
                    pq.push({-(gScore+1+h(&newStr)), newStr});
                }
            }
        }
    }

    cout << "-1";
}

 

 

 

2. Activity와 Fragment의 생명주기에 관하여

 안드로이드의 생명주기에 관해서 헷갈리는 부분이 있어서 이것저것 확인해보고 있었는데, 몰랐던 사실을 하나 발견했다. View가 이미 존재하는 상태에서 새로운 View를 생성하고, 기존의 View가 소멸할 때 생명주기 메소드들이 호출되는 순서를 보고 있었는데, Activity와 Fragment의 경우가 미세하게 달랐다.

 

 Activity의 경우, 기존 Activity의 onPause()가 호출되고, 새로운 Activity가 생성된 후 onResume()까지 호출된다. 이후 기존 Activity가 onStop() - onDestroy()를 거쳐 파괴되는 것을 알 수 있다. 물론 파괴 없이 기존 Activity를 백스택에 쌓아둔다면 onStop()까지만 호출되고,  새로운 Activity도 생성하는 게 아니라 백스택에 있는 것을 사용한다면 onStart()부터 시작할 것이다.

 

 

 하지만 Fragment의 경우, 기존 Fragment의 onPause()까지 호출되는 것이 아니라 onStop()까지 호출된다. 그리고 새로운 Fragment의 생성 과정을 거쳐 onResume()까지 진행하는 것이 아닌, onStart()까지만 호출되고 기존 Fragment가 파괴된 뒤에 onResume()이 마저 호출된다.

 

 물론 Activity에서 설명했던 경우처럼, 백스택을 이용하게 되면 차이가 있다. 백스택에 새로운 Fragment를(정확히는 Fragment를 replace하는 트랜잭션) 쌓을 때는 기존 Fragment 자체는 파괴되지 않고, 백스택에서 pop해서 기존 Fragment로 되돌아갈 땐 새로운 Fragment는 뷰를 비롯해 Fragment 그 자체까지 파괴된 후, 기존 Fragment는 뷰 생성부터 다시 시작하게 된다.

 

 어쨌든, 백스택을 이용할 때와 이용하지 않을 때 차이가 생기는 것은 당연한 것이고, 주목할 점은 Activity와 Fragment의 차이이다. 

- Activity 전환시: 기존 뷰 onPause()까지 진행 -> 새로운 뷰 생성 후, onResume()까지 진행 -> 기존 뷰 onStop() 후 소멸
- Fragment 전환시: 기존 뷰 onStop()까지 진행-> 새로운 뷰 생성 후, onStart()까지 진행 -> 기존 뷰 소멸 -> 새로운 뷰 onResume() 진입

 

 정확한 이유는 모르겠지만 추정컨대, Activity와 Fragment의 목적과 특징에 따른 의도된 설계일 것 같다.

 

 아무래도 Activity는 무거운 객체이고 생성 및 소멸 과정도 더 많은 리소스를 이용할 것이다. 그래서 유저 입장에서 좀 더 신속한 전환을 경험하도록 onPause()까지만 진행하고, 바로 새로운 View를 상호작용 할 수 있는 onResume()까지 진행한 뒤 기존의 뷰를 소멸시키는 것 같다. 

 

 Fragment는 상대적으로 가벼운 객체이고 호스트 Activity 내에서 잦은 전환, 생성, 소멸을 염두에 두고 만들어졌다. 그래서 기존 뷰를 onStop()까지 진행시켜 화면에서 완전하게 뷰를 제거하고 새로운 뷰를 생성해 화면에 노출시킨 뒤에(onStart()까지), 기존 뷰를 마저 소멸시키고 새로운 뷰를 상호작용 가능하게 만들어도 충분히 짧은 시간 안에 처리 가능한 것 같다.
 기존의 뷰가 확실하게 없어지고 새로운 뷰가 나타나는 형태가 더 합리적인지는 확신할 순 없지만, Fragment는 유연하게 동작할 필요가 있으므로 그런 측면에서 생각하면 옳은 방향인 것 같긴 하다.

 

 확실히 직접 하나하나 뜯어보면 모르던 걸 알게 된다.