Android內存優化(二):一分鐘發現內存泄漏

在上一篇文章Android內存優化(一):Java內存區域中已經大體上介紹了Java中的內存分布情況,這一篇主要講一下內存泄漏的產生原因、內存泄漏的危害、內存泄漏一鍵分析與定位、以及代碼中常見的內存泄漏。

1內存泄漏的產生原因

前方高能,18歲以下請避讓!!!
驚天大咪咪:內存泄漏產生的原因是對象占著茅坑不拉屎!!!
有必要講一下Android中的垃圾收集是怎么進行的,Android中使用標記-清除(Mark-Sweep)算法進行垃圾回收(garbage collection,簡稱GC),就是按照正常套路來說,在坑位(內存)不夠的情況下,垃圾收集器會遍歷全部對象,看哪些對象是可以被回收掉騰出內存的,這個過程稱為Mark(標記),Mark的時候要求除了垃圾收集線程之外,其它的線程都停止,這種吊炸天的現象在垃圾收集算法中稱為Stop The World,世界圍著他轉,這就造成了我們的程序會卡頓,但是一般情況下這個時間就幾十毫秒,我根本就感受不到好嗎。Mark完之后,就是釋放內存空間啦,這個過程稱為Sweep(清除)。

這一切看起來很美好,但是就是有內存泄漏發生,所以得提一下,不是所有的對象都是特侖蘇,阿呸,不是所有的對象都能被回收的,比如下面的傲嬌賤貨。

  • 垃圾回收的原則:被全局變量(static)、棧變量和寄存器等直接引用和間接引用的對象不能被回收。

    所以說,對象即使已經使用完,但卻一直被其它對象引用,就會導致這個對象無法被回收,造成內存的浪費,讓別的對象無屎可拉。對象無法被GC回收就是造成內存泄露的原因!

2內存泄漏的可能會造成的創傷

如果不是利用工具去找的話,一般情況下內存泄漏是比較難發現的,因為Java中不會報內存泄漏這種異常,所以在輕微的內存泄漏表面上看是跟正常情況下沒有區別的。

  • 2.1 內存泄漏跟內存溢出(OOM)的區別就是:量變和質變。一個兩個內存泄漏表面看起來沒毛病,但是量變可以導致質變,內存泄漏多了會炸的,就是報OOM異常,應用直接崩潰,連解釋的機會都沒有。
  • 2.2 堆得內存大小是確定的,出現內存泄漏后可用的內存會減小,這又會造成垃圾回收的頻率加劇,上面提到過,垃圾回收的Mark階段會有一種吊炸天的現象,就是Stop The World,除了垃圾回收線程之外的線程會停止,頻繁的垃圾回收卡頓明顯的感受到。
  • 2.3 應用后臺運行的時候,內存占用大,進程被系統殺死的概率就會大咯。

3內存泄漏的發現

內存泄漏的分析的話,必須使用工具才行,慶幸的是,各路大神已經給我們提供了很多強大的內存分析工具,我這里只會講最方便的。這里提供幾個套餐供選擇

3.1 套餐一:Studio自帶Heap Viewer

想不想知道你的應用到底有沒有內存泄漏呢?說真,就一分鐘的事。

  • 3.1.1打開Studio,連上你的應用,然后Android Monitor (1)->Monitors(2)->Memory,上面有四個圖標,暫停圖標是開啟內存使用狀態追蹤的開關,默認是開啟的,小車圖標就是手動GC(3),向下箭頭圖標(4)是查看堆的分配情況,最后的圖標allocation tracker用來跟蹤內存分配情況。

  • 3.1.2我講一下我的使用方式,在應用中操作,從activity1跳轉到activity2,然后跳回到activity1界面,這樣是為了分析activity2是否會產生內存泄漏。接下來就是真刀真槍的干了。

  • 3.1.3點擊小車圖標(3),手動GC進行垃圾回收,這樣才能更準確的判斷activity2是否有內存泄漏發生,最后點擊向下箭頭圖標(4),Studio會自動生成hprof文件并自動展示在Studio界面中。


  • 3.1.4這個就是內存的分析文件了,點擊Analyzer Tasks(5),這是讓Studio幫我們自動分析是否出現內存泄漏。


  • 3.1.5勾上Detect Leaked Activities(6),最后運行(7)就出現分析結果了


  • 3.1.6看到沒,activity2出現內存泄漏了(8),左下角是引用樹(9),通過引用樹就可以定位到內存泄漏的具體信息了。


3.2套餐二:Heap Viewer + MAT

是啊,發現有內存泄漏了,然而還有其它的選擇,這里就必須使用到其它的工具進行輔助了。


MAT(Memory Analyzer)內存分析工具,這個工具的使用我只簡單講一下,因為我一般不用,不要問為什么,因為用起來比較麻煩一些。

  • 3.2.1MAT下載,進入下載的官網,我電腦是64位的,所以選擇Windows(x86_64),整個下載安裝流程跟一般軟件沒啥區別,進入新頁面然后點擊DOWNLOAD

    點擊click here就可以下載使用了
  • 3.2.2 hprof文件導入,這個文件的獲取流程跟內存泄漏的發現流程基本一樣,按上面說的通過Studio的Heap工具獲取的,但是文件導入前需要進行一下轉換,因為MAT工具不能直接使用,轉換也

    不麻煩,Studio已經幫你簡化這個過程,一鍵導出轉換文件,請看過來
  • 3.2.3 用MAT打開hprof的轉換文件,其中Histogram和Dominator Tree比較常用,分析內存泄漏特別需要用到Histogram的兩份文件對比分析,就是獲取兩份內存泄漏前后的hprof轉換文件
  • 3.2.3 標題欄Window->Navigator History,打開 Navigator History面板,然后點擊打開Histogram


  • 3.2.4 右鍵histogram,將兩份分析文件的

    Histogram結果都添加到 Compare Basket中,點擊右上角的!圖標就會生成對比文件
  • 3.2.5 這就是最后生成的對比文件,你還可以自己選擇對比的方式,紅圈里面提供不同的對比方式,這樣就可以很直觀的看出差異,因為我對比的是同一份文件,所以對象間木有差異。


3.3套餐三:Leakcanary

square的開源內存泄漏分析框架,好用得不得了,配置很簡單

  • 3.3.1建議在app的build.gradle文件下添加下面的依賴
dependencies {
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
        testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }
  • 3.3.2在你的Application中的onCreate()方法中進行初始化
public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}
  • 3.3.3然后,就沒有然后了,編譯完后運行你的項目,會在項目安裝成功后出現附加的組件,里面會展示具體的內存泄漏路徑。


  • 3.3.4通過這個泄漏路徑,就對應進行內存泄漏的原因進行分析了,你也可以通過輸出的日志進行內存泄漏的定位。


注:到這里3個套餐已經講完了,關于MAT這個套餐我只是講一下基本的使用,其實已經夠用了,怎么說呢,用起來比較麻煩,所以我自己本身也很少用,我就按自己的使用對比一下三者。
套餐三>套餐一>套餐二
1.套餐三使用最方便,一勞永逸,解析hprof的速度有點慢,但是因為后臺自動解析,所以基本上沒多大關系;
2.套餐一使用最快,切換一下頁面分分鐘就知道有沒有內存泄漏,但是需要你每一次都要手動操作;
3.套餐三最麻煩,耗時耗力,但是自動分析工具并不能保證找出所有的內存泄漏,這個時候就需要通過MAT輔助分析了。

4代碼里頭內存泄漏的常見原因

代碼中內存泄漏大多數產生的原因是不遵循activity的生命周期。

  • 4.1單例模式(靜態activity):在你的Activity中定義了一個 static 變量引用了activity,因為static變量的生命周期和app一樣長,就算activity被銷毀,activity對象還是會被static變量持有,一直到app被銷毀,這也是單例模式最容易造成泄漏的原因,如果靜態的單例對象持有activity對象的引用,就會使得該對象不能被正常回收,從而導致了內存泄漏。解決辦法是使用Application的Context代替activity的context;
/**
  * 單例模式
 */
public class SingletonClass{
    private static SingletonClass instance;
    private Context context;
    public static SingletonClass getInstance(Context context){
            synchronized(SingletonClass.class){
                if(instance==null){
                    instance=new SingletonClass(Context context);
                }
            }
        return instance;
    }
    private SingletonClass(Context context){
       this.context = context; //傳入activity的context就會造成內存泄露咯
    }
}
  • 4.2靜態View:當一個view 被加入到界面中時,它就會持有 context 的強引用,也就是我們的 activity。如果我們通過一個static成員變量引用了這個 view,相當于直接引用了 activity,然后就泄漏了;
private static View view;
view = findViewById(R.id.sv_button);
  • 4.3非靜態內部類:我們都知道,內部類能夠引用外部類的成員,這正是內部類的好處所在,但是恰恰是這個優勢會導致activity內存泄漏,因為非靜態內部類默認持有外部類的引用。如果我們創建了一個內部類的對象,并且通過靜態變量持有這個對象,就會導致內存泄漏;
        private static InnerClass inner = new InnerClass();
        class InnerClass {
        }
  • 4.4匿名內部類:匿名類同樣會持有定義它們的對象的引用,如果在 activity 內定義了一個匿名的 AsyncTask 對象,就有可能發生內存泄漏了。因為在activity被銷毀之后AsyncTask可能仍然在運行,這樣只能等到AsyncTask執行結束才能回收activity;
new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
  • 4.5Handler+Runnable:定義一個匿名的 Runnable 對象并將其提交到 Handler 上也可能導致 activity 泄漏。Runnable對象引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷毀時還沒有被處理,那就會導致內存泄漏了。
new Handler() {
         @Override
         public void handleMessage(Message message) {
                super.handleMessage(message);
            }
        }.postDelayed(new Runnable() {
            @Override public void run() {
                while(true);
            }
        }, 1000);
  • 4.6Thread:原因類似4.5,盡管是在單獨的線程執行任務,但是線程還是會默認持有外部對象,任務沒有執行完成就不會釋放持有的引用;
new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
  • 4.7資源未關閉:如果使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,從而造成內存泄漏。
  • 4.8集合容器:在我們做緩存的時候會用一些數據結構來存儲一些數據,當我們不需要它時要及時清理,不然就會像滾雪球一樣會越來越大,想不泄露都難。

可以了,造成內存泄露還有很多原因,這就靠慢慢跳坑了,生活太艱難。再話癆一下,“千丈之堤,以螻蟻之穴潰;百尺之室,以突隙之煙焚。”,所以我推薦套餐三Leakcanary,讓你的整個開發過程伴隨著內存泄露的監控。

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

推薦閱讀更多精彩內容