blog

Android9.0とandroid.content.res.Resources$NotFoundExceptionの問題解決

主にシステム・フォンで、$という行のクラッシュがあります。エラーを報告した画像はpng形式で、xxdhpiディレクトリに保存されており、Apkに存在していました。参照......

Apr 8, 2020 · 6 min. read
シェア

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は返されます。

Read next

Javaワークフローの詳細

javaワークフロー詳細説明 ワークフローとは?ワークフロー:2人以上の人が、共通の目標のために、連続的または並列的に業務を完了させること。業務: ワークフローが参照する業務は、業務に関連する活動を対象とします。シリアルまたはパラレル: 業務のステップを次々と実行すること。

Apr 8, 2020 · 6 min read