1. 程式人生 > >Android 中 Activity的記憶體洩漏,原因以及處理方法

Android 中 Activity的記憶體洩漏,原因以及處理方法

文章參考:
八個造成 Android 應用記憶體洩露的原因
Android記憶體洩漏產生的原因以及解決方案OOM
android 常見記憶體洩漏原因及解決辦法
記憶體洩漏,說的更直白點,就是你想讓一個物件在下次GC的時候徹底被回收,但是呢,這個物件所處的條件不符合GC所認定的應當回收的條件,而導致實際上沒有被回收依然佔用著記憶體空間,像這樣的物件多了,,遲早會把記憶體撐爆引發大名鼎鼎的OOM問題。Android中最最露骨的就是Activity的記憶體洩漏。因為Activity持有了太多的東西,包括圖片,資料等等吧。。它的洩露勢必會浪費很大的記憶體空間。

那麼什麼是GC所認定的回收條件呢? 就是指向該物件的相關引用鏈斷了,致使這個物件相關的資源 成為了一個孤島樣子的狀態,及封閉了,與外界無聯絡了。這樣一種狀態,就是可以被回收的狀態了。所以要你搞清記憶體洩漏,你首先要搞清的問題就是記憶體相關的那波。包括怎麼存,以及看程式碼要弄明白各種情況下,到底是誰持有誰的引用。誰的地址,還被有效的指標指著,導致該刪的刪不掉。搞清楚這些,才能正確的斷掉引用鏈。

下面會講到幾種引發洩漏的幾種情況以及洩漏的原因,並且分析引用關係。

Handler引發的記憶體洩漏

android handler機制
如果目前對handler一無所知的話,建議看一下連線中的部落格。
Handler在實際的android專案中經常用於改變UI相關的介面變化。尤其是在子執行緒中的時候往往會用一個handler send一個message這樣的方式改變相關的介面。但是如果大家瞭解Handler的機制的話,好好分析,其實會發現如果handler使用不當的話,是很可能引發記憶體洩漏的。
首先Handler在整個機制內的引用關係會是這樣的,Handler會被它所傳送的Message引用,(Message裡面有個target的變數就是Handler),然後傳送完訊息之後Message會按照時間順序加入到一個叫messagequeque的佇列裡面去。這個messageQueue是Looper的成員變數。所涉及的引用關係如下:
每一個Thread只要呼叫了Looper的prepare方法,就會出現下面的現象。thread會持有一個values物件,values物件內部的資料結構儲存了(key, map) 諸如(sThreadLocal, 當前執行緒對應的looper)這樣的資料結構, 其中looper裡面便有messagerqueue的引用。這也就導致了messagequeue的存在週期是相當長的(執行緒有多長它就有多長,並且此時的執行緒多為UI執行緒,絕對長的週期),因為messagequeue在looper裡,然而looper被Thread中Value以鍵值對的形式儲存起來了。也就意味著thread不死,values就一直被引用,那麼looper中的messagequeue也就會一直存在。在android裡面,如果不特殊寫的話,唯有我們的主執行緒,間接持有messagequeque的。
所以這樣的話,handler的整條引用關係也就可以理出來, handler 被message.target持有, message.target又在message物件裡面存著,message被messagequeque持有,但是重點是messagequeue的週期相當長。。這樣的話貌似也與Activity沒有啥關係哈。。但是handler如果使用不當,恰好持有了Activity的引用呢???那就出問題了!
放一段恰好有這類問題的程式碼:

package com.example.forev.roundrectviewp;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends Activity {

private TextView mPostMessageTv;
private TextView mOpenActivityBTv;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setViews();
}

private void setViews() {
    mPostMessageTv = findViewById(R.id.tv_get);
    mOpenActivityBTv = findViewById(R.id.tv_post);

    mPostMessageTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Message message = new Message();
            message.what = MyHandler.WHAT_CHANGEBACKGROUD;
            //此時new出來的物件已經預設持有了 MainActivity的引用!執行了此句立馬點選跳轉B的話,會引發洩漏
            new MyHandler().sendMessageDelayed(message, 30 * 1000);
        }
    });

    mOpenActivityBTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, BActivity.class);
            startActivity(intent);
        }
    });
}

//內部類,在物件被new的那一刻,就預設已經持有了外部類的引用。
private class MyHandler extends Handler{

    public static final int WHAT_CHANGEBACKGROUD = 0x00;
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case WHAT_CHANGEBACKGROUD:
                mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));
                break;
        }
    }
}

}

從上面程式碼,Handeler作為內部類的時候,會持有外部類的強引用(很多情況下,內部類會持有外部類的引用,接下來會講這個),假設Activity點選了mPostMessageTv按鈕之後又點選了跳轉B的按鈕,如果記憶體吃緊的話,要GC,但是,因為Handler的Message還在佇列裡排隊沒有執行到,所以Activity就這樣被強引用鏈牽著,從而被GC判定為不可回收。就導致出現了OOM。

好,接下來簡單說下內部類:內部類可分為 成員內部類,區域性內部類, 匿名內部類, 靜態內部類。

成員內部類相當於一個外部類的成員變數,可以訪問外部類的所有成員變數和方法。上面的程式碼,MyHandler就是MainActivity的成員內部類。所以在改變mPostMessageTv背景的時候,我們是直接用的mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));。但是成員內部類是會持有外部類引用的。

區域性內部類:定義在方法裡,比方法的範圍還要小,是內部類中最少用的一種型別。與此同時,因為在方法體裡面,所以它會像區域性變數一樣不可以被public , protected, private, static修飾。並且它只能夠訪問final型別的區域性變數這樣的話,區域性內部類也就只能在其所屬的方法中被new,並且呼叫方法。那麼區域性內部類會不會持有外部類的引用呢?好像是不會持有。介紹的文章太少,走個例子吧,把上面的程式碼改一下

    private void setViews() {
    mPostMessageTv = findViewById(R.id.tv_get);
    mOpenActivityBTv = findViewById(R.id.tv_post);
    mPostMessageTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Message message = new Message();
            //將handler寫成區域性內部類
            class MyHandler extends Handler{

                public static final int WHAT_CHANGEBACKGROUD = 0x00;
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case WHAT_CHANGEBACKGROUD:
                            mPostMessageTv.setBackgroundColor(Color.parseColor("#FFFF00"));
                            break;
                    }
                }
            }
            message.what = MyHandler.WHAT_CHANGEBACKGROUD;
            MyHandler myHandler = new MyHandler();

            myHandler.sendMessageDelayed(message, 30 * 1000);
        }
    });

    mOpenActivityBTv.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, BActivity.class);
            startActivity(intent);
        }
    });
}

看下debug的圖片:
區域性內部類持有外部類引用的證明
從這裡可以看出來是持有外部類引用的。

匿名內部類
對於匿名內部類,可以說在介面程式設計裡面,幾乎每每都要用到這個。但是他的樣子也是怪怪的。下面貼一段程式碼看看這個匿名內部類

/**
 * 匿名內部類案例
 * @author forev
 *
 */
public class UnnameInnerClass {
public static void main(String[] args) {
    UnnameInnerClass unnameInnerClass = new UnnameInnerClass();
    unnameInnerClass.printBirdInfo(new Bird(){
        String getBirdName() {
            // TODO Auto-generated method stub
            return "大黃鴨";
        }

        String getBirdAge() {
            // TODO Auto-generated method stub
            return "1";
        }
    });
}

//也就是Bird是一個抽象類,但是儘管它沒有被實現,依然可以呼叫它的方法。
public void printBirdInfo(Bird bird){
    System.out.println("the bird name is :" + bird.getBirdName() + " and the bird age is :" + bird.getBirdAge());
}

}

bird程式碼:
abstract class Bird {
abstract String getBirdName();
abstract String getBirdAge();
}
使用匿名內部類的話,必須要做的是實現父類或者介面,當然也只能繼承一個父類或者實現一個介面,匿名內部類直接用一個new來生成物件的引用,當然這個引用是隱式的。
看到程式碼是不是感覺似曾相識?咱們view.setonclickListner()是不是裡面常常現new一個OnclickListner然後實現裡面的onclick方法呢?是的,這種用法就是典型的匿名內部類。
匿名內部類是能new出來的,所以它不能是一個抽象類或者介面,所以在new的時候必須要實現這些抽象方法,以確保該類方法裡面邏輯的完整性。同時呢,匿名內部類在哪裡被new,就會持有所在類物件的引用。
這樣,有關於內部類,單獨寫一篇博吧。這個知識點要講的並不少。總之現在記住,匿名內部類new出來的那個物件是會持有其外部類引用。
靜態內部類與靜態匿名內部類
記住一個,static修飾的東西不在類的管轄範圍之內。所以一旦被static修飾之後它不屬於任何物件。所以這兩種情況下是不會持有外部類引用的。

講到這裡,關於handler使用不當引發的記憶體洩漏自然也有了對應的解決方法, 1 要麼把Myhandler搞成靜態內部類。 2 或者另外立一個檔案專門寫一個Myhandler類。 這兩種一個可以避免內部類持有外部類,另一個更粗暴地不讓它當內部類。
當然寫使用Handler的時候,尤其是傳送一個延時訊息的時候,要做就做徹底些,除了避免Handler持有Activity引發的記憶體洩露之外,在Activity結束的時候最好要remove一下發出去的那個message。要養成一個良好的習慣。但是假設有的情況,Handler必須要一個Activity的物件來進行相關UI處理的話,又怎麼辦呢?此時我們不得不把Activity傳進去。那麼針對這種情況哈,不要忘記有一個叫弱引用的存在,,可以把activity搞成一個弱引用。這樣儘可能的避免記憶體洩漏。弱引用也可以在其他的情況下很好的解決記憶體洩漏等問題。但是要做好的是相關處理, 假設get()為null,總得有個相關的處理吧。

單例模式引發的記憶體洩漏

首先了解一下單例模式是個啥樣子。

public class SingletonDemo1 {
//這是一個很靠譜的單例模式,既避免了多執行緒訪問引發的資料篡改問題,又可以相對較快地
//得到這個單例
//這個靜態內部類並不希望被其他類訪問到,所以用private修飾
//並且static修飾的東西,會在第一次被使用的時候
//靜態內部類的載入不需要依附外部類,在使用時才載入。
private static class InstanceHolder{
    private final static SingletonDemo1 singleton = new SingletonDemo1();
}

public SingletonDemo1 getInstatnce(){
    return InstanceHolder.singleton;
}

}

看到單例模式的樣子,是不是感覺到一旦getInstance()之後,這個InstanceHolder.singleton的生命週期會相當長。對的,單例的週期是和app的生命週期一致。並且整個app內只有一個這樣的例項。但是由於其超長的生命週期,一旦它持有比較大的物件引用,並且這個物件的作用發揮完了還沒有釋放它的話,是極易引發記憶體洩漏的。尤其是這個物件如果是Activity的話,以Activity所持有的那些資源來看的話,洩露的空間可是相當可觀的。再看一段程式碼:

public class SingleTonLeackDemo {
private Context mContext;   
private static class InstanceHolder{
    private final static SingleTonLeackDemo INSTANCE = new SingleTonLeackDemo();
}

void SingleTonLeackDemo(Context context){
    this.mContext = context;
}

private SingleTonLeackDemo setContext(Context context) {
    this.mContext = context;
    return this;
}

//假設是單執行緒呼叫
public static SingleTonLeackDemo getInstance(Context context) {
    return InstanceHolder.INSTANCE.setContext(context);
}
}

假設上一段程式碼裡面的context,如果傳入了一個Activity物件,比如來了個SingleTonLeackDemo.getInstance(Activity.this)這樣的呼叫,就會導致記憶體洩漏。因為Activity傳進去的時候,就會被賦給SingleTonLeackDemo物件的成員變數mContext, 但是事實上SingleTonLeackDemo物件的生命週期卻和app的生命週期一樣長,這樣不對等的生命週期,這樣赤裸裸的引用關係,記憶體洩露也就在所難免了。但是這種情況也是可以避免的,解決方法大概有兩種。
1 確認SingleTonLeackDemo 那裡面的mContext 是幹嘛用的,有沒有必要非得把Activity.this傳進去。因為畢竟我們還有一個叫做applicationContext的東西也是Context的型別的,也可以做獲取資源,獲取服務等工作。沒必要非得傳Activity。一旦確認這裡其實可以被applicationContext代替的話,那就改程式碼,Context引數該去的去掉,直接給mContext賦值為 this.mContext = MyApplication.getAppInstance().getApplicationContext(),總之就是想辦法阻斷指activity例項的那個引用。
2 如果此處要傳遞的非得是Activity的話,那好吧,最好把mContext改成弱引用!

非靜態內部類的靜態例項引發的記憶體洩漏。

這個就很好理解了。看看如下程式碼:

/**
 * create by yayali
 * 本activity主要展現一種非靜態內部類的靜態例項引發記憶體洩漏的問題。
 */
public class InnerClassLeackAct extends AppCompatActivity {
private static InnerClass sInnerClass;  
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_inner_class_leack);
    setData();
}

private void setData() {
    if (sInnerClass == null) {
        sInnerClass = new InnerClass();
    }
}

private class InnerClass{

}
}

上面的程式碼裡面,首先InnerClass作為一個內部類,被new 的時候本身就隱式持有了其外部類Activity的引用,但是偏偏Activity又有了個靜態變數,後來又指向了innerclass的例項,靜態變數的宣告週期可是很長很長滴,activity就這樣也沒做啥卻被隱式引用了。流氓不撒手鍋從天上來。
這時候你可能會想,我才不會寫出這麼弱智的程式碼呢。但是呢,咱們編碼的時候是需要面臨一些情景的,有時候某些情景是真的需要我們用static的物件來記錄一些東西,以便於下次再呼叫到該類的時候能夠根據這裡面的資料做一些邏輯。那麼對於這種洩露如果解決的話,就想辦法使這個innerClass 不隱式引用Activity,這樣即使innerclass被靜態變數引用了,那起碼也跟Activity沒太大的關係。這樣的話,要麼把InnerClass搞成靜態內部類,要麼就另起個檔案寫這個類。

非靜態匿名內部類引發的洩露

這個講起來其實是和 第一種情況既handler使用不當引發的記憶體洩漏一模一樣。包括那個部分的案例程式碼都是妥妥的非靜態匿名內部類(MyHandler)引發的記憶體洩漏。
再給個栗子:

/**
 * create by yayali
 * 非靜態匿名內部類引發的記憶體洩漏
 */
public class InnerClassLeackAct extends AppCompatActivity { 
/**
 * 此時Runnable便是一個非靜態匿名內部類。
 * 那麼也意味著 mRun此時隱式持有當前物件的引用
 * 我們必須實現它的方法以便於它可以new出來。
 */
private Runnable mRun = new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(300 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_inner_class_leack);
    Thread thread = new Thread(mRun);
    thread.start();
}

}

那麼此時的做法是什麼呢?改為靜態匿名內部類不就得了。這樣就避免隱式持有Activity了。

其他的記憶體洩漏 摘抄的(因為工作主要做業務程式設計了至今我沒有太遇到過)

註冊、反註冊未成對使用引發的記憶體洩漏

在android開發中,我們經常會在Activity的onCreate()中註冊廣播接收器,EventBus等,如果忘記成對的使用反註冊,就可能會引發記憶體洩漏。開發過程中應當養成良好的習慣,在onCreate()onResume()中註冊,要記得相應的在onDestroy()onPause中反註冊。

資源物件沒有關閉引發的記憶體洩漏

在android中,資源性物件比如Cursor,File,BitMap,視訊等,系統都用了一些緩衝技術,在使用這些資源的時候,如果我們確保自己不再使用這些資源了,應當及時關閉,苟澤可能引起記憶體洩漏,因為有些操作不僅僅是涉及到dalvik虛擬機器,還涉及到底層c/c++等記憶體管理,不能完全寄託於虛擬機器幫我們完成記憶體管理。
在這些資源不使用的時候,記得要呼叫類似close(), destroy(),recycler(),release()等函式,這些函式往往會通過jni呼叫底層相關函式,完成相關的記憶體釋放。

集合物件沒有及時清理引發的記憶體洩漏

我們通常會把一些物件裝入到集合中,當不使用的時候一定要記得及時清理集合,讓相關物件不再被引用。如果集合是static、不斷的往裡面新增東西、又忘記去清理,肯定會引起記憶體洩漏。