web-dev-qa-db-ja.com

ビットマップ割り当てはOreoでどのように機能し、それらのメモリを調査する方法は?

バックグラウンド

過去数年間、Androidにあるヒープメモリの量と使用量を確認するために、次のようなものを使用できます。

@JvmStatic
fun getHeapMemStats(context: Context): String {
    val runtime = Runtime.getRuntime()
    val maxMemInBytes = runtime.maxMemory()
    val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory())
    val usedMemInBytes = maxMemInBytes - availableMemInBytes
    val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes
    return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
            Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)"
}

つまり、特にビットマップをメモリに格納することにより、使用するメモリが多いほど、アプリケーションで使用できる最大ヒープメモリに近づきます。最大値に達すると、アプリはOutOfMemory例外(OOM)でクラッシュします。

問題

Android O(私の場合は8.1ですが、おそらく8.0でもあります)では、上記のコードはビットマップの割り当ての影響を受けないことに気づきました。

さらに掘り下げてみると、Androidプロファイラーで、使用するメモリが多いほど(POCに大きなビットマップを保存する)、ネイティブメモリが多く使用されることに気付きました。

それがどのように機能するかをテストするために、私はそのような単純なループを作成しました:

    val list = ArrayList<Bitmap>()
    Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this))
    useMoreMemoryButton.setOnClickListener {
        AsyncTask.execute {
            for (i in 0..1000) {
                // list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888))
                list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image))
                Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this))
            }
        }
    }

場合によっては、1回の反復で作成し、場合によっては、ビットマップをデコードするのではなく、リストに作成しただけです(コメント内のコード)。これについては後で詳しく説明します...

これは、上記を実行した結果です。

enter image description here

グラフからわかるように、アプリは、私に報告された最大ヒープメモリ(201MB)をはるかに超える、膨大なメモリ使用量に達しました。

私が見つけたもの

私は多くの奇妙な行動を見つけました。このため、私はそれらについて報告することにしました、 ここ

  1. まず、実行時にメモリ統計を取得するために、上記のコードの代替を試みました。

     @JvmStatic
     fun getNativeMemStats(context: Context): String {
         val nativeHeapSize = Debug.getNativeHeapSize()
         val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
         val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
         val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
         return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
                 Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)"
     }
    

しかし、ヒープメモリチェックとは対照的に、最大ネイティブメモリは時間の経過とともにその値を変更するようです。つまり、実際の最大値が何であるかがわからないため、実際のアプリでは何を決定することができません。メモリキャッシュサイズはである必要があります。上記のコードの結果は次のとおりです。

heapMemStats:used: 2.0 MB / 201 MB (0%) nativeMemStats:used: 3.6 MB / 6.3 MB (57%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 290 MB / 310 MB (93%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 553 MB / 579 MB (95%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 821 MB / 847 MB (96%)
  1. OutOfMemory例外の代わりに、デバイスがビットマップを保存できなくなる(1.1GBまたはNexus 5xでは最大850MBで停止)ようになると、何も表示されません。アプリを閉じるだけです。クラッシュしたというダイアログすらありません。

  2. デコードする代わりに新しいビットマップを作成した場合(上記のコード、代わりにコメントで)、何トンものGBを使用し、何トンものネイティブメモリを使用できるという奇妙なログが表示されます。

enter image description here

また、ビットマップをデコードするときとは対照的に、ここ(ダイアログを含む)でクラッシュが発生しますが、OOMではありません。代わりに、それは... NPEです!

01-04 10:12:36.936 30598-31301/com.example.user.myapplication E/AndroidRuntime:致命的な例外:AsyncTask#1プロセス:com.example.user.myapplication、PID:30598 Java.lang.NullPointerException:試行Android.graphics.Bitmap.createBitmap(Bitmap.Java:980)のAndroid.graphics.Bitmap.createBitmap(Bitmap.Java:1046)のnullオブジェクト参照で仮想メソッド 'void Android.graphics.Bitmap.setHasAlpha(boolean)'を呼び出します。 )at Android.graphics.Bitmap.createBitmap(Bitmap.Java:930)at Android.graphics.Bitmap.createBitmap(Bitmap.Java:891)at com.example.user.myapplication.MainActivity $ onCreate $ 1 $ 1.run(MainActivity。 kt:21)at Android.os.AsyncTask $ SerialExecutor $ 1.run(AsyncTask.Java:245)at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1162)at Java.util.concurrent.ThreadPoolExecutor $ Worker。 run(ThreadPoolExecutor.Java:636)at Java.lang.Thread.run(Thread.Java:764)

プロファイラーグラフを見ると、さらに奇妙になります。メモリ使用量はそれほど増加していないようで、クラッシュポイントでは低下するだけです。

enter image description here

グラフを見ると、たくさんのGCアイコン(ゴミ箱)があります。メモリ圧縮を行っている可能性があると思います。

  1. 以前のバージョンのAndroidとは対照的に、(プロファイラーを使用して)メモリダンプを実行すると、ビットマップのプレビューが表示されなくなります。

enter image description here

質問

この新しい動作は多くの疑問を投げかけます。 OOMのクラッシュの数を減らすことができますが、それらの検出、メモリリークの検出、およびそれらの修正が非常に困難になる可能性もあります。たぶん私が見たもののいくつかは単なるバグですが、それでも...

  1. Android Oのメモリ使用量は正確に何が変わったのですか?そしてなぜですか?

  2. ビットマップはどのように処理されますか?

  3. メモリダンプレポート内のビットマップをプレビューすることは可能ですか?

  4. アプリが使用できる最大のネイティブメモリを取得し、ログに出力して、最大値を決定するための何かとして使用する正しい方法は何ですか?

  5. このトピックに関するビデオ/記事はありますか?追加されたメモリの最適化については話していませんが、ビットマップが現在どのように割り当てられているか、OOMを今どのように処理するかなどについて詳しく説明しています...

  6. この新しい動作は、一部のキャッシュライブラリに影響を与える可能性があると思いますよね?これは、代わりにヒープメモリサイズに依存する可能性があるためです。

  7. サイズが20,000x20,000(約1.6 GB)のビットマップをこれほど多く作成できたのに、サイズ7,680x7,680(約236 MB)の実際の画像からビットマップをいくつか作成できたのはどうしてでしょうか。 ?私が推測したように、それは本当にメモリ圧縮を行いますか?

  8. ビットマップを作成する場合、ネイティブメモリ関数はどのようにしてそのような巨大な値を返すことができますか?ビットマップをデコードしたときの値はもっと合理的ですか?それらはどういう意味ですか?

  9. ビットマップ作成の場合の奇妙なプロファイラーグラフとは何ですか?メモリ使用量はほとんど増加しませんが、最終的には(多くのアイテムが挿入された後)、それ以上作成できなくなるまでになりました。

  10. 奇妙な例外の動作とは何ですか?ビットマップデコードで、アプリの一部として例外やエラーログが表示されないのはなぜですか?それらを作成したときにNPEが表示されましたか?

  11. PlayストアはOOMを検出し、それが原因でアプリがクラッシュした場合に備えて、それらについて報告しますか?すべての場合にそれを検出しますか? Crashlyticsはそれを検出できますか?ユーザーからであれ、オフィスでの開発中であれ、そのようなことを知らせる方法はありますか?

19

アプリがLinuxOOMキラーによって殺されたようです。ネイティブメモリを積極的に使用するゲーム開発者やその他の人々は、それが常に発生していることを認識しています。

カーネルのオーバーコミットを有効にするとともに、ビットマップ割り当てのヒープベースの制限を解除すると、図が表示される場合があります。オーバーコミットについて少し読むことができます ここ

個人的には、アプリの停止について学習するためのOS APIを見たいのですが、息を止めません。


  1. アプリが使用できる最大のネイティブメモリを取得し、ログに出力して、最大値を決定するための何かとして使用する正しい方法は何ですか?

任意の値(たとえば、ヒープサイズの4分の1)を選択し、それを維持します。 onTrimMemory(OOM killerとネイティブメモリプレッシャーに直接関係している)が呼び出された場合は、消費量を減らすようにしてください。

  1. この新しい動作は、一部のキャッシュライブラリに影響を与える可能性があると思いますよね?これは、代わりにヒープメモリサイズに依存する可能性があるためです。

関係ありません— Androidヒープサイズは常に物理メモリの合計よりも小さくなります。ヒープサイズをガイドラインとして使用したキャッシュライブラリは、どちらの方法でも引き続き機能するはずです。

  1. それぞれサイズが20,000x20,000の非常に多くのビットマップを作成できたのはどうしてでしょうか。

魔法。

現在のバージョンのAndroid Oreoではメモリのオーバーコミットが許可されていると思います。手つかずのメモリは実際にはハードウェアから要求されないため、OSのアドレス可能なメモリ制限で許可されている限り多く使用できます(ビットx86では2ギガバイト未満、x64では数テラバイト)すべて 仮想メモリ はページで構成されます(通常はそれぞれ4Kb)。ページを使おうとすると、ページインされます。カーネルがそうでない場合プロセスのページをマップするのに十分な物理メモリがある場合、アプリはシグナルを受信して​​それを強制終了します。実際には、アプリはそれが発生する前にLinuxOOMキラーによって強制終了されます。

  1. ビットマップを作成する場合、ネイティブメモリ関数はどのようにしてそのような巨大な値を返すことができますか?ビットマップをデコードしたときの値はもっと合理的ですか?それらはどういう意味ですか?

  2. ビットマップ作成の場合の奇妙なプロファイラーグラフとは何ですか?メモリ使用量はほとんど増加しませんが、最終的には(多くのアイテムが挿入された後)、それ以上作成できなくなるまでになりました。

プロファイラーグラフは、ヒープメモリ使用量を示します。ビットマップがヒープにカウントされない場合、そのグラフには当然それらが表示されません。

ネイティブメモリ関数は、(当初は)意図したとおりに機能しているように見えます—virtual割り当てを正しく追跡しますが、各仮想に予約されている物理メモリの量を認識していませんカーネルによる割り当て(ユーザースペースには不透明です)。

また、ビットマップをデコードするときとは対照的に、ここ(ダイアログを含む)でクラッシュが発生しますが、OOMではありません。代わりに、それは... NPEです!

これらのページはいずれも使用していないため、物理メモリにマップされません。したがって、OOMキラーは(まだ)あなたを殺しません。物理メモリが不足している場合と比較して無害な仮想メモリが不足しているため、または他の種類のメモリ制限(cgroupsベースの制限など)に達したために、割り当てが失敗した可能性があります。無害。

  1. ... Crashlyticsはそれを検出できますか?ユーザーからであれ、オフィスでの開発中であれ、そのようなことを知らせる方法はありますか?

OOM killerは、SIGKILLを使用してアプリを破棄します(バックグラウンドに入った後にプロセスが終了した場合と同じです)。あなたのプロセスはそれに反応することができません。子プロセスによるプロセスの死を観察することは理論的には可能ですが、正確な理由を知るのは難しいかもしれません。 誰が私のプロセスを「殺した」のか、そしてその理由は? を参照してください。適切に作成されたライブラリは、メモリ使用量を定期的にチェックし、知識に基づいて推測できる場合があります。非常によく書かれたライブラリは、ネイティブのmalloc関数にフックすることで(たとえば、アプリケーションのインポートテーブルにホットパッチを適用するなどして)メモリ割り当てを検出できる場合があります。


仮想メモリ管理がどのように機能するかをよりよく示すために、それぞれ1Gbのビットマップを1000個割り当ててから、それぞれの1つのピクセルを変更することを想像してみましょう。 OSは最初にこれらのビットマップに物理メモリを割り当てないため、合計で約0バイトの物理メモリを使用します。ビットマップの単一の4バイトRGBAピクセルに触れると、カーネルはそのピクセルを格納するために単一のページを割り当てます。

OSはJavaオブジェクトとビットマップについて何も知りません—すべてのプロセスメモリをページの連続リストとして表示するだけです。

一般的に使用されるメモリページのサイズは4Kbです。 1000ピクセル(1Gbビットマップごとに1つ)に触れた後でも、実際のメモリの使用量は4Mb未満です。

4
user1643723