前言
內存管理的目的就是讓我們在開發過程中有效避免我們的應用程序出現內存泄露的問題。內存泄露相信大家都不陌生,我們可以這樣理解:「沒有用的對象無法回收的現象就是內存泄露」。
如果程序發生了內存泄露,則會帶來以下這些問題
應用可用的內存減少,增加了堆內存的壓力
降低了應用的性能,比如會觸發更頻繁的 GC
嚴重的時候可能會導致內存溢出錯誤,即 OOM Error
OOM 發生在,當我們嘗試進行創建對象,但是堆內存無法通過 GC 釋放足夠的空間,堆內存也無法再繼續增長,從而完成對象創建請求的時候,OOM 發生很有可能是內存泄露導致的,但并非所有的 OOM 都是由內存泄露引起的,內存泄露也并不一定引起 OOM。
一、基礎準備
如果真的想比較清楚的了解內存泄露的話,對于 Java 的內存管理以及引用類型有一個清晰的認識是必不可少的。
理解 Java 的內存管理能讓我們更深一層地了解 Java 虛擬機是怎樣使用內存的,一旦出現內存泄露,我們也能更加從容地排查問題。
了解 Java 的引用類型,能讓我們更加理解內存泄露出現的原因,以及常見的解決方法。
具體的內容,可以看下這篇文章你真的懂 Java 的內存管理和引用類型嗎?
二、Android 中內存泄露的常見場景 & 解決方案
1、單例造成的內存泄露
單例模式是非常常用的設計模式,使用單例模式的類,只會產生一個對象,這個對象看起來像是一直占用著內存,但這并不意味著就是浪費了內存,內存本來就是拿來裝東西的,只要這個對象一直都被高效的利用就不能叫做泄露。
但是過多的單例會讓內存占用過多,而且單例模式由于其靜態特性,其生命周期 = 應用程序的生命周期,不正確地使用單例模式也會造成內存泄露。
舉個例子:
publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context;? ? }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){? ? ? ? ? ? sInstance =newSingleInstanceTest(context);? ? ? ? }returnsInstance;? ? }}
上面是一個比較簡單的單例模式用法,需要外部傳入一個 Context 來獲取該類的實例,如果此時傳入的 Context 是 Activity 的話,此時單例就有持有該 Activity 的強引用(直到整個應用生命周期結束)。這樣的話,即使該 Activity 退出,該 Activity 的內存也不會被回收,這樣就造成了內存泄露,特別是一些比較大的 Activity,甚至還會導致 OOM(Out Of Memory)。
解決方法:單例模式引用的對象的生命周期 = 應用生命周期
publicclassSingleInstanceTest{privatestaticSingleInstanceTest sInstance;privateContext mContext;privateSingleInstanceTest(Context context){this.mContext = context.getApplicationContext();? ? }publicstaticSingleInstanceTestnewInstance(Context context){if(sInstance ==null){? ? ? ? ? ? sInstance =newSingleInstanceTest(context);? ? ? ? }returnsInstance;? ? }}
可以看到在 SingleInstanceTest 的構造函數中,將 context.getApplicationContext() 賦值給 mContext,此時單例引用的對象是 Application,而 Application 的生命周期本來就跟應用程序是一樣的,也就不存在內存泄露。
這里再拓展一點,很多時候我們在需要用到 Activity 或者 Context 的地方,會直接將 Activity 的實例作為參數傳給對應的類,就像這樣:
publicclassSample{privateContext mContext;publicSample(Context context){this.mContext = context;? ? }publicContextgetContext(){returnmContext;? ? }}// 外部調用Sample sample =newSample(MainActivity.this);
這種情況如果不注意的話,很容易就會造成內存泄露,比較好的寫法是使用弱引用(WeakReference)來進行改進。
publicclassSample{privateWeakReference mWeakReference;publicSample(Context context){this.mWeakReference =newWeakReference<>(context);? ? }publicContextgetContext(){if(mWeakReference.get() !=null){returnmWeakReference.get();? ? ? ? }returnnull;? ? }}// 外部調用Sample sample =newSample(MainActivity.this);
被弱引用關聯的對象只能存活到下一次垃圾回收之前,也就是說即使 Sample 持有 Activity 的引用,但由于 GC 會幫我們回收相關的引用,被銷毀的 Activity 也會被回收內存,這樣我們就不用擔心會發生內存泄露了。
2、非靜態內部類 / 匿名類
我們先來看看非靜態內部類(non static inner class)和 靜態內部類(static inner class)之間的區別。
class 對比static inner classnon static inner class
與外部 class 引用關系如果沒有傳入參數,就沒有引用關系自動獲得強引用
被調用時需要外部實例不需要需要
能否調用外部 class 中的變量和方法不能能
生命周期自主的生命周期依賴于外部類,甚至比外部類更長
可以看到非靜態內部類自動獲得外部類的強引用,而且它的生命周期甚至比外部類更長,這便埋下了內存泄露的隱患。如果一個 Activity 的非靜態內部類的生命周期比 Activity 更長,那么 Activity 的內存便無法被回收,也就是發生了內存泄露,而且還有可能發生難以預防的空指針問題。
舉個例子:
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);newMyAscnyTask().execute();? ? }classMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{? ? ? ? ? ? ? ? Thread.sleep(5000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }return"";? ? ? ? }? ? }}
可以看到我們在 Activity 中繼承 AsyncTask 自定義了一個非靜態內部類,在 doInbackground() 方法中做了耗時的操作,然后在 onCreate() 中啟動 MyAsyncTask。如果在耗時操作結束之前,Activity 被銷毀了,這時候因為 MyAsyncTask 持有 Activity 的強引用,便會導致 Activity 的內存無法被回收,這時候便會產生內存泄露。
解決方法:將 MyAsyncTask 變成靜態內部類
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);newMyAscnyTask().execute();? ? }staticclassMyAscnyTaskextendsAsyncTask{@OverrideprotectedStringdoInBackground(Void... params){try{? ? ? ? ? ? ? ? Thread.sleep(50000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }return"";? ? ? ? }? ? }}
這時候 MyAsyncTask 不再持有 Activity 的強引用,即使 AsyncTask 的耗時操作還在繼續,Activity 的內存也能順利地被回收。
匿名類和非靜態內部類最大的共同點就是都持有外部類的引用,因此,匿名類造成內存泄露的原因也跟靜態內部類基本是一樣的,下面舉個幾個比較常見的例子:
publicclassMainActivityextendsAppCompatActivity{privateHandler mHandler =newHandler(){@OverridepublicvoidhandleMessage(Message msg){super.handleMessage(msg);? ? ? ? }? ? };@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? ? ? setContentView(R.layout.activity_main);// ① 匿名線程持有 Activity 的引用,進行耗時操作newThread(newRunnable() {@Overridepublicvoidrun(){try{? ? ? ? ? ? ? ? ? ? Thread.sleep(50000);? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }).start();// ② 使用匿名 Handler 發送耗時消息Message message = Message.obtain();? ? ? ? mHandler.sendMessageDelayed(message,60000);? ? }
上面舉出了兩個比較常見的例子
new 出一個匿名的 Thread,進行耗時的操作,如果 MainActivity 被銷毀而 Thread 中的耗時操作沒有結束的話,便會產生內存泄露
new 出一個匿名的 Handler,這里我采用了 sendMessageDelayed() 方法來發送消息,這時如果 MainActivity 被銷毀,而 Handler 里面的消息還沒發送完畢的話,Activity 的內存也不會被回收
解決方法:
繼承 Thread? 實現靜態內部類
繼承 Handler 實現靜態內部類,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);
3、集合類
集合類添加元素后,仍引用著集合元素對象,導致該集合中的元素對象無法被回收,從而導致內存泄露,舉個例子:
staticList objectList =newArrayList<>();for(int i =0; i <10; i++) {Objectobj =newObject();? ? ? objectList.add(obj);? ? ? obj =null;? ? }
在這個例子中,循環多次將 new 出來的對象放入一個靜態的集合中,因為靜態變量的生命周期和應用程序一致,而且他們所引用的對象 Object 也不能釋放,這樣便造成了內存泄露。
解決方法:在集合元素使用之后從集合中刪除,等所有元素都使用完之后,將集合置空。
objectList.clear();? ? objectList =null;
4、其他的情況
除了上述 3 種常見情況外,還有其他的一些情況
1、需要手動關閉的對象沒有關閉
網絡、文件等流忘記關閉
手動注冊廣播時,退出時忘記 unregisterReceiver()
Service 執行完后忘記 stopSelf()
EventBus 等觀察者模式的框架忘記手動解除注冊
2、static 關鍵字修飾的成員變量
3、ListView 的 Item 泄露
三、利用工具進行內存泄露的排查
除了必須了解常見的內存泄露場景以及相應的解決方法之外,掌握一些好用的工具,能讓我們更有效率地解決內存泄露的問題。
1、Android Lint
Lint 是 Android Studio 提供的代碼掃描分析工具,它可以幫助我們發現代碼機構 / 質量問題,同時提供一些解決方案,檢測內存泄露當然也不在話下,使用也是非常的簡單,可以參考下這篇文章:Android 性能優化:使用 Lint 優化代碼、去除多余資源
LeakCanary 是 Square 公司開源的「Android 和 Java 的內存泄漏檢測庫」,Square 出品,必屬精品,功能很強大,使用也很簡單。建議直接看 Github 上的說明:leakcanary,也可以參考這篇文章:Android內存優化(六)LeakCanary使用詳解
作者:developerHaoz
鏈接:http://www.lxweimin.com/p/65f914e6a2f8
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。