如果一個(gè)無(wú)用對(duì)象(不需要再使用的對(duì)象)仍然被其他對(duì)象持有引用昔园,造成該對(duì)象無(wú)法被系統(tǒng)回
收卑吭,以致該對(duì)象在堆中所占用的內(nèi)存單元無(wú)法被釋放而造成內(nèi)存空間浪費(fèi),這中情況就是內(nèi)存泄
露。在 Android 開(kāi)發(fā)中峡懈,一些不好的編程習(xí)慣會(huì)導(dǎo)致我們的開(kāi)發(fā)的 app 存在內(nèi)存泄露的情況说墨。下面介紹一些在 Android 開(kāi)發(fā)中常見(jiàn)的內(nèi)存泄露場(chǎng)景及優(yōu)化方案骏全。
單例導(dǎo)致內(nèi)存泄露
單例模式在 Android 開(kāi)發(fā)中會(huì)經(jīng)常用到,但是如果使用不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄露尼斧。因?yàn)閱卫撵o態(tài)
特性使得它的生命周期同應(yīng)用的生命周期一樣長(zhǎng)姜贡,如果一個(gè)對(duì)象已經(jīng)沒(méi)有用處了,但是單例還持
有它的引用棺棵,那么在整個(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;
}
}
像上面代碼中這樣的單例,如果我們?cè)谡{(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 類(lèi)的單例 sInstance 就持有了 Activity 的引用缕陕,當(dāng)我們退出 Activity 時(shí),該 Activity 就沒(méi)有用了疙挺,但是因?yàn)?sIntance作為靜態(tài)單例(在應(yīng)用程序的整個(gè)生命周期中存在)會(huì)繼續(xù)持有這個(gè) Activity 的引用扛邑,導(dǎo)致這個(gè)Activity 對(duì)象無(wú)法被回收釋放,這就造成了內(nèi)存泄露铐然。為了避免這樣單例導(dǎo)致內(nèi)存泄露蔬崩,我們可以將 context 參數(shù)改為全局的上下文:
private AppSettings(Context context) {
this.mContext = context.getApplicationContext();
}
全局的上下文 Application Context 就是應(yīng)用程序的上下文,和單例的生命周期一樣長(zhǎng)搀暑,這樣就避免了內(nèi)存泄漏沥阳。單例模式對(duì)應(yīng)應(yīng)用程序的生命周期,所以我們?cè)跇?gòu)造單例的時(shí)候盡量避免使用 Activity的上下文自点,而是使用 Application 的上下文桐罕。
靜態(tài)變量導(dǎo)致內(nèi)存泄露
靜態(tài)變量存儲(chǔ)在方法區(qū),它的生命周期從類(lèi)加載開(kāi)始桂敛,到整個(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 長(zhǎng)借浊。所以當(dāng) Activity 退出后眶掌,sInfo 仍然引用了 Activity,Activity 不能被回收巴碗,這就導(dǎo)致了內(nèi)存泄露。
在 Android 開(kāi)發(fā)中即寒,靜態(tài)持有很多時(shí)候都有可能因?yàn)槠涫褂玫纳芷诓灰恢露鴮?dǎo)致內(nèi)存泄露橡淆,
所以我們?cè)谛陆o態(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)部類(lèi)導(dǎo)致內(nèi)存泄露
非靜態(tài)內(nèi)部類(lèi)(包括匿名內(nèi)部類(lèi))默認(rèn)就會(huì)持有外部類(lèi)的引用,當(dāng)非靜態(tài)內(nèi)部類(lèi)對(duì)象的生命周期
比外部類(lèi)對(duì)象的生命周期長(zhǎng)時(shí)趋艘,就會(huì)導(dǎo)致內(nèi)存泄露疲恢。
非靜態(tài)內(nèi)部類(lèi)導(dǎo)致的內(nèi)存泄露在 Android 開(kāi)發(fā)中有一種典型的場(chǎng)景就是使用 Handler,很多開(kāi)發(fā)
者在使用 Handler 是這樣寫(xiě)的:
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ì)說(shuō)瓷胧,mHandler 并未作為靜態(tài)變量持有 Activity 引用显拳,生命周期可能不會(huì)比 Activity 長(zhǎng),應(yīng)該不一定會(huì)導(dǎo)致內(nèi)存泄露呢搓萧,顯然不是這樣的杂数!
熟悉 Handler 消息機(jī)制的都知道,mHandler 會(huì)作為成員變量保存在發(fā)送的消息 msg 中瘸洛,即 msg 持有mHandler 的引用揍移,而 mHandler 是 Activity 的非靜態(tài)內(nèi)部類(lèi)實(shí)例,即 mHandler 持有 Activity 的引用反肋,那么我們就可以理解為 msg 間接持有 Activity 的引用那伐。msg 被發(fā)送后先放到消息隊(duì)列MessageQueue 中,然后等待 Looper 的輪詢(xún)處理(MessageQueue 和 Looper 都是與線程相關(guān)聯(lián)的囚玫,MessageQueue 是 Looper 引用的成員變量喧锦,而 Looper 是保存ThreadLocal 中的)。那么當(dāng) Activity退出后抓督,msg 可能仍然存在于消息對(duì)列 MessageQueue 中未處理或者正在處理燃少,那么這樣就會(huì)導(dǎo)致Activity 無(wú)法被回收,以致發(fā)生 Activity 的內(nèi)存泄露铃在。通常在 Android 開(kāi)發(fā)中如果要使用內(nèi)部類(lèi)阵具,但又要規(guī)避內(nèi)存泄露,一般都會(huì)采用靜態(tài)內(nèi)部類(lè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 通過(guò)弱引用的方式持有 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)沒(méi)有持有 Activity 的引用了畸陡,但是 msg 還是有可能存在消息隊(duì)列 MessageQueue 中鹰溜,所以更好的是在 Activity 銷(xiāo)毀時(shí)就將mHandler 的回調(diào)和發(fā)送的消息給移除掉虽填。
@Overrideprotected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
非靜態(tài)內(nèi)部類(lè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ù)曹动,殊不知這樣的寫(xiě)法非常地不友好斋日,這種方式新
建的子線程 Thread 和 AsyncTask 都是匿名內(nèi)部類(lèi)對(duì)象,默認(rèn)就隱式的持有外部 Activity 的引用墓陈,導(dǎo)致 Activity 內(nèi)存泄露恶守。要避免內(nèi)存泄露的話還是需要像上面 Handler 一樣使用靜態(tài)內(nèi)部類(lèi)+弱應(yīng)用的方式(代碼就不列了,參考上面 Hanlder 的正確寫(xiě)法)贡必。
未取消注冊(cè)或回調(diào)導(dǎo)致內(nèi)存泄露比如我們?cè)?Activity 中注冊(cè)廣播兔港,如果在 Activity 銷(xiāo)毀后不取消注冊(cè),那么這個(gè)剛播會(huì)一直存在系統(tǒng)中赊级,同上面所說(shuō)的非靜態(tài)內(nèi)部類(lèi)一樣持有 Activity 引用押框,導(dǎo)致內(nèi)存泄露。因此注冊(cè)廣播后在Activity 銷(xiāo)毀后一定要取消注冊(cè)理逊。
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);
}
}
在注冊(cè)觀察則模式的時(shí)候橡伞,如果不及時(shí)取消也會(huì)造成內(nèi)存泄露。比如使用 Retrofit+RxJava 注冊(cè)網(wǎng)絡(luò)請(qǐng)求的觀察者回調(diào)晋被,同樣作為匿名內(nèi)部類(lèi)持有外部引用兑徘,所以需要記得在不用或者銷(xiāo)毀的時(shí)候
取消注冊(cè)。
Timer 和 TimerTask 導(dǎo)致內(nèi)存泄露Timer 和 TimerTask 在 Android 中通常會(huì)被用來(lái)做一些計(jì)時(shí)或循環(huán)任務(wù)羡洛,比如實(shí)現(xiàn)無(wú)限輪播的
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() {
@Overridepublic 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 銷(xiāo)毀的時(shí)挂脑,有可能 Timer 還在繼續(xù)等待執(zhí)行 TimerTask,它持有 Activity 的引用不能被回收欲侮,因此當(dāng)我們 Activity 銷(xiāo)毀的時(shí)候要立即 cancel 掉 Timer 和 TimerTask崭闲,以避免發(fā)生內(nèi)存泄漏。
集合中的對(duì)象未清理造成內(nèi)存泄露
這個(gè)比較好理解威蕉,如果一個(gè)對(duì)象放入到 ArrayList刁俭、HashMap 等集合中,這個(gè)集合就會(huì)持有該對(duì)象的引用韧涨。當(dāng)我們不再需要這個(gè)對(duì)象時(shí)牍戚,也并沒(méi)有將它從集合中移除,這樣只要集合還在使用(而此對(duì)象已經(jīng)無(wú)用了)虑粥,這個(gè)對(duì)象就造成了內(nèi)存泄露如孝。并且如果集合被靜態(tài)引用的話,集合里面那
些沒(méi)有用的對(duì)象更會(huì)造成內(nèi)存泄露了娩贷。所以在使用集合時(shí)要及時(shí)將不用的對(duì)象從集合 remove第晰,或
者 clear 集合,以避免內(nèi)存泄漏。
資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露
在使用 IO但荤、File 流或者 Sqlite罗岖、Cursor 等資源時(shí)要及時(shí)關(guān)閉。這些資源在進(jìn)行讀寫(xiě)操作時(shí)通常都使用了緩沖腹躁,如果及時(shí)不關(guān)閉,這些緩沖對(duì)象就會(huì)一直被占用而得不到釋放南蓬,以致發(fā)生內(nèi)存泄露纺非。因此我們?cè)诓恍枰褂盟鼈兊臅r(shí)候就及時(shí)關(guān)閉,以便緩沖能及時(shí)得到釋放赘方,從而避免內(nèi)存泄露烧颖。
屬性動(dòng)畫(huà)造成內(nèi)存泄露
動(dòng)畫(huà)同樣是一個(gè)耗時(shí)任務(wù),比如在 Activity 中啟動(dòng)了屬性動(dòng)畫(huà)(ObjectAnimator)窄陡,但是在銷(xiāo)毀的時(shí)候炕淮,沒(méi)有調(diào)用 cancle 方法,雖然我們看不到動(dòng)畫(huà)了跳夭,但是這個(gè)動(dòng)畫(huà)依然會(huì)不斷地播放下去涂圆,動(dòng)畫(huà)引用所在的控件,所在的控件引用 Activity币叹,這就造成 Activity 無(wú)法正常釋放润歉。因此同樣要在 Activity 銷(xiāo)毀的時(shí)候 cancel 掉屬性動(dòng)畫(huà),避免發(fā)生內(nèi)存泄漏颈抚。
@Overrideprotected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
WebView 造成內(nèi)存泄露
關(guān)于 WebView 的內(nèi)存泄露踩衩,因?yàn)?WebView 在加載網(wǎng)頁(yè)后會(huì)長(zhǎng)期占用內(nèi)存而不能被釋放,因此我們?cè)?Activity 銷(xiāo)毀后要調(diào)用它的 destory()方法來(lái)銷(xiāo)毀它以釋放內(nèi)存贩汉。另外在查閱 WebView 內(nèi)存泄露相關(guān)資料時(shí)看到這種情況:
Webview 下面的 Callback 持有 Activity 引用驱富,造成 Webview 內(nèi)存無(wú)法釋放,即使是調(diào)用了
Webview.destory()等方法都無(wú)法解決問(wèn)題(Android5.1 之后)匹舞。最終的解決方案是:在銷(xiāo)毀 WebView 之前需要先將 WebView 從父容器中移除褐鸥,然后在銷(xiāo)毀WebView。詳細(xì)分析過(guò)程請(qǐng)參考這篇文章:
(http://blog.csdn.net/xygy8860/article/details/53334476?utm_source=itdadao&utm_medium
=referral)(http://blog.csdn.net/xygy8860/article/details/53334476)[WebView 內(nèi)存泄漏解決方法]策菜。
@Overrideprotected 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)存泄露我們
不一定就能注意到,所有在編碼的過(guò)程要養(yǎng)成良好的習(xí)慣又憨〈浠簦總結(jié)下來(lái)只要做到以下這幾點(diǎn)就能避
免大多數(shù)情況的內(nèi)存泄漏:
構(gòu)造單例的時(shí)候盡量別用 Activity 的引用;
靜態(tài)引用時(shí)注意應(yīng)用對(duì)象的置空或者少用靜態(tài)引用蠢莺;
使用靜態(tài)內(nèi)部類(lèi)+軟引用代替非靜態(tài)內(nèi)部類(lèi)寒匙;
及時(shí)取消廣播或者觀察者注冊(cè);
耗時(shí)任務(wù)、屬性動(dòng)畫(huà)在 Activity 銷(xiāo)毀時(shí)記得 cancel锄弱;
文件流考蕾、Cursor 等資源及時(shí)關(guān)閉;
Activity 銷(xiāo)毀時(shí) WebView 的移除和銷(xiāo)毀会宪。