上一篇介紹了Android內存溢出,今篇我來繼續介紹一下關於Android內存優化的內存泄露。
內存泄露的基礎理解
- 一般內存泄露的原因是:由忘記釋放分配的內存導致的。(如Cursor忘記關閉等)
- 邏輯內存泄露的原因是:當一個對象已經不需要再使用了,本該被回收時,而有另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中。
這樣一方面佔用了寶貴的內存空間,這樣容易導致後續需要分配內存的時候,空閒空間不足而出現內存溢出OOM。所以我麼要理解好內存泄露,泄露形象理解成煤氣泄露,是我們不用煤氣的時候,應該是關掉閥口,但是在不正常的情況中,閥口沒有關好,導致煤氣泄露了。
Android中常見的內存泄漏彙總
我們關鍵需要做到的是:及時回收沒有使用的對象
需手動關閉的對象沒有關閉,在try/catch/finally中的處理
- HTTP
- File
- ContendProvider
- Bitmap
- Uri
- Socket
- Cursor
onDestory()或者onPause()中未及時關閉對象
部分實例:- Handler泄露:我們的Handler是要求寫出static的,但是實際開發中我們並沒有寫到,因此我們要使用弱引用和在onDestory()中removeCallbacksAndMessages(null)手動關閉。
- 廣播泄露:在手動註冊廣播時:需要退出的時候unregisterReceiver()。
- Service泄露:使用Service進行某耗時操作結束後,需要手動stopself()。
- WebView需要手動調用WebView.onPause()和WebView.destory()。
- 常用第三方/開源框架泄露:ShareSDK、JPush、BaiduMap、ButterKnife等需要注意在對應的Activity的生命週期中關閉。
這些我就不一一說明了,大家或多或少都有接觸到,這些都是需要我們手動關閉,屬於一般內存泄露。
- 然而我們可以藉助LeakCanary、MAT等工具來檢測應用程序是否存在以下幾種情況內存泄漏。
單例造成的內存泄漏
由於單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就導致了內存泄漏。如下這個典例:
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;
}
}
這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要:
1、傳入的是Application的Context:這將沒有任何問題,因爲單例的生命週期和Application的一樣長。
2、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由於該Context和Activity的生命週期一樣長(Activity間接繼承於Context),所以當前Activity退出時它的內存並不會被回收,因爲單例對象持有該Activity的引用。
所以正確的單例應該修改爲下面這種方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
這樣不管傳入什麼Context最終將使用Application的Context,而單例的生命週期和應用的一樣長,這樣就防止了內存泄漏
靜態View
有時,當一個Activity經常啓動,但是對應的View讀取非常耗時,我們可以通過靜態View變量來保持對該Activity的rootView引用。這樣就可以不用每次啓動Activity都去讀取並渲染View了。這確實是一個提高Activity啓動速度的好方法!但是要注意,一旦View attach到我們的Window上,就會持有一個Context(即Activity)的引用。而我們的View有事一個靜態變量,所以導致Activity不被回收。當然了,也不是說不能使用靜態View,但是在使用靜態View時,需要確保在資源回收時,將靜態View detach掉。
內部類
我們知道,非靜態內部類持有外部類的一個引用。因此,如果我們在一個外部類中定義一個靜態變量,這個靜態變量是引用內部類對象。將會導致內存泄漏!因爲這相當於間接導致靜態引用外部類。
static InnerClass innerClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
innerClass = this.new InnerClass();
}
class InnerClass {
//
}
匿名類
與內部類一樣,匿名類也會持有外部類的引用。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
//另一個線程中持有Activity的引用,並且不釋放
while (true) ;
}
}.execute();
}
Handler類引起內存泄漏
在Activity中定義一下Handler類:
public class MainActivity extends Activity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//TODO
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler.sendMessageDelayed(Message.obtain(), 60000);//延遲一分鐘執行
finish();
}
}
我們知道非靜態內部類會持有外部類的引用,這裏Activity finish後,延時消息會繼續存在主線程消息隊列中1分鐘,然後處理消息。而該消息引用了Activity的Handler對象,然後這個Handler又引用了這個Activity。這些引用對象會保持到該消息被處理完,這樣就導致該Activity對象無法被回收,從而導致了上面說的 Activity泄露。
要修改這個問題,把Handler類定義爲靜態,然後通過WeakReference來持有外部的Activity對象。還在onDestory()中清掉Handler消息。
public class MainActivity extends Activity {
private CustomHandler mHandler;
private static class CustomHandler extends Handler {
private WeakReference<MainActivity> mWeakReference;
public CustomHandler(MainActivity activity) {
mWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = mWeakReference.get();
if (null != activity)
switch (msg.what) {
///
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new CustomHandler(this);
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
Thread對象引起內存泄露
同Handler對象可能造成內存泄露的原理一樣,Thread的生命週期不一定是和Activity生命週期一致。
而且因爲Thread主要面向多任務,往往會造成大量的Thread實例。
據此,Thread對象有2個需要注意的泄漏點:
- 創建過多的Thread對象
- Thread對象在Activity退出後依然在後臺執行
解決方案是:
- 使用ThreadPoolExecutor,在同時做很多異步事件的時候是很常用的,這個不細說。
- 當Activity退出的時候,退出Thread。
TimerTask引起泄露
TimerTask對象在和Timer的schedule()方法配合使用的時候極容易造成內存泄露。要在合適的時候進行Cancel即可。
private void cancelTimer(){
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
監聽器管理內存泄露
當我們需要使用系統服務時,比如執行某些後臺任務、爲硬件訪問提供接口等等系統服務。我們需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果在Activity銷燬的時候沒有註銷這些監聽器,會導致內存泄漏。
void registerListener() {
SensorManager sensorManager = (SensorManager) getSystemServic(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListene(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
}
然而對於中級Android工程師的面試過程中,如果你是一名老鳥,這個問題想必已經深入你的心。但是你是一個對內存模模糊糊的工程師,你的答案可能讓面試官並不滿意。下面的總結對你或許有點幫助。
常見的內存泄露問題
- 單例造成的泄露。
- 靜態View造成的泄露。
- 內部類持有外部類實例的強引用。
- Handler、Thread耗時操作劫持Activity的外部類。
- TimerTask對象沒有及時回收和置空。
- 系統監聽器的引起的泄露。
避免內存泄露的方法
- 注意Context上下文的使用。
- 謹慎使用static。
- 使用靜態內部類來代替內部類。
- 靜態內部類使用弱引用WeakReference來引用外部類。
- 在聲明週期結束的時候釋放資源。
- 優化佈局文件,減少層級關係。
- 謹慎使用第三方框架。
- 謹慎使用多線程。
- 橫豎屏切換引起的GC。
- 避免創建不必要的對象。
還有就是如何檢測內存泄露問題了。具體操作下次再詳細分析
- 善用Android Studio的標籤Memory。
- LeakCanary的使用。
- MAT的使用。