Androidで多くの画像を使用するアプリケーションを開発しました。
アプリが1回実行され、画面上の情報(Layouts
、Listviews
、Textviews
、ImageViews
など)が入力され、ユーザーが情報を読み取ります。
アニメーション、特殊効果、またはメモリを満たすことができるものはありません。時々、ドローアブルは変わる可能性があります。いくつかはAndroidリソースであり、いくつかはSDCARDのフォルダーに保存されたファイルです。
その後、ユーザーは終了し(onDestroy
メソッドが実行され、アプリはVMによってメモリにとどまります)、ユーザーが再び入力します。
ユーザーがアプリに入るたびに、ユーザーがJava.lang.OutOfMemoryError
を取得するまで、メモリがどんどん大きくなるのを見ることができます。
それでは、多くの画像を処理する最良/正しい方法は何ですか?
常にロードされないように、静的メソッドに配置する必要がありますか?レイアウトまたはレイアウトで使用されている画像を特別な方法でクリーニングする必要がありますか?
メモリリークがあるようです。問題は多くの画像を処理することではなく、アクティビティが破棄されたときに画像の割り当てが解除されないことです。
コードを見ずにこれがなぜ起こるのかを言うのは困難です。ただし、この記事には役立つヒントがいくつかあります。
http://Android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html
特に、静的変数を使用すると、状況が悪化する可能性が高くなります。アプリケーションの再描画時にコールバックを削除するコードを追加する必要がある場合がありますが、ここでも確かに言うのに十分な情報がありません。
Androidアプリの開発で見つかった最も一般的なエラーの1つは、「Java.lang.OutOfMemoryError:ビットマップサイズがVM Budget」を超えていることです。方向を変更した後、多くのビットマップを使用するアクティビティでこのエラーが頻繁に見つかりました。アクティビティは破棄され、再度作成され、ビットマップに使用可能なVMメモリを消費するXMLからレイアウトが「膨張」します。
以前のアクティビティレイアウトのビットマップは、アクティビティへの相互参照を持っているため、ガベージコレクタによって適切に割り当て解除されません。多くの実験の後、私はこの問題の非常に良い解決策を見つけました。
最初に、XMLレイアウトの親ビューで「id」属性を設定します。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
Android:layout_width="fill_parent"
Android:layout_height="fill_parent"
Android:id="@+id/RootView"
>
...
次に、アクティビティのonDestroy()
メソッドで、親ビューへの参照を渡すunbindDrawables()
メソッドを呼び出し、System.gc()
を実行します。
@Override
protected void onDestroy() {
super.onDestroy();
unbindDrawables(findViewById(R.id.RootView));
System.gc();
}
private void unbindDrawables(View view) {
if (view.getBackground() != null) {
view.getBackground().setCallback(null);
}
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
unbindDrawables(((ViewGroup) view).getChildAt(i));
}
((ViewGroup) view).removeAllViews();
}
}
このunbindDrawables()
メソッドは、ビューツリーを再帰的に探索し、次のことを行います。
この問題を回避するには、null
- ing Bitmap
オブジェクト(または別の値を設定)の前にネイティブメソッドBitmap.recycle()
を使用できます。例:
public final void setMyBitmap(Bitmap bitmap) {
if (this.myBitmap != null) {
this.myBitmap.recycle();
}
this.myBitmap = bitmap;
}
そして、次のようにmyBitmap
を変更できますSystem.gc()
の呼び出しなし:
setMyBitmap(null);
setMyBitmap(anotherBitmap);
この説明が役立つ場合があります: http://code.google.com/p/Android/issues/detail?id=8488#c8
「高速なヒント:
1)System.gc()を自分で呼び出さないでください。これはここで修正として伝播されており、機能しません。それをしません。私の説明で気づいた場合、OutOfMemoryErrorを取得する前に、JVMはすでにガベージコレクションを実行しているので、再度ガベージコレクションを実行する理由はありません(プログラムの速度が低下します)。アクティビティの最後に1つを実行するだけで、問題が隠されます。これにより、ビットマップがファイナライザキューにより速く配置される場合がありますが、代わりに各ビットマップで単にリサイクルを呼び出すことができなかった理由はありません。
2)不要になったビットマップでは、常にrecycle()を呼び出します。少なくとも、アクティビティのonDestroyでは、使用していたすべてのビットマップを処理してリサイクルします。また、ビットマップインスタンスをdalvikヒープからより速く収集したい場合、ビットマップへの参照をクリアしても問題はありません。
3)recycle()を呼び出してからSystem.gc()を呼び出しても、Dalvikヒープからビットマップが削除されない場合があります。これについて心配しないでください。 recycle()は、その仕事をしてネイティブメモリを解放しました。以前に説明した手順を実行して、Dalvikヒープからビットマップを実際に削除するには、少し時間がかかります。ネイティブメモリの大部分はすでに解放されているため、これは大した問題ではありません。
4)フレームワークに最後にバグがあると常に仮定します。 Dalvikは、本来行うべきことを正確に実行しています。それはあなたが期待しているものや望んでいるものではないかもしれませんが、その仕組みです。 」
私はこの正確な問題に遭遇しました。ヒープは非常に小さいため、これらのイメージはメモリに関してかなり迅速に制御できなくなります。 1つの方法は、ガベージコレクターに そのリサイクルメソッド を呼び出してビットマップのメモリを収集するためのヒントを与えることです。
また、onDestroyメソッドが呼び出されることは保証されていません。このロジック/クリーンアップをonPauseアクティビティに移動することもできます。詳細については、アクティビティライフサイクル図/表をご覧ください このページ .
次の点は本当に助けになりました。他のポイントもあるかもしれませんが、これらは非常に重要です:
私はまったく同じ問題を抱えていました。いくつかのテストを行った結果、このエラーは大きな画像のスケーリングで発生することがわかりました。画像のスケーリングを縮小し、問題はなくなりました。
追伸最初は、画像を縮小せずに画像サイズを縮小しようとしました。それはエラーを止めませんでした。
この問題を解決する便利な方法を提案します。エラーが発生したアクティビティのMainfest.xmlに従って、「Android:configChanges」属性値を割り当てます。このような:
<activity Android:name=".main.MainActivity"
Android:label="mainActivity"
Android:configChanges="orientation|keyboardHidden|navigation">
</activity>
最初に提供した解決策は、OOMエラーの頻度を実際に低レベルに減らしていました。しかし、それは問題を完全には解決しませんでした。そして、私は2番目の解決策を提供します:
OOMの詳細として、ランタイムメモリを使いすぎました。そこで、プロジェクトの〜/ res/drawableで画像サイズを縮小します。 128X128の解像度を持つ過修飾画像などは、64x64にサイズ変更できますが、これもアプリケーションに適しています。そして、写真の山でこれを行った後、OOMエラーは再び発生しません。
私もメモリ不足のバグにイライラしています。そして、はい、私もこのエラーが画像を拡大縮小するときに多く現れることを発見しました。最初はすべての密度の画像サイズを作成しようとしましたが、これによりアプリのサイズが大幅に増加することがわかりました。そのため、現在、すべての密度に対して1つの画像を使用し、画像をスケーリングしています。
ユーザーが1つのアクティビティから別のアクティビティに移動するたびに、アプリケーションはメモリ不足エラーをスローします。ドロアブルをnullに設定し、System.gc()を呼び出しても動作しませんでした。 Androidは、最初のアプローチでメモリ不足エラーをスローし続け、2番目のアプローチでリサイクルビットマップを使用しようとするとキャンバスエラーメッセージをスローします。
さらに3番目のアプローチを取りました。すべてのビューをnullに設定し、背景を黒に設定します。 onStop()メソッドでこのクリーンアップを行います。これは、アクティビティが表示されなくなるとすぐに呼び出されるメソッドです。 onPause()メソッドで実行すると、ユーザーには黒い背景が表示されます。理想的ではありません。 onDestroy()メソッドでそれを行うことに関しては、呼び出されるという保証はありません。
ユーザーがデバイスの戻るボタンを押したときに黒い画面が表示されないように、startActivity(getIntent())メソッドを呼び出してfinish()メソッドを呼び出して、onRestart()メソッドでアクティビティをリロードします。
注:背景を黒に変更する必要はありません。
Load Large Bitmaps Efficiently lesson で説明されているBitmapFactory.decode *メソッドは、ソースデータがディスクまたはネットワークの場所(または実際には他のソース)から読み取られる場合、メインUIスレッドで実行しないでください。メモリより)。このデータのロードにかかる時間は予測不能であり、さまざまな要因(ディスクまたはネットワークからの読み取り速度、イメージのサイズ、CPUのパワーなど)に依存します。これらのタスクの1つがUIスレッドをブロックすると、システムはアプリケーションに非応答としてフラグを立て、ユーザーはそれを閉じるオプションを選択できます(詳細については、応答性の設計を参照してください)。
FWIW、これは私がコーディングして数か月間使用した軽量のビットマップキャッシュです。それはすべてのbell音ではないので、使用する前にコードを読んでください。
/**
* Lightweight cache for Bitmap objects.
*
* There is no thread-safety built into this class.
*
* Note: you may wish to create bitmaps using the application-context, rather than the activity-context.
* I believe the activity-context has a reference to the Activity object.
* So for as long as the bitmap exists, it will have an indirect link to the activity,
* and prevent the garbaage collector from disposing the activity object, leading to memory leaks.
*/
public class BitmapCache {
private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();
private StringBuilder sb = new StringBuilder();
public BitmapCache() {
}
/**
* A Bitmap with the given width and height will be returned.
* It is removed from the cache.
*
* An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.
*
* Note that thread-safety is the caller's responsibility.
*/
public Bitmap get(int width, int height, Bitmap.Config config) {
String key = getKey(width, height, config);
ArrayList<Bitmap> list = getList(key);
int listSize = list.size();
if (listSize>0) {
return list.remove(listSize-1);
} else {
try {
return Bitmap.createBitmap(width, height, config);
} catch (RuntimeException e) {
// TODO: Test appendHockeyApp() works.
App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height);
throw e ;
}
}
}
/**
* Puts a Bitmap object into the cache.
*
* Note that thread-safety is the caller's responsibility.
*/
public void put(Bitmap bitmap) {
if (bitmap==null) return ;
String key = getKey(bitmap);
ArrayList<Bitmap> list = getList(key);
list.add(bitmap);
}
private ArrayList<Bitmap> getList(String key) {
ArrayList<Bitmap> list = hashtable.get(key);
if (list==null) {
list = new ArrayList<Bitmap>();
hashtable.put(key, list);
}
return list;
}
private String getKey(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Config config = bitmap.getConfig();
return getKey(width, height, config);
}
private String getKey(int width, int height, Config config) {
sb.setLength(0);
sb.append(width);
sb.append("x");
sb.append(height);
sb.append(" ");
switch (config) {
case ALPHA_8:
sb.append("ALPHA_8");
break;
case ARGB_4444:
sb.append("ARGB_4444");
break;
case ARGB_8888:
sb.append("ARGB_8888");
break;
case RGB_565:
sb.append("RGB_565");
break;
default:
sb.append("unknown");
break;
}
return sb.toString();
}
}
背景画像を適切なサイズに切り替えるだけでも同じ問題がありました。新しい画像を挿入する前にImageViewをnullに設定すると、より良い結果が得られました。
ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
さて、私はインターネットで見つけたすべてを試しましたが、どれも機能しませんでした。 System.gc()を呼び出すと、アプリの速度が低下するだけです。 onDestroyでビットマップをリサイクルすることも、私にとってはうまくいきませんでした。
現在動作する唯一のことは、すべてのビットマップの静的リストを保持して、再起動後もビットマップが生き残るようにすることです。また、アクティビティが再起動されるたびに新しいビットマップを作成する代わりに、保存されたビットマップを使用するだけです。
私の場合、コードは次のようになります。
private static BitmapDrawable currentBGDrawable;
if (new File(uriString).exists()) {
if (!uriString.equals(currentBGUri)) {
freeBackground();
bg = BitmapFactory.decodeFile(uriString);
currentBGUri = uriString;
bgDrawable = new BitmapDrawable(bg);
currentBGDrawable = bgDrawable;
} else {
bgDrawable = currentBGDrawable;
}
}