【译】Activitys, Threads和 内存泄露
Android編程中一個(gè)共同的困難就是協(xié)調(diào)Activity的生命周期和長(zhǎng)時(shí)間運(yùn)行的任務(wù)(task),并且要避免可能的內(nèi)存泄露。思考下面Activity的代碼,在它啟動(dòng)的時(shí)候開(kāi)啟一個(gè)線程并循環(huán)執(zhí)行任務(wù)。
1 /** 2 * 一個(gè)展示線程如何在配置變化中存活下來(lái)的例子(配置變化會(huì)導(dǎo)致創(chuàng) 3 * 建線程的Activity被銷毀)。代碼中的Activity泄露了,因?yàn)榫€程被實(shí) 4 * 例為一個(gè)匿名類實(shí)例,它隱式地持有外部Activity實(shí)例,因此阻止Activity 5 * 被回收。 6 */ 7 public class MainActivity extends Activity { 8 9 @Override 10 protected void onCreate(Bundle savedInstanceState) { 11 super.onCreate(savedInstanceState); 12 exampleOne(); 13 } 14 15 private void exampleOne() { 16 new Thread() { 17 @Override 18 public void run() { 19 while (true) { 20 SystemClock.sleep(1000); 21 } 22 } 23 }.start(); 24 } 25 }當(dāng)配置發(fā)生變化(如橫豎屏切換)時(shí),會(huì)導(dǎo)致整個(gè)Activity被銷毀并重新創(chuàng)建,很容易假定Android將會(huì)為我們清理和回收跟Activity相關(guān)的內(nèi)存及它運(yùn)行中的線程。然而,這并非如此。這兩者都會(huì)導(dǎo)致內(nèi)存泄露而且不會(huì)被回收, 后果是性能可能顯著地下降。
怎么樣讓一個(gè)Activity泄露
如果你讀過(guò)我前一篇關(guān)于Handler和內(nèi)部類的文章,那么第一種內(nèi)存泄露應(yīng)該很容易理解。在Java中,非靜態(tài)匿名類隱式地持有他們的外部類的引用。如果你不小心,保存這個(gè)引用可能導(dǎo)致Activity在可以被GC回收的時(shí)候被保存下來(lái)。Activity持有一個(gè)指向它們整個(gè)View繼承樹和它所持有的所有資源的引用,所以如果你泄露了一個(gè),很多內(nèi)存都會(huì)連帶著被泄露。
? ? 配置發(fā)生變化只加劇了這個(gè)問(wèn)題,它發(fā)出一個(gè)信號(hào)讓Activity銷毀并重新創(chuàng)建。比如,基于上面的代碼進(jìn)行10次橫豎屏變化后,我們可以看到(使用Eclipse Memory Analyzer)由于那些隱式的引用,每一個(gè)Activity對(duì)象其實(shí)都留存在內(nèi)存中:
? ? ? ? ? ? ? ? ?圖1.在10次配置發(fā)生變化后,存留在內(nèi)存中的Activity實(shí)例
? ??每一次配置發(fā)生變化后,Android系統(tǒng)都會(huì)創(chuàng)建一個(gè)新的Activity并讓舊的Activity可以被回收。然而,隱式持有舊Activity引用的線程,阻止他們被回收。所以每次泄露一個(gè)新的Activity,都會(huì)導(dǎo)致所有跟他們關(guān)聯(lián)的資源都沒(méi)有辦法被回收。
? ??解決方法也很簡(jiǎn)單,在我們確定了問(wèn)題的根源,那么只要將線程定義為private static內(nèi)部類,如下所示:
1 /** 2 * 這個(gè)例子通過(guò)將線程實(shí)例聲明為private static型的內(nèi)部 類,從而避免導(dǎo)致Activity泄 3 * 露,但是這個(gè)線程依舊會(huì)跨越配置變化存活下來(lái)。DVM有一個(gè)指向所有運(yùn)行中線程的 4 * 引用(無(wú)論這些線程是否 可以被垃圾回收),而線程能存活多長(zhǎng)時(shí)間以及什么時(shí)候可 5 * 以被回收跟Activity的生命周期沒(méi)有任何關(guān)系。 6 * 活動(dòng)線程會(huì)一直運(yùn)行下去,直到系統(tǒng)將你的應(yīng)用程序銷毀。 7 */ 8 public class MainActivity extends Activity { 9 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 exampleTwo(); 14 } 15 16 private void exampleTwo() { 17 new MyThread().start(); 18 } 19 20 private static class MyThread extends Thread { 21 @Override 22 public void run() { 23 while (true) { 24 SystemClock.sleep(1000); 25 } 26 } 27 } 28 }新的線程不會(huì)隱式地持有Activity的引用,并且Activity在配置發(fā)生變化后都會(huì)變得可以被回收。
怎么使一個(gè)Thread泄露
第二個(gè)問(wèn)題是每當(dāng)創(chuàng)建了一個(gè)新Activity,就會(huì)導(dǎo)致一個(gè)thread泄露并且不會(huì)被回收。在Java中,thread是GC Root也就是說(shuō)在系統(tǒng)中的Dalvik Virtual Machine (DVM)保存對(duì)所有活動(dòng) 中線程的強(qiáng)引用,這就導(dǎo)致了這些線程留存下來(lái)繼續(xù)運(yùn)行并且不會(huì)達(dá)到可以被回收的條件。因此你必須要考慮怎樣停止后臺(tái)線程。下面是一個(gè)例子:
1 /** 2 * 跟例子2一樣,除了這次我們實(shí)現(xiàn)了取消線程的機(jī)制,從而保證它不會(huì)泄露。 3 * onDestroy()常常被用來(lái)在Activity推出前取消線程。 4 */ 5 public class MainActivity extends Activity { 6 private MyThread mThread; 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 exampleThree(); 12 } 13 14 private void exampleThree() { 15 mThread = new MyThread(); 16 mThread.start(); 17 } 18 19 /** 20 * 靜態(tài)內(nèi)部類不會(huì)隱式地持有他們外部類的引用,所以Activity實(shí)例不會(huì)在配置變化 21 * 中被泄露 22 */ 23 private static class MyThread extends Thread { 24 private boolean mRunning = false; 25 26 @Override 27 public void run() { 28 mRunning = true; 29 while (mRunning) { 30 SystemClock.sleep(1000); 31 } 32 } 33 34 public void close() { 35 mRunning = false; 36 } 37 } 38 39 @Override 40 protected void onDestroy() { 41 super.onDestroy(); 42 mThread.close(); 43 } 44 }? ??在上面的代碼中,我們?cè)?span style="color:#cc0000;">onDestroy()中關(guān)閉線程保證了線程不會(huì)意外泄露。如果你想要在配置變化的時(shí)候保存線程的狀態(tài)(而不是每次都要關(guān)閉并重新創(chuàng)建一個(gè)新的線程)。考慮使用可留存(在配置變化中不會(huì)被銷毀)、沒(méi)有UI的fragment來(lái)執(zhí)行長(zhǎng)時(shí)間任務(wù)。看看我的博客,叫做《用Fragment解決屏幕旋轉(zhuǎn)(狀態(tài)發(fā)生變化)狀態(tài)不能保持的問(wèn)題》,里面有一個(gè)例子說(shuō)明實(shí)現(xiàn)這點(diǎn)。API Demo中也一個(gè)全面的例子。
總結(jié)
在Android中處理Activity生命周期與長(zhǎng)時(shí)間運(yùn)行的任務(wù)的關(guān)系可能很困難并且可能導(dǎo)致內(nèi)存泄露。下面有一些值得考慮的通用建議:
? ??優(yōu)先使用靜態(tài)內(nèi)部類而不是非靜態(tài)的。非靜態(tài)內(nèi)部類的每個(gè)實(shí)例都會(huì)有一個(gè)對(duì)它外部Activity實(shí)例的引用。當(dāng)Activity可以被GC回收時(shí),存儲(chǔ)在非靜態(tài)內(nèi)部類中的外部Activity引用可能導(dǎo)致垃圾回收失敗。如果你的靜態(tài)內(nèi)部類需要宿主Activity的引用來(lái)執(zhí)行某些東西,你要將這個(gè)引用封裝在一個(gè)WeakReference中,避免意外導(dǎo)致Activity泄露。
? ??不要假定Java最后總會(huì)為你清理運(yùn)行中的線程。在上面的例子中,很容易錯(cuò)誤地認(rèn)為用戶退出Activity后,Activity就會(huì)被回收,任何跟這個(gè)Activity關(guān)聯(lián)的線程也都將一并被回收。事實(shí)上不是這樣的。Java線程會(huì)繼續(xù)運(yùn)行下去,直到他們被顯式地關(guān)閉或者整個(gè)process被Android系統(tǒng)殺掉。因此,一定要記得記得為后臺(tái)線程實(shí)現(xiàn)對(duì)應(yīng)的取消策略,并且在Activity生命周期事件發(fā)生的時(shí)候使用合理的措施。
? ??考慮你是否真的應(yīng)該使用線程。Android Framework提供了很多旨在為開(kāi)發(fā)者簡(jiǎn)化后臺(tái)線程開(kāi)發(fā)的類。比如,考慮使用Loader而不是線程當(dāng)你需要配合Activity生命周期做一些短時(shí)間的異步后臺(tái)任務(wù)查詢類任務(wù)。考慮使用使用Service,然后向使用BrocastReceiver向UI反饋進(jìn)度、結(jié)果。最后,記住本篇文章中一切關(guān)于線程的討論也適用于AsyncTask(因?yàn)?span style="color:#cc0000;">Asynctask類使用ExecutorService來(lái)執(zhí)行它的任務(wù))。然而,鑒于AsyncTask只應(yīng)該用于短時(shí)間的操作(最多幾秒鐘,參照文檔),它倒不至于會(huì)導(dǎo)致像Activity或線程泄露那么大的問(wèn)題。
? ??這篇文章中的源代碼都可以從github下載。文章中的示例程序可以從Google play下載。
?
譯文鏈接:Activitys, Threads, & Memory Leaks
總結(jié)
以上是生活随笔為你收集整理的【译】Activitys, Threads和 内存泄露的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 技术专家:为什么我们最终选择Apache
- 下一篇: hadoop--Yarn资源调度器的基础