안드로이드 Context 는 어떻게 생성될까?
안드로이드 프레임워크 분석 — Context 초기화
안드로이드/코루틴 분석하기 시리즈의 첫 번째 대상은 “Context” 입니다. Context 는 안드로이드 프로그래밍에서 절대 빠질 수 없는 클래스며, 그만큼 가장 어려운 클래스이기도 합니다. 이번 글에서는 우리가 Context 를 사용하기 위해 그 안에서 무슨 일이 일어나고 있는지를 보려고 합니다.
Context 의 이론적인 내용은 딥다이브에서 벗어남으로 다루지 않습니다. 관련한 내용은 이 블로그를 참고해 주세요. 개인적으로 Context 이론을 공부하기 좋았던 글 입니다. (또한 성빈랜드 스터디에서 추천을 받은 글이기도 합니다)
Context 는 abstract class 에서 시작합니다.
public abstract class Context
Context 의 JavaDoc 은 Context 를 다음과 같이 설명하고 있습니다.
응용 프로그램 환경에 대한 전역 정보에 대한 인터페이스입니다. 이것은 Android 시스템에서 구현을 제공하는 추상 클래스입니다. 애플리케이션별 리소스 및 클래스에 대한 액세스는 물론 활동 시작, 브로드캐스트 및 수신 의도 등과 같은 애플리케이션 수준 작업에 대한 호출을 허용합니다.
이렇듯 Context 는 애플리케이션의 전역 정보를 제공하여(Context 초기화에 중요하게 작용합니다) 애플리케이션 수준의 작업에 접근할 수 있게 만들어주는 역할을 담당하고 있습니다. 이런 Context 는 abstract class 라 초기화하여 사용하기 위해선 다른 extends 클래스가 필요합니다. 이를 위해 ContextWrapper 클래스가 존재합니다.
public class ContextWrapper extends Context
ContextWrapper 클래스는 내부의 모든 구현을 다 Context 를 델리게이트하고 있는 말 그대로의 래퍼 클래스 입니다. 이는 원래의 Context 구현을 변경하지 않고 원하는 동작만 수정하기 위한 서브클래싱 용도로 사용됩니다.
다음으로 우리가 간접적으로 매번 사용하고 있는 ContextThemeWrapper 가 나옵니다.
public class ContextThemeWrapper extends ContextWrapper
ContextThemeWrapper 는 상속받은 Context 의 테마를 수정할 수 있는 ContextWrapper 의 래퍼입니다. 추후 이 ContextThemeWrapper 와 일반 ContextWrapper 에 대해서 개별 아티클이 작성될 예정입니다.
마지막으로 이 ContextThemeWrapper 를 Activity 에서 상속받고 있습니다.
public class Activity extends ContextThemeWrapper
이렇게 해서 Context 를 Activity 에서 제공받을 수 있는 것입니다. Activity 에서 Context 를 사용할 수 있는 방법은 이렇게 확인하였습니다. 하지만 Activity 로 부터 제공되는 Context 는 어디서 초기화되는 걸까요? 이 비밀을 알아내기 위해선 ContextThemeWrapper 로 되돌아가야 합니다. ContextThemeWrapper 에는 아래와 같은 생성자가 있습니다.
가장 먼저 mBase
변수가 등장하며, mBase
가 실제 Context 의 구현체가 됩니다. 이 mBase
를 두 번째 생성자를 통해 초기화하여 바로 Context 를 사용할 수 있으면 좋겠지만… (본문 내 언급되는 모든 mBase
는 ContextWrapper 에 존재하는 mBase
변수를 의미합니다)
public class Activity extends ContextThemeWrapper
Activity 를 다시 보면 extends 로 받고 있기에 아쉽게도 첫 번째 생성자로 초기화됩니다. 하지만 우리가 Context 를 사용하면서 NPE 을 본 적은 없습니다. 그렇다면 어디서 mBase
가 초기화되고 있는 것일까요? Activity 안에는 mBase
를 설정할 수 있는 attachBaseContext
함수가 있습니다.
이 함수 덕분에 mBase
가 초기화되어 Context 를 사용할 때 NPE 로 부터 안전하게 됩니다. 이 함수는 Activity 내에 있는 attach
함수를 통해 호출됩니다.
attach 함수의 인자를 보면 정말 많은 값들을 받고 있음을 볼 수 있습니다. 그만큼 다양한 작업들이 이 함수를 통해 초기화됩니다. 궁금하신 분들은 직접 보시길 권장드립니다. 이번 글의 주제는 Context 이므로 Context 를 초기화하는 가장 중요한 한 줄을 제외하고 코드를 전부 생략하였습니다. 이 함수가 호출되는 부분을 파악해 보면 Context 가 어떻게 초기화되며 언제 주입되는지를 알 수 있을거 같습니다. 이 함수는 ActivityThread 의 performLaunchActivity 함수에서 호출됩니다.
ActivityThread 는 애플리케이션의 시작점이며(정확하게는 ZygoteInit 에서 프로세스가 시작되지만, 메인 Looper 가 ActivityThread 에서 초기화되므로 애플리케이션의 시작점이라고 표현했습니다) 애플리케이션 프로세스에서 메인 스레드의 실행을 관리하고 액티비티 매니저(activity manager)가 요청하는 대로 액티비티, 브로드캐스트 및 기타 작업을 예약 및 실행하는 클래스입니다. 이 ActivityThread 에서 액티비티를 launch 하라는 message 를 받으면 액티비티를 launch 하는 트랜지션을 execute 하고 이어서 액티비티를 launch 할 때 performLaunchActivity 를 호출합니다.
performLaunchActivity 가 호출되기까지의 과정은 Activity 분석으로 개별 아티클이 작성될 예정입니다. 이제 performLaunchActivity 함수로 돌아가서 다시 한 번 보겠습니다.
performLaunchActivity 의 구현을 보면 appContext 와 app 이라는 변수로 ActivityContext 와 ApplicationContext 를 만들고 있습니다. 여기서 한 가지 사소한 정보는 이렇게 생성되는 app 은 Activity#getApplicationContext
에서 반환하는 ApplicationContext 가 아닙니다. 이 app 은 getApplicationContext() 가 아닌 getApplication() 구현에 사용됩니다. Context 클래스의 정의에는 getApplication() 함수가 있지 않습니다. 하지만 액티비티에서는 getApplication() 을 사용할 수 있습니다. 즉, Activity 에서 override 가 아닌 일반 함수로 getApplication() 을 만들어서 attach 함수의 인자로 받은 app 변수를 그대로 반환하게 됩니다.
Application 이 Context 를 상속하고 있고, ApplicationContext 는 싱글톤으로 제공되므로 Activity#getApplication
와 Activity#getApplicationContext
는 같은 값을 반환한다고 볼 수 있지만, 두 함수의 구현 방식에는 약간의 차이가 있습니다. 두 함수의 구현 방식은 추후 알아보는걸로 하고, 다시 performLaunchActivity 로 돌아가서 Context 를 생성하고 있는 함수들의 구현을 보겠습니다. 각각 Context 를 생성하기 위해 createBaseContextForActivity(r)
와 r.packageInfo.makeApplication(false, mInstrumentation)
를 사용하고 있습니다. 이 두 함수를 이해하기 위해선 먼저 ApplicationContext 가 언제 어디서 생성되고 있는지를 봐야 합니다. ApplicationContext 는 싱글톤이므로 애플리케이션이 시작될 때 초기화되고, 추후 이 ApplicationContext 에 의존하여 ActivityContext 와 같은 추가 Context 의 초기화가 진행됩니다.
애플리케이션의 시작점은 ActivityThread 이라고 언급하였습니다. 따라서 ApplicationContext 은 ActivityThread 에서 초기화됩니다. ActivityThread 의 main 함수 구현을 보겠습니다.
main 함수를 통해 애플리케이션이 실행되기 위한 초기화 과정을 진행하며, ApplicationContext 는 8번째 줄인 thread.attach(false, startSeq)
부분을 통해 초기화되고 싱글톤으로 설정됩니다.
가장 먼저 sCurrentActivityThread
를 자기 자신(ActivityThread)으로 설정하고 있습니다. 이어서 ActivityManager.getService()
를 통해 ActivityManagerService
를 가져옵니다.
// IActivityManager 는 네이티브 클래스 입니다. (frameworks/native/libs/binder/include_activitymanager/binder/IActivityManager.h)
public class ActivityManagerService extends IActivityManager.Stub
가져온 ActivityManagerService
에 attachApplication()
을 호출하고 있습니다.
이어서 6번째 줄에서 attachApplicationLocked
를 호출하고 있고,
attachApplicationLocked
는 내부에서 여러 작업을 걸쳐 결국 ApplicationThread#bindApplication
을 호출합니다.
ApplicationThread#bindApplication
는 인자로 받은 데이터들을 AppBundle
로 래핑하여 Handler#sendMessage
를 호출합니다.
이렇게 보내지는 메시지를 받으면 메시지의 번들을 가져와서 ActivityThread#handleBindApplication
를 호출하고 있습니다.
최종적으로 이 함수에서 ApplicationContext 을 만들어서 ActivityThread 의 변수인 mInitialApplication
변수로 ApplicationContext 를 지정함으로써 ApplicationContext 의 초기화가 모두 끝납니다.
sCurrentActivityThread
와 mInitialApplication
변수가 ApplicationContext 의 싱글톤 구현으로 작동합니다. 이 과정 이후로 ApplicationContext 에 참조가 필요해지면 currentApplication()
함수를 호출하여 ApplicationContext 를 사용하게 됩니다.
currentApplication()
은 static 함수이며 내부 구현에서 currentActivityThread()
를 사용하여 ActivityThread 를 가져오고, 해당 ActivityThread 에서 mInitialApplication
을 가져와 반환합니다. 이때 currentActivityThread()
는 위에서 본 ActivityThread#attach
에서 설정된 sCurrentActivityThread
를 바로 반환합니다. 이렇게 해서 ApplicationContext 의 싱글톤이 구현됩니다.
이제 ApplicationContext 를 초기화하는 과정을 보겠습니다.
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
ApplicationContext 를 만드는데 사용하고 있는 data.info
는 LoadedApk 라는 클래스를 제공합니다. 이 클래스는 현재 로드된 apk 에 대해 유지되는 로컬 상태를 제공하는 클래스입니다.
public final class LoadedApk
글을 시작하면서 Context 를 다음과 같이 소개했습니다.
Context 는 “애플리케이션의 전역 정보를 제공하여” 애플리케이션 수준의 작업에 접근할 수 있게 만들어주는 역할을 담당하고 있습니다.
애플리케이션의 전역 정보를 이 LoadedApk 클래스에서 로드하게 됩니다. 이어서 해당 LoadedApk 에 makeApplication 을 호출합니다.
makeApplication 에서는 ContextImpl#createAppContext
를 이용해 ContextImpl 인스턴스를 먼저 생성하고 있습니다.
이렇게 해서 7번째 줄을 통해 ContextImpl 이 생성됩니다.
ContextImpl 은 인자로 받은 ActivityThread 와 LoadedApk 를 mainThread
, PackageInfo
변수에 저장하고 있습니다. 이후 이렇게 만들어진 ContextImpl 을 반환하는 것으로 ContextImpl#createAppContext
가 끝납니다.
이어서 ActivityThread 의 Instrumentation 에 newApplication 을 호출하고 있습니다. Instrumentation 은 애플리케이션 계측(instrumentation) 코드를 구현하기 위한 기본 클래스입니다.
public class Instrumentation
이 클래스의 newAppliaction 함수를 통해 ApplicationContext 가 초기화됩니다.
newApplication 은 ClassLoader 를 이용해 Application 의 인스턴스를 생성합니다. 즉, instantiateApplication 에서 className 인자 값으로 Application 클래스의 패키지인 “android.app.Application” 가 들어옵니다. 이후 이렇게 만들어진 Application 에 newApplication 의 인자로 받은 ContextImpl 을 attach 하고, Application 을 반환하고 있습니다.
이렇게 해서 Context 의 mBase
가 ApplicationContext 로 초기화되고, Application 이 반환되면서 Instrumentation#newApplication
이 끝납니다.
이후 만들어진 ApplicationContext 를 mApplication
변수로 저장하고 해당 ApplicationContext 를 반환하며 LoadedApk#makeApplication
이 끝납니다.
LoadedApk#makeApplication
이 끝나면서 반환된 ApplicationContext 를 mInitialApplication
으로 지정함으로써 ActivityThread#handleBindApplication
까지 모두 끝나면서 전체적인 ApplicationContext 초기화 과정이 모두 마무리됩니다.
지금까지 과정을 보면 ApplicationContext 의 인스턴스로 Application 클래스를 사용한다는 걸 알 수 있습니다. Application 이 Context 를 상속하고 있기 때문입니다.
// Application 은 전역(global) 애플리케이션 상태를 유지하기 위한 기본 클래스입니다.
public class Application extends ContextWrapper
이제 다시 Context 초기화 분석의 시작점이였던 ActivityThread#performLaunchActiviy
로 돌아오겠습니다.
ApplicationContext 의 초기화 과정을 보았으니 이젠 ActivityContext 를 초기화하기 위한 함수인 ActivityThread#createBaseContextForActivity(r)
을 볼 수 있을거 같습니다. 인자로 받는 r
부터 보자면 ActivityThread#performLaunchActiviy
의 인자로 들어오는 ActivityClientRecord
의 값을 사용하고 있습니다. ActivityClientRecord
는 액티비티를 생성하기 위한 정보들이 담긴 ActivityThread 의 static nested class 입니다.
ActivityClientRecord
의 필드중 일부인 packageInfo 는 ActivityThread 의 currentApplication().mLoadedApk
를 통해 설정됩니다. 즉, ApplicationContext 의 값을 갖게 됩니다.
createBaseContextForActivity(r)
는 ContextImpl#createActivityContext
를 이용해 Context 초기화를 진행합니다.
createActivityContext 는 createAppContext 와 달리 Context 에 리소스를 직접 생성하여 지정하는 단계(11~25번 라인)가 추가됐음을 확인할 수 있습니다. 또한 Context 의 타입도 Activity 로 지정(7번 라인)하고 있고 Application 이 아닌 ContextWrapper 본래의 클래스를 그대로 사용합니다. 이 부분 덕분에 ActivityContext 와 ApplicationContext 간에 차이가 발생하게 됩니다.
이렇게 ActivityContext 는 초기화가 진행됩니다. 이어서 ApplicationContext 도 초기화를 진행합니다. r.packageInfo.makeApplication
을 통해 ApplicatonContext 를 만들게 됩니다. 여기서 사용되는 r.packageInfo
는 ActivityThread#attach
에서 ApplicationContext 를 초기화하면서 생성된 LoadedApk 를 나타냅니다. 따라서 이미 mApplication
이 지정된 상태이므로 LoadedApk#makeApplication
의 첫 번째 분기에 걸려서 기존에 초기화된 Application 을 그대로 반환하게 됩니다.
이렇게 해서 ApplicationContext 를 의미하는 app
변수까지 초기화가 끝납니다. 글 상단에서 잠깐 언급했던 내용이 있습니다.
performLaunchActivity 의 구현을 보면 appContext 와 app 이라는 변수로 ActivityContext 와 ApplicationContext 를 만들고 있습니다. 여기서 한 가지 사소한 정보는 이렇게 생성되는 app 은
Activity#getApplicationContext
에서 반환하는 ApplicationContext 가 아닙니다. 이 app 은 getApplicationContext() 가 아닌 getApplication() 구현에 사용됩니다.
getApplicationContext() 는 ActivityContext 를 의미하는 appContext
변수로 부터 구현됩니다. ContextImpl#getApplicationContext
을 보겠습니다.
ContextImpl 의 mPackageInfo
혹은 mMainThread
에서 getApplication() 을 통해 Context 를 반환하는 것으로 getApplicationContext() 가 구현되고, ContextImpl 의 주체는 appContext
변수가 mBase
로 설정되므로 결국 appContext
가 됩니다.
이렇게 해서 ActivityThread#performLaunchActiviy
를 통한 Activity 의 Context 초기화 과정이 모두 끝납니다.
끝!
우리가 매일 쓰는 Context 가 어떻게 구현됐고 초기화되는지 알아보았습니다. 제가 Context 를 공부하기 전에 궁금했던게 2가지가 있었습니다.
- 어느 시점에 초기화될까?
- 초기화가 되면서 어떤 정보들이 설정될까?
첫 번째 궁금증은 해결이 됐지만 두 번째 궁금증은 아직 모두 해결하지 못했습니다. 실제로 이 긴 본문에서도 ContextImpl 의 생성자와 리소스를 설정하는 부분을 깊게 다루지 않았습니다. ContextImpl 의 리소스 설정 부분을 모두 파악하고 싶었지만 이 부분을 다 파악하기엔 약간 난이도가 있어 여기까진 모두 진행하지 못했습니다. 따라서 이 부분 파악이 추후 어느정도 진행됐다면 ContextImpl 의 리소스 부분만 다루는 아티클이 별도로 작성될 예정입니다.
끝까지 읽어주셔서 감사합니다.
안드로이드 개발자 분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.