이 게시글은 아래의 글을 참고해서 작성했습니다.
JVM
자바로 작성한 코드를 컴퓨터에 이해시키리면 컴퓨터가 이해할 수 있는 언어로 컴파일해야 합니다. 다만 세상에는 다양한 운영체제가 있고, 각 운영체제에 맞는 언어로 컴파일하는 것은
자바가 말하는 WORA("Write Once Run Anywhere" - 한 번 작성하면 모든 곳에서 돌릴 수 있다)가 아닙니다.
이때 나타나는 것이 바로 JVM입니다. 각 운영체제에 맞는 JVM을 설치하면 JVM은 자바 바이트코드를 읽고 각 OS의 언어로 컴파일합니다.
JVM을 사용하면 한 번의 작성으로 모든 운영체제에서 실행할 수 있게 됩니다.
JVM에 대해 더 자세하게 알고 싶으면 아래의 게시글을 참고하면 좋습니다.
JVM과 Android
Android는 Java를 채택한 모바일 운영체제입니다. 그렇기에 Java를 사용한 바이트코드를 사용하기 위해서는 JVM을 거쳐야 합니다.
다만, JVM은 제한되지 않은 전원과 저장 공간을 가지고 있다는 것을 전제로 설계되었습니다. 그렇기에 우리가 사용하는 안드로이드 기기는 JVM의 개념을 가지고 조금 다른 방식으로 바이트코드를 컴파일 합니다.
Dalvik
Dalvik은 안드로이드가 JVM 대신 사용한 가상 머신(Virtual Machine)입니다. 안드로이드 4.4 KitKat 이전 버전까지 Dalvik VM이 사용되었습니다. Dalvik VM은 안드로이드 앱을 실행하기 위해 Java 바이트 코드를 Dalvik 바이트 코드로 변환하여 사용했습니다. 이렇게 함으로써 안드로이드 앱은 안드로이드 운영체제에서 실행되기 위한 최적화된 형태로 실행되었습니다.
일반적인 자바 바이트코드가 스택 기반으로 동작하는 반면, dex 바이트코드는 레지스터 기반으로 동작합니다.(모든 변수는 레지스터에 저장됩니다). dex 접근 방식은 일반적인 자바 바이트코드보다 훨씬 효율적이며 공간을 적게 필요로 합니다.
아래의 게시글이 바로 자바 바이트코드를 Dalvik에서 사용되기 위한 .dex 파일로 컴파일하는 과정을 보여줍니다.
위의 게시글을 요약하자면 .java 및 .kt 파일은 Java/Kotlin 컴파일러에 의해 .class 파일로 컴파일됩니다. 이러한 .class 파일은 Dex 컴파일러에 의해 .dex 파일로 변환되고, 마지막으로 .apk 파일에 패키징됩니다.
안드로이드 부팅
안드로이드에서 애플리케이션의 실행에 대해서 설명하기 위해 안드로이드의 부팅에 대해 설명하겠습니다.
안드로이드의 부팅은 아래의 그림으로 간략하게 나타낼 수 있습니다.
Step 1 - 전원 켜기 및 시스템 시작
전원을 키면 Boot ROM의 코드가 미리 정의된 위치에서 실행을 시작하며, RAM에 Boot Loader를 로드하고 실행합니다.
Step 2 - Boot Loader
부트로더는 Android 운영 체제 실행 전에 실행되는 작은 프로그램입니다.
부트로더는 실행을 두 단계로 수행하며, 첫 번째 단계는 외부 RAM을 감지하고 두 번째 단계에서는 커널을 실행하는 데 필요한 네트워크, 메모리 등을 설정합니다. 부트로더는 커널에게 특정 목적을 위한 구성 매개변수 또는 입력을 제공할 수 있습니다.
Step 3 - Kernel
안드로이드는 리눅스 기반의 플랫폼입니다. 위의 부트로더를 통해 리눅스 커널이 부팅되면 일반적인 리눅스 부팅처럼 커널 초기화 작업을 거칩니다. 메모리, 캐시 등을 셋업하고 스케쥴링하는 작업을 하고 드라이버도 로딩합니다. 커널이 이러한 시스템 셋업 과정들을 끝내고 부팅을 완료하면 마지막으로 시스템 파일 중에서 "init" 파일을 찾아 root process 또는 시스템의 첫 프로세스로 지정된 것을 실행합니다.
Step 4 - init
init 프로세스는 모든 프로세스의 조상이라 부를 수 있는 root process, 즉, 가장 최초의 프로세스입니다.
init 프로세스는 각종 디바이스를 초기화하는 작업을 비롯해서 안드로이드 프레임워크 동작에 필요한 각종 데몬, 시스템 매니저 (컨텍스트 매니저), 미디어 서버(Meia Server), Zygote 등을 실행하는 역할을 합니다.
Step 5 - daemon
init 프로세스는 해야 할 일을 기술한 init.rc 파일에 있는 데몬 어플들을 구동시킵니다.
우리가 안드로이드 개발을 하다가 들어본 ~~데몬이라고 하는 것들이 바로 여기에서 실행됩니다.
Step 6 - Zygote(app_process)
이 부분이 공부를 하면서 가장 헷갈리고 어려웠던 부분입니다.
각 안드로이드 어플리케이션은 독립적인 VM(Virtual Machine) 위에서 동작하는데, 실행될 때마다 자신이 동작할 VM 을 초기화하고 실행하는 과정에는 많은 시간이 소요되며 어플리케이션의 실행을 느리게 하는 요인이 됐습니다.
때문에 안드로이드 Zygote 는 애플리케이션이 실행되기 전에 실행된 VM 의 코드 및 메모리 정보를 공유함으로써 어플리케이션이 실행되는 시간을 단축시킬 수 있게 됐습니다.
Zygote 는 '분할 전의 세포나 수정란', 개체가 생성되기 이전의 불완전한 상태이란 뜻으로 안드로이드 애플리케이션의 로딩 시간을 단축하기 위해 고안된 프로세스입니다. 모든 자바 기반 안드로이드 애플리케이션은 Zygote 를 통해 미리 포크(fork)된 프로세스 상에서 동작합니다.
Zygote 는 아래와 같은 과정을 통해 실행됩니다.
Zygote 는 자바로 작성되어 있습니다. 그렇기에 VM을 생성해야 합니다. 그래서 Zygote 는 AppRuntime 객체를 생성하고 실행합니다.
AppRuntime 객체는 AndroidRuntime 클래스를 상속하고 있는데 AndroidRutime 클래스는 가상 머신을 초기화하고 실행합니다.
그럼 가상머신이 생성되고 초기화된 가상머신 위에서 ZygoteInit 클래스에 로딩하게 됩니다.
ZygoteInint에 진입하면 새로운 애플리케이션 실행 요청을 받기 위해 소켓 바인딩을 합니다.
그리고 System Server가 실행됩니다.
Step 7 - System Server
System Server 가 ZygoteInit 클래스에 의해 실행되면 Audio Flinger 와 Surface Flinger 라는 네이티브 서비스를 실행합니다.
필요한 네이티브 서비스가 실행되고 나면 System Server 는 안드로이드 프레임워크 시스템 서비스들을 시작합니다.
System Server와 카메라 같은 하드웨어를 담당하는 Media Server란 안드로이드에서 제공하는 애플리케이션 프레임워크와 하드웨어 사이를 연결해주는 기능을 담당하는 서비스로 이 둘을 Android System Services라 부릅니다.
Android System Services는 수십개의 기능들로 구성되어 있으며 애플리케이션 프로그래밍을할 때 필요로 하는 기능들을 제공합니다.
이런 서비스들은 통신을 수행할때 Binder를 사용합니다.
Step 7 - Binder
안드로이드는 아래의 그림과 같은 수 많은 기술이 존재합니다. 각 안드로이드의 컴포넌트들은 Linux의 프로세스로 표현되며 각각 격리되어 보호되면서 동작합니다.
그렇기에 모든 시스템 서비스가 통신을 하기 위해서는 다른 프로세스로 요청과 응답을 보내는 매커니즘이 필요합니다. 이 매커니즘이 바로 바인더입니다.
바인더 매커니즘은 "모든 프로세스가 공유할 수 있는 영역에 요청 내용과 응답 내용을 쓰고, 각 프로세스가 메모리 주소를 참조하게 하자." 라는 것입니다.
커널 공간을 사용하도록 바인더 드라이버가 구현되어 있습니다. 바인더 드라이버의 역할은 각 프로세스가 매핑해 놓은 메모리 주소와 커널 공간의 메모리 주소를 변환하여 참조해 사용할 수 있도록 하는 것입니다.
Step 8 - Service Manager(Context Manager)
바인더를 이용하여 시스템 서비스와 클라이언트가 통신을 할 때 서비스들을 관리하는 무언가가 필요합니다. 바인더 내부에서 할 수 도 있겠지만 안드로이드에서는 Service Manager라는 서비스 서버를 만들어 관리합니다.
그래서 애플리케이션이나 프레임워크의 내무 모듈은 Service Manager에 이용을 요청하고 그 이후에 바인더를 사용해서 시스템 서비스를 이용합니다.
다시 한번 아래의 그림을 보겠습니다. 이제 안드로이드가 어떻게 부팅되는지 조금 알게 된것 같지 않은가요??
이제는 한번 앱을 실행시켜 보겠습니다. 어떻게 VM이 만들어졌는지는 이제 알것이라 생각합니다. ㅎㅎ
혹시 모를 수 있으니까 안드로이드가 부팅되기 까지의 과정을 정말 간단하게 쭉 말해보겠습니다.
- 안드로이드 앱이 부팅됩니다.
- 부트 로더가 커널을 실행합니다.
- 커널은 init을 찾아 실행합니다.
- init은 데몬, Zygote, 시스템 매니저, 미디어 서버를 실행합니다.
- Zygote는 VM을 만들고 ZygoteInit 클래스를 실행합니다.
- ZygoteInit 클래스는 시스템 서버를 실행하고 시스템 서버와 미디어 서버는 시스템 매니저가 관리합니다.
- 새로운 애플리케이션을 실행하면 Zygote가 자식 Zygote를 fork합니다.
- 자식 Zygote에서 생성된 VM에서 프로세스가 실행됩니다.
Dalvik에서의 앱의 실행
.apk 파일에는 .dex 파일과 여러 리소스가 있습니다. 우리가 안드로이드 앱을 실행하면 .dex 코드는 실행 엔진(Interpreter나 JIT 컴파일러)에 의해 런타임에 컴파일 됩니다.
Interpreter와 JIT 컴파일러
이 두 기능은 서로 협력하여 작동합니다. 우리가 프로그램을 실행할 때마다, Interpreter는 바이트 코드를 선택하고 이를 기계 코드로 해석합니다. Interpreter의 단점은 특정 메서드가 여러 번 호출될 때마다 매번 새로운 해석이 필요하다는 것입니다.
그리고 JIT 컴파일러가 등장하여 Interpreter의 단점을 보완합니다. 실행 엔진은 Interpreter의 도움을 받아 바이트 코드를 변환하지만, 반복 코드가 발견되면 JIT 컴파일러를 사용합니다. JIT 컴파일러는 가능한 한 많은 바이트 코드(일정 임계값까지)를 컴파일하고 네이티브 코드로 변환합니다. 이 네이티브 코드는 반복되는 메서드 호출에 직접 사용되며 시스템의 성능을 향상시킵니다. 반복되는 코드를 "Hot Code"라고도 부릅니다.
ART(Android Runtime)
Dalvik도 충분히 괜찮았지만 제한사항이 존재했습니다. 그래서 Google은 개선된 JVM인 ART를 도입했습니다. 주요한 차이점은 ART는 런타임에서 Interpreter/JIT를 실행하지 않는다는 것입니다.
ART는 앱을 실행하기 전에 바이트 코드를 미리 컴파일하여 변환하는 "AOT(Ahead-Of-Time) 컴파일" 방식을 사용합니다.
바이트 코드는 .oat 바이너리로 컴파일 됩니다. ART는 앱을 실행할 때 Dalvik처럼 런타임에 바이트 코드를 실시간으로 해석하지 않고 .oat 바이너리에서 가져오기만 하면 되기에 실행 속도가 빨라지고 효율성을 향상시켰습니다.
하지만... 😭
ART가 좋은 점만 있는것은 아니였습니다. .dex 파일을 .oat 바이너리로 컴파일하는 작업은 설치 프로세스의 일부였으며, 앱을 설치하거나 업그레이드하는 데 걸리는 시간에 큰 영향을 미쳤습니다.
또한 전체 .dex 파일이 .oat 바이너리로 컴파일 되기에, 사용자가 거의 사용하지 않거나 전혀 사용하지 않은 부분까지 (예: 사용자가 한 번 설정하고 다시 사용하지 않는 앱 설정 또는 로그인 화면) 모두 포함되었습니다. 결국, 디스크 공간을 낭비하게 되었고, 저장 공간이 제한된 저가형 기기에 문제가 발생했습니다.
👍 Interpreter, JIT, AOT의 결합!!!
Google은 이 문제를 AOT와 Interpreter, JIT를 함께 사용하는 방식으로 해결했습니다.
위의 그림은 다음과 같은 프로세스를 나타냅니다.
1. 처음 앱을 실행할 때는 어떠한 .oat 바이너리도 없습니다. ART는 Interpreter를 사용하여 코드를 실행합니다.
2 .HOT Code(빈번하게 실행되는 코드)가 감지되면 JIT 컴파일러를 사용하여 컴파일됩니다.
3. JIT 컴파일된 코드와 컴파일 프로파일은 캐시에 저장됩니다. 이후의 실행은 이 캐시를 사용합니다.
4. 기기가 휴면 상태(화면이 꺼지거나 충전 중)가 되면, HOT Code가 AOT 컴파일러와 컴파일 프로파일을 사용하여 다시 컴파일됩니다.
5. 앱을 실행하면, .oat 바이너리의 코드가 한 번에 더 나은 성능으로 실행됩니다. .oat 바이너리에 코드가 없으면 단계 1로 돌아갑니다.
최적화
여기서 더 큰 최적화 방법이 등장합니다. 바로 유사한 장치 간에 컴파일 프로파일을 공유하는 것입니다.
기기가 휴면 상태이고 Wi-Fi 네트워크에 연결된 경우, Google Play 서비스를 통해 컴파일 프로파일 파일을 공유합니다. 나중에 같은 기기를 가진 다른 사용자가 Play 스토어에서 앱을 다운로드하면, 해당 기기는 이러한 프로파일을 받아들여 AOT가 가이드 컴파일을 수행하게 됩니다. 결과적으로 사용자들은 처음 사용부터 최적화된 앱을 받게 됩니다.
마지막
이렇게 해서 우리가 만든 앱이 어떻게 실행되는지에 대해 알아 보았습니다.
Reference
https://madhusudhanrc.blogspot.com/2013/08/android-boot-sequence-process.html
'Android' 카테고리의 다른 글
Android 4대 컴포넌트 - BroadCast receiver (0) | 2023.08.24 |
---|---|
Android 4대 컴포넌트 - Service (0) | 2023.08.21 |
Android의 Build Process (0) | 2023.08.20 |
UI 상태 저장과 ViewModel로 상태 저장하기 (0) | 2023.08.05 |
작업 및 백 스택 이해 (0) | 2023.08.04 |