341

私はAndroidでのメモリリークに関するいくつかの記事を読んでいて、このテーマに関するGoogle I/Oからのこの興味深いビデオを見てきました。

それでも、私はその概念を完全には理解していません。特に、アクティビティ内の内部クラスを使用することが安全または危険である場合はそうです。

これは私が理解したことです:

内部クラスのインスタンスが外部クラス(アクティビティ)よりも長く存続すると、メモリリークが発生します。->これはどのような状況で発生する可能性がありますか?

OnClickListenerこの例では、拡張する匿名クラスがアクティビティより長く存続する方法がないため、リークのリスクはないと思いますよね?

    final Dialog dialog = new Dialog(this);
    dialog.setContentView(R.layout.dialog_generic);
    Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok);
    TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title);

    // *** Handle button click
    okButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            dialog.dismiss();
        }
    });

    titleTv.setText("dialog title");
    dialog.show();

さて、この例は危険ですか、そしてなぜですか?

// We are still inside an Activity
_handlerToDelayDroidMove = new Handler();
_handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000);

private Runnable _droidPlayRunnable = new Runnable() { 
    public void run() {
        _someFieldOfTheActivity.performLongCalculation();
    }
};

このトピックを理解することは、アクティビティが破棄されて再作成されたときに何が保持されるかを詳細に理解することと関係があるという事実に疑問があります。

それは...ですか?

デバイスの向きを変更したとしましょう(これがリークの最も一般的な原因です)。いつsuper.onCreate(savedInstanceState)呼び出されるonCreate()のですか?これにより、フィールドの値が復元されますか(向きが変わる前の状態)?これにより、内部クラスの状態も復元されますか?

私の質問はあまり正確ではないことは承知していますが、物事をより明確にすることができる説明をいただければ幸いです。

4

3 に答える 3

682

あなたが求めているのはかなり難しい質問です。たった1つの質問だと思うかもしれませんが、実際には一度に複数の質問をしています。私はそれをカバーしなければならないという知識で最善を尽くします、そしてうまくいけば、私が逃すかもしれないものをカバーするために他の何人かが参加するでしょう。

ネストされたクラス:はじめに

JavaでのOOPにどれほど慣れているかわからないので、これはいくつかの基本に当てはまります。ネストされたクラスとは、クラス定義が別のクラスに含まれている場合です。基本的に、静的ネストクラスと内部クラスの2つのタイプがあります。これらの本当の違いは次のとおりです。

  • 静的ネストクラス:
    • 「トップレベル」と見なされます。
    • 含まれているクラスのインスタンスを作成する必要はありません。
    • 明示的な参照なしに、含まれているクラスメンバーを参照することはできません。
    • 自分の生涯を持っています。
  • インナーネストクラス:
    • 常に、含むクラスのインスタンスを構築する必要があります。
    • 含まれているインスタンスへの暗黙の参照を自動的に持ちます。
    • 参照なしでコンテナのクラスメンバーにアクセスできます。
    • 寿命はコンテナの寿命より長くないと思われます。

ガベージコレクションとインナークラス

ガベージコレクションは自動ですが、オブジェクトが使用されていると思われるかどうかに基づいてオブジェクトを削除しようとします。ガベージコレクターはかなり賢いですが、完璧ではありません。オブジェクトへのアクティブな参照があるかどうかによってのみ、何かが使用されているかどうかを判断できます。

ここでの本当の問題は、内部クラスがそのコンテナーよりも長く存続している場合です。これは、含まれているクラスへの暗黙の参照が原因です。これが発生する唯一の方法は、包含クラスの外部のオブジェクトが、包含オブジェクトに関係なく、内部オブジェクトへの参照を保持している場合です。

これにより、内部オブジェクトが(参照を介して)生きているが、含まれているオブジェクトへの参照が他のすべてのオブジェクトからすでに削除されている状況が発生する可能性があります。したがって、内部オブジェクトは、常にそれへの参照を持っているため、包含オブジェクトを存続させます。これに伴う問題は、プログラムされていない限り、含まれているオブジェクトに戻って、それが生きているかどうかを確認する方法がないことです。

この認識の最も重要な側面は、アクティビティ内にあるかドローアブルであるかに関係なく、違いがないことです。内部クラスを使用するときは常に系統立てて、コンテナのオブジェクトよりも長持ちしないようにする必要があります。幸いなことに、それがコードのコアオブジェクトでない場合は、リークは比較的小さい可能性があります。残念ながら、これらは見つけるのが最も難しいリークの一部です。なぜなら、それらの多くがリークするまで気付かれない可能性があるからです。

ソリューション:内部クラス

  • 含まれているオブジェクトから一時的な参照を取得します。
  • 内部オブジェクトへの長期参照を保持する唯一のオブジェクトを含むオブジェクトを許可します。
  • Factoryなどの確立されたパターンを使用します。
  • 内部クラスがそれを含むクラスメンバーへのアクセスを必要としない場合は、それを静的クラスに変換することを検討してください。
  • アクティビティにあるかどうかに関係なく、注意して使用してください。

活動と見解:はじめに

アクティビティには、実行および表示できる多くの情報が含まれています。アクティビティは、ビューが必要であるという特性によって定義されます。また、特定の自動ハンドラーもあります。指定するかどうかに関係なく、アクティビティには、含まれるビューへの暗黙の参照があります。

ビューを作成するには、ビューを作成する場所と、表示できるように子が存在するかどうかを認識している必要があります。これは、すべてのビューにアクティビティへの参照があることを意味します(経由getContext())。さらに、すべてのビューはその子(つまりgetChildAt())への参照を保持します。最後に、各ビューは、その表示を表すレンダリングされたビットマップへの参照を保持します。

アクティビティ(またはアクティビティコンテキスト)への参照がある場合は常に、これは、レイアウト階層のチェーン全体をたどることができることを意味します。これが、アクティビティまたはビューに関するメモリリークが非常に大きな問題である理由です。大量のメモリが一度にリークされる可能性があります。

アクティビティ、ビュー、インナークラス

インナークラスに関する上記の情報を考えると、これらは最も一般的なメモリリークですが、最も一般的に回避されるものでもあります。内部クラスがActivitiesクラスのメンバーに直接アクセスできることが望ましいのですが、多くの場合、潜在的な問題を回避するために、それらを静的にすることをいとわないでしょう。アクティビティとビューの問題は、それよりもはるかに深刻です。

リークされたアクティビティ、ビュー、およびアクティビティコンテキスト

それはすべて、コンテキストとライフサイクルに帰着します。アクティビティコンテキストを強制終了する特定のイベント(オリエンテーションなど)があります。非常に多くのクラスとメソッドがコンテキストを必要とするため、開発者はコンテキストへの参照を取得してそれを保持することにより、コードを保存しようとすることがあります。アクティビティを実行するために作成する必要のあるオブジェクトの多くは、アクティビティが必要なことを実行できるようにするために、アクティビティライフサイクルの外部に存在する必要があります。オブジェクトのいずれかが破棄されたときにアクティビティ、そのコンテキスト、またはそのビューのいずれかへの参照を持っている場合は、そのアクティビティとそのビューツリー全体がリークされています。

ソリューション:アクティビティとビュー

  • ビューまたはアクティビティへの静的参照を作成することは、絶対に避けてください。
  • アクティビティコンテキストへのすべての参照は短命である必要があります(関数の期間)
  • 存続期間の長いコンテキストが必要な場合は、アプリケーションコンテキスト(getBaseContext()またはgetApplicationContext())を使用してください。これらは暗黙的に参照を保持しません。
  • または、構成の変更をオーバーライドすることで、アクティビティの破棄を制限することもできます。ただし、これは他の潜在的なイベントによるアクティビティの破壊を阻止するものではありません。これを行うことはできますが、それでも上記の方法を参照することをお勧めします。

Runnables:はじめに

ランナブルは実際にはそれほど悪くはありません。つまり、そうなる可能性がありますが、実際には、すでにほとんどの危険ゾーンに到達しています。Runnableは、作成されたスレッドから独立したタスクを実行する非同期操作です。ほとんどのランナブルは、UIスレッドからインスタンス化されます。本質的に、Runnableを使用すると、もう少し管理された別のスレッドが作成されます。Runnableを標準クラスのように分類し、上記のガイドラインに従うと、いくつかの問題が発生するはずです。現実には、多くの開発者はこれを行いません。

使いやすさ、読みやすさ、論理的なプログラムフローから、多くの開発者は、上記で作成した例のように、匿名内部クラスを使用してランナブルを定義します。これにより、上記で入力したような例になります。匿名内部クラスは、基本的に個別の内部クラスです。まったく新しい定義を作成して、適切なメソッドをオーバーライドする必要はありません。他のすべての点で、それは内部クラスです。つまり、コンテナへの暗黙の参照を保持します。

ランナブルとアクティビティ/ビュー

わーい!このセクションは短くすることができます!Runnablesは現在のスレッドの外部で実行されるため、これらの危険性は長時間実行される非同期操作になります。ランナブルがアクティビティまたはビューで匿名内部クラスまたはネストされた内部クラスとして定義されている場合、いくつかの非常に深刻な危険があります。これは、前述のように、コンテナが誰であるかを知る必要があるためです。向きの変更(またはシステムキル)を入力します。ここで、前のセクションに戻って、何が起こったのかを理解してください。はい、あなたの例は非常に危険です。

ソリューション:Runnables

  • コードのロジックを壊さない場合は、Runnableを拡張してみてください。
  • 拡張Runnableをネストされたクラスにする必要がある場合は、静的にするために最善を尽くしてください。
  • 匿名ランナブルを使用する必要がある場合は、使用中のアクティビティまたはビューへの長期参照があるオブジェクトでそれらを作成しないでください。
  • 多くのRunnableは、同じように簡単にAsyncTasksである可能性があります。AsyncTaskはデフォルトでVM管理されているため、使用を検討してください。

最後の質問への回答ここで、この投稿の他のセクションで直接取り上げられ なかった質問に回答します。あなたは「内側のクラスのオブジェクトが外側のクラスより長く生き残ることができるのはいつですか?」と尋ねました。これに到達する前に、強調しておきます。アクティビティでこれを心配するのは正しいですが、どこでもリークが発生する可能性があります。実例を示すために、(アクティビティを使用せずに)簡単な例を示します。

以下は、基本的なファクトリの一般的な例です(コードがありません)。

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

これは一般的な例ではありませんが、説明するのに十分簡単です。ここで重要なのはコンストラクターです...

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Gotta have a Factory to make my holes
        LeakFactory _holeDriller = new LeakFactory()
    // Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//Store them in the class member
            myHoles[i] = _holeDriller.createLeak();
        }

    // Yay! We're done! 

    // Buh-bye LeakFactory. I don't need you anymore...
    }
}

現在、リークはありますが、ファクトリーはありません。ファクトリをリリースしましたが、すべてのリークにファクトリへの参照があるため、ファクトリはメモリに残ります。外部クラスにデータがないことも問題ではありません。これは、想像以上に頻繁に発生します。作成者は必要ありません。作成者だけが必要です。そのため、一時的に作成しますが、無期限に使用します。

コンストラクターを少し変更するとどうなるか想像してみてください。

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//WOW! I don't even have to create a Factory... 
        // This is SOOOO much prettier....
            myHoles[i] = new LeakFactory().createLeak();
        }
    }
}

さて、これらの新しいLeakFactoriesのすべてがリークされました。それについてどう思いますか?これらは、内部クラスが任意のタイプの外部クラスよりも長生きする方法の2つの非常に一般的な例です。その外側のクラスがアクティビティだったとしたら、それがどれほど悪化したか想像してみてください。

結論

これらは、これらのオブジェクトを不適切に使用することの主に既知の危険性を示しています。一般的に、この投稿はあなたの質問のほとんどをカバーしているはずですが、それは非常に長い投稿だったと思いますので、説明が必要な場合は、私に知らせてください。上記の方法に従えば、漏れの心配はほとんどありません。

于 2012-06-10T12:27:43.680 に答える
2

1つの投稿に2つの質問があります。

  1. 内部クラスをとして宣言せずに使用することは決して安全ではありませんstatic。Androidだけでなく、Javaの世界全体に適用できます。

ここでより詳細な説明

リスト(または、タブ+ページレイアウト()、ドロップダウン、およびstatic class InnerAdapterAsyncTaskサブクラス)を使用しているかどうかを確認するための一般的な内部クラスの例class InnerAdapterListViewRecyclerViewViewPager

  1. Handler + Runnable、AsyncTask、RxJavaなどを使用するかどうかは関係ありません。アクティビティ/フラグメント/ビューが破棄された後に操作が完了すると、アクティビティ/フラグメント/ビューオブジェクトのルージュ参照が作成されます(これらは巨大)ガベージコレクションできない(解放できないメモリスロット)

したがって、これらの長時間実行されるタスクをonDestroy()それ以前にキャンセルするようにしてください。そうすれば、メモリリークは発生しません。

于 2020-09-01T13:12:43.090 に答える
2

内部(匿名)クラスのライフサイクルが外部クラスと短いかまったく同じであることがわかっている限り、それらを安全に使用できます。

たとえば、setOnClickListener()Androidボタンには、ほとんどの場合、匿名クラスを使用します。これは、その参照を保持するオブジェクトが他になく、リスナー内で長いプロセスを実行しないためです。外側のクラスが破棄されると、内側のクラスも破棄される可能性があります。

ここに画像の説明を入力してください

LocationCallbackメモリリークの問題がある別の例は、ブローの例としてのAndroidです。

public class MainActivity extends AppCompatActivity {

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

  private void initLocationLibraries() {
    mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
    mSettingsClient = LocationServices.getSettingsClient(this);

    mLocationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);
            // location is received
            mCurrentLocation = locationResult.getLastLocation();
            updateLocationUI();
        }
    };

    mRequestingLocationUpdates = false;

    mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
    builder.addLocationRequest(mLocationRequest);
    mLocationSettingsRequest = builder.build();
  }
}

これで、ActivityがLocationCallbackの参照を保持するだけでなく、AndroidGMSサービスもそれを保持します。GMSサービスのライフサイクルはアクティビティよりもはるかに長くなります。アクティビティへのメモリリークが発生します。 ここに画像の説明を入力してください

詳細については、こちらをご覧ください。

于 2020-09-21T12:48:45.260 に答える