在Activity中使用Thread導致的內(nèi)存泄漏

做 Android 開發(fā)最常遇到的問題就是在 Activity 的生命周期中協(xié)調(diào)耗時任務哨啃,避免執(zhí)行任務導致不易察覺的內(nèi)存泄漏。不妨先讀一讀下面的代碼熟掂,代碼寫了一個簡單的 Activity嗜侮,Activity 在啟動后就會開啟一個線程稽犁,并循環(huán)執(zhí)行該線程中的任務:

/** 
 *  示例向我們展示了在 Activity 的配置改變時(配置改變會導致其下的 Activity 實例被銷
 *  毀)存活沮翔。此外陨帆,Activity 的 context 也是內(nèi)存泄漏的一部分,因為每一個線程都被初始
 *  化為匿名內(nèi)部類采蚀,使得每一個線程都持有一個外部 Activity 實例的隱式引用疲牵,使得
 *  Activity 不會被 Java 的垃圾回收機制回收。
 */  
    public class MainActivity extends Activity {

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleOne();
      }

      private void exampleOne() {
        new Thread() {
          @Override
          public void run() {
            while (true) {
              SystemClock.sleep(1000);
            }
          }
        }.start();
      }
    }

Activity 配置發(fā)生改變會使 Activity 被銷毀榆鼠,并新建一個 Activity瑰步,我們總會覺得 Android 系統(tǒng)會將與被銷毀的 Activity 相關的一切清理干凈,例如回收與 Activity 關聯(lián)的內(nèi)存璧眠,Activity 執(zhí)行的線程等等……然而,現(xiàn)實總是很殘酷的,剛剛提到的這些東西都不會被回收责静,并導致內(nèi)存泄漏袁滥,從而顯著地影響應用的性能表現(xiàn)。

Activity 內(nèi)存泄漏的根源

在 Java 中灾螃,非靜態(tài)匿名內(nèi)部類會持有其外部類的隱式引用题翻,如果你沒有考慮過這一點,那么存儲該引用會導致 Activity 被保留腰鬼,而不是被垃圾回收機制回收嵌赠。Activity 對象持有其 View 層以及相關聯(lián)的所有資源文件的引用,換句話說熄赡,如果你的內(nèi)存泄漏發(fā)生在 Activity 中姜挺,那么你將損失大量的內(nèi)存空間。
而這樣的問題在 Activity 配置改變時會更加嚴重彼硫,因為 Activity 的配置改變表示 Android 系統(tǒng)將要銷毀當前 Activity 并新建一個 Activity炊豪。舉例來說吧,在使用應用的時候拧篮,你執(zhí)行了10次橫屏/豎屏操作词渤,每一次方向的改變都會執(zhí)行下面的代碼,那么我們會發(fā)現(xiàn)(使用 Eclipse 的內(nèi)存分析工具可以看到)每一個 Activity 對象都會因為留有一個隱式引用而被保留在內(nèi)存中串绩。

image.png

每一次配置的改變都會使 Android 系統(tǒng)新建一個 Activity 并把改變前的 Activity 交給垃圾回收機制回收缺虐。但因為線程持有舊 Activity 的隱式引用,使該 Activity 沒有被垃圾回收機制回收礁凡。這樣的問題會導致每一個新建的 Activity 都將發(fā)生內(nèi)存泄漏高氮,與 Activity 相關的所有資源文件也不會被回收,其中的內(nèi)存泄漏有多嚴重可想而知把篓。

看到這里可能你會很害怕纫溃,很惶恐,很無助韧掩,那我們該怎么辦……莫慌紊浩,解決辦法非常簡單,既然我們已經(jīng)確定了問題的根源疗锐,那么對癥下藥就可以了:我們把該線程類聲明為私有的靜態(tài)內(nèi)部類就可以解決這個問題:

     /**
      * 示例通過將線程類聲明為私有的靜態(tài)內(nèi)部類避免了 Activity context 的內(nèi)存泄漏問題坊谁,但
      * 在配置發(fā)生改變后,線程仍然會執(zhí)行滑臊。原因在于口芍,DVM 虛擬機持有所有運行線程的引用,無論
      * 這些線程是否被回收雇卷,都與 Activity 的生命周期無關鬓椭。運行中的線程只會繼續(xù)運行颠猴,直到
      * Android 系統(tǒng)將整個應用進程殺死
      */ 
    public class MainActivity extends Activity {

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleTwo();
      }

      private void exampleTwo() {
        new MyThread().start();
      }

      private static class MyThread extends Thread {
        @Override
        public void run() {
          while (true) {
            SystemClock.sleep(1000);
          }
        }
      }
    }

通過上面的代碼,新線程再也不會持有一個外部 Activity 的隱式引用小染,而且該 Activity 也會在配置改變后被回收翘瓮。

線程內(nèi)存泄漏的根源

第二個問題是:對于每個新建 Activity,如果 Activity 中的線程發(fā)生發(fā)生內(nèi)存泄漏。在Java中線程是垃圾回收機制的根源裤翩,也就是說资盅,在運行系統(tǒng)中DVM虛擬機總會使硬件持有所有運行狀態(tài)的進程的引用,結(jié)果導致處于運行狀態(tài)的線程將永遠不會被回收踊赠。因此呵扛,你必須為你的后臺線程實現(xiàn)銷毀邏輯!下面是一種解決辦法:

    /**
     * 除了我們需要實現(xiàn)銷毀邏輯以保證線程不會發(fā)生內(nèi)存泄漏筐带,其他代碼和示例2相同今穿。在退出當前
     * Activity 前使用 onDestroy() 方法結(jié)束你的運行中線程是個不錯的選擇
     */
    public class MainActivity extends Activity {
      private MyThread mThread;

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleThree();
      }

      private void exampleThree() {
        mThread = new MyThread();
        mThread.start();
      }

      /**
       * 私有的靜態(tài)內(nèi)部類不會持有其外部類的引用,使得 Activity 實例不會在配置改變時發(fā)生內(nèi)
       * 存泄漏
       */
      private static class MyThread extends Thread {
        private boolean mRunning = false;

        @Override
        public void run() {
          mRunning = true;
          while (mRunning) {
            SystemClock.sleep(1000);
          }
        }

        public void close() {
          mRunning = false;
        }
      }

      @Override
      protected void onDestroy() {
        super.onDestroy();
        mThread.close();
      }
    }

通過上面的代碼烫堤,我們在 onDestroy() 方法中結(jié)束了線程荣赶,確保不會發(fā)生意外的線程的內(nèi)存泄漏問題。如果你想要在配置改變后保留該線程(而不是每一次在關閉 Activity 后都要新建一個線程)鸽斟,那我建議你使用 Fragment 去完成該耗時任務拔创。你可以翻我以前的博文,一名叫作“Handling Configuration Changes with Fragments”應該能滿足你的需求富蓄,在API demo中也提供了很好理解的例子來為你闡述相關概念剩燥。

結(jié)論

Android 開發(fā)過程中,在 Activity 的生命周期里協(xié)調(diào)耗時任務可能會很困難立倍,你一不小心就會導致內(nèi)存泄漏問題灭红。下面是一些小提示,能幫助你預防內(nèi)存泄漏問題的發(fā)生:

  • 盡可能使用靜態(tài)內(nèi)部類而不是非靜態(tài)內(nèi)部類口注。每一個非靜態(tài)內(nèi)部類實例都會持有一個外部類的引用变擒,若該引用是 Activity 的引用,那么該 Activity 在被銷毀時將無法被回收寝志。如果你的靜態(tài)內(nèi)部類需要一個相關 Activity 的引用以確保功能能夠正常運行娇斑,那么你得確保你在對象中使用的是一個 Activity 的弱引用,否則你的 Activity 將會發(fā)生意外的內(nèi)存泄漏材部。
  • 不要總想著 Java 的垃圾回收機制會幫你解決所有內(nèi)存回收問題毫缆。就像上面的示例,我們以為垃圾回收機制會幫我們將不需要使用的內(nèi)存回收乐导,例如:我們需要結(jié)束一個 Activity苦丁,那么它的實例和相關的線程都該被回收。但現(xiàn)實并不會像我們劇本那樣走物臂。Java 線程會一直存活旺拉,直到他們都被顯式關閉产上,抑或是其進程被 Android 系統(tǒng)殺死。所以蛾狗,為你的后臺線程實現(xiàn)銷毀邏輯是你在使用線程時必須時刻銘記的細節(jié)蒂秘,此外,你在設計銷毀邏輯時要根據(jù) Activity 的生命周期去設計淘太,避免出現(xiàn) Bug。
  • 考慮你是否真的需要使用線程规丽。Android 應用的框架層為我們提供了很多便于開發(fā)者執(zhí)行后臺操作的類蒲牧。例如:我們可以使用 Loader 代替在 Activity 的生命周期中用線程通過注入執(zhí)行短暫的異步后臺查詢操作,考慮用 Service 將結(jié)構(gòu)通知給 UI 的 BroadcastReceiver赌莺。最后冰抢,記住,這篇博文中對線程進行的討論同樣適用于 AsyncTask(因為 AsyncTask 使用 ExecutorService 執(zhí)行它的任務)艘狭。然而挎扰,雖說 ExecutorService 只能在短暫操作(文檔說最多幾秒)中被使用,那么這些方法導致的 Activity 內(nèi)存泄漏應該永遠不會發(fā)生巢音。

這篇博文的源碼可以在GitHub 中下載遵倦。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市官撼,隨后出現(xiàn)的幾起案子梧躺,更是在濱河造成了極大的恐慌,老刑警劉巖傲绣,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掠哥,死亡現(xiàn)場離奇詭異,居然都是意外死亡秃诵,警方通過查閱死者的電腦和手機续搀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菠净,“玉大人禁舷,你說我怎么就攤上這事∴土罚” “怎么了榛了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長煞抬。 經(jīng)常有香客問我霜大,道長,這世上最難降的妖魔是什么革答? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任战坤,我火速辦了婚禮曙强,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘途茫。我一直安慰自己碟嘴,他們只是感情好,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布囊卜。 她就那樣靜靜地躺著娜扇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪栅组。 梳的紋絲不亂的頭發(fā)上雀瓢,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天,我揣著相機與錄音玉掸,去河邊找鬼刃麸。 笑死,一個胖子當著我的面吹牛司浪,可吹牛的內(nèi)容都是我干的泊业。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼啊易,長吁一口氣:“原來是場噩夢啊……” “哼吁伺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起认罩,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤箱蝠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后垦垂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宦搬,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年劫拗,在試婚紗的時候發(fā)現(xiàn)自己被綠了间校。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡页慷,死狀恐怖憔足,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酒繁,我是刑警寧澤滓彰,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站州袒,受9級特大地震影響揭绑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一他匪、第九天 我趴在偏房一處隱蔽的房頂上張望菇存。 院中可真熱鬧,春花似錦邦蜜、人聲如沸依鸥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贱迟。三九已至,卻和暖如春絮供,著一層夾襖步出監(jiān)牢的瞬間关筒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工杯缺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睡榆。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓萍肆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胀屿。 傳聞我的和親對象是個殘疾皇子塘揣,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容