線程持久化
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
的引用,而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); }
為什么?
創建的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
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
,以避免發生內存泄漏。
為什么?
這里內存泄漏在於Timer和TimerTask沒有進行Cancel,從而導致Timer和TimerTask一直引用外部類Activity。
怎么解決?
在適當的時機進行Cancel。
內存泄漏5:屬性動畫造成內存泄露
動畫同樣是一個耗時任務,比如在Activity
中啟動了屬性動畫(ObjectAnimator
),但是在銷毀的時候,沒有調用cancle
方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity
,這就造成Activity
無法正常釋放。因此同樣要在Activity
銷毀的時候cancel
掉屬性動畫,避免發生內存泄漏。
@Override protected void onDestroy() { super.onDestroy(); mAnimator.cancel(); }