如果一個無用物件(不需要再使用的物件)仍然被其他物件持有參考,造成該物件無法被系統回收,以致該物件在堆中所占用的記憶體單元無法被釋放而造成記憶體空間浪費,這中情況就是記憶體泄露,
在Android開發中,一些不好的編程習慣會導致我們的開發的app存在記憶體泄露的情況,下面介紹一些在Android開發面試中常見的記憶體泄露場景及優化方案,
單例導致記憶體泄露
單例模式在Android開發中會經常用到,但是如果使用不當就會導致記憶體泄露,因為單例的靜態特性使得它的生命周期同應用的生命周期一樣長,如果一個物件已經沒有用處了,但是單例還持有它的參考,那么在整個應用程式的生命周期它都不能正常被回收,從而導致記憶體泄露,
public class AppSettings {
private static AppSettings sInstance;
private Context mContext;
private AppSettings(Context context) {
this.mContext = context;
}
public static AppSettings getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppSettings(context);
}
return sInstance;
}
}
像上面代碼中這樣的單例,如果我們在呼叫getInstance(Context context)方法的時候傳入的context引數是Activity、Service等背景關系,就會導致記憶體泄露,
以Activity為例,當我們啟動一個Activity,并呼叫getInstance(Context context)方法去獲取AppSettings的單例,傳入Activity.this作為context,這樣AppSettings類的單例sInstance就持有了Activity的參考,當我們退出Activity時,該Activity就沒有用了,但是因為sIntance作為靜態單例(在應用程式的整個生命周期中存在)會繼續持有這個Activity的參考,導致這個Activity物件無法被回收釋放,這就造成了記憶體泄露,
為了避免這樣單例導致記憶體泄露,我們可以將context引數改為全域的背景關系:
private AppSettings(Context context) {
this.mContext = context.getApplicationContext();
}
全域的背景關系Application Context就是應用程式的背景關系,和單例的生命周期一樣長,這樣就避免了記憶體泄漏,
單例模式對應應用程式的生命周期,所以我們在構造單例的時候盡量避免使用Activity的背景關系,而是使用Application的背景關系,
靜態變數導致記憶體泄露
靜態變數存盤在方法區,它的生命周期從類加載開始,到整個行程結束,一旦靜態變數初始化后,它所持有的參考只有等到行程結束才會釋放,
比如下面這樣的情況,在Activity中為了避免重復的創建info,將sInfo作為靜態變數:
public class MainActivity extends AppCompatActivity {
private static Info sInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInfo != null) {
sInfo = new Info(this);
}
}
}
class Info {
public Info(Activity activity) {
}
}
Info作為Activity的靜態成員,并且持有Activity的參考,但是sInfo作為靜態變數,生命周期肯定比Activity長,所以當Activity退出后,sInfo仍然參考了Activity,Activity不能被回收,這就導致了記憶體泄露,
在Android開發中,靜態持有很多時候都有可能因為其使用的生命周期不一致而導致記憶體泄露,所以我們在新建靜態持有的變數的時候需要多考慮一下各個成員之間的參考關系,并且盡量少地使用靜態持有的變數,以避免發生記憶體泄露,當然,我們也可以在適當的時候講靜態量重置為null,使其不再持有參考,這樣也可以避免記憶體泄露,
非靜態內部類導致記憶體泄露
非靜態內部類(包括匿名內部類)默認就會持有外部類的參考,當非靜態內部類物件的生命周期比外部類物件的生命周期長時,就會導致記憶體泄露,
非靜態內部類導致的記憶體泄露在Android開發中有一種典型的場景就是使用Handler,很多開發者在使用Handler是這樣寫的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相應邏輯
}
}
};
}
也許有人會說,mHandler并未作為靜態變數持有Activity參考,生命周期可能不會比Activity長,應該不一定會導致記憶體泄露呢,顯然不是這樣的!
熟悉Handler訊息機制的都知道,mHandler會作為成員變數保存在發送的訊息msg中,即msg持有mHandler的參考,而mHandler是Activity的非靜態內部類實體,即mHandler持有Activity的參考,那么我們就可以理解為msg間接持有Activity的參考,msg被發送后先放到訊息佇列MessageQueue中,然后等待Looper的輪詢處理(MessageQueue和Looper都是與執行緒相關聯的,MessageQueue是Looper參考的成員變數,而Looper是保存在ThreadLocal中的),那么當Activity退出后,msg可能仍然存在于訊息對列MessageQueue中未處理或者正在處理,那么這樣就會導致Activity無法被回收,以致發生Activity的記憶體泄露,
通常在Android開發中如果要使用內部類,但又要規避記憶體泄露,一般都會采用靜態內部類+弱參考的方式,
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相應邏輯
}
}
}
}
}
mHandler通過弱參考的方式持有Activity,當GC執行垃圾回收時,遇到Activity就會回收并釋放所占據的記憶體單元,這樣就不會發生記憶體泄露了,
上面的做法確實避免了Activity導致的記憶體泄露,發送的msg不再已經沒有持有Activity的參考了,但是msg還是有可能存在訊息佇列MessageQueue中,所以更好的是在Activity銷毀時就將mHandler的回呼和發送的訊息給移除掉,
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
非靜態內部類造成記憶體泄露還有一種情況就是使用Thread或者AsyncTask,
比如在Activity中直接new一個子執行緒Thread:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
// 模擬相應耗時邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
或者直接新建AsyncTask異步任務:
public class MainActivity extends AppCompatActivity {
@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) {
// 模擬相應耗時邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}
很多初學者都會像上面這樣新建執行緒和異步任務,殊不知這樣的寫法非常地不友好,這種方式新建的子執行緒Thread和AsyncTask都是匿名內部類物件,默認就隱式的持有外部Activity的參考,導致Activity記憶體泄露,要避免記憶體泄露的話還是需要像上面Handler一樣使用靜態內部類+弱應用的方式(代碼就不列了,參考上面Hanlder的正確寫法),
未取消注冊或回呼導致記憶體泄露
比如我們在Activity中注冊廣播,如果在Activity銷毀后不取消注冊,那么這個剛播會一直存在系統中,同上面所說的非靜態內部類一樣持有Activity參考,導致記憶體泄露,因此注冊廣播后在Activity銷毀后一定要取消注冊,
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.registerReceiver(mReceiver, new IntentFilter());
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 接收到廣播需要做的邏輯
}
};
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}
}
在注冊觀察則模式的時候,如果不及時取消也會造成記憶體泄露,比如使用Retrofit+RxJava注冊網路請求的觀察者回呼,同樣作為匿名內部類持有外部參考,所以需要記得在不用或者銷毀的時候取消注冊,
Timer和TimerTask導致記憶體泄露
Timer和TimerTask在Android中通常會被用來做一些計時或回圈任務,比如實作無限輪播的ViewPager:
public class MainActivity extends AppCompatActivity {
private ViewPager mViewPager;
private PagerAdapter mAdapter;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}
private void init() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mAdapter = new ViewPagerAdapter();
mViewPager.setAdapter(mAdapter);
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}
private void loopViewpager() {
if (mAdapter.getCount() > 0) {
int curPos = mViewPager.getCurrentItem();
curPos = (++curPos) % mAdapter.getCount();
mViewPager.setCurrentItem(curPos);
}
}
private void stopLoopViewPager() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopLoopViewPager();
}
}
當我們Activity銷毀的時,有可能Timer還在繼續等待執行TimerTask,它持有Activity的參考不能被回收,因此當我們Activity銷毀的時候要立即cancel掉Timer和TimerTask,以避免發生記憶體泄漏,
集合中的物件未清理造成記憶體泄露
這個比較好理解,如果一個物件放入到ArrayList、HashMap等集合中,這個集合就會持有該物件的參考,當我們不再需要這個物件時,也并沒有將它從集合中移除,這樣只要集合還在使用(而此物件已經無用了),這個物件就造成了記憶體泄露,并且如果集合被靜態參考的話,集合里面那些沒有用的物件更會造成記憶體泄露了,所以在使用集合時要及時將不用的物件從集合remove,或者clear集合,以避免記憶體泄漏,
資源未關倍訓釋放導致記憶體泄露
在使用IO、File流或者Sqlite、Cursor等資源時要及時關閉,這些資源在進行讀寫操作時通常都使用了緩沖,如果及時不關閉,這些緩沖物件就會一直被占用而得不到釋放,以致發生記憶體泄露,因此我們在不需要使用它們的時候就及時關閉,以便緩沖能及時得到釋放,從而避免記憶體泄露,
屬性影片造成記憶體泄露
影片同樣是一個耗時任務,比如在Activity中啟動了屬性影片(ObjectAnimator),但是在銷毀的時候,沒有呼叫cancle方法,雖然我們看不到影片了,但是這個影片依然會不斷地播放下去,影片參考所在的控制元件,所在的控制元件參考Activity,這就造成Activity無法正常釋放,因此同樣要在Activity銷毀的時候cancel掉屬性影片,避免發生記憶體泄漏,
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
WebView造成記憶體泄露
關于WebView的記憶體泄露,因為WebView在加載網頁后會長期占用記憶體而不能被釋放,因此我們在Activity銷毀后要呼叫它的destory()方法來銷毀它以釋放記憶體,
另外在查閱WebView記憶體泄露相關資料時看到這種情況:
Webview下面的Callback持有Activity參考,造成Webview記憶體無法釋放,即使是呼叫了Webview.destory()等方法都無法解決問題(Android5.1之后),
最終的解決方案是:在銷毀WebView之前需要先將WebView從父容器中移除,然后在銷毀WebView,詳細分析程序請參考這篇文章:WebView記憶體泄漏解決方法,
@Override
protected void onDestroy() {
super.onDestroy();
// 先從父控制元件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
總結
記憶體泄露在Android記憶體優化是一個比較重要的一個方面,很多時候程式中發生了記憶體泄露我們不一定就能注意到,所有在編碼的程序要養成良好的習慣,總結下來只要做到以下這幾點就能避免大多數情況的記憶體泄漏:
- 構造單例的時候盡量別用
Activity的參考; - 靜態參考時注意應用物件的置慷訓者少用靜態參考;
- 使用靜態內部類+軟參考代替非靜態內部類;
- 及時取消廣播或者觀察者注冊;
- 耗時任務、屬性影片在
Activity銷毀時記得cancel; - 檔案流、
Cursor等資源及時關閉; Activity銷毀時WebView的移除和銷毀,
PS:關于我(作者)

本人是一個擁有6年開發經驗的帥氣Android攻城獅,記得看完點贊,養成良好的閱讀習慣,微信搜一搜「 程式猿養成中心 」關注這個喜歡寫干貨的程式員,
另外耗時兩年整理收集的Android一線大廠面試完整考點PDF出爐,資料【完整版】已更新在我的【Github】,有面試需要的朋友們可以去參考參考,如果對你有幫助,可以點個Star哦!
地址:【https://github.com/733gh/xiongfan】
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/199104.html
標籤:其他
上一篇:rem在專案中的具體使用分析
下一篇:釘釘導航欄分享按鈕的顯示/隱藏
