6

私はAndroidアプリで、ホーム画面に戻るたびに、ヒープサイズ(リーク)がByteArrayOutputStreamの量だけ増加することに気づきました。私が管理できた最高のものは、追加することです

this.mByteArrayOutputStream = null;

run()の最後に、ヒープサイズが絶えず増加するのを防ぎます。誰かが私を啓発することができれば、私は非常に感謝するでしょう。問題を説明する次の例を書きました。

MainActivity.java

public class MainActivity extends Activity {
    private Controller mController;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);        
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    protected void onStart() {
        super.onStart();

        this.mController = new Controller();
        mController.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();

        mController.quit();
    }
}

Controller.java

public class Controller {
    public volatile ReaderThread mThread;

    public Controller() {
        super();
    }

    public void connect() {
        mThread = new ReaderThread("ReaderThread");
        mThread.start();
    }

    public void quit() {
        mThread.quit();
    }

    public static class ReaderThread extends Thread {
        private volatile boolean isProcessing;
        private ByteArrayOutputStream mByteArrayOutputStream;

        public ReaderThread(String threadName) {
            super(threadName);  
        }

        @Override
        public void run() {
            this.isProcessing = true;

            Log.d(getClass().getCanonicalName(), "START");
            this.mByteArrayOutputStream = new ByteArrayOutputStream(2048000);

            int i = 0;
            while (isProcessing) {
                Log.d(getClass().getCanonicalName(), "Iteration: " + i++);
                mByteArrayOutputStream.write(1);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }

            try {
                mByteArrayOutputStream.reset();
                mByteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d(getClass().getCanonicalName(), "STOP");
        }

        public void quit() {
            this.isProcessing = false;
        }
    }
}
4

5 に答える 5

9

スレッドはガベージコレクションのルートであるため、GCの影響を受けません。そのため、JVMがReaderThreadメンバー変数への割り当てとともにメモリ内に保持している可能性があり、リークが発生します。

お気づきのようにByteArrayOutputStream、を無効にすると、バッファリングされたデータ(ReaderThreadそれ自体ではない)がGCで使用できるようになります。

編集

調査の結果、Androidデバッガーがリークの原因であることがわかりました。

VMは、デバッガーが認識しているオブジェクトが、デバッガーが切断されるまでガベージコレクションされないことを保証します。これにより、デバッガーが接続されている間、時間の経過とともにオブジェクトが蓄積する可能性があります。たとえば、デバッガーが実行中のスレッドを検出した場合、スレッドが終了した後でも、関連付けられたThreadオブジェクトはガベージコレクションされません。

于 2012-10-18T14:58:16.633 に答える
3

Androidのデバッグページから:

デバッガーとガベージコレクターは現在、緩く統合されています。VMは、デバッガーが認識しているオブジェクトが、デバッガーが切断されるまでガベージコレクションされないことを保証します。これにより、デバッガーが接続されている間、時間の経過とともにオブジェクトが蓄積する可能性があります。たとえば、デバッガーが実行中のスレッドを検出した場合、スレッドが終了した後でも、関連付けられたThreadオブジェクトはガベージコレクションされません。

これにより、DDMSヒープ監視の価値が低下しますね。

于 2012-10-19T09:39:56.940 に答える
2

この問題は非常に興味深いものです。私はそれを見て、漏れのない実用的な解決策を得ることができました。揮発性変数を対応するアトミック変数に置き換えました。また、スレッドの代わりにAsyncTaskを使用しました。それでうまくいったようです。これ以上のリークはありません。

コード

class Controller {
    public ReaderThread mThread;
    private AtomicBoolean isProcessing = new AtomicBoolean(false);
    public Controller() {
        super();
    }

    public void connect() {
        ReaderThread readerThread = new ReaderThread("ReaderThread",isProcessing);
        readerThread.execute(new Integer[0]);
    }

    public void quit() {
        isProcessing.set(false);
    }

    public static class ReaderThread extends AsyncTask<Integer, Integer, Integer> {
        private AtomicBoolean isProcessing;
        private ByteArrayOutputStream mByteArrayOutputStream;

        public ReaderThread(String threadName, AtomicBoolean state) {
            this.isProcessing = state;
        }

        public void run() {
            this.isProcessing.set(true);

            Log.d(getClass().getCanonicalName(), "START");
            this.mByteArrayOutputStream = new ByteArrayOutputStream(2048000);

            int i = 0;
            while (isProcessing.get()) {
                Log.d(getClass().getCanonicalName(), "Iteration: " + i++);
                mByteArrayOutputStream.write(1);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }

            try {
                mByteArrayOutputStream.reset();
                mByteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d(getClass().getCanonicalName(), "STOP");
        }

        public void quit() {
            this.isProcessing.set(false);
        }

        @Override
        protected Integer doInBackground(Integer... params)
        {
            run();
            return null;
        }
    }
}

これは、ホーム画面に数回出入りした後の両方のコードサンプルの比較です。あなたは最初のものでbyte[]リークを見ることができます。

ソリューションのHprof比較

これが発生する理由については、非同期操作にAsyncTaskまたはServiceを使用することをお勧めします。私は彼らのビデオの1つがスレッドに対して明示的にアドバイスしていることを覚えています。プレゼンテーションを行う人は副作用について警告しました。これはそのうちの1つだと思いますか?アクティビティが復活したときにリークがあることははっきりとわかりますが、リークが発生する理由の説明はありません(少なくともJVMでは。dalvikVMの内部といつ発生するかはわかりません)。スレッドが完全に停止したと見なします)。弱参照/コントローラーのnullなどを試しましたが、これらのアプローチはどれも機能しませんでした。

このリークが報告される理由については、この回答を参照してください-https://stackoverflow.com/a/12971464/830964。誤検知です。

非同期タスクで明示的にnullにすることなく、BAOSが占有しているメモリを取り除くことができました。他の変数でも機能するはずです。それで解決するかどうか教えてください。

于 2012-10-18T16:42:06.180 に答える
1

使用法を示したコードに基づいて、インスタンスは、ByteArrayOutputStreamインスタンスがリークした場合にのみリークできReaderThreadます。そして、それは、スレッドがまだ実行されているか、どこかに到達可能な参照がある場合にのみ発生する可能性があります。

インスタンスがどのようReaderThreadリークするかを理解することに焦点を当てます。

于 2012-10-18T14:16:44.093 に答える
1

私も同様の問題を抱えていましたが、スレッドを無効にすることで解決しました。

quit()次のように、エミュレーター(SDK 16)の変更方法でコードを試しました。

public void quit() { 
    mThread.quit(); 
    mThread = null;  // add this line
} 

DDMSで、リークが効果的に停止したことを確認しました。ラインを外すと、リークが戻ります。

-編集済み--

SDK 10を使用した実際のデバイスでもこれを試しましたが、結果を以下に示します。

テストコードまたは完全なコードでスレッドヌルを試しましたか?実際のデバイスでもエミュレーターでも、スレッドをヌルにした後にテストコードがリークしないことを示しています。

最初の起動後のスクリーンショット(割り当て済み7.2MB /使用済み:4.6MB):

ステップ1

数回の再起動後のスクリーンショット(割り当て済み7.2MB /使用済み:4.6MB):

ステップ2

数回の非常に高速な再起動後のスクリーンショット、および回転デバイスのservral時間(割り当てられた13.2MB /使用済み:4.6MB):

ステップ3

最後の画面で割り当てられたメモリは最初のメモリよりも大幅に高くなっていますが、割り当てられたメモリは4.6MBのままなので、リークは発生しません。

于 2012-10-18T19:14:35.040 に答える