안드로이드의 Resources 초기화 및 관리

안드로이드 프레임워크 분석 — Resources

Ji Sungbin
성빈랜드
18 min readSep 24, 2022

--

Photo by Martin Adams on Unsplash

지난번에는 Context 의 생성에 대해 보았습니다. 이번 글에서는 Context 가 생성되면서 Resources 는 어떤 식으로 초기화되고 설정되는지를 살펴보려고 합니다. 이 글은 아래와 같은 내용은 포함하고 있지 않습니다.

  • ApplicationContext 의 Resources vs ActivityContext 의 Resources: 2가지 방식의 핵심 차이점을 찾기 위해 노력했지만 결국 찾을 수 없었습니다. 혹시 아시는 분이 계신다면 지식 공유해주시면 감사하겠습니다.
  • 리소스를 실제로 가져오는 방법: 리소스 요청(getString 등등)의 마지막 과정을 열어보면 결국 JNI 을 이용해 가져오고 있습니다. JNI 구현까지 다 파악하기엔 무리가 있었습니다.

아쉽게도 모든 내용을 파악하진 못하였고 혹시나 위 내용들을 기대하고 오신 분들이 있을까봐 미리 언급하였습니다. 전체적으로 볼 흐름도는 아래 링크에서 확인하실 수 있습니다.

ActivityContext

이번엔 ApplicationContext 가 아닌 ActivityContext 에서 Resources 가 설정되는 방법을 먼저 보겠습니다. ActivityContext 는 ContextImpl#createActivityContext 를 통해 생성되고 있습니다.

12번째 줄을 보면 ResourcesManager#createBaseTokenResources 을 통해 리소스를 설정하고 있는걸 볼 수 있습니다.

이 함수는 크게 5가지의 중요한 작업을 하고 있습니다.

먼저 14번째 줄을 보면 생성할 리소스의 키를 생성하고 있습니다. 여기서 생성하는 ResourcesKey 인스턴스는 다음과 같은 내부 필드를 갖고 있습니다(또한 해당 필드 값으로 초기화가 진행됩니다).

ResourcesKey 에는 JavaDoc 이 없어서 정확히 무슨 역할을 하는 클래스인지는 확인하지 못했지만, 내부 필드와 리소스 초기화 흐름을 보니 리소스를 생성하는데 필요한 정보들을 모아둔 클래스 정도로 저는 추측하고 있습니다. 참고로 mResDir, mSplitResDirs, mLibDirs 등등 dir 경로로 저는 /res/value 와 같은 값이 들어올 것으로 예상하고 있었지만 디버깅을 통해 확인해 보니 base.apk 파일 경로가 들어오고 있었습니다. 곧 언급하겠지만 리소스는 .apk 파일에서 JNI 를 통해 바로 읽어오고 있는데 이것과 연관이 있을 것이라고 생각하고 있습니다. 또한 ResourcesLoader 클래스도 배열로 받고 있음을 23번째 줄에서 볼 수 있습니다.

ResourcesLoader 는 Resources 객체에 ResourcesProvider 를 제공하기 위한 컨테이너입니다. 따라서 내부 필드에서 ResourcesProvider[] 타입인 mProviders 변수가 있는걸 확인할 수 있고, ResourcesLoader#addProvider 를 통해 mProviders 에 값를 추가하게 됩니다. 또한 mProviders 변수 위에 ApkAssets[] 타입의 mApkAssets 변수도 존재합니다. ApkAssets 은 .apk 파일에서 로드된 변경 불가능한 메모리 내 값들이며 ResourcesProvider 에서 getApkAssets() 을 통해 가져올 수 있습니다. mProviders 를 순회하며 getApkAssets() 를 통해 가져온 값들을 mApkAssets 변수로 제공하게 됩니다.

ApkAssets 은 .apk 에서 리소스를 읽어오기 위해 모든 구현들이 다 JNI 로 구현돼 있습니다.

이렇게 비교적 중요해 보이는 정보들을 담고 있는 ResourcesKey 를 만들기 위한 인자로 ApplicationContext 를 초기화하면서 생성된 LoadedApk 를 대부분의 곳에 사용하게 됩니다.

이렇듯 결국 ActivityContext 의 리소스도 ApplicationContext 의 리소스와 출처는 같다는 것을 확인하였습니다.

지금까지 ResourcesManager#createBaseTokenResources 의 첫 번째 작업인 ResourcesKey 생성을 보았습니다.

다음으로 26번째 줄에 있는 ResourcesManager#getOrCreateActivityResourcesStructLocked 를 봐야 합니다.

만약 mActivityResourceReferences에 주어진 activityToken 을 키로 가진 값이 없다면 ActivityResources 를 새로 만들어서 주어진 activityToken 과 함께 mActivityResourceReferences 에 추가하고 있습니다. 이어서 기존 값을 가져오거나 새로 만든 ActivityResources 값을 반환하고 있는 간단한 함수입니다.

이 함수에서 메인으로 다루고 있는 ActivityResources 는 ActivityResource(끝에 s 가 없음) 배열과 Configuration 객체를 담고 있는 컨테이너입니다.

ActivityResource 는 기본 Configuration 의 override 및 액티비티 또는 WindowContext 와 연결된 리소스 집합을 포함하는 클래스입니다. WindowContext 는 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 윈도우 같이 액티비티와 관련이 없는 윈도우들을 사용할 때 사용되는 Context 입니다. ActivityResources 와 비슷한 형태로 구현됐습니다.

지금까지 ResourcesManager#getOrCreateActivityResourcesStructLocked 를 보았습니다. 이 함수는 리소스 생성 과정에서 거의 모든 과정마다 첫 번째로 사용되는 함수입니다. 그만큼 아주 중요한 함수이므로 기억해 두시길 바랍니다.

이어서 28번째 줄에 있는 ResourcesManager#updateResourcesForActivity 를 보겠습니다.

이 함수는 기존에 생성된 Resources 를 업데이트하는 함수며, 먼저 ActivityResources 를 ResourcesManager#getOrCreateActivityResourcesStructLocked 통해 가져오고 있습니다.

final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken);

즉, 현재의 함수인 updateResourcesForActivity 가 실행되기 바로 전 단계에서 실행했던 함수의 결과로 생성된 ActivityResources 를 그대로 다시 가져오게 됩니다.

다음으로 이전의 요청과 현재의 요청에서 달라진 값이 있나 검사하는 절차가 진행됩니다.

만약 동일한 task id 를 가지고 있고 Configuration 도 동일하다면 함수를 바로 return 하고 있습니다.

만약 변경 사항이 있다면 새로 주어진 Configuration 이 null 이 아니라면 해당 값으로 ActivityResources 의 configuration 을 업데이트 하고 있습니다. 만약 null 이라면 Configuration#unset 을 통해 ActivityResources 의 configuration 을 초기화해주게 됩니다.

activityResources.overrideDisplayId = displayId;

다음으로 activityResources 의 displayId 를 새로운 값으로 업데이트 해주고,

activityResources 를 순회하면서 resource 가 있다면 해당 리소스에 대한 새로운 Key 생성 및 ResourceImpl 을 지정하고 있습니다. 새로운 ResourcesKey 생성으로 ResourcesManager#rebaseActivityOverrideConfig 를 사용하고 있습니다.

이 함수는 인자로 받은 ActivityResource 에 초기화된 Resources 가 있다면 ResourcesManager#findKeyForResourceImplLocked 를 통해 해당 리소스의 Key 를 가져오고 있습니다.

이 함수는 간단하게 mResourceImpls 배열을 순회하며 인자로 받은 ResourcesImpl 을 값으로 가지는 키를 가져오고 있습니다. ResourcesImpl 은 리소스를 가져오는 메서드들을 구현하는 클래스입니다. Resources 를 사용하기 위해선 Resources#setImpl 을 통해 ResourcesImpl 을 설정해줘야 하고, 이렇게 설정된 ResourcesImpl 로 모든 Resources 의 구현을 위임하고 있습니다.

다시 rebaseActivityOverrideConfig 함수로 돌아와서, 이어서 새로운 Configuration 과 displayId 로 ResourcesKey 를 만들고 반환하게 됩니다.

이렇게 해서 ResourcesManager#rebaseActivityOverrideConfig 부분이 끝납니다.

다음으로 새로 계산된 ResourcesKey 를 이용하여 ResourcesManager#findOrCreateResourcesImplForKeyLocked 를 실행합니다.

이 함수는 크게 2가지 작업으로 나뉩니다. 먼저 ResourcesManager#findResourcesImplForKeyLocked 를 이용해 주어진 ResourcesKey 를 키로 가지는 ResourcesImpl 을 찾습니다.

만약 찾은 경우에는 ResourcesImpl 에서 AssetManager 을 가져와 해당 값이 최신 값인지 확인(impl.getAssets().isUpToDate())하고 있습니다. 만약 최신인 경우라면 해당 ResourcesImpl 을 반환하고 그렇지 않으면 null 을 반환합니다. 다시 findOrCreateResourcesImplForKeyLocked 를 보면…

findResourcesImplForKeyLocked 의 결과를 impl 변수로 받고, 만약 impl 값이 null 이라면 ResourcesManager#createResourcesImpl 을 한 값으로 impl 을 업데이트하고, impl 을 반환하는 것으로 함수가 끝납니다.

createResourcesImpl 함수는 ResourcesManager#createAssetManager 를 통해 AssetManager 를 만들고 있고, 이어서 ResourcesImpl 을 초기화하여 반환하고 있습니다.

createAssetManager 는 인자로 받은 ResourcesKey 를 이용하여 AssetManager.Builder 로 AssetManager 를 만드는 간단한 함수입니다. AssetManager 에 추가할 ApkAssets 을 만들기 위해 ResourcesManager#loadApkAssets 함수를 사용하는데, 이 함수에서도 ApkAssets#isUpToDate 를 이용하여 최신 버전일 때만 기존 값을 재사용하고 그렇지 않다면 새로운 값을 생성하여 반환하고 있습니다.

지금까지의 과정을 모두 거쳐서 ResourcesManager#findOrCreateResourcesImplForKeyLocked 함수가 끝납니다.

이 함수 역시 리소스 생성 과정에서 많이 사용되는 함수입니다. 위 함수가 끝나면서 반환된 ResourcesImpl 값을 resources 에 impl 로 설정하는 것으로 ResourcesManager#updateResourcesForActivity 의 마지막 과정이 끝납니다.

다시 ResourcesManager#createBaseTokenResources 함수를 보겠습니다.

이제 남은 함수는 ResourcesManager#findResourcesForActivityLockedResourcesManager#createResourcesForActivity 입니다. findResourcesForActivityLocked 먼저 보겠습니다.

우선 ResourcesManager#getOrCreateActivityResourcesStructLocked 을 통해 ActivityResources 를 가져오고 있고, 찾고자 하는 값과 해당 값의 Resources 가 일치한다면 해당 Resources 를 반환하는 것으로 findResourcesForActivityLocked 함수가 끝납니다.

이제 마지막으로 ResourcesManager#createResourcesForActivity 함수를 보겠습니다.

먼저 ResourcesManager#findOrCreateResourcesImplForKeyLocked 를 통해 ResourcesImpl 을 구하고 있고, 이어서 ResourcesManager#createResourcesForActivityLocked 로 return 을 하고 있습니다.

createResourcesForActivityLocked 에서는ResourcesManage#rgetOrCreateActivityResourcesStructLocked 로 함수를 시작하고 있으며, 인자로 받은 조건에 따라 Resources 를 생성한 후 인자로 받은 ResourcesImpl 로 impl 을 설정하고 있습니다.

인자 조건에 따라 생성되는 CompatResources 는 Resources 의 extends 클래스이며, SDK 26 미만을 타겟팅하는 앱에서 사용되는 Resources 입니다. 또한 UpdateHandler 클래스를 Resources 에 콜백으로 등록하고 있습니다.

UpdateHandler 는 리소스가 업데이트 됐다는 알림을 받으면 새로운 ResourcesKey 를 생성하고 이에 맞는 ResourcesImpl 로 Resources 의 impl 을 업데이트 하는 클래스 입니다.

다시 createResourcesForActivityLocked 를 보면 Resources 에 콜백 설정을 하고 이어서 새로운 ActivityResource 생성 후, 인자로 받은 값들로 업데이트 한 후 ActivityResources 에 추가합니다. 다음으로 함수가 resources 로 반환되면서 끝납니다.

이렇게 해서 ResourcesManager#createResourcesForActivit 함수도 같이 끝나게 됩니다. createResourcesForActivitResourcesManager#createBaseTokenResources 에서 마지막으로 쓰이던 함수였습니다.

따라서 createBaseTokenResources 역시 같이 끝나게 되고,

이를 사용하고 있던 ContextImpl#createActivityContext 의 13번째 줄인 ContextImpl#setResources 가 마무리되면서 ActivityContext 에 최종적으로 Resources 가 등록됩니다.

지금까지 이 모든 과정을 살펴보았습니다.

ApplicationContext

이어서 ApplicationContext 의 Resources 설정도 보겠습니다. ApplicationContext 는 ContextImpl#createAppContext 를 통해 생성됩니다.

3번째 줄을 보면 LoadedApk 에서 바로 Resources 을 얻어오고 있습니다.

LoadedApk#getResourcesResourcesManage#getResources 를 통해 mResources 를 초기화하고 있고, mResources 를 반환하는 것으로 구현돼 있습니다. ResourcesManager#getResources 를 보겠습니다. (이렇게 ApplicationContext 의 Resources 도 싱글톤이 구현됩니다)

먼저 인자로 받은 값들을 이용해 ResourcesKey 를 생성하고, 해당 키로 ResourcesManager#createApkAssetsSupplierNotLocked 를 하고 있습니다.

createApkAssetsSupplierNotLocked 함수는 ApkAssetsSupplier 를 생성하고 계산된 ApkKey 들로 부터 ApkAssets 들을 로드하여 캐싱하는 함수입니다. ApkAssetsSupplier 클래스는 코드를 통해 확인할 수 있듯이 ApkKey — ApkAssets 형태로 ApkAssets 을 캐싱하기 위한 클래스입니다.

ApkAssets 들이 캐싱된 ApkAssetsSupplier 를 반환하는 것으로 이 함수는 끝납니다.

이어서 createApkAssetsSupplierNotLocked 함수로 얻은 ApkAssetsSupplier 을 이용하여 ResourcesManager#createResources 를 하고 있습니다.

이 함수는 먼저 익숙한 ResourcesManager#findOrCreateResourcesImplForKeyLocked 을 통해 ResourcesImpl 을 가져오고 해당 값으로 ResourcesManager#createResourcesLocked 를 하고 있습니다.

이 함수를 통해 ApplicationContext 의 Resources 를 만들게 되고, ActivityContext 의 Resources 를 생성하는 과정에서 보았던 ResourcesManager#createResourcesForActivityLocked 와 유사한 형태를 띄고 있음을 볼 수 있습니다.

createResourcesForActivityLocked 와의 큰 차이점은 ActivityResources 관련 정보들이 빠지고 mResourcesReferences 라는 배열에 Resources 를 추가하는 과정이 추가됐다는 점 입니다. createResourcesLockedmResourcesReferences#add 과정을 통해 Resources 레퍼런스를 재사용하며 ResourcesImpl 만 바꿔서 리소스를 업데이트 하는 식으로 Resources 를 유지보수하게 됩니다.

이렇게 해서 반환된 Resources 가 계속해서 전달되면서 LoadedApk#getResources 과정이 끝납니다.

ApplicationThreadConstants.PACKAGE_REPLACED

마지막으로 알아볼 과정은 DFM 이나 split apk 로 인해 패키지에 변경에 생겼을 때 입니다. 패키지에 변경이 생겼다면 리소스 또한 변경이 있을 수 있으므로 리소스를 업데이트 하는 과정이 진행됩니다. 이 과정은 ApplicationThreadConstants.PACKAGE_REPLACED 메시지를 받음으로써 시작됩니다.

많은 작업들이 진행되지만 크게 3가지의 작업으로 볼 수 있습니다. 먼저 LoadedApk#updateApplicationInfo 를 통해 mResources 를 업데이트하고 있습니다.

많은 작업들을 하고 있지만 결국 ResourcesManager#getResources 를 통해 mResources 를 업데이트하게 됩니다.

이어서 두 번째 작업인 ResourcesManager#appendPendingAppInfoUpdate 를 보겠습니다.

이 함수는 인자로 받은 값들을 mPendingAppInfoUpdates 배열에 Pair 로 추가하고 있습니다.

마지막 작업인 ResourcesManager#applyAllPendingAppInfoUpdates 를 보겠습니다.

appendPendingAppInfoUpdate 함수에서 추가한 Pair 들을 순회하며 ResourcesManager#applyNewResourcesDirsLocked 를 호출하고 있습니다.

applyNewResourcesDirsLocked 에서는 mResourceImpls 을 순회하며 업데이트해야 하는 ResourcesImpl 를 updatedResourceKeys 배열에 추가하고 있습니다. 이후 해당 배열에 ResourcesManager#redirectResourcesToNewImplLocked 를 호출하고 있습니다.

redirectResourcesToNewImplLocked 함수를 통해 현재 Resources 들의 impl 을 최신 상태로 업데이트 해주게 됩니다.

이렇게 해서 ApplicationThreadConstants.PACKAGE_REPLACED 과정이 끝납니다.

끝!

이번 글에선 ApplicationContext 의 Resources, ActivityContext 의 Resources 그리고 Resources 의 업데이트 방법에 대해 알아보았습니다. 오랜 시간을 투자해 공부를 하였지만 목표했던 내용을 모두 얻진 못해서 아쉬움이 꽤 있지만, 그래도 안드로이드에서 리소스가 어떻게 관리되는지를 알 수 있었던 좋은 공부였던거 같습니다. 끝까지 읽어주셔서 감사합니다.

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

[목차로 돌아가기]

--

--

Ji Sungbin
성빈랜드

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