詳解Android內(nèi)存泄露及優(yōu)化方案
一、常見(jiàn)的內(nèi)存泄露應(yīng)用場(chǎng)景?
1、單例的不恰當(dāng)使用
單例是我們開(kāi)發(fā)中最常見(jiàn)和使用最頻繁的設(shè)計(jì)模式之一,所以如果使用不當(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)用程序的生命周期這個(gè)對(duì)象都不能正常被回收,從而導(dǎo)致內(nèi)存泄露。
如:
public class App { private static App sInstance; private Context mContext; private App(Context context) { this.mContext = context; } public static App getInstance(Context context) { if (sInstance == null) { sInstance = new App(context); } return sInstance; } }
調(diào)用getInstance(Context context)方法時(shí)傳入的上下文如果為當(dāng)前活動(dòng)Activity或者當(dāng)前服務(wù)的Service以及當(dāng)前fragment的上下文,當(dāng)他們銷(xiāo)毀時(shí),這個(gè)靜態(tài)單例sIntance還會(huì)持用他們的引用,從而導(dǎo)致當(dāng)前活動(dòng)、服務(wù)、fragment等對(duì)象不能被回收釋放,從而導(dǎo)致內(nèi)存泄漏。這種上下文的使用很多時(shí)候處理不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄漏,需要我們多注意編碼規(guī)范。
2、靜態(tài)變量導(dǎo)致內(nèi)存泄露
靜態(tài)變量存儲(chǔ)在方法區(qū),它的生命周期從類(lèi)加載開(kāi)始,到整個(gè)進(jìn)程結(jié)束。一旦靜態(tài)變量初始化后, 它所持有的引用只有等到進(jìn)程結(jié)束才會(huì)釋放。
如下代碼:
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)存泄露。
3、非靜態(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)存泄露。這類(lèi)內(nèi)存泄漏很典型的Handler的使用,這么一說(shuō)大家應(yīng)該就很熟悉了吧,大家都知道怎么處理這類(lèi)內(nèi)存泄漏。
Handler的使用示例:
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) { // ui更新 } } };
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)存泄露。
如何避免:
1、采用靜態(tài)內(nèi)部類(lèi)+弱引用的方式
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)存泄露了。但是 msg 還是有可能存在消息隊(duì)列 MessageQueue 中。
2、Activity 銷(xiāo)毀時(shí)就將 mHandler 的回調(diào)和發(fā)送的消息給移除掉。
@Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
非靜態(tài)內(nèi)部類(lèi)造成內(nèi)存泄露還有一種情況就是使用 Thread 或者 AsyncTask異步調(diào)用:
如示例:
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) { // UI線程處理 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }.execute(); } }
以上新建的子線程 Thread 和 AsyncTask 都是匿名內(nèi)部類(lèi)對(duì)象,默認(rèn)就隱式的持有外部 Activity 的引用, 導(dǎo)致 Activity 內(nèi)存泄露。要避免內(nèi)存泄露的話還是需要像上面 Handler 一樣使用采用靜態(tài)內(nèi)部類(lèi)+弱引用的方式(如上面Hanlder采用靜態(tài)內(nèi)部類(lèi)+弱引用的方式)。
4、未取消注冊(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è)。
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è)。
5、定時(shí)器Timer 和 TimerTask 導(dǎo)致內(nèi)存泄露
當(dāng)我們 Activity 銷(xiāo)毀的時(shí),有可能 Timer 還在繼續(xù)等待執(zhí)行 TimerTask,它持有 Activity 的引用不 能被回收,因此當(dāng)我們 Activity 銷(xiāo)毀的時(shí)候要立即 cancel 掉 Timer 和 TimerTask,以避免發(fā)生內(nèi)存 泄漏。
6、集合中的對(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)存泄漏。
7、資源未關(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)存泄露。
8、動(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)存泄漏。
@Override protected void onDestroy() { super.onDestroy(); mAnimator.cancel(); }
9、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。
@Override protected void onDestroy() { super.onDestroy(); // 先從父控件中移除 WebView mWebViewContainer.removeView(mWebView); mWebView.stopLoading(); mWebView.getSettings().setJavaScriptEnabled(false); mWebView.clearHistory(); mWebView.removeAllViews(); mWebView.destroy(); }
總結(jié)
構(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)毀。
下一篇繼續(xù):詳解Android內(nèi)存優(yōu)化策略
ps:內(nèi)存泄漏是開(kāi)發(fā)中的一個(gè)痛點(diǎn),需要我們有很好的良好編碼習(xí)慣。奧里給?。。。。。。。?!
到此這篇關(guān)于詳解Android內(nèi)存泄露及優(yōu)化方案一的文章就介紹到這了,更多相關(guān)Android內(nèi)存優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開(kāi)發(fā)之BroadcastReceiver用法實(shí)例分析
這篇文章主要介紹了Android開(kāi)發(fā)之BroadcastReceiver用法,實(shí)例分析了Android中廣播的相關(guān)使用技巧,需要的朋友可以參考下2015-05-05Eclipse NDK遷移到Android Studio的方法示例
本篇文章主要介紹了Eclipse NDK遷移到Android Studio的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03Android Studio 中aidl的自定義類(lèi)的使用詳解
這篇文章主要介紹了Android Studio 中aidl的自定義類(lèi)的使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03漂亮的Android音樂(lè)歌詞控件 仿網(wǎng)易云音樂(lè)滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了漂亮的Android音樂(lè)歌詞控件,仿網(wǎng)易云音樂(lè)滑動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Android Studio實(shí)現(xiàn)簡(jiǎn)單的QQ登錄界面的示例代碼
這篇文章主要介紹了Android Studio實(shí)現(xiàn)簡(jiǎn)單的QQ登錄界面的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Android四種數(shù)據(jù)存儲(chǔ)的應(yīng)用方式
這篇文章主要介紹了Android四種數(shù)據(jù)存儲(chǔ)的應(yīng)用方式的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家理解掌握Android存儲(chǔ)數(shù)據(jù)的方法,需要的朋友可以參考下2017-10-10Android屏幕適配工具類(lèi) Android自動(dòng)生成不同分辨率的值
這篇文章主要為大家詳細(xì)介紹了Android屏幕適配工具類(lèi),Android自動(dòng)生成不同分辨率的值,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03Android圖像視圖ImageView實(shí)現(xiàn)圖像拉伸效果
這篇文章主要為大家詳細(xì)介紹了Android圖像視圖ImageView實(shí)現(xiàn)圖像拉伸演示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05