[譯]內存泄露的八種花樣

具有垃圾回收特性的語言(如Java)的優點在于,它使得開發者不需要顯式的對內存的分配和回收進行管理。這個特性降低引發段錯誤引發應用崩潰的風險,避免沒有釋放的內存長期占據堆內存,從而編寫出更加安全的代碼。可惜這并不是銀彈,在Java里還是有其他方式導致內存泄露,這意味著我們的Android App依然存在浪費不必要的內存,最終由于內存不足(OOM)導致Crash的可能性。原文鏈接

傳統的內存泄露方式是:在所有相關的引用離開作用域后,沒有釋放之前申請的內存空間。邏輯上的內存泄露,是沒有釋放不再需要的對象的引用的結果。如果一個對象的強引用依然存在,垃圾回收器就不能把這個對象從內存里回收。在Android開發里,Context上下文的泄露就通常就屬于這種泄露。因為Context對象如Activity通常引用了一大堆內存,如View的層級和其他資源。如果泄露了Context對象,通常意味著它所引用的所有對象也跟著泄露。Android應用運行在內存受限的設備上,如果有多處地方泄露的話,應用很容易耗光所有的可用內存。

如果對象沒有明確的生命周期,那么檢測邏輯上的內存泄露更像是一個主觀的問題。幸運的是,Activity擁有明確定義的生命周期,因此我們能明確的知道一個Activity實例是否已經泄露。Activity的onDestroy()方法在Activity的生命周期的最后被調用,意味著它在編程意圖上或Android系統調度上需要進行一些內存的回收。如果這個方法調用完畢后,Activity實例依舊能從堆的根通過強引用鏈被訪問到,垃圾回收器也就無法將它標記為可從內存回收——盡管從原本的意圖是將它從內存中刪除。因此,我們可以將一個在生命周期結束后依舊存在的Activity對象標記為被泄露。

Activity是一個很重的對象,因此你不應該選擇干預Android框架對它們的調度處理。然而,依舊有方法不經意的導致Activity泄露。在Android上,所有導致內存泄露的陷阱都離不開兩個基礎場景。第一個內存泄露的類別是進程級別的全局共享靜態變量,它們的存在狀態不取決于應用的狀態,同時還持有指向Activity的引用鏈。另一個內存泄露類別是因為線程的運行時間比Activity的生命周期還長,忽視了清除一個指向Activity的強引用鏈。下面我們來看下幾種可能會遇到的內存泄露的情況。

1. 靜態Activity

最容易泄露Activity的方式莫過于定義一個類,類的內部通過靜態變量的方式持有Activity,然后在運行中,將Activity實例賦值給這個變量。如果這個靜態變量的引用在Activity的生命周期結束前沒有置空的話,Activity實例就泄露了。因為被靜態變量持有的對象,它將會被保持在內存中,在App的運行過程中一直存在。如果有一個靜態變量持有了Activity的引用,那么這個Activity就無法被垃圾回收器回收。完整代碼

void setStaticActivity() {
  activity = this;
}

View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticActivity();
    nextActivity();
  }
});
Activity內存泄露

2. 靜態View

另一個類似的場景:如果一個Activity需要經常被訪問,那么我們可能會選擇使用單例模式,保持一個實例在內存里,以便它可以被快速的使用到。然而,若前所述,干預Activity的生命周期并將它保持在內存里是一件很危險也沒有必要的事情,應該盡可能的避免這么做。

但如果我們有一個View對象,需要花費很大的代價去創建它,而它在Activity的不同的生命周期里保持不變,那么我們能不能把在這個實例存在靜態變量里,再講他附加到View的層級結構里去?讓我們來看下。完整代碼當我們的Activity被回收的時候,大部分的內存可以被回收。

void setStaticView() {
  view = findViewById(R.id.sv_button);
}

View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    setStaticView();
    nextActivity();
  }
});
靜態View內存泄露

等下!看到沒。你知道一個attach了的view內部會持有一個指向Context的引用,換句話說,那就是我們的Activity。通過吧一個View設為靜態變量,我們創建了一個能長期持有Activity的引用鏈,導致Activity被泄露了。千萬不要把attach的view設為靜態變量,如果實在必須這么做,至少保證在Activity的生命周期結束前把它從View的層級結構里detach掉。

3. 內部類

除了這,讓我們在我們的Activity類里在定義一個類,也就是內部類。為了提高代碼的可讀性和健壯性,封裝程序邏輯,我們可能會這么做。如果我們創建了一個這樣的內部類的實例,并通過靜態變量持有了它,會怎樣呢?你應該能猜到這又是一個內存泄露的點。

void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});
內部類導致的內存泄露

不幸的是,由于內部類可以直接訪問到它的外部類的變量,這個特性意味著內部類會隱式的持有一個對它的外部類的引用,這間接導致了我們不小心又泄露了Activity。

4. 匿名類

同樣的,匿名類也持有一個指向它申明的地方所在的類的引用。如果你在Activity內定義和實例化一個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();
    }
});
AsyncTask的內存泄露

5. Handler

同樣的原則也適用于后臺任務:定義一個匿名的Runnable,然后將它加入Handler的處理隊列里。這個Runnable對象會隱含的持有一個指向它定義的時候所在的Activity的引用,然后它會作為一個消息對象加入到Handler的消息隊列里去。在Activity生命周期結束之后,只要這個消息還沒被Activity處理,那就有一條引用鏈指向我們的Activity對象,使得Activity對象無法被回收,進而泄露。

void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}


View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
Handler導致的內存泄露

6. 線程

類似的問題我們可以在線程定時任務(TimerTask)里發現。

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();
  }
});
線程使用不當導致內存泄露

7. 定時任務

只要它們是通過匿名類的方式定義和實例化的,即便是工作在另外的線程,依舊會在Activity被destroy之后,存在一條指向Activity的引用鏈,導致Activity泄露。

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

TimerTask導致的內存泄露

8. 系統服務

最后,還有一些系統服務可以通過上下文Context對象上的getSystemService方法獲取到。這些服務運行在他們各自的進程里,協助應用執行某種類型的的后臺任務,或者和設備的硬件進行交互。如果Context對象需要系統服務內的某個事件發生的時候通知到這個Context,那么它需要把自身作為一個監聽器注冊給系統服務。系統服務也由此持有了一個對Activity對象的應用。如果我們在Activity的生命周期結束的時候忘了去反注冊這個監聽器,就會發生泄露。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});
傳感器管理器導致的內存泄露

我們已經見識到了一系列內存泄露,也知道他們是多么容易不小心就泄露一堆的內存。記住,盡管最壞的可能性也就是導致你的應用因為內存不足而崩潰,也不一定會一直這樣。但是它會吃掉你應用內的一大部分不必要的內存。在這個時候,你的應用將會缺少內存來生成別的對象,進而導致垃圾回收器頻繁的執行,以便釋放內存給新的對象使用。垃圾回收是一個非常昂貴(耗時)的操作,還會產生用戶可感知的卡頓。因此,需要對可能存在的內存泄露保持警惕,并時常對內存泄露進行測試。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 800評論 0 5
  • 被文同時發布在CSDN上,歡迎查看。 APP內存的使用,是評價一款應用性能高低的一個重要指標。雖然現在智能手機的內...
    大圣代閱讀 4,848評論 2 54
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,387評論 0 12
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,649評論 0 8
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,237評論 2 7