什么是內存泄漏?
簡單點說囤锉,就是指一個對象不再使用坦弟,本應該被回收,但由于某些原因導致對象無法回收官地,仍然占用著內存酿傍,這就是內存泄漏。
為什么會產生內存泄漏驱入,內存泄漏會導致什么問題赤炒?
相比C++需要手動去管理對象的創(chuàng)建和回收,Java有著自己的一套垃圾回收機制,它能夠自動回收內存,但是它往往會因為某些原因而變得“不靠譜”割卖。
在Android開發(fā)中,一些不好的編碼習慣就很可能會導致內存泄漏癣朗,而這些內存泄漏會導致應用內存越占越大,使得應用變得卡頓旺罢,甚至造成OOM(Out Of Memory)內存溢出問題旷余,同時也使應用變得極其不穩(wěn)定,因為當內存不足的時候扁达,系統(tǒng)會優(yōu)先回收那些“內存占比”大的應用正卧。
Java的內存分配機制
首先我們先來了解下Java的內存分配機制,Java 程序運行時的內存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配跪解,對應的炉旷,三種存儲策略使用的內存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)叉讥。
靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)窘行、全局 static 數(shù)據(jù)和常量。這塊內存在程序編譯時就已經(jīng)分配好图仓,并且在程序整個運行期間都存在罐盔。
棧區(qū) :當方法被執(zhí)行時,方法體內的局部變量(其中包括基礎數(shù)據(jù)類型救崔、對象的引用)都在棧上創(chuàng)建惶看,并在方法執(zhí)行結束時這些局部變量所持有的內存將會自動被釋放捏顺。因為棧內存分配運算內置于處理器的指令集中,效率很高纬黎,但是分配的內存容量有限幅骄。
堆區(qū) : 又稱動態(tài)內存分配,通常就是指在程序運行時直接 new 出來的內存本今,也就是對象的實例拆座。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。
那什么樣的對象會被回收呢诈泼?
為了更好理解 GC 的工作原理懂拾,我們可以將對象考慮為有向圖的頂點煤禽,將引用關系考慮為圖的有向邊铐达,有向邊從引用者指向被引對象。另外檬果,每個線程對象可以作為一個圖的起始頂點瓮孙,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹选脊。在這個有向圖中杭抠,根頂點可達的對象都是有效對象,GC將不回收這些對象恳啥。如果某個對象 (連通子圖)與這個根頂點不可達(注意偏灿,該圖為有向圖),那么我們認為這個(這些)對象不再被引用钝的,可以被 GC 回收翁垂。
常見的內存泄漏和解決方案
1、單例引起的內存泄漏
由于單例的靜態(tài)特性導致它的生命周期和整個應用的生命周期一樣長硝桩,如果有對象已經(jīng)不再使用了沿猜,但又卻被單例持有引用,那么就會導致這個對象就沒辦法被回收碗脊,從而導致內存泄漏啼肩。
// 使用了單例模式
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
問題所在:
從上面的代碼我們可以看出,在創(chuàng)建單例對象的時候衙伶,引入了一個Context上下文對象祈坠,如果我們把Activity注入進來,會導致這個Activity一直被單例對象持有引用矢劲,當這個Activity銷毀的時候赦拘,對象也是沒有辦法被回收的。
解決方案:
在這里我們只需要讓這個上下文對象指向應用的上下文即可(this.context=context.getApplicationContext()
)卧须,因為應用的上下文對象的生命周期和整個應用一樣長另绩。
2儒陨、非靜態(tài)內部類創(chuàng)建靜態(tài)實例引起的內存泄漏
由于非靜態(tài)內部類會默認持有外部類的引用,如果我們在外部類中去創(chuàng)建這個內部類對象笋籽,當頻繁打開關閉Activity蹦漠,會導致重復創(chuàng)建對象,造成資源的浪費车海,為了避免這個問題我們一般會把這個實例設置為靜態(tài)笛园,這樣雖然解決了重復創(chuàng)建實例,但是會引發(fā)出另一個問題侍芝,就是靜態(tài)成員變量它的生命周期是和應用的生命周期一樣長的研铆,然而這個靜態(tài)成員變量又持有該Activity的引用,所以導致這個Activity銷毀的時候州叠,對象也是無法被回收的棵红。
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
問題所在:
其實這個和上面單例對象的內容泄漏問題是一樣的,由于靜態(tài)對象持有Activity的引用咧栗,導致Activity沒辦法被回收逆甜。
解決方案:
在這里我們只需要把非靜態(tài)內部類改成靜態(tài)內部類即可(static class TestResource
)。
3致板、Handler引起的內存泄漏
記得我們剛學習Handler的時候交煞,網(wǎng)上資料甚至學校教材“教科書”式的寫法都是這樣的
Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//to do something..
switch (msg.what){
case 0:
//to do something..
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
//to do something..
mHandler.sendEmptyMessage(0);
}
}).start();
}
問題所在:
別看上面短短幾行代碼,其實涉及到了很多問題斟或,首先我們知道程序啟動時在主線程中會創(chuàng)建一個Looper對象素征,這個Looper里維護著一個MessageQueue消息隊列,這個消息隊列里會按時間順序存放著Message萝挤,不清楚的朋友可以看下我之前寫的這篇文章《從源碼的角度徹底理解Android的消息處理機制》御毅,然后上面的Handler是通過內部類來創(chuàng)建的,內部類會持有外部類的引用平斩,也就是Handler持有Activity的引用亚享,而消息隊列中的消息target是指向Handler的,也就等同消息持有Handler的引用绘面,也就是說當消息隊列中的消息如果還沒有處理完欺税,這些未處理的消息(也可以理解成延遲操作)是持有Activity的引用的,此時如果關閉Activity揭璃,是沒辦法回收的晚凿,從而就會導致內存泄露。
解決方案:
和上文一樣瘦馍,我們需要先把非靜態(tài)內部類改成靜態(tài)內部類(如果是Runnable類也需要改成靜態(tài))歼秽,然后在Activity的onDestroy中移除對應的消息,再來需要在Handler內部用弱引用持有Activity情组,因為讓內部類不再持有外部類的引用時燥筷,程序也就不允許Handler操作Activity對象了箩祥。
MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
myHandler.sendMessage(Message.obtain());
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//移除對應的Runnable或者是Message
//mHandler.removeCallbacks(runnable);
//mHandler.removeMessages(what);
mHandler.removeCallbacksAndMessages(null);
}
private static class MyHandler extends Handler {
private WeakReference<Activity> mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() == null) {
return;
}
//to do something..
}
};
4、WebView引起的內存泄露
關于WebView的內存泄漏肆氓,這是個絕對的大大大大大坑袍祖!不同版本都存在著不同版本的問題,這里我只能給出我平時的處理方法谢揪,可能不同機型上存在的差異蕉陋,只能靠積累了。
方法一:
首先不要在xml去定義<WebView/>拨扶,定義一個ViewGroup就行凳鬓,然后動態(tài)在代碼中new WebView(Context context)
(傳入的Context采取弱引用),再通過addView添加到ViewGroup中患民,最后在頁面銷毀執(zhí)行onDestroy()的時候把WebView移除缩举。
方法二:
簡單粗暴,直接為WebView新開辟一個進程酒奶,在結束操作的時候直接System.exit(0)
結束掉進程蚁孔,這里需要注意進程間的通訊,可以采取Aidl惋嚎,Messager,Content Provider站刑,Broadcast等方式另伍。
5、Asynctask引起的內存泄露
這部分和Handler比較像绞旅,其實也是因為內部類持有外部類引用摆尝,一樣的改成靜態(tài)內部類,然后在onDestory方法中取消任務即可因悲。
6堕汞、資源對象未關閉引起的內存泄露
這塊就比較簡單了,比如我們經(jīng)常使用的廣播接收者晃琳,數(shù)據(jù)庫的游標讯检,多媒體,文檔卫旱,套接字等人灼。
7、其他一些
還有一些需要注意的顾翼,比如注冊了EventBus沒注銷投放,添加Activity到棧中,銷毀的時候沒移除等适贸。
好了灸芳,以上就是比較常見的內存泄露原因和對應的解決方案涝桅,當然還有一些其他的,這里沒有辦法一一闡述烙样,還是需要大家平時不斷去積累苹支,總結,這里提供一個可以檢查內存泄露的工具LeakCanary误阻,只需要幾行代碼就可以輕松在應用內集成內存監(jiān)控功能了债蜜。