天天看點

Android記憶體洩露淺析

概述

在了解MVP引起的記憶體洩露問題之前,我們首先要了解在Android中記憶體洩露是啥玩意?簡單的講記憶體洩漏就是 本該被釋放記憶體的對象沒有被釋放 。最近也和同學@iamxiarui就記憶體洩露這個問題進行了一些讨論,最後發現要搞清楚這個東西,還要從Java層上找原因。學習Android的同學都應該知道,Java這門語言有一個垃圾回收器,一般來說我們是無需關心記憶體回收的問題。但是玩過LOL或者DOTA的同學都知道,一個豬隊友和一個神對手究竟哪個威脅更大一些,我們不能當GC的*隊友,是以多了解一些這玩意吧。

在C++中會有析構函數這個概念,在C++中銷毀對象必須用到這個函數,如此說來C++中是可以手動釋放記憶體的。你可能會說Java中不也有finalize()方法嗎?是的,是有這東西,讓我們來看看這玩意。

finalize()

關于這貨,在《Thinking in java》裡說:

  • 不能指望finalize()。

在《effective java》裡說:

  • 避免使用終結方法:終結方法通常是不可預測的,也是很危險的,一般情況下是不必要的。

至于為什麼那麼描述finalize()方法,原因如下:

終結方法的缺點在于不能保證會被及時的執行。

你以為這就完了?《effective java》中還有一段描述:

Java語言規範不僅不保證終結方法會被及時的執行,而且根本就不保證它們會被執行。

那麼在很多由于生命周期所引發的記憶體洩漏問題上,我們就不能想着手動釋放記憶體了,因為我們需要“及時”的釋放記憶體,但是finalize()并不能滿足我們的需求。那麼我們應該想一些辦法,“告訴”GC:我這是可以回收的,請回收這部分記憶體吧!

那麼問題來了:我們該用怎樣的方式告訴GC,并且讓GC可以回收這部分記憶體呢?這是我們今天主要要解決的問題,但是我們首先要弄明白的是Java中關于記憶體的一些事。

Java記憶體配置設定政策

Java程式運作時的記憶體配置設定政策有三種,分别是靜态配置設定,棧式配置設定和堆式配置設定。對應的,三種存儲政策使用的記憶體空間主要分别是靜态存儲區、棧區和堆區。

  • 靜态存儲區:編譯時就配置設定好,在程式整個運作期間都存在。主要存放靜态資料和常量。
  • 棧區:當方法執行時,會在棧去記憶體中建立方法體内部的局部變量,方法結束後自動釋放記憶體。
  • 堆區:通常存放new出來的對象,由Java垃圾回收器管理記憶體的回收。

很明顯,本文需要關注的就是堆區了,堆記憶體用于存放對象執行個體,至于堆内如何劃分,如何存放對象,這些東西都由具體的實作來決定。

Java記憶體管理

在我們對Java記憶體管理作了解之前我們需要抓住這個問題的核心:

  • 如何判定可回收對象
  • 采用什麼政策

引用計數

首先介紹一種用于說明垃圾收集工作方式的政策, 引用計數 :

每個對象都含有一個引用計數器,當有引用連接配接至對象時,引用計數加1。當引用離開作用域或者被置為null時,引用計數減1。垃圾回收器在周遊所有對象時發現引用計數為0便釋放其記憶體。這種政策很難處理循環引用的情況。不過我們無需過多的考慮此政策有何優缺點,這僅僅是用來讓你了解一些垃圾回收的工作方式。而且現在JVM大多也不用這種政策來進行垃圾回收。

以上我們簡單的了解了一下垃圾回收的大緻流程,那麼接下來我們來了解一下垃圾回收器如何判斷一個對象是否可回收。

可達性分析算法(根搜尋算法)

既然引用計數有缺點,那麼可以采用其他的政策,Java采用了一種新的算法:可達性分析算法。

對象引用周遊從一組對象開始(GC Roots),沿着整個對象圖上的每條連結,遞歸确定可到達(reachable)對象并生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。之後垃圾回收器在進行垃圾回收的時候便可以回收那些不可達的對象。

我們以一個經典的例子來說明以上的東西:

public static void main(String[] args){
        Object o1 = new Object();
        Object o2 = new Object();
        o2 = o1;
        //此為第五行
    }                

用一張圖來表示到第三行為止時的示意圖:

Android記憶體洩露淺析

第三行.png

而到了第五行時,這個情況發生了變化:

Android記憶體洩露淺析

第五行.png

此時Obj2便是不可達對象,垃圾回收器在進行回收時便可以将Obj2的記憶體回收。以上是垃圾回收如确定可回收對象,接下來簡要介紹一下垃圾回收的政策。

記憶體回收政策

  • 标記——清除(标記回收算法 或 Mark-Sweep)

從堆棧和靜态存儲區出發,周遊所有引用,進而标記出所有存活對象,在整個标記過程中不會有回收工作發生。當标記工作完成時,清理動作才會開始。在清理過程中,沒有标記的對象将會被釋放。

這種政策的缺點很容易想到,配置設定記憶體的時候是連續的堆空間,但是在釋放之後記憶體空間是不連續的,如果要配置設定較大的記憶體,這些記憶體碎片是不行的。如果想要得到連續的記憶體空間就得提前觸發gc整理記憶體空間。

一種對Mark-Sweep進行優化的便是Mark-Compact(标記整理算法)。該算法标記階段和Mark-Sweep一樣,但是在完成标記之後,它不是直接清理可回收對象,而是将存活對象都向一端移動,然後清理掉端邊界以外的記憶體。這樣就不會産生特别多的記憶體碎片了。

  • 停止——複制(複制算法)

垃圾回收動作發生的同時,程式将會被暫停(gc stop the world)。複制算法将可用記憶體分為大小相等的兩塊,在垃圾回收器釋放記憶體之前,這塊記憶體記憶體活的對象都會被複制到另外一塊記憶體中,之後将已使用的記憶體空間清理掉。這麼做優點是不容易産生記憶體碎片,缺點也是顯而易見的,存活對象非常多的話,其效率會降低。

  • 分代回收算法

根據對象存活的生命周期将記憶體劃分若幹個不同的區域,一般劃分為老年代(Old Generation)和新生代(Young Generation)。老年代的特點是每次gc時隻有少量對象需要被回收,而新生代的特點是每次gc都有大量的對象需要被回收。這樣就可以根據不同代的特點采取合适的政策,對于新生代采用copying算法,對于老年代使用Mark-Compact。

說真的,本來還想簡介一下Davik或者ART虛拟機的,資料也找到了,但是從本篇來說不需要介紹那麼多了,事實上甚至我覺得關于記憶體回收政策也不需要介紹……

四種引用類型

根據以上我們對于Java如何判定可回收對象的簡介,我們可以對發生記憶體洩露的對象總結出以下特征:

1.在引用樹上可達(被引用)

2.程式以後不會再使用這些對象了

強引用(Strong Reference)

在第一點中我們說對象被引用,其實指的是被強引用。說的好像很高大上的樣子,其實我們平時用的大多都是強引用。

Person p = new Person();
List<String> list = new ArrayList<String>();                

這類對象JVM自己抛OOM也不會通過GC回收這類對象。這是非常容易了解的,因為我們寫代碼需要一切是“可預料的”,如果我聲明以上兩個對象,竟然會莫名其妙的被JVM回收,那我隻能和JAVA說再見了。當然了,我們也可以“提醒”gc回收該對象,比如将其引用置null----->p = null; list = null。這樣這兩個對象便沒有引用指向他了,下一次GC這兩個對象便可以被回收了。

軟引用(Soft Reference)

當一個對象隻有軟引用存在時,系統記憶體不足時會回收此對象。聽起來還不錯,但是在Android2.3以後,gc會很頻繁,導緻釋放軟引用的頻率也很高,這無疑增加了程式維護的難度和不穩定性。是以如果有可替代的東西,就用别的來實作。

弱引用(Weak Reference)

發現就會被幹掉的存在。

虛引用(Phantom Reference)

不做介紹。

MVP中的記憶體洩露

前面鋪墊了

while(true){
    System.out.println("那麼");
}                

長,終于說到重點了……是不是很激動,很期待?不管你是怎麼想的,反正我是激動了~

在本篇中,其他可能引發記憶體洩露的東西,嗯,不分析。隻分析耗時操作所引發的Activity或者其他V層實作類的記憶體洩露問題。熟悉MVP套路的同學應該會清除這麼幾點:

1.Model層擷取資料

2.View層實作類執行回調的邏輯

3.Presenter層解除M和V的耦合,使M和V通過P層互動。

這麼做肯定是有好處的,解除了M和V的耦合,他們倆互不感覺,但是P層作為中間互動層不得不持有一個V層的引用和一個M層的執行個體。而當M層在進行一個耗時的操作時,由于P層是調用M層的邏輯實作一些功能,是以也可以将P層視為是一個耗時的操作。而且前面也說了,P層會持有一個V層的引用,如果在這個時候我們想要銷毀這個Activity,那麼這個Activity因為仍有P在持有Activity的引用進而導緻其不會被回收,也就導緻了記憶體洩露[ 注1 ]。恩,可能你看了我的純文字描述有點頭疼,沒事,畫張圖你就好了解了(關系并不一定是這樣的,但是友善了解):

Android記憶體洩露淺析

畫張圖

可能你會有點奇怪,中間那個p咋整的啊,三個箭頭指的你都暈了,但是MVP就是這麼個套路啊~我們現在想要幹的是釋放activity的記憶體,那麼按照我們之前說過的套路,雖然activity已經去掉了指向a的引用,但是p還沒有去掉指向a的引用。那麼顯而易見的是如果presenter的生命周期長于activity的生命周期,恩,恭喜你記憶體洩露了。這種記憶體考慮值得我們更多的考慮[ 注2 ]

先放一個模拟MVP記憶體洩露的代碼

首先是Model層的接口和實作類:

import android.os.Handler;

/**
 * Created by Luo on 2016/9/30.
 * desc:
 */
public interface TestModel {
    void sendMessageDelayed(Handler handler);
}


//實作類
package com.xiasuhuei321.studyforrxjava;


import android.os.Handler;

/**
 * Created by Luo on 2016/9/30.
 * desc:
 */
public class TestModeImpl implements TestModel {
    public static final int MESSAGE_DELAY = ;

    @Override
    public void sendMessageDelayed(final Handler handler) {
        new Thread(
                new Runnable() {
            @Override
            public void run() {
                handler.sendEmptyMessageDelayed(MESSAGE_DELAY, );
            }
        }).start();
    }
}                

恩,上面我寫的200000是我深深的怨念,本來寫個2000,結果leakcanary這個檢測記憶體洩露的工具貌似會調gc,結果成功回收了……尼瑪我隻要個現象啊……嗯,關于這一塊回收掉的情況,我會在之後的情況裡說明。接下來,放Presenter

import android.os.Handler;
import android.os.Message;

/**
 * Created by Luo on 2016/9/30.
 * desc:
 */
public class TestPresenter {

    TestView testView;
    TestModel testModel;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case TestModeImpl.MESSAGE_DELAY:
                    TestPresenter.this.testView.getMessage();
                    break;
            }
        }
    };

    public TestPresenter(TestView testView) {
        this.testView = testView;
        testModel = new TestModeImpl();
    }

    public void getMessage() {
        testModel.sendMessageDelayed(handler);
    }
}                

View層的接口和Activity:

/**
 * Created by Luo on 2016/9/30.
 * desc:
 */
public interface TestView {
    void getMessage();
}


//activity
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;

import com.squareup.leakcanary.RefWatcher;

public class MainActivity extends AppCompatActivity implements TestView {

    private Button btTest;
    private TestPresenter p;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
        refWatcher.watch(this);
        p = new TestPresenter(this);
        refWatcher.watch(p);
        btTest = (Button) findViewById(R.id.bt_test);
        btTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                p.getMessage();
                startActivity(new Intent(MainActivity.this, SecondActivity.class));
                MainActivity.this.finish();
            }
        });
    }

    @Override
    public void getMessage() {
        Log.e("MainActivity", "asdf");
    }
}                

來看一下記憶體洩露檢測的情況

Android記憶體洩露淺析

記憶體洩漏

結合代碼我們可以發現的确是因為在presenter中因為持有testView的引用導緻了MainActivity的記憶體洩露。

關于這種記憶體洩漏,我們利用以上對于Java記憶體回收、管理的政策的了解,可以這麼解決:我們将presenter的生命周期和Activity的生命周期關聯起來:

  • 在presenter中聲明一個onDestroy()方法,在這個方法中将testView置為null,然後在presenter中凡是使用到testView的使用的,都判斷一下是否為空。
  • 在activity的onDestroy()方法中調用presenter.onDestroy(),同時也将activity持有的presenter置空。

這樣就可以解決MVP中由耗時操作和強引用導緻的記憶體洩露的問題,是不是簡單而優雅?(才怪)

當然了,以上方法裡還有兩個記憶體洩露我麼有解決,那就是handler導緻的activity記憶體洩露。handler在建立的時候會拿到目前線程的Looper,如果目前線程沒有Looper就會報錯,根據這個特性我猜測是因為取到線程這事導緻的記憶體洩露。不過隻是猜測,如果各位看官有知道這其中緣故還請告訴我。第二個是非靜态内部類Handler所引發的記憶體洩露,Handler生命周期長于presenter,是以會引發presenter的記憶體洩露,你說我為啥不搞定?原理我都給你說了,剛好給你個機會去實踐~(逃...)

Android記憶體洩露淺析

記憶體洩漏

恩,剛和大佬越越聊了一下handler這事,他說了一個東西handler.removeCallbacks(null),我把他放在presenter的onDestroy()裡搞定了……

這種解決的方式使我們根據自己的經驗得出的最簡單粗暴的解決方式,這樣能有效的避免因testView持有activity的引用而導緻的記憶體洩露問題。本來想試一下Rxjava+MVP,然後在對應的生命周期裡unsubscribe()來解決記憶體洩漏的問題,但是用leakcancry檢測一直會報和上面一樣的記憶體洩露,而我試了各種方法都沒能解決。雖然在leakcancry的android sdk所導緻的記憶體洩露中貌似找到了這個

Android記憶體洩露淺析

sdk記憶體洩露

問題是我的手機系統版本是6.0.1,按照他這個來說應該是被修複了的。而且很奇怪的一點是我在startActivty跳轉到第二界面并finish自身才會報之前的記憶體洩露,不然的話直接傳回桌面并finish是不會有記憶體洩露的,暫時沒弄懂是什麼狀況,如果有人知道是為什麼請務必告訴我,謝謝!

因為這個問題沒解決,暫時不往下寫了,但是我以上寫的原理肯定是對的。我沒能解決的問題那是因為我現在還不是Android系統的好隊友,嗯,豬隊友吧,先去搞會Android壓壓驚。關于注1,2我想說的其實是一件事:其實有的時候這種記憶體洩漏是 可以接受 的,比如有時可能這種記憶體洩露所引發的後果隻是 本次GC 無法回收這塊記憶體,但是下一次呢?下一次耗時操作過了,這塊記憶體沒有引用指向他了,是可以被回收的。但是這也取決于你,你要是覺得不能忍受,那就麻溜的修複這些東西。

如果你對我發現的這個問題有興趣,問題代碼已經放在了:

demo位址

一不小心上傳了配置檔案……