결론부터 말하자면 View의 Background를 리소스로 설정시에 color 리소스가 아닌 drawable 리소스만을 사용해야 합니다.
(물론 대다수의 경우들은 drawable만 사용합니다....)

Android UI 작업을 하다보면 View의 Background를 설정하는 경우는 매우 흔한 일입니다.

이때 pressed 효과를 위해 StateListDrawable 형태의 drawable 리소스를 쓰는 경우도 많이 생깁니다.

그리고 Android OS 10에서부터는 ColorStateList 형태의 color 리소스도 View의 Background에서 사용이 가능해졌습니다.

(흔히 말하는 selector 리소스 형태들입니다.)

  • ColorStateList 리소스 : https://developer.android.com/guide/topics/resources/color-list-resource
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="<http://schemas.android.com/apk/res/android>">
        <item android:color="@color/white" android:state_selected="true" />
        <item android:color="@color/white" android:state_pressed="true" />
        <item android:color="@color/black" />
    </selector>
    color/selector_rect_color.xml
  • Drawable 리소스 : https://developer.android.com/guide/topics/resources/drawable-resource
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="<http://schemas.android.com/apk/res/android>">
        <item android:drawable="@color/white" android:state_selected="true" />
        <item android:drawable="@color/white" android:state_enabled="true" />
        <item android:drawable="@color/black" />
    </selector>
    drawable/selector_rect.xml

즉 최신 Android OS에서 ColorStateList 리소스로 View.setBackgroundResource() 를 사용할 수 있지만, OS 9 이하에서는 Resource NotFoundException을 겪게 됩니다.

1. 문제 재현해보기

먼저 문제를 재현해 보고, 정확히 문제가 되는 부분을 찾아봅니다.

try {
    val drawable = context.getDrawable(R.drawable.selector_rect)
    Log.d(TAG, "ResourceTest :: 0. $drawable")
    val drawable2 = context.getDrawable(R.color.selector_rect_color)
    Log.d(TAG, "ResourceTest :: 1. $drawable2")
} catch(e: Exception) {
    Log.d(TAG, "ResourceTest :: 2. $e")
}

앱에서 테스트할 부분에 위 코드를 추가 후 OS 10과 OS 9 기기에 올려서 확인해봅니다.

(기기가 없어도 에뮬레이터로 확인 가능합니다.)

각가 아래와 같은 로그를 얻을 수 있습니다.

  • Android OS 10
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@ba98d70
ResourceTest :: 1. android.graphics.drawable.ColorStateListDrawable@c5c796e
  • Android OS 9
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@e71743d
ResourceTest :: 2. android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3

 

2. 문제 범위 확인

문제 발생시 CallStack을 확인하여 비교할 코드 범위를 좁혀봅니다.

android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3
Caused by: android.content.res.Resources$NotFoundException: File res/color/selector_rect_color.xml from drawable resource ID #0x7f0601c3
    at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:847)
    at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
    at android.content.res.Resources.getDrawableForDensity(Resources.java:888)
    at android.content.res.Resources.getDrawable(Resources.java:827)
    at android.content.Context.getDrawable(Context.java:626)
    ...
Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable
    at android.graphics.drawable.StateListDrawable.inflateChildElements(StateListDrawable.java:190)
    at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:122)
    at android.graphics.drawable.DrawableInflater.inflateFromXmlForDensity(DrawableInflater.java:142)
    at android.graphics.drawable.Drawable.createFromXmlInnerForDensity(Drawable.java:1332)
    at android.graphics.drawable.Drawable.createFromXmlForDensity(Drawable.java:1291)
    at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:833)
	... 20 more

위 로그로 ResourcesImpl.loadDrawableForCookie 에서 Exception을 발생함을 확인할 수 있습니다.

 

3. AOSP를 통한 소스 확인

이 부분은 aosp 소스로 그 차이를 확인할 수 있습니다.

	@Nullable
    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
    ...
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        LookupStack stack = mLookupStack.get();
        try {
            // Perform a linear search to check if we have already referenced this resource before.
            if (stack.contains(id)) {
                throw new Exception("Recursive reference in drawable");
            }
            stack.push(id);
            try {
                if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                    rp.close();
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
            } finally {
                stack.pop();
            }
        } catch (Exception | StackOverflowError e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
    ...

 

@Nullable
    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
    ...
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        LookupStack stack = mLookupStack.get();
        try {
            // Perform a linear search to check if we have already referenced this resource before.
            if (stack.contains(id)) {
                throw new Exception("Recursive reference in drawable");
            }
            stack.push(id);
            try {
                if (file.endsWith(".xml")) {
                    if (file.startsWith("res/color/")) {
                        dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
                    } else {
                        dr = loadXmlDrawable(wrapper, value, id, density, file);
                    }
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
            } finally {
                stack.pop();
            }
        } catch (Exception | StackOverflowError e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
    ...
    private Drawable loadColorOrXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density, String file) {
        try {
            ColorStateList csl = loadColorStateList(wrapper, value, id, null);
            return new ColorStateListDrawable(csl);
        } catch (NotFoundException originalException) {
            // If we fail to load as color, try as normal XML drawable
            try {
                return loadXmlDrawable(wrapper, value, id, density, file);
            } catch (Exception ignored) {
                // If fallback also fails, throw the original exception
                throw originalException;
            }
        }
    }
    ...
    private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density, String file)
            throws IOException, XmlPullParserException {
        try (
                XmlResourceParser rp =
                        loadXmlResourceParser(file, id, value.assetCookie, "drawable")
        ) {
            return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
        }
    }

 

위 소스 확인으로 OS 10 이후부터는 ColorStateList 리소스도 처리가 가능하지만 그 미만 버전에서 문제가 되는 경우가 발생하는 부분을 확인할 수 있습니다.

 

컴파일시 에러가 나거나 버전 분기가 필요한 부분으로 체크 되지도 않기 때문에 생각없이 지나치기 쉬운 부분이 될 수도 있습니다.

만약 앱의 minSdkVersion이 29 미만이라면 항상 위 내용을 주의할 필요가 있습니다.

그야말로 별 생각없이 단순작업을 하다 벌어진 일이었습니다.

Java로 작성된 코드들을 일부 Kotlin으로 변환하고 문법을 조금씩 손을 보는 작업을 진행 중이었죠.

컴파일 오류도 없고 빨간 줄 하나 없이 잘 빌드되었고 문제가 없는 줄 알았습니다.

하지만 앱에는 내부적으로 문제가 있었죠. 그 원인은 이런 것이었습니다.

 

Java 클래스 2개가 있었고 일부 동일한 멤버변수들이 가지고 있는 클래스들이었습니다.

이 클래스들을 서로 다른 레이어의 Model로 있으면 레이어 간 데이터 전달시 서로 Mapping되는 역할이 있었죠.

그러다보니 동일한 멤버들을 서로의 멤버에 부어서 변환을 해주는 메서드들이 있었고요.

여기서 apply로 빌더처럼 데이터를 전달하다가 문제가 발생하였습니다.

 

해당코드를 샘플로 다시 작성 해봤습니다.

class Human {
    var sleep: Int = 0
    var eat: Int = 0
    var play: Int = 0
}

class Student {
    var sleep: Int = 0
    var eat: Int = 0
    var study: Int = 0
    
    fun getHuman(): Human {
        return Human().apply {
            sleep = sleep
            eat = eat
        }
    }
}

Human 과 Student 클래스가 있고 Student 클래스를 Human으로 맵핑하면서 동일한 데이터를 전달하는 과정입니다.

아래를 보면 바로 Human 생성 후 return을 하는 것이 보이고 apply로 필요한 데이터를 전달하는 것을 볼 수 있습니다.

하지만 이때 sleep과 eat 변수들을 주의깊게 보질 않았던 것이 문제였습니다.

        return Human().apply {
            sleep = sleep
            eat = eat
        }

위에서 보이는 sleep과 eat는 apply 내에서 this 즉 Human의 멤버들을 뜻하게 됩니다.

그래서 제가 Human에 넘기려던 Student의 데이터들이 제대로 복사가 안되었던 것입니다.

this.sleep = sleep 도 결과는 동일하였습니다.

그래서 아래와 같이 변경하였습니다.

        return Human().also {
            it.sleep = sleep
            it.eat = eat
        }

apply를 also로 변경하고 Human은 it으로 명시적으로 지정을 해주었습니다.

Kotlin의 Scope 함수를 잘 이해하고 조금만 신경 썼으면 틀릴 부분은 아니었습니다.

다만 IDE에서도 문법적으로는 문제가 없어 컴파일 오류로 잡아주질 않아 자칫 잘못하면 헤매기 쉬웠던 문제였죠.

문법들이 편해지는 만큼 방심하기 쉬워지는 것을 다시 한번 느끼긴 했습니다.

 

// 오랫만에 남기는 포스팅이라 뭐라 끝을 맺을지는 모르겠습니다... 그냥 끝!!!

Android WebView는 OS 버전에 따라 몇번의 변화가 있었습니다.

Android 10에서도 또 한번의 변화가 있어 간단히 정리해봤습니다.

Android 4.4 (Kitkat) 이전

커스텀하게 내장된 엔진을 사용하였으며, OS 업데이트시만 업데이트 되었습니다.

Android 4.4 (Kitkat)

Chromium 특정 버전을 사용하며, OS 업데이트시만 업데이트 되었습니다.

Android 5 (Lollipop)

OS 번들이 아니라 System WebView 로 제공되었으며, Play Store를 통해 업데이트가 되었습니다.

Android 7 (Nouga)

Chrome 앱의 엔진이 WebView에 사용되었으며, Chrome 앱 업데이트로 업데이트시 WebView도 업데이트가 되었습니다.

기존 System WebView와 Chrome WebView 중 선택이 가능합니다.

Android 10 (Q)

다시 System WebView로 제공되었고 Play Store를 통해 업데이트가 되었습니다.

System WebView Canary/Dev/Beta 버전들이 제공되며, 선택이 가능합니다.

Chrome WebView는 제공하지 않습니다.

관련 링크

https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/prerelease.md#Android-10-Q

https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/channels.md

https://www.androidpolice.com/2019/10/24/android-10-no-longer-uses-chrome-app-to-render-pages-inside-apps/

https://www.xda-developers.com/google-chrome-no-longer-webview-provider-android-10/

+ Recent posts