內存泄露的原因
內存泄露是指不在需要的對象仍然被引用不能被GC回收釋放,這句話你可能看到過不止一遍了,下面我們來深入研究一下這句話。
首先了解一下兩個名詞:
GC: 垃圾回收器,會自動回收不在被引用的內存數據
GC Roots:不能被回收的對象(這裏的解釋不是很好,往下看就明白了)
GC Roots持有的對象都不能被垃圾回收器回收,所以這裏的Object D不能被回收,Object G可以被回收,那麼那些對象可以作爲GC Roots呢,有以下對象:
JavaStack(棧)中的引用的對象
方法區中靜態引用指向的對象
方法區中常量引用指向的對象
Native方法中JNI引用的對象
Thread——活着的線程
可能現在你對這些還不是很明白,不要着急,下面我們講具體的內存泄露的例子你就清楚了。
1.單例模式導致的內存泄露
直接上代碼:
public class MyUtil {
private volatile static MyUtil myUtil;
private Context context;
private MyUtil(Context context) {
this.context = context;
}
public static MyUtil getMyUtil(Context context) {
if (myUtil == null) {
synchronized (MyUtil.class){
if (myUtil == null) {
myUtil = new MyUtil(context);
}
}
}
return myUtil;
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyUtil myUtil = MyUtil.getMyUtil(this);
}
}
可以看到上面的MyUtil是以一個很普遍的單例類並且在第一次創建的時候傳入context,然後在Activity中初始化這個單例,這樣單例對象級持有了這個Activity的引用,當退出Activity的時候由於被單例對象所持有就不能被GC回收,這就造成了內存泄漏。解決方法也很簡單,在application中初始化就可以了,這樣單例對象會持有application的引用。
注:其實這就是我們上面說的GC Roots中的靜態引用,靜態對象引用context導致對象不能被回收。
2.匿名內部類導致的內存泄露
其實內部類並不會直接導致內存泄露,而是很多情況下我們使用不當導致的。
我們知道內部類會持有外部類的引用,當內部類對象被GC Roots直接或間接持有導致不能回收釋放的時候外部類也會被連帶着不能回收釋放,這就是導致內存泄露的原因。
我們看一個非常常見的例子:
public class MainActivity extends AppCompatActivity {
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, 1000 * 60);
}
}
這裏我們創建了一個匿名內部類並實例化了一個對象handler,然後在Activity中發送一個延時事件,這時退出當前Activity會發生內存泄露嗎,如果延時任務已經結束就不會內存泄漏,如果延時任務沒有結束就會發生內存泄露,因爲當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主線程中,它持有該 Activity 的 Handler 引用,然後又因 爲 Handler 爲匿名內部類,它會持有外部類的引用,所以此時 finish() 掉的 Activity 就不會被回收了,從而造成內存泄漏。
修復方法:在 Activity 中避免使用非靜態內部類或匿名內部類,比如將 Handler 聲明爲靜態的,則其存活期跟 Activity 的生命週期就無關了。如果需要用到Activity,就通過弱引用的方式引入 Activity,避免直接將 Activity 作爲 context 傳進去。另外, Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。
public class MainActivity extends AppCompatActivity {
private MyHandler handler
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = activityWeakReference.get();
if (activity != null) {
//do something
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new MyHandler(this);
handler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, 1000 * 60);
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
}
關於引用類型這裏提一下:
Java中的引用有四種,分別爲強引用,軟引用,弱引用,虛引用。
強引用(Strong References):強應用的對象永遠不會被回收,即使系統內存溢出也不會回收
軟應用(Soft References):只有當系統的內存不夠的情況下才會去回收
弱引用(Weak References):弱引用會被jvm忽略,也就說在GC進行垃圾收集的時候,如果一個對象只有弱引用指向它,那麼和沒有引用指向它是一樣的效果,jvm都會對它就行果斷的銷燬,釋放內存。
虛引用(Phantom References):虛幻應用和弱引用的回收機制差不多,都是可以被隨時回收的。但是不同的地方是,它的構造方法必須強制傳入ReferenceQueue,因爲在jvm回收前(重點: 對,就是回收前,軟引用和弱引用都是回收後),會將PhantomReference對象加入ReferenceQueue中; 還有一點就是PhantomReference.get()方法永遠返回空,不管對象有沒有被回收。
3.InputMethodManager導致的內存泄露
InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1
當我們在代碼中獲取InputMethodManager之後再退出當前的頁面就會發生內存泄漏,這是Android的一個bug,直到Android6.0之後才修復,下面我們看一下解決辦法:
public static void fixSoftInputLeaks(final Context context) {
if (context == null) return;
InputMethodManager imm =
(InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return;
String[] strArr = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
for (int i = 0; i < 3; i++) {
try {
Field declaredField = imm.getClass().getDeclaredField(strArr[i]);
if (declaredField == null) continue;
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
Object obj = declaredField.get(imm);
if (obj == null || !(obj instanceof View)) continue;
View view = (View) obj;
if (view.getContext() == context) {
declaredField.set(imm, null);
} else {
return;
}
} catch (Throwable th) {
th.printStackTrace();
}
}
}
這裏是通過反射將InputMethodManager中的”mCurRootView”, “mServedView”, “mNextServedView”三個變量致空。
檢測內存泄漏的工具
LeakCanary是Android平臺一個非常好用的內存泄漏檢測工具,我們只需要將他放到我們的項目中,然後打包app運行到手機上就能自動幫我們找到內存泄漏的位置,但是他也不是百分比能檢測出來,而且有時可能還會出現檢測出錯的情況,但這絲毫不影響他好用的優點。