본문 바로가기

Android/Tech

내부 리소스 접근에 왜 Context 가 필요한 걸까?

Unsplasy, Andrew Neel.

안드로이드 프로그래밍을 하다 보면, 시간의 흐름이나 사용자 액션에 따라 내부의 특정한 리소스에 접근해야 하는 경우가 왕왕 있습니다. 대체로 꼭 Context 를 요구하는데, 문득 이에 대한 이유가 궁금했습니다.

 


 

Android Context Details (feat.LocalContext)

Context 안드로이드 프로그래밍에 필수불가결한 존재인 Context 는 정말 자주 사용됩니다. 그 경우의 수가 어마무시하게 많은데, 그렇게 자주 사용하고 있으면서도, 'Context 가 정확히 뭐야?' 라는 얘

blothhundr.tistory.com

 

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 에 ActivitymResources 프로퍼티를 전달하게 되므로, 사실상 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 객체는 ContextgetResources() 메서드를 이용하거나 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 을 획득하는 전체 과정을 도식화 해 보았습니다.

 

이후에는 가져오려는 리소스가 캐시에 있으면 캐시를 사용하고, 그렇지 않으면 새로운 Drawable 객체를 생성하고 쿠키 내 TypedValue 에 작성된 값을 스트림으로 생성, 이를 활용합니다.

 


 

아무렇지 않게 사용해왔던 기능인데, 내부를 살펴보니 C++ 코드까지 찾아보아야 할 정도로 깊게 설계되어 있음을 확인할 수 있어 재미있는 경험이었습니다. C++ 코드를 읽을 수 있는 능력을 더욱 함양한다면, 안드로이드 내부가 어떻게 돌아가는지 파악하는 것에 큰 도움이 될 것 같습니다.