Android 內存優化-常見內存泄露

沒有用的對象無法回收的現象就是內存泄露

如果程序發生了內存泄露,則會帶來如下的問題

應用可用的內存減少,增加了堆內存的壓力
降低了應用的性能,比如會觸犯更頻繁的GC
嚴重的時候可能會導致內存溢出錯誤,即OOM Error
在正式介紹內存泄露之前,我們有必要介紹一些必要的預備知識。

預備知識1: Java中的對象

當我們使用new指令生成對象時,堆內存將會爲此開闢一份空間存放該對象
創建的對象可以被局部變量,實例變量和類變量引用。
通常情況下,類變量持有的對象生命週期最長,實例變量次之,局部變量最短。
垃圾回收器回收非存活的對象,並釋放對應的內存空間。
預備知識2:Java中的GC

和C++不同,對象的釋放不需要手動完成,而是由垃圾回收器自動完成。
垃圾回收器運行在JVM中
通常GC有兩種算法:引用計數和GC根節點遍歷
引用計數

每個對象有對應的引用計數器
當一個對象被引用(被複制給變量,傳入方法中),引用計數器加1
當一個對象不被引用(離開變量作用域),引用計數器就會減1
基於這種算法的垃圾回收器效率較高
循環引用的問題引用計數算法的垃圾回收器無法解決。
主流的JVM很少使用基於這種算法的垃圾回收器實現。
GC根節點遍歷

識別對象爲垃圾從被稱爲GC 根節點出發
每一個被遍歷的強引用可到達對象,都會被標記爲存活
在遍歷結束後,沒有被標記爲存活的對象都被視爲垃圾,需要後續進行回收處理
主流的JVM一般都採用這種算法的垃圾回收器實現

以上圖爲例,我們可以知道

最下層的兩個節點爲GC Roots,即GC Tracing的起點
中間的一層的對象,可以強引用到達GC根節點,所以被標記爲存活
最上層的三個對象,無法強引用達到GC根節點,所以無法標記爲存活,也就是所謂的垃圾,需要被後續回收掉。
上面的垃圾回收中,我們提到的兩個概念,一個是GC根節點,另一個是強引用

在Java中,可以作爲GC 根節點的有

類,由系統類加載器加載的類。這些類從不會被卸載,它們可以通過靜態屬性的方式持有對象的引用。注意,一般情況下由自定義的類加載器加載的類不能成爲GC Roots
線程,存活的線程
Java方法棧中的局部變量或者參數
JNI方法棧中的局部變量或者參數
JNI全局引用
用做同步監控的對象
被JVM持有的對象,這些對象由於特殊的目的不被GC回收。這些對象可能是系統的類加載器,一些重要的異常處理類,一些爲處理異常預留的對象,以及一些正在執行類加載的自定義的類加載器。但是具體有哪些前面提到的對象依賴於具體的JVM實現。
提到強引用,有必要系統說一下Java中的引用類型。Java中的引用類型可以分爲一下四種:

強引用: 默認的引用類型,例如StringBuffer buffer = new StringBuffer();就是buffer變量持有的爲StringBuilder的強引用類型。
軟引用:即SoftReference,其指向的對象只有在內存不足的時候進行回收。
弱引用:即WeakReference,其指向的對象在GC執行時會被回收。
虛引用:即PhantomReference,與ReferenceQueue結合,用作記錄該引用指向的對象已被銷燬。
補充了預備知識,我們就需要具體講一講Android中的內存泄漏了。

Android中的內存泄漏

歸納而言,Android中的內存泄漏有以下幾個特點:

相對而言,Android中的內存泄漏更加容易出現。
由於Android系統爲每個App分配的內存空間有限,在一個內存泄漏嚴重的App中,很容易導致OOM,即內存溢出錯誤。
內存泄漏會隨着App的推出而消失(即進程結束)。
在Android中的內存泄漏場景有很多,按照類型劃分可以歸納爲

長期持有(Activity)Context導致的
忘記註銷監聽器或者觀察者
由非靜態內部類導致的
此外,如果按照泄漏的程度,可以分爲

長時間泄漏,即泄漏只能等待進程退出才消失
短時間泄漏,被泄漏的對象後續會被回收掉。
長時間持有Activity實例

在Android中,Activity是我們常用的組件,通常情況下,一個Activity會包含了一些複雜的UI視圖,而視圖中如果含有ImageView,則有可能會使用比較大的Bitmap對象。因而一個Activity持有的內存會相對很多,如果造成了Activity的泄漏,勢必造成一大塊內存無法回收,發生泄漏。

這裏舉個簡單的例子,說明Activity的內存泄漏,比如我們有一個叫做AppSettings的類,它是一個單例模式的應用。

public class AppSettings {
private Context mAppContext;
private static AppSettings sInstance = new AppSettings();
//some other codes
public static AppSettings getInstance() {
return sInstance;
}
public final void setup(Context context) {
mAppContext = context;
}
}
當我們傳入Activity作爲Context參數時,則AppSettings實例會持有這個Activity的實例。

當我們旋轉設備時,Android系統會銷燬當前的Activity,創建新的Activity來加載合適的佈局。如果出現Activity被單例實例持有,那麼旋轉過程中的舊Activity無法被銷燬掉。就發生了我們所說的內存泄漏。

想要解決這個問題也不難,那就是使用Application的Context對象,因爲它和AppSettings實例具有相同的生命週期。這裏是通過使用Context.getApplicationContext()方法來實現。所以修改如下

public class AppSettings {
private Context mAppContext;
private static AppSettings sInstance = new AppSettings();
//some other codes
public static AppSettings getInstance() {
return sInstance;
}
public final void setup(Context context) {
mAppContext = context.getApplicationContext();
}
}
忘記反註冊監聽器

在Android中我們會使用很多listener,observer。這些都是作爲觀察者模式的實現。當我們註冊一個listener時,這個listener的實例會被主題所引用。如果主題的生命週期要明顯大於listener,那麼就有可能發生內存泄漏。

以下面的代碼爲例

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NetworkManager.getInstance().registerListener(this);
}
@Override
public void onNetworkUp() {
}
@Override
public void onNetworkDown() {
}
}
上述代碼處理的業務,可以理解爲

AppCompatActivity實現了OnNetworkChangedListener接口,用來監聽網絡的可用性變化
NetworkManager爲單例模式實現,其registerListener接收了MainActivity實例
又是單例模式,可知NetworkManager會持有MainActivity的實例引用,因而屏幕旋轉時,MainActivity同樣無法被回收,進而造成了內存泄漏。

對於這種類型的內存泄漏,解決方法是這樣的。即在MainActivity的onDestroy方法中加入反註銷的方法調用。

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NetworkManager.getInstance().registerListener(this);
}
@Override
public void onNetworkUp() {
}
@Override
public void onNetworkDown() {
}
@Override
protected void onDestroy() {
super.onDestroy();
NetworkManager.getInstance().unregisterListener(this);
}
}
非靜態內部類導致的內存泄漏

在Java中,非靜態內部類會隱式持有外部類的實例引用。想要了解更多,可以參考這篇文章細話Java:”失效”的private修飾符

通常情況下,我們會書寫類似這樣的代碼

public class SensorListenerActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
sensorManager.registerListener(new SensorListener() {
@Override
public void onSensorChanged(int sensor, float[] values) {
}
@Override
public void onAccuracyChanged(int sensor, int accuracy) {
}
}, SensorManager.SENSOR_ALL);
}
}
其中上面的SensorListner實例是一個匿名內部類的實例,也是非靜態內部類的一種。因此SensorListner也會持有外部SensorListenerActivity的實例引用。

而SensorManager作爲單例模式實現,其生命週期與Application相同,和SensorListner對象生命週期不同,有可能間接導致SensorListenerActivity發生內存泄漏。

解決這種問題的方法可以是

使用實例變量存儲SensonListener實例,在Activity的onDestroy方法進行反註冊。
如果registerListener方法可以修改,可以使用弱引用或者WeakHashMap來解決。
除了上面的三種場景外,Android的內存泄漏還有可能出現在以下情況

使用Activity.getSystemService()使用不當,也會導致內存泄漏。
資源未關閉也會造成內存泄漏
Handler使用不當也可以造成內存泄漏的發生
延遲的任務也可能導致內存泄漏
解決內存泄漏

想要解決內存泄漏無非如下兩種方法

手動解除不必要的強引用關係
使用弱引用或者軟引用替換強引用關係
下面會簡單介紹一些內存泄漏檢測和解決的工具

Strictmode

StrictMode,嚴格模式,是Android中的一種檢測VM和線程違例的工具。
使用detectAll()或者detectActivityLeaks()可以檢測Activity的內存泄漏
使用setClassInstanceLimit()可以限定類的實例個數,可以輔助判斷某些類是否發生了內存泄漏
但是StrictMode只能檢測出現象,並不能提供更多具體的信息。
瞭解更多關於StrictMode,請訪問Android性能調優利器StrictMode
Android Memory Monitors

Android Memory Monitor內置於Android Studio中,用於展示應用內存的使用和釋放情況。它大致長成這個樣子

當你的App佔用的內存持續增加,而且你同時出發GC,也沒有進行釋放,那麼你的App很有可能發生了內存泄漏問題。

LeakCanary

LeakCanary是一個檢測Java和Android內存泄漏的庫
由Square公司開發
集成LeakCanary之後,只需要等待內存泄漏出現就可以了,無需認爲進行主動檢測。
關於如何使用LeakCanary,可以參考這篇文章 Android內存泄漏檢測利器:LeakCanary
Heap Dump

一個Heap dump就是某一時間點的內存快照
它包含了某個時間點的Java對象和類信息。
我們可以通上述提到的Android Heap Monitor進行Heap Dump,當然LeakCanary也會生成Heap Dump文件。
生成的Heap Dump文件擴展名爲.hprof 即Heap Profile.
通常情況下,一個heap profile需要轉換後才能被MAT使用分析。
Shallow Heap VS Retained Heap

Shallow Heap 指的是對象自身的佔用的內存大小。
對象x的Retained Set指的是如果對象x被GC移除,可以釋放總的對象的集合。
對象x的Retained Heap指的就是上述x的Retained Set的佔用內存大小。

以上圖做個例子,進行分析

A,B,C,D四個對象的Shallow Heap均爲1M
B,C,D的Retained Heap均爲1M
A的Retained Heap爲4M
真實情況下如何計算泄漏內存大小

上述的Retained Heap的大小獲取是基於假設的,而現實在進行分析中不可能基於這種方法,那麼實際上計算泄漏內存的大小的方法其實是這樣的。

這裏我們需要一個概念,就是Dominator Tree(統治者樹)。

如果對象x統治對象y,那麼每條從GC根節點到y對象的路徑都會經過x,即x是GC根節點到y的必經之路。
上述情況下,我們可以說x是y的統治者
最近統治者指的是離對象y最近的統治者。

上圖中

A和B都不無法統治C對象,即C對象被A和B的父對象統治
H不受F,G,D,E統治,但是受C統治
F和D是循環引用,但是按照路徑的方向(從根節點到對象),D統治F
內存泄漏與OOM

OOM全稱Out Of Memory Error 內存溢出錯誤
OOM發生在,當我們嘗試進行創建對象,但是堆內存無法通過GC釋放足夠的空間,堆內存也無法在繼續增長,從而完成對象創建請求,所以發生了OOM
OOM發生很有可能是內存泄漏導致
但是並非所有的OOM都是由內存泄漏引起
內存泄漏也並不一定引起OOM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章