Android內存優化11 內存泄漏常見情況2 內部類泄漏


線程持久化

Java中的Thread有一個特點就是她們都是直接被GC Root所引用,也就是說Dalvik虛擬機對所有被激活狀態的線程都是持有強引用,導致GC永遠都無法回收掉這些線程對象,除非線程被手動停止並置為null或者用戶直接kill進程操作。所以當使用線程時,一定要考慮在Activity退出時,及時將線程也停止並釋放掉

內存泄漏1:AsyncTask

void startAsyncTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { while(true); } }.execute(); } super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View aicButton = findViewById(R.id.at_button); aicButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); nextActivity(); } });

 

使用LeakCanary檢測到的內存泄漏:

這里寫圖片描述

為什么?
上面代碼在activity中創建了一個匿名類AsyncTask,匿名類和非靜態內部類相同,會持有外部類對象,這里也就是activity,因此如果你在Activity里聲明且實例化一個匿名的AsyncTask對象,則可能會發生內存泄漏,如果這個線程在Activity銷毀后還一直在后台執行,那這個線程會繼續持有這個Activity的引用從而不會被GC回收,直到線程執行完成。

怎么解決?
自定義靜態AsyncTask類,並且讓AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期結束時要將AsyncTask cancel掉。

內存泄漏2:Handler

非靜態內部類導致的內存泄露在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.sendMessageDelayed(msg,1000); } private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { // 做相應邏輯 } } }; } 

也許有人會說,mHandler並未作為靜態變量持有Activity引用,生命周期可能不會比Activity長,應該不一定會導致內存泄露呢,顯然不是這樣的!

熟悉Handler消息機制的都知道,mHandler會作為成員變量保存在發送的消息msg中,即msg持有mHandler的引用,而mHandlerActivity的非靜態內部類實例,即mHandler持有Activity的引用,那么我們就可以理解為msg間接持有Activity的引用。msg被發送后先放到消息隊列MessageQueue中,然后等待Looper的輪詢處理(MessageQueueLooper都是與線程相關聯的,MessageQueueLooper引用的成員變量,而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); }

為什么?

創建的Handler對象為匿名類,匿名類默認持有外部類activity, Handler通過發送Message與主線程交互,Message發出之后是存儲在MessageQueue中的,有些Message也不是馬上就被處理的。這時activity被handler持有
handler被message持有,message被messagequeue持有,message queue被loop持有,主線程的loop是全局存在的,這時就造成activity被臨時性持久化,造成臨時性內存泄漏

怎么解決?
可以由上面的結論看出,產生泄漏的根源在於匿名類持有Activity的引用,因此可以自定義Handler和Runnable類並聲明成靜態的內部類,來解除和Activity的引用。或者在activity 結束時,將發送的Message移除

內存泄漏3:Thread

代碼如下:
MainActivity.java

void spawnThread() { new Thread() { @Override public void run() { while(true); } }.start(); } View tButton = findViewById(R.id.t_button); tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { spawnThread(); nextActivity(); } }); 

 

為什么?
Java中的Thread有一個特點就是她們都是直接被GC Root所引用,也就是說Dalvik虛擬機對所有被激活狀態的線程都是持有強引用,導致GC永遠都無法回收掉這些線程對象,除非線程被手動停止並置為null或者用戶直接kill進程操作。看到這相信你應該也是心中有答案了吧 : 我在每一個MainActivity中都創建了一個線程,此線程會持有MainActivity的引用,即使退出Activity當前線程因為是直接被GC Root引用所以不會被回收掉,導致MainActivity也無法被GC回收

怎么解決?
當使用線程時,一定要考慮在Activity退出時,及時將線程也停止並釋放掉

內存泄漏4:Timer Tasks

TimerTimerTask在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銷毀的時候要立即cancelTimerTimerTask,以避免發生內存泄漏。

為什么?
這里內存泄漏在於Timer和TimerTask沒有進行Cancel,從而導致Timer和TimerTask一直引用外部類Activity。

怎么解決?
在適當的時機進行Cancel。

內存泄漏5:屬性動畫造成內存泄露

動畫同樣是一個耗時任務,比如在Activity中啟動了屬性動畫(ObjectAnimator),但是在銷毀的時候,沒有調用cancle方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity,這就造成Activity無法正常釋放。因此同樣要在Activity銷毀的時候cancel掉屬性動畫,避免發生內存泄漏。

@Override protected void onDestroy() { super.onDestroy(); mAnimator.cancel(); } 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM