1. 程式人生 > >Andorid效能優化(二) 之 記憶體洩漏場景介紹

Andorid效能優化(二) 之 記憶體洩漏場景介紹

1 相關概念

1.1 記憶體洩漏

記憶體洩漏是指程式在向系統申請分配記憶體空間後,也就是說new了物件後,在使用完畢後沒有對其進行釋放。結果導致一直佔據該記憶體單元。簡單的說,在C/C++語言中,如果向堆中分配了記憶體(new了物件)後,沒有對其進行釋放掉(沒有delete物件),那就是記憶體洩漏。在Java中由於有了垃圾回收機制,不再需要開發者手動去delete物件了,所以在Java中記憶體洩漏是指記憶體物件明明已經不需要的時候,但還仍然保留著這塊記憶體和它的訪問方式。

1.2 記憶體溢位

Android系統為每個應用程序都分配一個有封頂的堆記憶體值,當應用記憶體佔用過高到沒有足夠的記憶體來提供給新物件分配並且垃圾回收機制也已經沒有空間可回收時就會記憶體溢位或者叫OOM(OOut Of Memory)。大量的記憶體洩漏就會導致OOM。

1.3 JVM的記憶體

Java是在JVM所虛擬出的記憶體環境中執行的,JVM的記憶體分三個區:棧區(Stack)、堆區(Heap)和 方法區(Method)。

棧區:棧區中只儲存基本資料型別的物件和自定義物件的引用。每個執行緒包含一個棧,每個棧中的資料都是私有的,其他棧不能訪問。棧最顯著的特徵是:LIFO(Last In,First Out,後進先出)。

堆區:堆區用於存放由new建立的物件和陣列。JVM只有一個堆區被所有執行緒共享。在堆中分配的記憶體,由Java虛擬機器自動垃圾回收器來管理。

方法區:方法區又叫靜態區,它包含整個程式中永遠唯一的元素,如class和static變數。它跟堆一樣也是被所有的執行緒共享

1.4 記憶體回收機制

在Java中JVM的棧記錄了方法的呼叫,每個執行緒包含一個。線上程的執行過程當中,執行到一個新的方法呼叫,就在棧中增加一個記憶體單元。記憶體單元中儲存有該方法呼叫的引數、區域性變數和返回地址。當呼叫的方法結束時,該方法對應的記憶體單元就會從棧中自動刪除,其所佔用的記憶體內間也隨之釋放。而記憶體是存放著被創建出來的物件,它是不會隨著方法的結束而清空。也就是說,方法結束後棧中記憶體得到了釋放,但方法內創建出來的區域性變數在方法結束後是依然存活在堆記憶體中的。所以Java引入了垃圾回收(Garbage Collection, 簡稱GC)機制來處理堆記憶體的回收。該機制可以自動清空堆中不再使用的物件,也就是沒有引用指向的物件。但是如果一個明明已經沒有使用價值的物件一直被引用著就會變成無法被回收,這就造成了記憶體的浪費,這就是Java的記憶體洩漏。

實現思想:

我們將棧定義為root,所以在GC root時就會遍歷棧中的所有物件,如果這個物件有引用指向就會標記起來,然後對棧再次遍歷,如果發現沒有被標記上的物件,這些物件就是要進行垃圾回收的物件,就要將其清除。

GC觸發時機:

GC又分為 Minor GC 和 Full GC (也稱為 Major GC )。堆記憶體分為新生代和老年代,新生代有一個Eden區,兩個Survivor區。新生成的物件直接放在Eden區,Eden區滿了就放進Survivor1,當Survivor1滿了就會觸發一次Minor GC:將存活的物件放入Survivor2,然後清空Eden和Survivor1,再將Survivor區的交換,保證Survivor2為空。當Survivor2不足以存放Eden和Survivor1的存活物件時,就會放入老年區。較大的物件和長期存活的物件直接進入老年區。當即將進入老年區的物件超過老年區剩餘大小時,觸發一次Full GC。這個邏輯過程具體到什麼時刻執行,這個是由系統來進行決定,是無法預測的。

1.5 物件引用

從JDK 1.2版本開始,把物件的引用分為4種級別,從而使程式能更加靈活地控制物件的生命週期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。

強引用(Strong reference):

強引用是實際開發中最常見的一種引用型別,如:String str=”abc”中變數str就是字串物件”abc”的一個強引用。如果持有物件的強引用,垃圾回收器是無法在記憶體中回收這個物件。

軟引用(Soft Reference):

軟引用通過SoftReference類來實現,當系統記憶體空間足夠時,它不會被系統回收,但當系統記憶體不足時,而其指示的物件沒有任何強引用物件指向時,系統將會回收它,軟引用通常用於對記憶體敏感的程式中,使用如:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

弱引用(Weak Reference)

弱引用通過WeakReference類來實現,當系統垃圾回收機制執行時,不管系統記憶體是否足夠,如果其指示的物件沒有任何強引用物件指向時,系統就會回收它,使用如:

String str = "abc";
SoftReference srStr = new SoftReference(str);
str = null;
// 列印結果:abc
System.out.println(srStr.get());
//強制進行垃圾回收
System.gc();
System.runFinalization();
// 列印結果:null
System.out.println(srStr.get());

虛引用(Phantom Reference):

虛引用通過PhantomReference類實現,如果一個物件只有一個虛引用時。那它和沒有引用的效果大致相同。虛引用主要用於跟蹤物件被垃圾回收的狀態,虛引用不能單獨使用,虛引用必須和引用佇列(ReferenceQueue)聯合使用,如:

String str = "abc";
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference prStr = new PhantomReference(str, referenceQueue);
str = null;
// 列印結果:null
System.out.println(prStr.get());
//強制進行垃圾回收
System.gc();
System.runFinalization();
// 列印結果:true
System.out.println(referenceQueue.poll() == prStr);

2 導致記憶體洩漏的場景

在Android開發中,最容易引發記憶體洩漏是Activity該退出時沒有正常退出。Activity是重量級物件,如若洩漏了Activity,也意味著洩漏它指向的所有物件。Android機器記憶體有限,太多的記憶體洩漏容易導致OOM。下面我們來列舉一些比較常見的記憶體洩漏場景。

2.1 靜態引用大物件

有時候,我們出現一些目的和開發上的便利,會定義一些全域性的靜態變數,但如果此類物件越來越多、越來越大時,就會產生對應的記憶體一直被佔用著。此類記憶體是無法被釋放的,因為static變數是慣穿整個App的生命週期。同時一定要避免使用static Activity和static View之類的程式碼的存在。

2.2 Activity或Service作為Context被傳入單例中

有時候,往往在一個單例邏輯中要傳入一個Context,這本來也沒什麼大問題,但使用者有時不注意會在Activity或Service中傳入了this。這樣問題就來了,單例的生命週期有時候也跟static差不多,也可能是慣穿整個App的生命週期,但是Activity或Service通常的可能性就是存在一小段時間,只要我們不需要就會將其釋放掉,但是如果被單例引用住,即使我們退出了Activity或Service,但它也不會被釋放。

2.3 BroadcastReceiver沒有反註冊

有時候,我們使用BroadcastReceiver在Activity中的onCreate中動態註冊,而這個BroadcastReceiver的生命週期本來是跟Activity一樣的,但是在Activity的onDestroy中忘記了反註冊。這樣的話,這個廣播就會一直存在於應用中,而這個廣播會一直持有著Activity的引用,從而也導致了Activity無法釋放。

2.4 沒有正確呼叫close

有時候,我們在使用IO流、Socket或Cursor時,忘記呼叫其close或沒有正確呼叫close,也會導致記憶體洩漏。所以在一般情況下,都是建議使用try catch時,將需要釋放的程式碼寫在finally中。

2.5 內部類導致外部無法被釋放

在Java中,非靜態內部類和匿名內部類會持有外部類的隱式引用,而靜態內部類則不會。所以要避免在外部類該結束時被內部類佔用著引用,從而導致外部類不能被釋放的情況,請看下面一個錯誤的例項:

public class TestActivity extends Activity {
    private  final static int MSG_TEST = 1;
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_TEST) {
                // TODO...
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000 * 60 * 10);
        finish();
    }
}

程式碼中,當Activity生命週期本該結束時,執行了一個延時10分鐘的Message,該Message持有了所在Activity的Handler的引用,而Handler持有外部類TestActivity的隱式引用。該引用會繼續存在直到Message被處理完畢。所以這裡就阻止了本該結束的Activity的回收,從而導致記憶體洩漏。解決這種情況的問題,可以將程式碼修改成這樣:

public class TestActivity extends Activity {
    private  final static int MSG_TEST = 1;
    private static class MyHandler extends Handler {
        private final WeakReference<TestActivity> mActivity;
        public MyHandler(TestActivity activity) {
            mActivity = new WeakReference<TestActivity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            TestActivity activity = mActivity.get();
            if (activity == null) {
                return;
            }
            if (msg.what == MSG_TEST) {
                // TODO...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000 * 60 * 10);
        finish();
    }
}

修改後的程式碼中,新建一個Handler的靜態子類。因為靜態內部類不會持有外部類的隱式引用,所以就不會導致記憶體洩漏。此刻若是內部類需要呼叫外部類的方法,可以讓Handler持有一個Activity的弱引用物件。弱引用的特徵是,當系統垃圾回收機制執行時,如果其指示的物件沒有任何強引用物件指向時,系統就會回收它,所以這裡很好地解決延遲執行還強佔Activity的問題。

內部類的使用看似是正常不過的邏輯,但實質上隱藏著玄機,一不小心就會踩中坑了,所以我們在日常開發中都應該注意這種情況,要避免在Activity中使用非靜態內部類,如果該內部類的例項會存在於Activity的生命週期之外,那必須要使用靜態內部類持有一個外部類的弱引用替代。

2.6 WebView大坑

WebView是Android提供的比較重量級的控制元件,我們在平時使用WebView時,一定要記得呼叫其destory()方法來釋放內部持有的資料物件。否則它內部的一些資源是不會被釋放的從而導致記憶體洩漏。WebView本身也是算在Android開發中比較多坑的控制元件。一般情況下,都是建議將其放置在一個單獨的程序中來做事,然後待事情做完了後將此程序殺掉一了百了,不用擔心使用後記憶體出什麼大問題。因為WebView可能會存在不同的核心和不同的廠商也可能對其進行過修改,所以它本身算是一個比較不穩定的東西。

2.7 屬性動畫沒有停止

如果在Activity中播放屬性動畫沒有進行過停止動畫,包括沒有在onDestroy中去停止動畫,那麼動畫就會一直播放下去,儘管已經無法在介面上看到動畫效果,並且這時Activity的View會被動畫持有,而View又持有了Activity,最終Activity無法釋放。解決方法是在Activity的onDestroy中呼叫animator.cancel()來停止動畫。

 

更多場景有待補充!!