I: 背景の説明
主にAndroid 9.0の携帯電話で、android.content.res.Resources$NotFoundExceptionという一連のクラッシュが発生しています。報告されたエラーのイメージ形式はpngで、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)
重要でないログの中間部分は無視してください。
II: 分析
ログを解析したところ、イメージは見つかりました。ただ、イメージを拡大縮小する際に、計算された高さが0になり、エラーになりました。ソースコードの見方
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、これは1/3のスケールを示しています。
scaledHeight == 0
最初の疑問は、なぜAndroid 9.0では電話機密度が1になっているのか、ということです。
III: 和解
pngイメージには次のような問題があるので、どうすれば解決できるでしょうか。もっと悔しい方法は、高さ1のイメージを使わないことです。高さ3のイメージを使うこともできますが、UIのピクセルの目から逃れることはできません。
この記事では、pngメソッドに代わる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;
}
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メソッドでは、渡された元のピクセルが0より大きい限り、返される値も0より大きくなければならないことが保証されています。クラッシュした携帯電話では、四捨五入された計算が0になるかもしれませんが、少なくとも1は返されます。




