如果一個(gè)無用對象(不需要再使用的對象)仍然被其他對象持有引用,造成該對象無法被系統(tǒng)回收咬像,以致該對象在堆中所占用的內(nèi)存單元無法被釋放而造成內(nèi)存空間浪費(fèi)算撮,這中情況就是內(nèi)存泄露。
在Android開發(fā)中县昂,一些不好的編程習(xí)慣會(huì)導(dǎo)致我們的開發(fā)的app存在內(nèi)存泄露的情況肮柜。下面介紹一些在Android開發(fā)中常見的內(nèi)存泄露場景及優(yōu)化方案。
單例導(dǎo)致內(nèi)存泄露
單例模式在Android開發(fā)中會(huì)經(jīng)常用到倒彰,但是如果使用不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄露审洞。因?yàn)閱卫撵o態(tài)特性使得它的生命周期同應(yīng)用的生命周期一樣長,如果一個(gè)對象已經(jīng)沒有用處了待讳,但是單例還持有它的引用芒澜,那么在整個(gè)應(yīng)用程序的生命周期它都不能正常被回收,從而導(dǎo)致內(nèi)存泄露创淡。
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;
}
}
像上面代碼中這樣的單例痴晦,如果我們在調(diào)用getInstance(Context context)
方法的時(shí)候傳入的context
參數(shù)是Activity
、Service
等上下文琳彩,就會(huì)導(dǎo)致內(nèi)存泄露誊酌。
以Activity
為例,當(dāng)我們啟動(dòng)一個(gè)Activity
露乏,并調(diào)用getInstance(Context context)
方法去獲取AppSettings
的單例碧浊,傳入Activity.this
作為context
,這樣AppSettings
類的單例sInstance
就持有了Activity
的引用瘟仿,當(dāng)我們退出Activity
時(shí)箱锐,該Activity
就沒有用了,但是因?yàn)?code>sIntance作為靜態(tài)單例(在應(yīng)用程序的整個(gè)生命周期中存在)會(huì)繼續(xù)持有這個(gè)Activity
的引用猾骡,導(dǎo)致這個(gè)Activity
對象無法被回收釋放瑞躺,這就造成了內(nèi)存泄露敷搪。
為了避免這樣單例導(dǎo)致內(nèi)存泄露,我們可以將context
參數(shù)改為全局的上下文:
private AppSettings(Context context) {
this.mContext = context.getApplicationContext();
}
全局的上下文Application Context
就是應(yīng)用程序的上下文幢哨,和單例的生命周期一樣長赡勘,這樣就避免了內(nèi)存泄漏。
單例模式對應(yīng)應(yīng)用程序的生命周期捞镰,所以我們在構(gòu)造單例的時(shí)候盡量避免使用Activity
的上下文闸与,而是使用Application
的上下文。
靜態(tài)變量導(dǎo)致內(nèi)存泄露
靜態(tài)變量存儲(chǔ)在方法區(qū)岸售,它的生命周期從類加載開始践樱,到整個(gè)進(jìn)程結(jié)束。一旦靜態(tài)變量初始化后凸丸,它所持有的引用只有等到進(jìn)程結(jié)束才會(huì)釋放拷邢。
比如下面這樣的情況,在Activity
中為了避免重復(fù)的創(chuàng)建info
屎慢,將sInfo
作為靜態(tài)變量:
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
的靜態(tài)成員瞭稼,并且持有Activity
的引用,但是sInfo
作為靜態(tài)變量腻惠,生命周期肯定比Activity
長环肘。所以當(dāng)Activity
退出后,sInfo
仍然引用了Activity
集灌,Activity
不能被回收悔雹,這就導(dǎo)致了內(nèi)存泄露。
在Android開發(fā)中欣喧,靜態(tài)持有很多時(shí)候都有可能因?yàn)槠涫褂玫纳芷诓灰恢露鴮?dǎo)致內(nèi)存泄露腌零,所以我們在新建靜態(tài)持有的變量的時(shí)候需要多考慮一下各個(gè)成員之間的引用關(guān)系,并且盡量少地使用靜態(tài)持有的變量续誉,以避免發(fā)生內(nèi)存泄露莱没。當(dāng)然,我們也可以在適當(dāng)?shù)臅r(shí)候講靜態(tài)量重置為null酷鸦,使其不再持有引用饰躲,這樣也可以避免內(nèi)存泄露。
非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄露
非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)默認(rèn)就會(huì)持有外部類的引用臼隔,當(dāng)非靜態(tài)內(nèi)部類對象的生命周期比外部類對象的生命周期長時(shí)嘹裂,就會(huì)導(dǎo)致內(nèi)存泄露。
非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄露在Android開發(fā)中有一種典型的場景就是使用Handler
摔握,很多開發(fā)者在使用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) {
// 做相應(yīng)邏輯
}
}
};
}
也許有人會(huì)說寄狼,mHandler
并未作為靜態(tài)變量持有Activity
引用,生命周期可能不會(huì)比Activity
長,應(yīng)該不一定會(huì)導(dǎo)致內(nèi)存泄露呢泊愧,顯然不是這樣的伊磺!
熟悉Handler
消息機(jī)制的都知道,mHandler
會(huì)作為成員變量保存在發(fā)送的消息msg
中删咱,即msg
持有mHandler
的引用屑埋,而mHandler
是Activity
的非靜態(tài)內(nèi)部類實(shí)例,即mHandler
持有Activity
的引用痰滋,那么我們就可以理解為msg
間接持有Activity
的引用摘能。msg
被發(fā)送后先放到消息隊(duì)列MessageQueue
中,然后等待Looper
的輪詢處理(MessageQueue
和Looper
都是與線程相關(guān)聯(lián)的敲街,MessageQueue
是Looper
引用的成員變量团搞,而Looper
是保存在ThreadLocal
中的)。那么當(dāng)Activity
退出后多艇,msg
可能仍然存在于消息對列MessageQueue
中未處理或者正在處理逻恐,那么這樣就會(huì)導(dǎo)致Activity
無法被回收,以致發(fā)生Activity
的內(nèi)存泄露墩蔓。
通常在Android開發(fā)中如果要使用內(nèi)部類梢莽,但又要規(guī)避內(nèi)存泄露,一般都會(huì)采用靜態(tài)內(nèi)部類+弱引用的方式奸披。
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) {
// 做相應(yīng)邏輯
}
}
}
}
}
mHandler
通過弱引用的方式持有Activity
,當(dāng)GC執(zhí)行垃圾回收時(shí)涮雷,遇到Activity
就會(huì)回收并釋放所占據(jù)的內(nèi)存單元阵面。這樣就不會(huì)發(fā)生內(nèi)存泄露了。
上面的做法確實(shí)避免了Activity
導(dǎo)致的內(nèi)存泄露洪鸭,發(fā)送的msg
不再已經(jīng)沒有持有Activity
的引用了样刷,但是msg
還是有可能存在消息隊(duì)列MessageQueue
中,所以更好的是在Activity
銷毀時(shí)就將mHandler
的回調(diào)和發(fā)送的消息給移除掉览爵。
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
非靜態(tài)內(nèi)部類造成內(nèi)存泄露還有一種情況就是使用Thread
或者AsyncTask
置鼻。
比如在Activity中直接new
一個(gè)子線程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() {
// 模擬相應(yīng)耗時(shí)邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
或者直接新建AsyncTask
異步任務(wù):
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) {
// 模擬相應(yīng)耗時(shí)邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}
很多初學(xué)者都會(huì)像上面這樣新建線程和異步任務(wù),殊不知這樣的寫法非常地不友好蜓竹,這種方式新建的子線程Thread
和AsyncTask
都是匿名內(nèi)部類對象箕母,默認(rèn)就隱式的持有外部Activity
的引用,導(dǎo)致Activity
內(nèi)存泄露俱济。要避免內(nèi)存泄露的話還是需要像上面Handler
一樣使用靜態(tài)內(nèi)部類+弱應(yīng)用的方式(代碼就不列了嘶是,參考上面Hanlder
的正確寫法)。
未取消注冊或回調(diào)導(dǎo)致內(nèi)存泄露
比如我們在Activity
中注冊廣播蛛碌,如果在Activity
銷毀后不取消注冊聂喇,那么這個(gè)剛播會(huì)一直存在系統(tǒng)中,同上面所說的非靜態(tài)內(nèi)部類一樣持有Activity
引用蔚携,導(dǎo)致內(nèi)存泄露希太。因此注冊廣播后在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);
}
}
在注冊觀察則模式的時(shí)候,如果不及時(shí)取消也會(huì)造成內(nèi)存泄露誊辉。比如使用Retrofit+RxJava
注冊網(wǎng)絡(luò)請求的觀察者回調(diào)矾湃,同樣作為匿名內(nèi)部類持有外部引用,所以需要記得在不用或者銷毀的時(shí)候取消注冊芥映。
Timer和TimerTask導(dǎo)致內(nèi)存泄露
Timer
和TimerTask
在Android中通常會(huì)被用來做一些計(jì)時(shí)或循環(huán)任務(wù)洲尊,比如實(shí)現(xiàn)無限輪播的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();
}
}
當(dāng)我們Activity
銷毀的時(shí),有可能Timer
還在繼續(xù)等待執(zhí)行TimerTask
奈偏,它持有Activity的引用不能被回收坞嘀,因此當(dāng)我們Activity銷毀的時(shí)候要立即cancel
掉Timer
和TimerTask
,以避免發(fā)生內(nèi)存泄漏惊来。
集合中的對象未清理造成內(nèi)存泄露
這個(gè)比較好理解丽涩,如果一個(gè)對象放入到ArrayList
、HashMap
等集合中裁蚁,這個(gè)集合就會(huì)持有該對象的引用矢渊。當(dāng)我們不再需要這個(gè)對象時(shí),也并沒有將它從集合中移除枉证,這樣只要集合還在使用(而此對象已經(jīng)無用了)矮男,這個(gè)對象就造成了內(nèi)存泄露。并且如果集合被靜態(tài)引用的話室谚,集合里面那些沒有用的對象更會(huì)造成內(nèi)存泄露了毡鉴。所以在使用集合時(shí)要及時(shí)將不用的對象從集合remove
,或者clear
集合秒赤,以避免內(nèi)存泄漏猪瞬。
資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露
在使用IO
、File
流或者Sqlite
入篮、Cursor
等資源時(shí)要及時(shí)關(guān)閉陈瘦。這些資源在進(jìn)行讀寫操作時(shí)通常都使用了緩沖,如果及時(shí)不關(guān)閉潮售,這些緩沖對象就會(huì)一直被占用而得不到釋放痊项,以致發(fā)生內(nèi)存泄露。因此我們在不需要使用它們的時(shí)候就及時(shí)關(guān)閉饲做,以便緩沖能及時(shí)得到釋放线婚,從而避免內(nèi)存泄露。
屬性動(dòng)畫造成內(nèi)存泄露
動(dòng)畫同樣是一個(gè)耗時(shí)任務(wù)盆均,比如在Activity
中啟動(dòng)了屬性動(dòng)畫(ObjectAnimator
)塞弊,但是在銷毀的時(shí)候,沒有調(diào)用cancle
方法,雖然我們看不到動(dòng)畫了游沿,但是這個(gè)動(dòng)畫依然會(huì)不斷地播放下去饰抒,動(dòng)畫引用所在的控件,所在的控件引用Activity
诀黍,這就造成Activity
無法正常釋放袋坑。因此同樣要在Activity
銷毀的時(shí)候cancel
掉屬性動(dòng)畫,避免發(fā)生內(nèi)存泄漏眯勾。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
WebView造成內(nèi)存泄露
關(guān)于WebView的內(nèi)存泄露枣宫,因?yàn)閃ebView在加載網(wǎng)頁后會(huì)長期占用內(nèi)存而不能被釋放,因此我們在Activity銷毀后要調(diào)用它的destory()
方法來銷毀它以釋放內(nèi)存吃环。
另外在查閱WebView
內(nèi)存泄露相關(guān)資料時(shí)看到這種情況:
Webview
下面的Callback
持有Activity
引用也颤,造成Webview
內(nèi)存無法釋放,即使是調(diào)用了Webview.destory()
等方法都無法解決問題(Android5.1之后)郁轻。
最終的解決方案是:在銷毀WebView
之前需要先將WebView從
父容器中移除翅娶,然后在銷毀WebView
。詳細(xì)分析過程請參考這篇文章:WebView內(nèi)存泄漏解決方法好唯。
@Override
protected void onDestroy() {
super.onDestroy();
// 先從父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
總結(jié)
內(nèi)存泄露在Android內(nèi)存優(yōu)化是一個(gè)比較重要的一個(gè)方面竭沫,很多時(shí)候程序中發(fā)生了內(nèi)存泄露我們不一定就能注意到,所有在編碼的過程要養(yǎng)成良好的習(xí)慣骑篙。