안드로이드 프로그래밍을 하다 보면, 시간의 흐름이나 사용자 액션에 따라 내부의 특정한 리소스에 접근해야 하는 경우가 왕왕 있습니다. 대체로 꼭 Context 를 요구하는데, 문득 이에 대한 이유가 궁금했습니다.
Context 에 대한 간단한 개요에 대해서는 위 포스팅에서 확인하실 수 있습니다. 덤으로 다양한 정보도 함께 작성하였으니, 참고하셔도 좋을 것 같습니다. 해당 포스트는 Drawble 을 기준으로 설명합니다.
추상화된 단계
안드로이드 프로그래밍 시, 내부 리소스에 접근하여 Drawble 을 가져오기 위해서는 ResourcesCompat, ContextCompat 등의 클래스를 이용하게 됩니다.
ContextCompat
androidx.core.contet 패키지에 포함되어 있으며, 안드로이드의 Context 관련 작업을 보다 쉽게 처리할 수 있게 도와주는 클래스입니다. Compatibility 를 의미하는 Compat 키워드가 붙었으므로, 다양한 API 버전을 지원하는 것도 알 수 있습니다.
ResourcesCompat
androidx.core.content.res 에 포함된 클래스이며, 내부 리소스에 조금 더 쉽게 액세스하게 도와주는 유틸리티 클래스입니다. 앞선 ContextCompat 과 같이, Compat 키워드가 붙어 API 버전에 대한 폭넓은 지원이 있습니다.
🤔 ResourcesCompat.getDrawble() 메서드는 Context 가 필요 없는데요?
첫 번째 인자인 res 에 Activity 의 mResources 프로퍼티를 전달하게 되므로, 사실상 Context 가 필요하다고 봐야겠죠. ContextCompat 의 getDrawable() 메서드 내부를 살펴보면, SDK 16 이상, 20 이하의 경우, 전달된 Context 를 통해 Activity 의 mResource 프로퍼티에 접근하고 있습니다.
@Nullable
public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) {
if (Build.VERSION.SDK_INT >= 21) {
return Api21Impl.getDrawable(context, id);
} else if (Build.VERSION.SDK_INT >= 16) {
return context.getResources().getDrawable(id);
} else {
// Prior to JELLY_BEAN, Resources.getDrawable() would not correctly
// retrieve the final configuration density when the resource ID
// is a reference another Drawable resource. As a workaround, try
// to resolve the drawable reference manually.
final int resolvedId;
synchronized (sLock) {
if (sTempValue == null) {
sTempValue = new TypedValue();
}
context.getResources().getValue(id, sTempValue, true);
resolvedId = sTempValue.resourceId;
}
return context.getResources().getDrawable(resolvedId);
}
}
SDK 21 이상의 경우, Api21Impl 클래스 내 getDrawble() 메서드의 첫 번째 인자로 Context 를 사용하고 있는데요, 사실 똑같습니다. Context 가 obj 라는 이름으로 사용되며, Context.getDrawable() 을 호출하고 있습니다. 해당 메서드는 내부에서 getResources() 메서드를 실행하고 있고요. 결국에는 Resources 에 접근하여야 함을 알 수 있습니다.
즉, 주제인 '내부 리소스 접근에 왜 Context 가 필요한 걸까?' 라는 질문에 대한 답변은 다음과 같습니다.
Resources 객체에 내부 리소스를 가져 올 수 있는 함수들이 존재하고, Resources 객체는 Context 의 getResources() 메서드를 이용하거나 Context 를 상속받는 Activity 클래스의 mResources 프로퍼티를 통해 획득할 수 있기 때문입니다.
그래서, 어떻게?
Context 가 어떤 식으로 활용되는지는 파악하였으니, 그 이후의 과정도 살펴보면 좋겠지요.
Resources 클래스의 getDrawble() 메서드를 호출하여 Drawable 을 가져오는 건 모두 같습니다. API 21 이상의 버전에서는 Theme 인자도 추가로 적용할 수 있는데, Drawable 을 획득할 때 주어진 Theme 에 맞춰 획득합니다.
@Nullable
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValueForDensity(id, density, value, true);
return loadDrawable(value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
TypedValue 는 얻어 온 Drawable 의 반환을 위해 사용되는 컨테이너 클래스입니다. 우리가 주목해야 할 부분은 ResourcesImpl 클래스인데요. 실질적으로 내부 리소스에 접근하는 클래스이며, 해당 클래스 최상단 주석에는 'Resources is just a thing wrapper around this class.' 라는 내용이 작성되어있을 만큼, 리소스 접근에 핵심적인 역할을 수행하는 클래스입니다.
해당 impl 객체는 getValueForDensity() 메서드를 호출하고 있습니다. 해당 메서드에서 TypeValue 타입 변수인 value 는 outValue 라는 명칭으로 사용되는데, 가져 온 리소스가 담겨지는 컨테이너 역할을 수행합니다. 메서드는 AssetManager 클래스의 getResourceValue() 메서드를 실행합니다. 메서드명만 봐도 실제로 리소스를 가져오는 메서드임을 알 수 있습니다.
/*
Provides access to an application's raw asset files;
see Resources for the way most applications will want to retrieve their resource data.
This class presents a lower-level API that allows you to open and read raw files that
have been bundled with the application as a simple stream of bytes.
*/
해당 클래스 최상단에 작성된 주석입니다. AssetManager 가 결국 최종적으로 원본 리소스 데이터에 접근하는 역할을 수행함을 알 수 있습니다. 이제 getResourceValue() 메서드를 살펴보면 끝입니다. 작성된 짧은 주석은 다음과 같습니다.
/*
Populates outValue with the data associated a particular resource identifier
for the current configuration.
*/
현재 구성의 특정 리소스 식별자와 연결된 데이터로 outValue 를 채운다고 작성되어 있네요. 메서드의 본문은 다음과 같습니다.
@UnsupportedAppUsage
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
boolean resolveRefs) {
Objects.requireNonNull(outValue, "outValue");
synchronized (this) {
ensureValidLocked();
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (cookie <= 0) {
return false;
}
// Convert the changing configurations flags populated by native code.
outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
return false;
}
}
return true;
}
}
우리가 주목해야 할 부분은 outValue 인자가 유의미하게 활용되는, nativeGetResourceValue() 메서드일 겁니다. 아쉽게도, 해당 메서드는 네이티브 키워드가 붙어 IDE 에서는 확인할 수 없습니다. 해당 메서드는 C++ 로 작성되어 있으며, 이를 다음의 코드 스니펫에서 확인할 수 있습니다.
static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
jshort density, jobject typed_value,
jboolean resolve_references) {
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
auto value = assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
static_cast<uint16_t>(density));
if (!value.has_value()) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
if (resolve_references) {
auto result = assetmanager->ResolveReference(value.value());
if (!result.has_value()) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
}
return CopyValue(env, *value, typed_value);
}
가져 오는 리소스 값이 유효한지, 참조 해결은 필요한지를 체크하고 이들이 정상적이지 않은 경우 유효하지 않은 캐시를 반환하며 메서드 실행이 끝납니다.
모든 체크를 정상적으로 지나오면 CopyValue() 메서드를 통해 값을 복사합니다.
static jint CopyValue(JNIEnv* env, ApkAssetsCookie cookie, const Res_value& value, uint32_t ref,
uint32_t type_spec_flags, ResTable_config* config, jobject out_typed_value) {
env->SetIntField(out_typed_value, gTypedValueOffsets.mType, value.dataType);
env->SetIntField(out_typed_value, gTypedValueOffsets.mAssetCookie,
ApkAssetsCookieToJavaCookie(cookie));
env->SetIntField(out_typed_value, gTypedValueOffsets.mData, value.data);
env->SetObjectField(out_typed_value, gTypedValueOffsets.mString, nullptr);
env->SetIntField(out_typed_value, gTypedValueOffsets.mResourceId, ref);
env->SetIntField(out_typed_value, gTypedValueOffsets.mChangingConfigurations, type_spec_flags);
if (config != nullptr) {
env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, config->density);
}
return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie));
}
CopyValue() 메서드 본문입니다. TypedValue 객체에 값을 복사하는 역할을 수행합니다. 그 과정 중, mType, mAssetCookie, mData, mResourceId, mChangingConfigurations, mDensity 등의 필드에 값들을 설정하여, 이에 맞게 리소스를 복사하고 이를 쿠키 형태로 가져옵니다. 받아 온 쿠키는 애플리케이션에서 하나의 컨테이너로 작동합니다. 해당 쿠키 내에 TypedValue 객체가 있으므로 이를 꺼내어 사용하는 방식입니다.
이후에는 가져오려는 리소스가 캐시에 있으면 캐시를 사용하고, 그렇지 않으면 새로운 Drawable 객체를 생성하고 쿠키 내 TypedValue 에 작성된 값을 스트림으로 생성, 이를 활용합니다.
아무렇지 않게 사용해왔던 기능인데, 내부를 살펴보니 C++ 코드까지 찾아보아야 할 정도로 깊게 설계되어 있음을 확인할 수 있어 재미있는 경험이었습니다. C++ 코드를 읽을 수 있는 능력을 더욱 함양한다면, 안드로이드 내부가 어떻게 돌아가는지 파악하는 것에 큰 도움이 될 것 같습니다.
'Android > Tech' 카테고리의 다른 글
@Retention 은 어떻게 동작하는가? (0) | 2023.09.05 |
---|---|
Kapt, 그리고 KSP (0) | 2023.09.01 |
우리는 어떻게 Modularization 해야 하는가? (0) | 2023.08.13 |
HLS, DASH, 그리고 오디오 포맷 (feat.속도 비교) (0) | 2023.07.07 |
Android Context Details (feat.LocalContext) (0) | 2023.06.28 |