제임스딘딘의
Tech & Life

개발자의 기록 노트/Android

[안드로이드] 안드로이드의 Process & Threads

제임스-딘딘 2012. 2. 8. 17:09

안드로이드의 Process & Threads

안드로이드의 프로세스(Process)와 스레드(Threads)에 대해서 공부해 보았다.

학교에서 배운 운영체제, 시스템 프로그래밍 등의 과목에서 배운 개념과 크게 다른 점은 없지만, 안드로이드의 프로세스와 스레드에는 분명 안드로이드 고유의 특성이 추가적으로 포함되어 있다.

다음 링크의 자료를 참조 하였다. 


시작하며

 안드로이드 어플리케이션 컴포넌트(components)가 실행하려 할 때, 이미 시작된(running) 다른 컴포넌트가 없다면, 시스템은 단일한 하나의 스레드에서 실행되도록 하는 어플리케이션 프로세스를 시작한다. 기본적으로, 같은 어플리케이션에서 실행되는 모든 컴포넌트들은 같은 프로세스와 (main thread 라고 불리는) 스레드에서 돌아간다. 만약 어플리케이션이 컴포넌트를 시작하려고 할 때, 이미 해당 어플리케이션의 프로세스가 돌아가고 있는 중이라면, 이제 새롭게 시작하는 어플리케이션 컴포넌트는 그 프로세스에서 시작되어 동일한 스레드를 이용하게 된다. 하지만 개발자는 필요에 따라, 어떤 컴포넌트들은 다른 프로세스나 스레드에서 돌아가도록 할 수도 있다.

 
* 안드로이드 컴포넌트 : activity, service, receiver, provider

 

프로세스(process)

앞에서도 설명했듯이 기본적으로 같은 어플리케이션에서 시작되는 모든 컴포넌트들은 같은 프로세스에서 돌아가고, 실제도 대부분의 어플리케이션은 이를 수정하지 않는다. 그러나, 가령 특정한 컴포넌트가 속한 프로세스를 관리하려 할 필요가 있을 수 있는데, 이를 manifest.xml 파일에서 관리 해 줄 수 있다.
 
 각 컴포넌트들의 manifest.xml 파일의 진입 포인트 - <activity> <service> <receiver> <provider> - 에서는 android:process 라는 속성을 지원한다. 이 속성을 사용하게 되면, 이 속성을 사용한 컴포넌트는, 이 속성에서 명시한 프로세스에서 돌아가게 된다. 그러므로 이 속성을 활용하면, 개발자가 필요에 따라 적절하게 컴포넌트들이 어떤 프로세스에서 돌아가야 하는지를 명시할 수 있고, 혹은 여러개의 컴포넌트들이 하나의 프로세스를 공유할 수도 있다. 또한, 다른 어플리케이션에서도 프로세스에 접근 가능하도록 할 수도 있다.
 
<application> 도 또한 android:process 속성을 가지는데, 해당 어플리케이션의 모든 컴포넌트들에게 기본값에 해당하는 프로세스를 지정할 때 쓰인다.
 
 안드로이드에서는 다른 프로세스를 실행하려는데 메모리가 부족할 때, 기존에 동작하고 있는 프로세스를 죽임으로써 메모리를 확보하려고 한다. 이 때 프로세스가 죽게되면, 그 프로세스에서 돌아가고 있던 컴포넌트들 역시 죽게 된다. 시스템에서 프로세스를 죽일 때는, 유저의 입장에서 덜 중요한 프로세스 부터 죽이게 된다. 예를 들어, 화면에 보여지는 컴포넌트를 가지고 있는 프로세스보다는 화면에 보이지 않는 컴포넌트를 가진 프로세스를 먼저 죽이게 된다. 결론은, 프로세스를 죽이는 기준은, 그 프로세스에서 돌아가고 있는 컴포넌트들의 상태에 의해 결정된다.

Process life-cycle
 안드로이드 시스템은 할 수만 있다면 최대한으로 어플리케이션 프로세스를 유지하려고 한다. 그러나, 메모리 공간은 한계가 있기 때문에, 메모리 공간이 부족하게 되면 어쩔 수 없이 프로세스를 종료시켜야 한다. 이 때 시스템에서 어떤 프로세스를 먼저 죽여야할 지 중요도를 결정짓게 되는데, 그 중요도라는 것이 앞에서 언급한대로, 프로세스에서 돌아가고 있는 컴포넌트들이 얼마나 중요한지에 따라 결정되는 것이다.
 
프로세스는 중요도에 따라 총 다섯가지 단계로 나누어 지며, 가장 덜 중요한 프로세스부터 죽게 된다.

 1) Foreground process
유저가 뭔가를 하고 있어서 지금 필요로 하는 프로세스가 여기에 해당한다. 구체적으로, 아래의 조건에 해당하는 프로세스들을 의미한다.
- 유저와 상호작용하고 있는 activity 를 호스팅한 프로세스 (액티비티의 onResume() 메소드가 호출된 상태)
- 유저와 상호작용하고 있는 activity에 바운드(bound) 된 서비스를 호스팅한 프로세스
- in the foreground 에서 돌아가고 있는 서비스를 호스팅한 프로세스 (서비스가 startForeground() 를 호출한 상태)
- 다음의 생명주기(lifecycle) 콜백 메소드 - onCreate(), onStart(), onDestroy() - 중에서 하나를 실행한 서비스를 호스팅한 프로세스
- onReceive() 메소드를 실행한 BroadcastReceiver 를 호스팅한 프로세스
 
2) Visible process
foreground components 를 가지고 있진 않지만, 여전히 화면에 보여지는 컴포넌트들을 가지는 프로세스가 이에 해당한다.
- foreground 에 있진 않지만 여전히 유저의 눈에 보이는 액티비티를 가지는 프로세스 (onPause() 까지 호출 된 상태)
- 눈에 보이거나 혹은 foreground 에 있는 액티비티에 바운딩(bound) 된 서비스를 호스팅한 프로세스
 
3) Service process
상위 두 개의 카테고리에 속하지 않으면서, startService() 메소드를 실행한 서비스(service) 를 동작하는 프로세스가 여기에 해당한다.  가령, 배경음악을 듣는다던지, 그런것들이 속한다.
 
4) Background process
눈에 보이지 않는 액티비티(onStop() 이 호출된 액티비티)를 가지고 있는 프로세스가 여기에 해당한다. 이들은 유저에게 직접적으로 영향을 끼치지 않기 때문에 시스템이 언제든지 삭제를 할 수 있다. background 프로세스들은 다양할 수 있기 때문에, 이들 프로세스들은 LRU(least recently used) 리스트에서 관리되며, 그래서 가장 최근에 보여진 액티비티를 가지는 프로세스가 가장 나중에 죽을 수 있도록 해준다.
 
5) Empty process
어떠한 활성화된 컴포넌트들도 가지고 있지 않은 프로세스이다. 그럼에도 불구하고 이런 종류의 프로세스를 유지하는 이유는, 캐시(cache) 를 활용하여 다음번 컴포넌트가 실행될 때 실행 시간을 빨리하기 위함이다.
 
안드로이드에서 프로세스의 중요도를 계산할 때는, 현재 프로세스에서 활성화된 컴포넌트의 중요도에 따라 결정짓는다. 예를 들어, 프로세스가 서비스 컴포넌트와 눈에 보이는 액티비티 컴포넌트를 가지고 있다면, 프로세스의 중요도는 눈에 보이는 액티비티의 중요도와 같아 진다. (만약, 서비스 컴포넌트의 중요도로 프로세스 중요도를 결정한다면, 이 프로세스가 눈에 보이는 액티비티를 가지고 있기 때문에, 함부로 죽으면 안됨에도 불구하고, 시스템에서는 서비스 컴포넌트 만큼의 중요도를 가지고 있다고 생각하고, 눈에 보이는 액비비티가 있는 상황에서 프로세스를 죽여버리는 불상사가 발생하게 될 것이다.)
 
또한 프로세스의 랭킹은 다른 프로세스와의 관계에 따라 결정된다. 만약 A 프로세스가 B 프로세스를 서브(serve) 하고 있다면, A 프로세스의 랭킹이 B 프로세스보다 낮을 수 없다.

 

스레드(threads)

어플리케이션이 시작하게 되면 시스템은 어플리케이션을 실행하기 위해 thread 를 만든다. 이 스레드가 main thread 라고 불린다. 이 스레드가 중요한 것이 이벤트가 발생하면 (drawing 이벤트를 포함하여) 그 이벤트를 적절한 유저 인터페이스 위젯에게 할당하는 역할을 한다. 안드로이드 UI toolkit 과 컴포넌트 사이의 상호작용도 이 스레드 내에서 일어난다. main thread 는 UI thread 라고 불리기도 한다.
 
시스템은 각 안드로이드 컴포넌트에 대해서 새로운 스레드 인스턴스(thread instance) 를 만들지 않는다. 같은 프로세스에서 동작하는 모든 컴포넌트들은 UI thread 에서 시작되며(instantiate), 각 컴포넌트들에 대한 시스템 콜(system call) 은 바로 이 스레드로 부터 처리된다(dispatch). 그러므로 시스템 콜백에 응답하는 메소드들 - onKeyDown() 이나 life cycle callback 같은 메소드들 - 은 모두 같은 프로세스의 UI 스레드에서 돌아간다.
 
만약 개발자가 개발하려는 어플리케이션의 무겁고, 강도높은 일을 할 경우, 이 모든 일을 UI 스레드에서 하게 한다면, 퍼포먼스가 상당히 떨어질 것이다. 특히, 네트워크나, DB 로부터 데이터를 읽어오는 이런 일을 할 경우, 동작 시간이 길어지면서 전체 UI 를 블락(block) 시키게 된다. thread 가 블락(block) 되게 되면, 드로잉 이벤트를 포함하여 어떤 이벤트도 처리되지 못한다. 유저 입장에서는 어플리케이션이 멈춘 것처럼 느낄 것이다. 게다가 만약 UI thread 가 5 초 이상 지속하게 되면, ANR(application not responding) 다이어로그 를 받게된다.
 
안드로이드 UI tookit 은 thread-safe 가 아니다. 그러므로, worker-thread 에서 UI 를 직접 조작하면 안된다.
UI (user interface) 에 관한 부분은 UI thread 에서 반드시 처리해 줘야 한다.
 
결론적으로 안드로이드 UI는 단일 스레드 모델이고, 다음의 간단한 두 가지 룰만 따르면 된다.
 
- UI thread 를 블락(block) 시키지 말 것! (5초 이상의 시간이 소요되는 작업 수행)
- UI thread 외의 다른 thread(worker-thread) 에서 안드로이드 UI toolkit 에 접근하지 말 것!


worker threads (UI thread를 제외한 필요에 의해 생성된 다른 thread 들) 
 위에서 봤듯이, UI thread 를 블락 시키지 않으면서 하나의 스레드 만으로 어플리케이션을 만들 경우 그 퍼포먼스는 보장할 수 없다. 그러므로 순간으로 해결할 수 있는 문제가 아니라면, 분리된 스레드 (background or worker thread) 에서 처리 해 주는 것이 좋다.

 

예를 통해 살펴보자. 아래 예는  분리된 스레드 로 부터 이미지를 다운 받아서, ImageView 에 보여주는 코드이다.

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

위의 코드의 문제는 싱글 스레드 모델의 두 번째 룰을 깨고 있다. 

즉, worker thread 에서 UI toolkit 에 접근한 것이다. 이럴 경우, 빌드 자체가 안되거나 런타임 에러가 발생할 것이다. (예상하기도 힘든 행동을 보여줄 수도 있다.)

현재는 CalledFromWrongThreadException 이 발생하는 것으로 확인 했다. 

이를 고치기 위해 안드로이드에서는 다른 thread 에서 UI thread 로 접근할 수 있는 몇 가지 방법을 제시한다.

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)


위의 처음 코드를 View.post(Runnable) 메소드를 적용하여 수정하면 아래와 같다.

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}


위 처럼 수정함으로써 이제 thread-safe 한 상태가 되었다.

그러나 이렇게 작성하게 되면, 코드가 길어짐에 따라 상당히 복잡해 보이고 직관적으로 이해하기 어려워질 수 있다. 보다 복잡한 작업을 위해서는 work thread 에 Handler 를 이용하는 방법이 있다. 그러나 가장 최고의 선택은, AsyncTask 클래스를 상속하여 사용하는 것이라고 한다.



AsyncTask 클래스 사용하기

AsynchTask는 유저 인터페이스에 대한 비동기적인 작업을 수행하는 것을 가능하게 한다. worker thread 에서의 작업(operation)을 블락(block) 한 후에 그 결과는 UI thread 로 보낸다. 이 때, 개발자가 thread 나 handler 를 조종하도록 요구하지 않는다.

 

이 클래스를 활용하기 위해선 AsynchTask 를 상속하여, doInBackground() 콜백 메소드를 구현한다. 이 메소드는 background thread(worker thread 라고 봐도 무방할 듯한 thread) 의 pool 에서 동작한다. 그리고 UI 를 업데이트 하기 위해서는 onPostExecute() 를 구현해야 한다. onPostExecute() 메소드는 doInBackground() 의 결과를 전달하며, UI thread 에서 동작한다. 그래서 UI (user interface) 를 안전하게 업데이트 할 수 있다. 그러고 구현하고 나서 사용방법은 해당 task 를 UI thread 에서 execute() 를 호출함으로써 실행시킬 수 있다.

 

아래는 맨 처음 코드를 AsyncTask 클래스를 상속하여 해결한 방법이다.

 

이렇게 함으로써 UI 는 thread-safe 하고 코드도 간결해졌다.
AsyncTask 의 간단한 정보는 아래와 같다.

 

  • - You can specify the type of the parameters, the progress values, and the final value of the task, using generics
  • - The method doInBackground() executes automatically on a worker thread
  • onPreExecute()onPostExecute(), and onProgressUpdate() are all invoked on the UI thread
  • - The value returned by doInBackground() is sent to onPostExecute()
  • - You can call publishProgress() at anytime in doInBackground() to execute onProgressUpdate() on the UI thread
  • - You can cancel the task at any time, from any thread