編輯:關於Android編程
本篇博客主要是記錄一下Android內存洩露相關的問題。網上有很多介紹內存洩露的文章,自己摘取了一些比較有價值的內容,同時增加了一些自己的理解。
在分析內存洩露前,我們必須先對內存洩露的定義有所了解。
簡單來講,Android對內存洩露的定義,與Java中的定義基本一致,即:正常情況下,當一個對象已經不需要再被使用時,它占用的內存就能夠被系統回收。如果一個本該被回收的無用對象,由於被其它有效對象引用,使得對應的內存不能被系統回收,就稱之為內存洩漏。
我們知道Android系統為每個應用分配的內存有限,當一個應用中產生的內存洩漏比較多時,就難免會導致應用所需要的內存超過系統分配的極限,於是就造成應用出現了OOM錯誤。因此,每個開發人員有必要對內存洩露的原理、出問題的場景及分析工具有一定的了解。
一、Java內存分配策略
Java 程序運行時的內存分配策略有三種,分別是靜態分配、棧式分配和堆式分配。
對應的,三種分配策略使用的內存空間分別是靜態存儲區、棧區和堆區。
其中:
1、靜態存儲區:主要存放靜態數據和常量。
這部分內存在程序編譯時就已經分配好,並且在程序整個運行期間都存在。
2、棧區 :當方法被執行時,方法體內的局部變量都在棧上創建,並在方法執行結束時,自動釋放其持有的內存。
由於棧內存分配相關的運算,內置於處理器的指令集中,因此執行效率很高,不過其分配的內存容量有限。
3、堆區 : 主要用於存儲動態分配的對象。
這部分內存在不使用時,將會由 Java 的垃圾回收器來負責回收。
舉例來說:
public class Example { //靜態存儲區 static int e1 = 0; //堆區 int e2 = 1; Example e3 = new Example(); void method() { //棧區 int e4 = 2; Example e5 = new Example(); } }
如上面代碼所示,e1作為靜態變量,是與Example這個類關聯的,將被分配到靜態存儲區。
e2和e3均是一個具體對象的成員變量,由於對象必須被動態創建出來,因此e2和e3均將被分配到堆區。
即類中定義的非靜態成員變量全部存儲於堆中,包括基本數據類型、引用和引用指向的對象實體。
對於一個具體的方法來說,如代碼中的method,當方法執行時,其內部的臨時變量e4、e5均分配在棧區;
當方法執行完畢後,e4和e5的內存均會被自動釋放。
這裡需要注意的是,e5分配在棧區,但其指向的對象是分配在堆區的。
由此可以看出,局部變量的基本數據類型和引用存儲於棧區,引用指向的對象實體存儲於堆區。
二、Java內存釋放策略
1、原理
Java的內存釋放策略是由GC(Garbage Collection)機制決定和執行的,主要針對的是堆區內存。
GC為了能夠准確及時地釋放對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。
關於GC釋放內存的原理,參考了一些資料,個人覺得一種比較好的理解方式是:
將堆區對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。
將每個線程對象作為一個圖的起始頂點,從起始頂點可達的對象都是有效對象,不會被GC回收;
如果某個對象從起始頂點出發不可達,那麼這個對象就可以被認為是無效的,可以被 GC 回收。
對於程序運行的每一個時刻,都可以用一個有向圖表示JVM的內存分配情況。
舉例來說,對於下面的代碼:
public class Solution { public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Test(); o2 = o1; //運行到此處,看一下對應的GC有向圖 ................. } } public class Test { private Object o3; Test() { o3 = new Object(); } }
程序運行到注釋行時,對應的GC有向圖大致如下:
main函數所在進程為有向圖的根節點。
當函數運行到注釋行時,沒有從根節點到Test對象和Obj 3的路徑,
因此Test和Obj 3對象均可以被GC回收。
注意對象能否被回收的依據是,是否存在從根節點到該對象的路徑,
因此雖然Obj 3被引用了,但依然會被回收。
由上述例子可以看出,Java的GC機制使用有向圖的方式進行內存管理,可以消除引用循環的問題。
例如有三個對象相互引用,只要它們對根進程而言是不可達的,那麼GC也可以對它們進行回收。
GC的這種內存管理方式的優點是精度很高,但是效率較低。
另外一種常用的內存管理技術是使用計數器,它與有向圖相比精度較低(很難處理循環引用的問題),但執行效率較高。
最後需要提一點的是,對於程序員來說,GC對應的操作基本是透明的。
雖然可以主動調用幾個GC相關的函數,例如System.gc()等,但是根據Java語言規范定義,
這些函數並不保證GC線程一定會進行實際的工作。
這是因為不同的JVM可能使用不同的算法管理GC,例如:
有的JVM檢測到內存使用量到達門限時,才調度GC線程進行工作;
有的JVM是定時調度GC線程進行工作等。
2、Java內存洩露的例子
結合Java的GC機制,我們知道了,對於Java中分配在堆內存的對象而言:
如果某個對象是可達的,即在有向圖中,存在從根節點到這些對象的路徑;
同時這個對象是無用的,即程序以後不會再使用這個對象;
那麼該對象就被判定為Java中的內存洩漏。
關於Java內存洩露的例子,可以參考下面的代碼:
public class Example { private static ArrayList
類似上面的例子,如果集合類是全局性的變量,同時沒有相應的刪除機制,則很可能導致集合所占用的內存只增不減。
三、Android中的內存洩露舉例
接下來我們看看Android中一些內存洩露的例子。
1、靜態單例對象引入的洩露
靜態對象生命周期的長度與整個應用一致,
如果靜態對象持有了一個生命周期較短的對象,
例如Activity等,那麼就會導致內存洩露。
這種錯誤經常出現在使用單例對象的場景中,例如:
public class SingleInstance { private final static Object LOCK = new Object(); //單例模式需要靜態對象 private static SingleInstance singleInstance; //靜態對象持有Context就可能導致內存洩露 private Context mContext; public static SingleInstance getInstance(Context context) { synchronized (LOCK) { if (singleInstance == null) { return new SingleInstance(context); } return singleInstance; } } private SingleInstance(Context context) { mContext = context; } }
如上面的代碼所示:
如果獲取單例模式時傳的是Application的Context,
由於Application的生命周期就是整個應用的生命周期,
即Context與靜態對象的生命周期一致,沒有任何問題;
如果傳入的是 Activity 等的 Context,那麼當這個 Context 所對應的 Activity 退出時,
由於該 Context 的引用被靜態單例對象所持有,而單例對象將持續到應用結束,
於是即使當前 Activity 退出,它的內存也不會被回收,就造成了內存洩漏。
由此可以看出,在Android中盡量不要讓靜態對象持有Context。
如果靜態對象一定要持有Context,就讓它持有Application Context,
即上面代碼需要更改為:
public class SingleInstance { private final static Object LOCK = new Object(); //Android Studio的靜態代碼檢查,會提示不要將Context類置於靜態引用中,可能會導致內存洩露 private static SingleInstance singleInstance; private Context mContext; public static SingleInstance getInstance(Context context) { synchronized (LOCK) { if (singleInstance == null) { return new SingleInstance(context); } return singleInstance; } } private SingleInstance(Context context) { //若實在需要用,就獲取Application Context mContext = context.getApplicationContext(); } }
不過Application Context也不是萬能的,有些場景下Application Context是無法使用的,例如創建一個Dialog。
關於Application、Activity和Service三者的Context應用場景,自己也沒有總結過,
就截一下參考資料中的圖吧,有機會再深入研究一下:
圖中,NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。
2、非靜態內部類引入的洩露
如下代碼所示,在MainActivity的onCreate函數中,創建了一個非靜態內部類對象,
該對象被Activity中的一個靜態對像引用。
public class MainActivity extends AppCompatActivity { private static Resource mResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (mResource == null) { mResource = new Resource(); } } private class Resource { //........ } }
在上述代碼對應的場景中,由於非靜態內部類默認會持有外部類的引用,
而內部類的一個實例又被一個靜態對象持有,於是最終導致外部類Activity被一個靜態對象持有。、
正如前文提及的,由於靜態對象一直存在,於是Activity退出時,對應的內存也沒法被GC機制回收。
這種問題的解決方案就是,將非靜態內部類變為靜態內部類,或抽取成一個單獨的類。
3、自定義Handler引入的洩露
非靜態內部類引入內存洩漏的場景中,比較典型的就是自定義Handler引入的內存洩露:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //這種方式獲取Handler,實際上綁定的是sThreadLocal中的Looper Handler handler = new MayLeakHandler(); //延遲處理一個消息 //這裡匿名內部類其實也會持有外部類的引用 handler.postDelayed(new Runnable() { @Override public void run() { //....... } }, 6000); //Activity界面關閉,但其內存還是將被Handler對應的靜態線程持有 finish(); } //定義一個非靜態內部類 private class MayLeakHandler extends Handler { @Override public void handleMessage(Message msg) { //............. } } }
上面代碼產生內存洩露的原因是:
MayLeakHandler是一個非靜態內部類,持有對Activity的引用。
當Activity退出時,MayLeakHandler仍被有效對象引用,
於是Activity對應的內存也無法被釋放。
為了比較好的理解這個問題,我們看看Handler涉及到的一些源碼。
當創建Handler時,最終調用的源碼片段如下:
public Handler(Callback callback, boolean async) { ............ //調用sThreadLocal.get(),即從應用主線程獲取Looper mLooper = Looper.myLooper(); ............ //獲取主線程的MessageQueue mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; }
當Handler發送消息或Runnable對象時,最終將調用到如下源碼:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { //this指handle,即Msg將持有handler msg.target = this; .......... //Msg被加入到MessageQueue中,被MessageQueue持有 return queue.enqueueMessage(msg, uptimeMillis); }
根據上面的源碼可以看出,當定義一個非靜態內部類Handler時,
該Handler將被應用主線程的MessageQueue持有;
而Handler又持有了Activity的引用,於是即使Activity界面結束,
若Msg被有被處理掉,MessageQueue將一直持有Activity導致內存洩露。
對於上面那種使用Handler的方式,通常的修改方式是:
public class MainActivity extends AppCompatActivity { private MayLeakHandler mHandler = new MayLeakHandler(this); //Runnable也必須變成靜態的,否則也會內存洩漏 private static Runnable mRunnable = new Runnable() { @Override public void run() { //....... } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mHandler.postDelayed(mRunnable, 6000); finish(); } private static class MayLeakHandler extends Handler { //如果MayLeakHandler需要訪問Activity中的變量,就持有Activity的弱引用 //這樣垃圾回收時,就可以清除Activity的內存 private WeakReference mActivity; MayLeakHandler(Activity activity) { super(); mActivity = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { if (mActivity.get() != null) { //............. } } } @Override protected void onDestroy() { //最後根據需要需要,在Activity的onDestroy中清除mHandler處理的Message和Runnable //也可以調用其它接口單獨清理msg或runnable //對於這個例子,Runnable是靜態的,所以不需要 //但其它情況msg和runnable可能會持有Activity,所以需要清理 mHandler.removeCallbacksAndMessages(null); super.onDestroy(); } }
4、匿名內部類引入的洩露
匿名內部類也會持有外部類的引用,
因此與非靜態內部類一樣,也有可能導致內存洩露,
上面例子中初始定義的匿名Runnable,就會導致這個問題。
比較一般的場景是,如果匿名內部類被異步線程持有,
當異步線程與外部類的生命周期不一致時,就會導致內存洩露。
舉例如下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Runnable runnable = new Runnable() { @Override public void run() { //......... } }; //假設workThread是一個長期運行的HandlerThread WorkThread workThread = WorkThread.getWorkThread(); Handler handler = new Handler(workThread.getLooper()); handler.post(runnable); } }
上面的代碼中,定義了一個匿名內部類runnable,該runable對象持有對Activity的引用。
將該runnable對象遞交給WorkThread處理時,workThread就會持有該runable對象的引用,進而持有Activity對象。
如果workThread之前在進行某個耗時操作,那麼可能Activity結束時,runable對象還未執行完畢,
於是Activity對應的內存沒有及時釋放,導致內存洩露。
這種類型的問題的解決方法,可能只有將runable寫成靜態類或單獨抽取成一個獨立的類。
5、線程相關的內存洩漏
在界面中使用線程對象,稍不注意也會造成內存洩露:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); leakOne(); } private void leakOne() { new Thread() { @Override public void run() { while (true) { SystemClock.sleep(1000); } } }.start(); } }
很明顯,這個問題與匿名內部類引起的內存洩露一樣,由於Thread持有對Activity的引用,
同時Thread一直在運行,因此當Activity結束時,對應內存也不會被釋放,導致內存洩露的放生。
現在,我們修改一下代碼:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); leakTwo(); } private void leakTwo() { new LeakThread().start(); } private static class LeakThread extends Thread{ @Override public void run() { while (true) { SystemClock.sleep(1000); } } } }
可以看到,現在Thread變成了一個靜態內部類,不再持有對Activity的引用,
因此Activity退出後,對應的內存可以被釋放掉。
然而,這段代碼還是有問題。
Activity每次創建時,均會創建一個新的永不結束的Thread。
JVM會持有每個運行Thread的引用,因此Activity創建出的Thread將不會被釋放掉。
於是,不斷的關閉打開Activity,將導致JVM持有的Thread越來越多。
因此上述代碼需要修改為:
public class MainActivity extends AppCompatActivity { private LeakThread mLeakThread; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); leakThree(); } private void leakThree() { mLeakThread = new LeakThread(); mLeakThread.start(); } private static class LeakThread extends Thread{ private boolean mRunning = false; @Override public void run() { mRunning = true; while (mRunning) { SystemClock.sleep(1000); } } void close() { mRunning = false; } } @Override protected void onDestroy() { mLeakThread.close(); super.onDestroy(); } }
修改比較簡單,就是在Activity結束時,主動停止Thread。
6、資源未關閉造成的內存洩漏
解決最後這一類的內存洩露,主要就是要注意編程細節了。
使用BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源時,
應該在不再使用時,及時關閉或者注銷。
四、總結
對Android中的內存洩露就先總結到這裡了。
如何避免內存洩露,在上述例子中已經有對應的解決方案了,此處就不做贅述。
總之,代碼看到多寫的多,自然會養成良好的編程習慣,死記硬背一些規則,效率肯定比較低。
最後提一下檢測內存洩露的工具,MAT有很多的資料,此處不做說明了。
本文實例為大家分享了Android動態GridView控件使用的具體代碼,供大家參考,具體內容如下MainActivity.java代碼:package siso.hah
MVP模式是MVC模式在Android上的一種變體,要介紹MVP就得先介紹MVC。在MVC模式中,Activity應該屬於view這一層,而在實際開發中,它既承擔了vi
ProGuard能夠對Java類中的代碼進行壓縮(Shrink),優化(Optimize),混淆(Obfuscate),預檢(Preveirfy)。 1. 壓縮(Sh
不管是開發android應用程序還是java應用程序,異步任務都是經常用到的,尤其是android本身做為線程不安全,只要稍微耗時的操作都要用到異步任務,而無論是java
實現功能:實現NetMusicListAdapter(網絡音樂列表適配器