一:背景描述
线上出现一组崩溃,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。这样就解决了我们的问题。