內存泄漏緣由及解決方案

什麼是內存泄漏?

內存泄漏是當程序再也不使用到的內存時,釋放內存失敗而產生了無用的內存消耗。內存泄漏並非指物理上的內存消失,這裏的內存泄漏是值由程序分配的內存可是因爲程序邏輯錯誤而致使程序失去了對該內存的控制,使得內存浪費。java

怎樣會致使內存泄漏?

  • 資源對象沒關閉形成的內存泄漏,如查詢數據庫後沒有關閉遊標cursor
  • 構造Adapter時,沒有使用 convertView 重用
  • Bitmap對象不在使用時調用recycle()釋放內存
  • 對象被生命週期長的對象引用,如activity被靜態集合引用致使activity不能釋放

在接下來的篇幅裏,咱們重點講有關Activity常見的內存泄漏。數據庫

內存泄漏1:靜態Activities(static Activities)

代碼以下:
MainActivity.javaapp

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //使用LeakCanary觀察是否有內存泄漏
        MyApplication.getRefWatcher().watch(this);
    }
}

LeakCanary檢測出的內存泄漏:ide

這裏寫圖片描述

爲何?
在上面代碼中,咱們聲明瞭一個靜態的Activity變量而且在TextView的OnClick事件裏引用了當前正在運行的Activity實例,因此 若是在activity的生命週期結束以前沒有清除這個引用,則會引發內存泄漏。由於聲明的activity是靜態的,會常駐內存,若是該對象不清除,則 垃圾回收器沒法回收變量。oop

怎麼解決?
在onDestory方法中將靜態變量activity置空,這樣垃圾回收器就能夠將靜態變量回收。post

@Override
    protected void onDestroy() {
        super.onDestroy();
        activity = null;
        //使用LeakCanary觀察是否有內存泄漏
        MyApplication.getRefWatcher().watch(this);
    }

內存泄漏2:靜態View

代碼以下:
MainActivity.javathis

...
    private static View view;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

LeakCanary檢測到的內存泄漏spa

這裏寫圖片描述

爲何?
上面代碼看似沒有問題,在Activity裏聲明一個靜態變量view,而後初始化,當Activity生命週期結束了內存也釋放了,可是 LeakCanary卻顯示出現了內存泄漏,爲何?問題出在這裏,View一旦被加載到界面中將會持有一個Context對象的引用,在這個例子中,這 個context對象是咱們的Activity,聲明一個靜態變量引用這個View,也就引用了activity,因此當activity生命週期結束 了,靜態View沒有清除掉,還持有activity的引用,所以內存泄漏了。線程

怎麼解決?
在onDestroy方法裏將靜態變量置空。code

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;
    MyApplication.getRefWatcher().watch(this);
}

內存泄漏3:內部類

代碼以下:
MainActivity.java

private static Object inner;
void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

使用LeakCanary檢測到的內存泄漏:

這裏寫圖片描述

爲何?
非靜態內部類會持有外部類的引用,在上面代碼中內部類持有Activity的引用,所以inner會一直持有Activity,若是Activity生命週期結束沒有清除這個引用,這樣就發生了內存泄漏。

怎麼解決?
由於非靜態內部類隱式持有外部類的強引用,因此咱們將內部類聲明成靜態的就能夠了。

void createInnerClass() {
    static class InnerClass {
    }
    inner = new InnerClass();
}

內存泄漏4:匿名類

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

使用LeakCanary檢測到的內存泄漏:

這裏寫圖片描述

爲何?
上面代碼在activity中建立了一個匿名類AsyncTask,匿名類和非靜態內部類相同,會持有外部類對象,這裏也就是activity,所以若是 你在Activity裏聲明且實例化一個匿名的AsyncTask對象,則可能會發生內存泄漏,若是這個線程在Activity銷燬後還一直在後臺執行, 那這個線程會繼續持有這個Activity的引用從而不會被GC回收,直到線程執行完成。

怎麼解決?
自定義靜態AsyncTask類,而且讓AsyncTask的週期和Activity週期保持一致,也就是在Activity生命週期結束時要將AsyncTask cancel掉。

內存泄漏5:Handler

代碼以下:
MainActivity.java

...
void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, 1000);
}

...
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
...

爲何?
當Android Application啓動之後,framework會首先幫助咱們完成UI線程的消息循環,也就是在UI線程中,Loop、MessageQueue、 Message等等這些實例已經由framework幫咱們實現了。全部的Application主要事件,好比Activity的生命週期方法、 Button的點擊事件都包含在這個Message裏面,這些Message都會加入到MessageQueue中去,因此,UI線程的消息循環貫穿於整 個Application生命週期,因此當你在UI線程中生成Handler的實例,就會持有Loop以及MessageQueue的引用。而且在 Java中非靜態內部類和匿名內持有外部類的引用,而靜態內部類則不會持有外部類的引用。

怎麼解決?
能夠由上面的結論看出,產生泄漏的根源在於匿名類持有Activity的引用,所以能夠自定義Handler和Runnable類並聲明成靜態的內部類,來解除和Activity的引用。

內存泄漏6:Thread

代碼以下:
MainActivity.java

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

爲何?
同AsyncTask同樣,這裏就不過多贅述。

怎麼解決?
那咱們自定義Thread並聲明成static這樣能夠嗎?其實這樣的作法並不推薦,由於Thread位於GC根部,DVM會和全部的活動線程保持 hard references關係,因此運行中的Thread毫不會被GC無故回收了,因此正確的解決辦法是在自定義靜態內部類的基礎上給線程加上取消機制,所以 咱們能夠在Activity的onDestroy方法中將thread關閉掉。

內存泄漏7:Timer Tasks

代碼以下:
MainActivity.java

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    },1000);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }

爲何?
這裏內存泄漏在於Timer和TimerTask沒有進行Cancel,從而致使Timer和TimerTask一直引用外部類Activity。

怎麼解決?
在適當的時機進行Cancel。

內存泄漏8:Sensor Manager

代碼以下:
MainActivity.java

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

爲何?
經過Context調用getSystemService獲取系統服務,這些服務運行在他們本身的進程執行一系列後臺工做或者提供和硬件交互的接口,若是 Context對象須要在一個Service內部事件發生時隨時收到通知,則須要把本身做爲一個監聽器註冊進去,這樣服務就會持有一個Activity, 若是開發者忘記了在Activity被銷燬前註銷這個監聽器,這樣就致使內存泄漏。

怎麼解決?
在onDestroy方法裏註銷監聽器。

總結

在開發中,內存泄漏最壞的狀況是app耗盡內存致使崩潰,可是每每真實狀況不是這樣的,相反它只會耗盡大量內存但不至於閃退,可分配的內存少 了,GC便會更多的工做釋放內存,GC是很是耗時的操做,所以會使得頁面卡頓。咱們在開發中必定要注意當在Activity裏實例化一個對象時看看是否有 潛在的內存泄漏,必定要常常對內存泄漏進行檢測。

相關文章
相關標籤/搜索