在android開發中,記憶體洩漏是比較常見的問題,有過一些android程式設計經驗的童鞋應該都遇到過,但為什麼會出現記憶體洩漏呢?記憶體洩漏又有什麼影響呢?
在android程式開發中,當一個物件已經不需要再使用了,本該被回收時,而另外一個正在使用的物件持有它的引用從而導致它不能被回收,這就導致本該被回收的物件不能被回收而停留在堆記憶體中,記憶體洩漏就產生了。
記憶體洩漏有何影響?它是造成應用程式OOM的主要原因之一。由於android系統為每個應用程式分配的記憶體有限,當一個應用程式中產生的記憶體洩漏比較多時,就難免會導致應用程式所需的記憶體超過這個系統分配的記憶體限額,這就造成了記憶體溢位而導致應用Crash。
了解了記憶體洩漏的原因及影響後,我們需要做的就是掌握常見的記憶體洩漏,並在以後的android程式開發中,盡量避免它。
Java 中的記憶體分配
靜態儲存區:編譯時就分配好,在程式整個運行期間都存在。它主要存放靜態資料和常數;
棧區:當方法執行時,會在堆疊區記憶體中建立方法體內部的局部變量,方法結束後自動釋放記憶體;
堆區:通常存放new 出來的對象。由 Java 垃圾回收器回收。
四種引用類型的介紹
強引用(StrongReference):JVM 寧可拋出OOM ,也不會讓GC 回收具有強引用的物件;
軟引用(SoftReference):只有在記憶體空間不足時,才只有在記憶體空間不足時,才會會被回的對象;
弱引用(WeakReference):在GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存;
虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以透過判斷引用佇列中是否存在該物件的虛引用,來了解這個物件是否將要被回收。可以用來作為GC回收Object的標誌。
我們常說的記憶體洩漏是指new出來的Object無法被GC回收,即為強引用:
內存洩漏發生時的主要表現為內存抖動,可用內存慢慢變少:
Andriod 中分析記憶體洩漏的工具MAT
MAT(Memory Analyzer Tools)是一個Eclipse 插件,它是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們找到記憶體洩漏和減少記憶體消耗。
QQ 和Qzone內存洩漏如何監控
QQ和Qzone 的內存洩漏採用SNGAPM解決方案,SNGAPM是一個性能監控、分析的統一解決方案,它從終端收集性能信息,上報到一個後台,後台將監控類資訊聚合展示為圖表,將分析類別資訊進行分析並提單,通知開發者;
SNGAPM由App(MagnifierApp)和web server(MagnifierServer)兩部分組成;
MagnifierApp在自動記憶體洩漏檢測中是一個銜接偵測偵測元件(LeakInspector)和自動化雲端分析(MagnifierCloud)的中間性平台,它從LeakInspector的記憶體dump自動化上傳MagnifierServer;
MagnifierServer後台會定時提交分析任務到MagnifierCloud上,同時以bug單形式通知開發者。
常見的記憶體洩漏案例
case 1. 單例造成的記憶體外洩
單例的靜態特性導致其生命週期同應用一樣長。
解決方案:
將該屬性的引用方式改為弱引用;
如果傳入Context,使用ApplicationContext;
example:洩漏程式碼片段
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; }
,Solution:使用WeakRefered內部類別
在Java中,非靜態內部類別和匿名類別都會潛在的引用它們所屬的外部類別,但是,靜態內部類別卻不會。如果這個非靜態內部類別實例做了一些耗時的操作,就會造成外圍物件不會被回收,導致記憶體洩漏。
解決方案:
將內部類別變成靜態內部類別;
如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用;
在業務允許的情況下,當Activity執行onDestory時,結束這些耗時任務;
example:
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); }
Solution:
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(); } }
在Android應用程式中通常可以使用兩種和Context Application。當類別或方法需要Context物件的時候常見的做法是使用第一個作為Context參數。這樣就意味著View物件對整個Activity保持引用,因此也就保持對Activty的所有的引用。
假設一個場景,當應用程式有個比較大的Bitmap類型的圖片,每次旋轉是都重新加載圖片所花費的時間較多。為了提高螢幕旋轉是Activity的創建速度,最簡單的方法時將這個Bitmap物件使用Static修飾。 當一個Drawable綁定在View上,其實這個View物件就會成為這份Drawable的一個Callback成員變數。而靜態變數的生命週期要長於Activity。導致了當旋轉螢幕時,Activity無法被回收,而造成記憶體洩漏。
解決方案:
使用ApplicationContext代替ActivityContext,因為ApplicationContext會隨著應用程序的存在而存在,而不依賴於activity的生命週期;
對Context的引用不要超過它本身的生命週期,慎重的對Context使用“static”關鍵字。 Context裡如果有線程,一定要在onDestroy()及時停掉。
example:
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(); } }
Solution:
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); }
case 4. Handler引起的記憶體洩漏
當Handler中有延遲的的任務或是等待執行的任務隊列過長,由於訊息持有對Handler的引用,而Handler又持有對其外部類別的潛在引用,這條引用關係會一直保持到訊息得到處理,而導致了Activity無法被垃圾回收器回收,而導致了內存洩漏。
解決方案:
可以把Handler類別放在單獨的類別檔案中,或者使用靜態內部類別便可以避免洩漏;
如果想在Handler內部去調用所在的Activity,那麼可以在handler內部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關係的目的。
Solution:
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); }
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)。