大家新年好,由于工作繁忙原因,有好一段時間沒有更新博文了(當然Github是一直都有更新的),趁着年底有點放假時間,我覺得抽空更新下部落格,總結一下工作中最常見記憶體洩漏問題,也是自己之前踩過的坑,為了讓大家少走彎路,系統全面總結一下記憶體洩漏問題分析原因及尋找解決方案。
概念
首先要了解什麼叫做記憶體洩漏(Memory Leak),有很多人把記憶體洩漏和記憶體溢出(Out of Memory)混為一談,其實他們兩者并不相同。當然有些人可能“記憶體洩漏”打成“記憶體洩露”,個人認為前者更加準确地描述它本身的含義。
大家都知道Android中的應用的最大使用記憶體是有限的,而不是可以占完所有的手機剩餘記憶體(具體又有Java Heap和Native Heap差別,而且又有版本的差別,是以這裡不做詳細叙述,可以查閱其他文章看看),而Android采用的是記憶體自動回收機制,也就是對于不使用的記憶體,Android會自動回收等待複用。由于這兩個原因:如果配置設定記憶體超出了應用可以使用的最大記憶體,就會發生記憶體溢出;如果一個應該被回收的對象沒有被Android自動回收機制回收,這種情況稱為記憶體洩漏。
簡單總結為:
- 記憶體洩漏(Memory Leak):應該被回收的對象沒有被回收
- 記憶體溢出(Out of Memory):配置設定記憶體超出了應用可以使用的最大記憶體
值得注意,兩者并沒有必然關系,記憶體洩漏不一定導緻記憶體溢出,記憶體溢出時不一定發生了記憶體洩漏。
常見原因及解決方案
了解了記憶體洩漏的概念後,我們知道,如果一個對象配置設定記憶體使用完畢後并沒有被記憶體回收機制自動回收,這就會導緻這個對象記憶體洩漏,下面來說說記憶體洩漏的幾種常見原因。
-
資源使用未關閉
這是最常見的記憶體洩漏原因之一,對于使用了檔案、圖檔、廣播、資料庫指針、流等資源的時候,應該在使用完成後關閉資源,否則可能導緻對象回收時無法回收記憶體。值得注意的是,調用關閉的位置不正确,也會導緻記憶體洩漏。
比如說,下面一段代碼:
初看這一段代碼并沒有什麼問題,但是如果具體操作過程中出現了IOException,這樣最後的try { InputStream inputStream = new FileInputStream("path"); //inputStream.read();具體操作 inputStream.close(); } catch (IOException e) { e.printStackTrace(); }
inputStream.close();
就不會執行,導緻流實際上并沒有關閉,進而導緻使用流的對象最終無法回收。
這種問題解決方法應該把close方法放到finally裡面,保證其一定執行:
InputStream inputStream = null; try { inputStream = new FileInputStream("path"); //inputStream.read();具體操作 } catch (IOException e) { e.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
-
單例對象成員為可回收對象
這種情況也是常見的記憶體洩漏原因,很多第三方SDK早期版本甚至最新版也存在這個問題。
最常見就是一個單例類持有Activity對象作為成員變量,例如下面代碼:
上面這段代碼,如果傳入的Context是Activity的話,就會導緻Activity銷毀時候由于被其他對象所持有,自動回收機制不會回收這個對象,進而導緻Activity記憶體洩漏。public class SampleClass { private Context context; //一堆單例代碼... public void init(Context context) { this.context = context; //... } }
-
外部類建立非靜态内部類的靜态執行個體
這個比較隐晦一些,也是經常會出現的錯誤。比如下面的代碼
public class SampleClass extends Activity{ private static TestInner testInner; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); testInner = new TestInner(); } class TestInner{} }
上述的TestInner生命周期比Activity長,Activity銷毀後,TestInner依然持有Activity的對象引用,導緻Activity的記憶體無法回收。
正确的方法應該把TestInner改成靜态内部類,當然也可以把它抽取出來作為單例類。
這裡要注意了,有人說自己不會犯這種錯,但是有時候使用系統的某些對象作為靜态成員也會出現這種情況:
要注意處理上述的兩種情況,實質上匿名内部類也是一種非靜态的内部類,比如Handler洩漏、AsyncTask以及線程洩漏等等,這兩種是非常常見的。這些和上述的類似,Handler這個典型的問題,Android Studio中Lint還可以檢查出來,如果一定要使用Activity對象的話,可以采用WeakReference來處理。以Handler内部類為例:最典型就是使用getDrawable獲得對象作為靜态成員變量,Drawable對象設定到View身上時候,會持有View對象,View對象則會持有Activity對象,而Drawable對象生命周期比Activity長,最後Activity銷毀後無法被自動回收。
private static class MyHandler extends Handler { //注意這個是内部類 private final WeakReference<Context> hContext; private MyHandler(Context hContext) { this.hContext = new WeakReference<>(hContext); } @Override public void handleMessage(Message msg) { //利用get()方法獲得執行個體,注意判斷非空 Context context = this.hContext.get(); if (context != null) { //... } } }
檢測記憶體洩漏
除了上述的原因以外還有其他可能導緻記憶體洩漏,而且我們不可能在實際開發中重新review一遍代碼來解決記憶體洩漏,我們需要一些工具來幫助我們解決記憶體洩漏。
這裡介紹一款我在實際項目中使用的工具Leakcanary,這個工具使用非常簡單:
- 首先添加Gradle依賴并且同步
- 在Application中注冊初始化,添加下面的代碼
LeakCanary.install(this);
- 然後就可以交給測試測試了,出現了問題會記錄成log,并且彈出對應問題,如下圖(借用下官方的圖)
這樣就可以定位到哪裡有記憶體洩漏了,更多使用方法可以參考官方的Github文檔,這裡就不一一闡述了。
總結
到此為止我們了解了什麼是記憶體洩漏,記憶體洩漏原因以及如何檢測記憶體洩漏。
實際上在實際開發項目中,大部分的錯誤都是上述的常見原因,是以開發的時候要注意:
- 關閉使用了的資源
- 不要随便傳遞Activity,盡量使用Application Context或者使用弱引用
- 謹慎使用statiic成員,使用的時候要注意生命周期問題
做到這些記憶體洩漏就不再是令人頭疼的問題。
聲明
原創文章,歡迎轉載,請保留出處。
有任何錯誤、疑問或者建議,歡迎指出。
我的郵箱:[email protected]