안드로이드 Context 는 어떻게 생성될까?

안드로이드 프레임워크 분석 — Context 초기화

Ji Sungbin
성빈랜드
17 min readSep 17, 2022

--

Photo by Hello I’m Nik on Unsplash / 사진은 편안하지만 전혀 편안하지 않은 본문… 🥲 😇

안드로이드/코루틴 분석하기 시리즈의 첫 번째 대상은 “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#getApplicationActivity#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

가져온 ActivityManagerServiceattachApplication() 을 호출하고 있습니다.

이어서 6번째 줄에서 attachApplicationLocked 를 호출하고 있고,

attachApplicationLocked 는 내부에서 여러 작업을 걸쳐 결국 ApplicationThread#bindApplication 을 호출합니다.

ApplicationThread#bindApplication 는 인자로 받은 데이터들을 AppBundle 로 래핑하여 Handler#sendMessage 를 호출합니다.

이렇게 보내지는 메시지를 받으면 메시지의 번들을 가져와서 ActivityThread#handleBindApplication 를 호출하고 있습니다.

최종적으로 이 함수에서 ApplicationContext 을 만들어서 ActivityThread 의 변수인 mInitialApplication 변수로 ApplicationContext 를 지정함으로써 ApplicationContext 의 초기화가 모두 끝납니다.

지금까지 mgr.attachApplication(mAppThread, startSeq) 함수의 과정을 보았습니다.

sCurrentActivityThreadmInitialApplication 변수가 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.packageInfoActivityThread#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 의 리소스 부분만 다루는 아티클이 별도로 작성될 예정입니다.

끝까지 읽어주셔서 감사합니다.

[목차로 돌아가기]

안드로이드 개발자 분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.

--

--

Ji Sungbin
성빈랜드

Experience Engineers for us. I love development that creates references.