Android9.0出现android.content.res.Resources$NotFoundException问题解决

3,331 阅读4分钟

一:背景描述

线上出现一组崩溃,android.content.res.Resources$NotFoundException,主要集中在Android9.0系统手机(并非Android9.0必现)。报错的图片格式为png(大小为36*1),存放在xxdhpi目录下,且在Apk中的确存在。看崩溃日志:

07-23 13:30:24.747 23337 23337 E AndroidRuntime: Caused by: android.content.res.Resources$NotFoundException: File res/drawable-xxhdpi-v4/divider.png from drawable resource ID #0x7f0806e4
07-23 13:30:24.747 23337 23337 E AndroidRuntime: 	at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:847)
07-23 13:30:24.747 23337 23337 E AndroidRuntime: 	at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
07-23 13:30:24.747 23337 23337 E AndroidRuntime: 	at android.content.res.Resources.loadDrawable(Resources.java:897)
07-23 13:30:24.747 23337 23337 E AndroidRuntime: 	at android.content.res.TypedArray.getDrawableForDensity(TypedArray.java:955)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.content.res.TypedArray.getDrawable(TypedArray.java:930)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.widget.ImageView.(ImageView.java:189)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.widget.ImageView.(ImageView.java:172)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.widget.ImageView.(ImageView.java:168)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at java.lang.reflect.Constructor.newInstance0(Native Method)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.view.LayoutInflater.createView(LayoutInflater.java:647)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at com.android.internal.policy.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:58)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.view.LayoutInflater.onCreateView(LayoutInflater.java:720)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:788)
...
07-23 13:30:24.748 23337 23337 E AndroidRuntime: Caused by: java.lang.IllegalArgumentException: Dimensions must be positive! provided (12, 0)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.graphics.ImageDecoder.setTargetSize(ImageDecoder.java:1033)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.graphics.ImageDecoder.computeDensity(ImageDecoder.java:1823)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.graphics.ImageDecoder.decodeDrawableImpl(ImageDecoder.java:1670)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.graphics.ImageDecoder.decodeDrawable(ImageDecoder.java:1645)
07-23 13:30:24.748 23337 23337 E AndroidRuntime: 	at android.content.res.ResourcesImpl.decodeImageDrawable(ResourcesImpl.java:766)

忽略中间一部分不重要日志。

二:分析

从日志上分析来看,图片是被找到了,只是给图片进行缩放的时候,计算出的高度为0,导致出错。 如何看源代码:www.androidos.net.cn/sourcecode(…

看下ImageDecoder.computeDensity方法:

private int computeDensity(@NonNull Source src) {
    if (this.requestedResize()) {
        return Bitmap.DENSITY_NONE;
    }

    final int srcDensity = src.getDensity();
    if (srcDensity == Bitmap.DENSITY_NONE) {
        return srcDensity;
    }

    if (mIsNinePatch && mPostProcessor == null) {
        return srcDensity;
    }

    Resources res = src.getResources();
    if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
        return srcDensity;
    }

    final int dstDensity = src.computeDstDensity();
    if (srcDensity == dstDensity) {
        return srcDensity;
    }

    if (srcDensity < dstDensity && sApiLevel >= Build.VERSION_CODES.P) {
        return srcDensity;
    }

    float scale = (float) dstDensity / srcDensity;
    int scaledWidth = (int) (mWidth * scale + 0.5f);
    int scaledHeight = (int) (mHeight * scale + 0.5f);
    this.setTargetSize(scaledWidth, scaledHeight);
    return dstDensity;
}

日志中走到了最下方,计算出来的scaledWidth为12,scaledHeight为0,而图片原始大小为36*1,则说明scale为1/3。

scaledHeight = (int)(1 * 1/3 +0.5f) = 0

那么第一个问题就是为什么在部分Android9.0的手机上,手机密度获取到为1?(目前未搞明白为什么)

三:解决

既然png的图片存在如下问题,那么如何解决?比较挫的方式就是别用高度为1的图片,可以用高度为3的图片,但是逃不过UI的像素眼。

本文采用的方式是用写drawable.xml来替代png方式(一般来说高度为1的图片都是分割线,所以这种活drawable.xml也能完成)。那从代码角度看下为啥drawable能解决。

回到我们最开始如何加载一个Drawable。

android.content.res.ResourcesImpl#loadDrawableForCookie

无论是在xml中直接使用drawable还是用java代码的方式去加载图片,最终都会调用到这个方法。

private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,int id, int density) {
    final Drawable dr;

    Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
    LookupStack stack = mLookupStack.get();
    try {
        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;
        }
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return dr;
    }

该方法只截取了关键代码。可以看到如果是xml结尾的资源,则直接走了if中的分支,而png格式则走了else分支,也就会走到我们崩溃日志中的代码里。

重点看下Drawable.createFromXmlForDensity方法,一直跟踪到android.graphics.drawable.DrawableInflater#inflateFromXmlForDensity。

Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
        @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    if (name.equals("drawable")) {
        name = attrs.getAttributeValue(null, "class");
        if (name == null) {
            throw new InflateException("<drawable> tag must specify class attribute");
        }
    }

    Drawable drawable = inflateFromTag(name);
    if (drawable == null) {
        drawable = inflateFromClass(name);
    }
    drawable.setSrcDensityOverride(density);
    drawable.inflate(mRes, parser, attrs, theme);
    return drawable;
}

因为我们在drawable.xml中写的是shape,那么inflateFromTag中创建的是GradientDrawable。继续往下看drawable.inflate,看下GradientDrawable中的inflate方法:

public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
    @NonNull AttributeSet attrs, @Nullable Theme theme)
    throws XmlPullParserException, IOException {
    super.inflate(r, parser, attrs, theme);

    mGradientState.setDensity(Drawable.resolveDensity(r, 0));

    final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawable);
    updateStateFromTypedArray(a);
    a.recycle();

    inflateChildElements(r, parser, attrs, theme);

    updateLocalState(r);
    }

在mGradientState.setDensity方法中,调用到了applyDensityScaling方法,在该方法中,会根据手机密度和drawable所处目录进行缩放计算。

private void applyDensityScaling(int sourceDensity, int targetDensity) {
    //...省略部分代码
    if (mWidth > 0) {
        mWidth = Drawable.scaleFromDensity(mWidth, sourceDensity, targetDensity, true);
    }
    if (mHeight > 0) {
        mHeight = Drawable.scaleFromDensity(mHeight, sourceDensity, targetDensity, true);
    }
}

static int scaleFromDensity(int pixels, int sourceDensity, int targetDensity, boolean isSize) {
    if (pixels == 0 || sourceDensity == targetDensity) {
        return pixels;
    }

    final float result = pixels * targetDensity / (float) sourceDensity;
    if (!isSize) {
        return (int) result;
    }

    final int rounded = Math.round(result);
    if (rounded != 0) {
        return rounded;
    } else if (pixels > 0) {
        return 1;
    } else {
        return -1;
    }
}

在scaleFromDensity方法中,保证只要传进来的原始pixels大于0,那么返回的值一定大于0。虽然可能在崩溃的手机上,算出来的rounded为0,但是会至少返回1。这样就解决了我们的问题。