Android 開発では、メモリ リークは比較的一般的な問題です。Android プログラミングの経験がある子供なら一度は遭遇したことがあると思いますが、なぜメモリ リークが発生するのでしょうか。メモリリークの影響は何ですか?
Android プログラム開発では、オブジェクトが不要になりリサイクルする必要がある場合、使用中の別のオブジェクトがそのオブジェクトへの参照を保持しているため、そのオブジェクトがリサイクルされなくなり、その結果オブジェクトがリサイクルされなくなります。ヒープメモリに滞留し、メモリリークが発生します。
メモリリークの影響は何ですか?これはアプリケーション OOM の主な原因の 1 つです。 Android システムが各アプリケーションに割り当てるメモリには限界があるため、アプリケーションでメモリ リークが多発すると、必然的にアプリケーションが必要とするメモリがシステムによって割り当てられるメモリ制限を超え、メモリ オーバーフローが発生し、アプリケーションがクラッシュする原因となります。
メモリ リークの原因と影響を理解した後、私たちがしなければならないことは、一般的なメモリ リークをマスターし、将来の Android プログラム開発でそれらを回避するように努めることです。
Java におけるメモリ割り当て
静的ストレージ領域: コンパイル中に割り当てられ、プログラムの実行中に存在します。主に静的データと定数を保存します。
スタック領域: メソッドが実行されると、メソッド本体内のローカル変数がスタック領域メモリに作成され、メソッド終了後にメモリが自動的に解放されます。通常、新しいオブジェクトを格納します。 Java ガベージ コレクターによってリサイクルされます。
4 つの参照タイプの紹介
強参照: JVM は、GC に強参照を持つオブジェクトをリサイクルさせるよりもむしろ OOM をスローします。弱い参照 (WeakReference): GC 中に、弱い参照のみを持つオブジェクトが見つかると、現在のメモリ領域が十分であるかどうかに関係なく、そのメモリがリサイクルされます。
ファントム参照 (PhantomReference): GC によってリサイクルできます。ガベージ コレクターがオブジェクトをリサイクルする準備をしているときに、オブジェクトにまだ仮想参照があることが判明すると、オブジェクトのメモリをリサイクルする前に、それに関連付けられた参照キューに仮想参照を追加します。プログラムは、参照キューにオブジェクトへの仮想参照があるかどうかを判断することで、オブジェクトがリサイクルされるかどうかを知ることができます。 GC がオブジェクトをリサイクルするためのフラグとして使用できます。
私たちがよく話すメモリ リークは、新しいオブジェクトが GC によってリサイクルできないこと、つまり、強い参照であることを意味します:
メモリ リークが発生したときの主な症状は、メモリ ジッターと利用可能なメモリです。ゆっくりと減少します:Andriod のメモリ リーク分析ツール MATMAT (Memory Analyzer Tools) は、メモリ リークの発見に役立つ高速で機能豊富な JAVA ヒープ分析ツールです。そしてメモリ消費量を削減します。
QQ と Qzone のメモリ リークを監視する方法
QQ と Qzone のメモリ リークは、SNGAPM ソリューションを採用しており、パフォーマンスの監視と分析を行うための統合ソリューションです。端末からパフォーマンス情報を収集し、バックグラウンドに報告します。カテゴリを監視します。情報の集約はグラフとして表示され、分析情報が分析されて課金され、開発者に通知されます。
SNGAPMはアプリ(MagnifierApp)とWebサーバー(MagnifierServer)で構成されます。自動メモリ リーク検出での検出 コンポーネント (LeakInspector) と自動クラウド分析 (MagnifierCloud) の間の中間プラットフォームであり、LeakInspector のメモリ ダンプを MagnifierServer に自動的にアップロードします。分析が完了すると、データは拡大鏡 Web に更新され、バグ チケットの形式で開発者に通知されます。
一般的なメモリ リークのケース
ケース 1. シングルトンによって引き起こされるメモリ リーク
シングルトンの静的な特性により、そのライフ サイクルはアプリケーションと同じ長さになります。
解決策:
この属性の参照メソッドを弱い参照に変更します。
Context で渡す場合は、ApplicationContext を使用します。
例: リークされたコード スニペット
private static ScrollHelper mInstance; private ScrollHelper() { } public static ScrollHelper getInstance() { if (mInstance == null) { synchronized (ScrollHelper.class) { if (mInstance == null) { mInstance = new ScrollHelper(); } } } return mInstance; } /** * 被点击的view */ private View mScrolledView = null; public void setScrolledView(View scrolledView) { mScrolledView = scrolledView; }
解決策: WeakReference
private static ScrollHelper mInstance; private ScrollHelper() { } public static ScrollHelper getInstance() { if (mInstance == null) { synchronized (ScrollHelper.class) { if (mInstance == null) { mInstance = new ScrollHelper(); } } } return mInstance; } /** * 被点击的view */ private WeakReference<View> mScrolledViewWeakRef = null; public void setScrolledView(View scrolledView) { mScrolledViewWeakRef = new WeakReference<View>(scrolledView); }
case 2 を使用します。匿名内部クラス
Java では、非静的内部クラスと匿名クラスは、それらが属する外部クラスを参照する可能性がありますが、静的内部クラスは参照しません。この非静的内部クラス インスタンスが時間のかかる操作を実行すると、周囲のオブジェクトがリサイクルされず、メモリ リークが発生します。
解決策:
内部クラスを静的内部クラスに変更します。
アクティビティ内に属性への参照メソッドがある場合は、その属性の参照メソッドを弱い参照に変更します。
ビジネスで許可されている場合は、アクティビティ onDestory が実行されると、これらの時間のかかるタスクを終了します。
例:
public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //这儿发生泄漏 public void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
解決策:
public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //加上static,变成静态匿名内部类 public static void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
ケース 3. アクティビティ コンテキストの誤った使用
通常、使用できる Context オブジェクトには 2 つのタイプがあります。 Android アプリケーションで使用されます: アクティビティとアプリケーション。クラスまたはメソッドで Context オブジェクトが必要な場合、最初のオブジェクトを Context パラメータとして使用するのが一般的です。これは、View オブジェクトがアクティビティ全体への参照を保持し、したがってアクティビティへのすべての参照を保持することを意味します。
アプリケーションに比較的大きなビットマップ タイプの画像があり、回転するたびに画像を再読み込みするのに時間がかかるシナリオがあるとします。画面の回転やアクティビティ作成の速度を向上させるための最も簡単な方法は、この Bitmap オブジェクトに Static 装飾を使用することです。 Drawable が View にバインドされると、View オブジェクトは実際には Drawable の Callback メンバー変数になります。静的変数のライフサイクルはアクティビティのライフサイクルよりも長くなります。その結果、画面を回転するとアクティビティをリサイクルできなくなり、メモリ リークが発生します。
解決策:
ActivityContextの代わりにApplicationContextを使用します。ApplicationContextはアプリケーションの存在とともに存在し、アクティビティのライフサイクルに依存しないためです。
Contextへの参照は、それ自体のライフサイクルを超えないよう注意してください。 「静的」キーワードを使用します。 Context 内にスレッドがある場合は、時間内に onDestroy() でスレッドを停止する必要があります。
例:
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
解決策:
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getApplicationContext().getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
ケース4. ハンドラーによって引き起こされるメモリリーク
ハンドラー内に遅延したタスクがあるか、実行を待っているタスクのキューが長すぎる場合、メッセージにへの参照が保持されているため、この参照関係は、メッセージが処理されるまで残り、ガベージ コレクターによってアクティビティがリサイクルできなくなり、メモリ リークが発生します。
解決策:
ハンドラークラスを別のクラスファイルに置くか、静的内部クラスを使用して漏洩を避けることができます
ハンドラー内でアクティビティを呼び出したい場合は、ハンドラーメソッド内で弱い参照を使用できます。ハンドラーとアクティビティ間の参照関係を切断する目的を達成するには、Static + WeakReference メソッドを使用します。
解決策:
@Override protected void doOnDestroy() { super.doOnDestroy(); if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } mHandler = null; mRenderCallback = null; }
case 5. 登録リスナーの漏洩
系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。
解决方案:
使用ApplicationContext代替ActivityContext;
在Activity执行onDestory时,调用反注册;
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
Solution:
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
下面是容易造成内存泄漏的系统服务:
InputMethodManager imm = (InputMethodManager) context.getApplicationContext().get SystemService(Context.INPUT_METHOD_SERVICE);
Solution:
protected void onDetachedFromWindow() { if (this.mActionShell != null) { this.mActionShell.setOnClickListener((OnAreaClickListener)null); } if (this.mButtonShell != null) { this.mButtonShell.setOnClickListener((OnAreaClickListener)null); } if (this.mCountShell != this.mCountShell) { this.mCountShell.setOnClickListener((OnAreaClickListener)null); } super.onDetachedFromWindow(); }
case 6. Cursor,Stream没有close,View没有recyle
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。
Solution:
调用onRecycled()
@Override public void onRecycled() { reset(); mSinglePicArea.onRecycled(); }
在View中调用reset()
public void reset() { if (mHasRecyled) { return; } ... SubAreaShell.recycle(mActionBtnShell); mActionBtnShell = null; ... mIsDoingAvatartRedPocketAnim = false; if (mAvatarArea != null) { mAvatarArea.reset(); } if (mNickNameArea != null) { mNickNameArea.reset(); } }
case 7. 集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
解决方案:
在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。
Solution
private List<EmotionPanelInfo> data; public void onDestory() { if (data != null) { data.clear(); data = null; } }
case 8. WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
解决方案:
为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
case 9. 构造Adapter时,没有使用缓存的ConvertView
初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。
当向上滚动ListView时,原先位于最上面的List Item的View对象会被回收,然后被用来构造新出现的最下面的List Item。
这个构造过程就是由getView()方法完成的,getView()的第二个形参View ConvertView就是被缓存起来的List Item的View对象(初始化时缓存中没有View对象则ConvertView是null)。