1. 程式人生 > >Android效能優化:手把手帶你全面瞭解 記憶體洩露 & 解決方案

Android效能優化:手把手帶你全面瞭解 記憶體洩露 & 解決方案

前言

  • Android中,記憶體洩露的現象十分常見;而記憶體洩露導致的後果會使得應用Crash
  • 本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,最終提供一些常見的記憶體洩露分析工具,希望你們會喜歡。
  • 掃碼檢視公眾號:

目錄

示意圖

1. 簡介

  • ML (Memory Leak)
  • 指 程式在申請記憶體後,當該記憶體不需再使用 但 卻無法被釋放 & 歸還給 程式的現象

2. 對應用程式的影響

  • 容易使得應用程式發生記憶體溢位,即 OOM

    記憶體溢位 簡介:
    示意圖

3. 發生記憶體洩露的本質原因

  • 具體描述

示意圖

  • 特別注意
    從機制上的角度來說,由於 Java
    存在垃圾回收機制(GC),理應不存在記憶體洩露;出現記憶體洩露的原因僅僅是外部人為原因 = 無意識地持有物件引用,使得 持有引用者的生命週期 > 被引用者的生命週期

4. 儲備知識:Android 記憶體管理機制

4.1 簡介

示意圖

下面,將針對回收 程序、物件 、變數的記憶體分配 & 回收進行詳細講解

4.2 針對程序的記憶體策略

a. 記憶體分配策略

ActivityManagerService 集中管理 所有程序的記憶體分配

b. 記憶體回收策略

  • 步驟1:Application Framework 決定回收的程序型別
    Android中的程序 是託管的;當程序空間緊張時,會 按程序優先順序低->>高的順序 自動回收程序

    Android將程序分為5個優先等級,具體如下:


示意圖

  • 步驟2:Linux 核心真正回收具體程序
    1. ActivityManagerService 對 所有程序進行評分(評分存放在變數adj中)
    2. 更新評分到Linux 核心
    3. Linux 核心完成真正的記憶體回收
      此處僅總結流程,這其中的過程複雜,有興趣的讀者可研究系統原始碼ActivityManagerService.java

4.2 針對物件、變數的記憶體策略

  • Android的對於物件、變數的記憶體策略同 Java
  • 記憶體管理 = 物件 / 變數的記憶體分配 + 記憶體釋放

下面,將詳細講解記憶體分配 & 記憶體釋放策略

a. 記憶體分配策略

  • 物件 / 變數的記憶體分配 由程式自動 負責
  • 共有3種:靜態分配、棧式分配、 & 堆式分配,分別面向靜態變數、區域性變數 & 物件例項
  • 具體介紹如下

示意圖

注:用1個例項講解 記憶體分配

public class Sample {    
    int s1 = 0;
    Sample mSample1 = new Sample();   

    // 方法中的區域性變數s2、mSample2存放在 棧記憶體
    // 變數mSample2所指向的物件例項存放在 堆記憶體
      // 該例項的成員變數s1、mSample1也存放在棧中
    public void method() {        
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
    // 變數mSample3所指向的物件例項存放在堆記憶體
    // 該例項的成員變數s1、mSample1也存放在棧中
    Sample mSample3 = new Sample();

b. 記憶體釋放策略

  • 物件 / 變數的記憶體釋放 由Java垃圾回收器(GC) / 幀棧 負責
  • 此處主要講解物件分配(即堆式分配)的記憶體釋放策略 = Java垃圾回收器(GC

    由於靜態分配不需釋放、棧式分配僅 通過幀棧自動出、入棧,較簡單,故不詳細描述

  • Java垃圾回收器(GC)的記憶體釋放 = 垃圾回收演算法,主要包括:

垃圾收集演算法型別

  • 具體介紹如下

總結

5. 常見的記憶體洩露原因 & 解決方案

  • 常見引發記憶體洩露原因主要有:

    1. 集合類
    2. Static關鍵字修飾的成員變數
    3. 非靜態內部類 / 匿名類
    4. 資源物件使用後未關閉
  • 下面,我將詳細介紹每個引發記憶體洩露的原因

5.1 集合類

  • 記憶體洩露原因
    集合類 新增元素後,仍引用著 集合元素物件,導致該集合元素物件不可被回收,從而 導致記憶體洩漏

  • 例項演示

// 通過 迴圈申請Object 物件 & 將申請的物件逐個放入到集合List
List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 雖釋放了集合元素引用的本身:o=null)
// 但集合List 仍然引用該物件,故垃圾回收器GC 依然不可回收該物件
  • 解決方案
    集合類 新增集合元素物件 後,在使用後必須從集合中刪除
    由於1個集合中有許多元素,故最簡單的方法 = 清空集合物件 & 設定為null
 // 釋放objectList
        objectList.clear();
        objectList=null;

5.2 Static 關鍵字修飾的成員變數

  • 儲備知識
    Static 關鍵字修飾的成員變數的生命週期 = 應用程式的生命週期
  • 洩露原因
    若使被 Static 關鍵字修飾的成員變數 引用耗費資源過多的例項(如Context),則容易出現該成員變數的生命週期 > 引用例項生命週期的情況,當引用例項需結束生命週期銷燬時,會因靜態變數的持有而無法被回收,從而出現記憶體洩露

  • 例項講解

public class ClassName {
 // 定義1個靜態變數
 private static Context mContext;
 //...
// 引用的是Activity的context
 mContext = context; 

// 當Activity需銷燬時,由於mContext = 靜態 & 生命週期 = 應用程式的生命週期,故 Activity無法被回收,從而出現記憶體洩露

}
  • 解決方案

    1. 儘量避免 Static 成員變數引用資源耗費過多的例項(如 Context

      若需引用 Context,則儘量使用ApplicaitonContext

    2. 使用 弱引用(WeakReference) 代替 強引用 持有例項

注:靜態成員變數有個非常典型的例子 = 單例模式

  • 儲備知識
    單例模式 由於其靜態特性,其生命週期的長度 = 應用程式的生命週期
  • 洩露原因
    若1個物件已不需再使用 而單例物件還持有該物件的引用,那麼該物件將不能被正常回收 從而 導致記憶體洩漏

  • 例項演示

// 建立單例時,需傳入一個Context
// 若傳入的是Activity的Context,此時單例 則持有該Activity的引用
// 由於單例一直持有該Activity的引用(直到整個應用生命週期結束),即使該Activity退出,該Activity的記憶體也不會被回收
// 特別是一些龐大的Activity,此處非常容易導致OOM

public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context; // 傳遞的是Activity的context
    }  

    public SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}
  • 解決方案
    單例模式引用的物件的生命週期 = 應用的生命週期
    如上述例項,應傳遞ApplicationContext,因Application的生命週期 = 整個應用的生命週期
public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context.getApplicationContext(); // 傳遞的是Application 的context
    }    

    public SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}

5.3 非靜態內部類 / 匿名類

  • 儲備知識
    非靜態內部類 / 匿名類 預設持有 外部類的引用;而靜態內部類則不會
  • 常見情況
    3種,分別是:非靜態內部類的例項 = 靜態、多執行緒、訊息傳遞機制(Handler

5.3.1 非靜態內部類的例項 = 靜態

  • 洩露原因
    若 非靜態內部類所建立的例項 = 靜態(其生命週期 = 應用的生命週期),會因 非靜態內部類預設持有外部類的引用 而導致外部類無法釋放,最終 造成記憶體洩露

    即 外部類中 持有 非靜態內部類的靜態物件

  • 例項演示

// 背景:
   a. 在啟動頻繁的Activity中,為了避免重複建立相同的資料資源,會在Activity內部建立一個非靜態內部類的單例
   b. 每次啟動Activity時都會使用該單例的資料

public class TestActivity extends AppCompatActivity {  

    // 非靜態內部類的例項的引用
    // 注:設定為靜態  
    public static InnerClass innerClass = null; 

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

        // 保證非靜態內部類的例項只有1個
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非靜態內部類的定義    
    private class InnerClass {        
        //...
    }
}

// 造成記憶體洩露的原因:
    // a. 當TestActivity銷燬時,因非靜態內部類單例的引用(innerClass)的生命週期 = 應用App的生命週期、持有外部類TestActivity的引用
    // b. 故 TestActivity無法被GC回收,從而導致記憶體洩漏
  • 解決方案
    1. 將非靜態內部類設定為:靜態內部類(靜態內部類預設不持有外部類的引用)
    2. 該內部類抽取出來封裝成一個單例
    3. 儘量 避免 非靜態內部類所建立的例項 = 靜態
      若需使用Context,建議使用 ApplicationContext

5.3.2 多執行緒:AsyncTask、實現Runnable介面、繼承Thread類

  • 儲備知識
    多執行緒的使用方法 = 非靜態內部類 / 匿名類;即 執行緒類 屬於 非靜態內部類 / 匿名類
  • 洩露原因
    當 工作執行緒正在處理任務 & 外部類需銷燬時, 由於 工作執行緒例項 持有外部類引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 記憶體洩露

    1. 多執行緒主要使用的是:AsyncTask、實現Runnable介面 & 繼承Thread
    2. 前3者記憶體洩露的原理相同,此處主要以繼承Thread類 為例說明
  • 例項演示

   /** 
     * 方式1:新建Thread子類(內部類)
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通過建立的內部類 實現多執行緒
            new MyThread().start();

        }
        // 自定義的Thread子類
        private class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 方式2:匿名Thread內部類
     */ 
     public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 通過匿名內部類 實現多執行緒
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }
}


/** 
  * 分析:記憶體洩露原因
  */ 
  // 工作執行緒Thread類屬於非靜態內部類 / 匿名內部類,執行時預設持有外部類的引用
  // 當工作執行緒執行時,若外部類MainActivity需銷燬
  // 由於此時工作執行緒類例項持有外部類的引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 記憶體洩露
  • 解決方案
    從上面可看出,造成記憶體洩露的原因有2個關鍵條件:
    1. 存在 ”工作執行緒例項 持有外部類引用“ 的引用關係
    2. 工作執行緒例項的生命週期 > 外部類的生命週期,即工作執行緒仍在執行 而 外部類需銷燬

解決方案的思路 = 使得上述任1條件不成立 即可。

// 共有2個解決方案:靜態內部類 & 當外部類結束生命週期時,強制結束執行緒
// 具體描述如下

   /** 
     * 解決方式1:靜態內部類
     * 原理:靜態內部類 不預設持有外部類的引用,從而使得 “工作執行緒例項 持有 外部類引用” 的引用關係 不復存在
     * 具體實現:將Thread的子類設定成 靜態內部類
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通過建立的內部類 實現多執行緒
            new MyThread().start();

        }
        // 分析1:自定義Thread子類
        // 設定為:靜態內部類
        private static class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 解決方案2:當外部類結束生命週期時,強制結束執行緒
     * 原理:使得 工作執行緒例項的生命週期 與 外部類的生命週期 同步
     * 具體實現:當 外部類(此處以Activity為例) 結束生命週期時(此時系統會呼叫onDestroy()),強制結束執行緒(呼叫stop())
     */ 
     @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部類Activity生命週期結束時,強制結束執行緒
    }

5.3.3 訊息傳遞機制:Handler

5.4 資源物件使用後未關閉

  • 洩露原因
    對於資源的使用(如 廣播BraodcastReceiver、檔案流File、資料庫遊標Cursor、圖片資源Bitmap等),若在Activity銷燬時無及時關閉 / 登出這些資源,則這些資源將不會被回收,從而造成記憶體洩漏

  • 解決方案
    Activity銷燬時 及時關閉 / 登出資源

// 對於 廣播BraodcastReceiver:登出註冊
unregisterReceiver()

// 對於 檔案流File:關閉流
InputStream / OutputStream.close()

// 對於資料庫遊標cursor:使用後關閉遊標
cursor.close()

// 對於 圖片資源Bitmap:Android分配給圖片的記憶體只有8M,若1個Bitmap物件佔記憶體較多,當它不再被使用時,應呼叫recycle()回收此物件的畫素所佔用的記憶體;最後再賦為null 
Bitmap.recycle();
Bitmap = null;

// 對於動畫(屬性動畫)
// 將動畫設定成無限迴圈播放repeatCount = “infinite”後
// 在Activity退出時記得停止動畫

5.5 其他使用

  • 除了上述4種常見情況,還有一些日常的使用會導致記憶體洩露
  • 主要包括:ContextWebViewAdapter,具體介紹如下

示意圖

5.6 總結

下面,我將用一張圖總結Android中記憶體洩露的原因 & 解決方案

示意圖

6. 輔助分析記憶體洩露的工具

  • 哪怕完全瞭解 記憶體洩露的原因,但難免還是會出現記憶體洩露的現象
  • 下面將簡單介紹幾個主流的分析記憶體洩露的工具,分別是
    1. MAT(Memory Analysis Tools)
    2. Heap Viewer
    3. Allocation Tracker
    4. Android Studio 的 Memory Monitor
    5. LeakCanary

6.1 MAT(Memory Analysis Tools)

  • 定義:一個EclipseJava Heap 記憶體分析工具 ->>下載地址
  • 作用:檢視當前記憶體佔用情況
    通過分析 Java 程序的記憶體快照 HPROF 分析,快速計算出在記憶體中物件佔用的大小,檢視哪些物件不能被垃圾收集器回收 & 可通過檢視直觀地檢視可能造成這種結果的物件

6.2 Heap Viewer

  • 定義:一個的 Java Heap 記憶體分析工具
  • 作用:檢視當前記憶體快照
    可檢視 分別有哪些型別的資料在堆記憶體總 & 各種型別資料的佔比情況

6.3 Allocation Tracker

  • 簡介:一個記憶體追蹤分析工具
  • 作用:追蹤記憶體分配資訊,按順序排列

6.4 Memory Monitor

  • 簡介:一個 Android Studio 自帶 的圖形化檢測記憶體工具
  • 作用:跟蹤系統 / 應用的記憶體使用情況。核心功能如下
    示意圖

6.5 LeakCanary

7. 總結

  • 本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,希望大家在開發時儘量避免出現記憶體洩露

示意圖

相關推薦

Android效能優化手把手全面瞭解 記憶體洩露 & 解決方案

前言 在Android中,記憶體洩露的現象十分常見;而記憶體洩露導致的後果會使得應用Crash 本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,最終提供一些常見的記憶體洩露分析工具,希望你們會喜歡。 掃碼檢視公眾號: 目錄 1. 簡介 即 ML (

Android效能優化手把手全面瞭解 繪製優化

前言 在 Android開發中,效能優化策略十分重要 本文主要講解效能優化中的繪製優化,希望你們會喜歡。 目錄 1. 影響的效能 繪製效能的好壞 主要影響 :Android應用中的頁面顯示速度 2. 如何影響效能 繪製

Android性能優化手把手全面了解 內存泄露 & 解決方案

new t 簡單介紹 新建 cti 接口 stat you bit ray . 簡介 即 ML (Memory Leak)指 程序在申請內存後,當該內存不需再使用 但 卻無法被釋放 & 歸還給 程序的現象2. 對應用程序的影響 容易使得應用程序發生內存溢出,即 OO

Android效能優化手把手如何讓App更快、更穩、更省(含記憶體、佈局優化等)

前言 在 Android開發中,效能優化策略十分重要 因為其決定了應

Android手把手 深入讀懂 Retrofit 2.0 原始碼

前言 在Android開發中,網路請求十分常用 而在Android網路請求庫中,Retrofit是當下最熱的一個網路請求庫 Github截圖 今天,我將手把手帶你深入剖析Retrofit v2.0的原始碼,希望你們會喜歡 請儘量在PC端

Android手把手分析 Protocol Buffer使用 原始碼

前言 習慣用 Json、XML 資料儲存格式的你們,相信大多都沒聽過Protocol Buffer Protocol Buffer 其實 是 Google出品的一種輕量 & 高效的結構化資料儲存格式,效能比 Json、XML 真的強!太!多!

Android手把手深入剖析 Retrofit 2.0 原始碼

前言 在Andrroid開發中,網路請求十分常用 而在Android網路請求庫中,Retrofit是當下最熱的一個網路請求庫 今天,我將手把手帶你深入剖析Retrofit v2.0的原始碼,希望你們會喜歡 目錄 1. 簡介

Android手把手入門神祕的 Rxjava

前言 Rxjava由於其基於事件流的鏈式呼叫、邏輯簡潔 & 使用簡單的特點,深受各大 Android開發者的歡迎。 本文主要: 面向 剛接觸Rxjava的初學者 提供了一份 清晰、簡潔、易懂的Rxjava入門教程 涵蓋 基本介紹、

專家推薦 | 阿里雲高階技術專家全面瞭解雲主機效能評測

錢超,花名西邪,阿里雲高階技術專家,超12年老阿里,是雲主機效能領域的知名專家。 在目前的雲端計算測評領域,很多效能測評存在營銷的包裝,容易引起誤導:比如用瞬時效能引導讀者得出結論,而不去關注穩定性和隔離性等根本特性。 如何幫助讀者揭開迷霧和誤導,用最合理、客觀的方法去構建雲主機評測的基本框架? 在20

阿里雲高階技術專家全面瞭解雲主機效能評測

錢超,花名西邪,阿里雲高階技術專家,超12年老阿里,是雲主機效能領域的知名專家。 在目前的雲端計算測評領域,很多效能測評存在營銷的包裝,容易引起誤導:比如用瞬時效能引導讀者得出結論,而不去關注穩定性和隔離性等根本特性。 如何幫助讀者揭開迷霧和誤導,用最合理、客觀的方法去構建雲主機評測的基本框架? 在20

Android圖片載入框架最全解析(八),全面瞭解Glide 4的用法

本文同步發表於我的微信公眾號,掃一掃文章底部的二維碼或在微信搜尋 郭霖 即可關注,每天都有文章更新。 本篇將是我們這個Glide系列的最後一篇文章。 其實在寫這個系列第一篇文章的時候,Glide就推出4.0.0的RC版了。那個時候因為我一直研究的

Android 效能優化使用 Lint 優化程式碼、去除多餘資源

*本篇文章已授權微信公眾號(郭霖)獨家釋出 讀完本文你將瞭解到: 前言 在保證程式碼沒有功能問題,完成業務開發之餘,有追求的程式設計師還要追求程式碼的規範、可維護性。 今天,以“成為優秀的程式設計師”為目標的拭心將和大家一起精益求精,學習使用 Li

Android效能優化乾貨分享;的 APP 為何啟動那麼慢?

App啟動方式 冷啟動(Cold start) 冷啟動是指APP在手機啟動後第一次執行,或者APP程序被kill掉後在再次啟動。 可見冷啟動的必要條件是該APP程序不存在,這就意味著系統需要建立程序,APP需要初始化。在這三種啟動方式中,冷啟動耗時最長,對於冷啟動的優化也是最具挑戰的。因此本

Android 效能優化使用 TraceView 找到卡頓的元凶

讀完本文你將瞭解到:前言作者在文中為了定位啟動耗時的問題,使用了 TraceView。之前知道但是一直沒用過這個工具,今天拭心和大家一起學習下它 (ง •̀_•́)ง。TraceView 是什麼TraceView 是 Android SDK 中內建的一個工具,它可以載入 trace 檔案,用圖形的形式展示程式

Android 效能優化多執行緒

前言 Android Performance Patterns Season 5 主要介紹了 Android 多執行緒環境下的效能問題。通過介紹 Android 提供的多種多執行緒工具類 (AsyncTask, HandlerThread, Inte

曹工雜談手把手讀懂 JVM 的 gc 日誌

 一、前言 今天下午本來在划水,突然看到微信聯絡人那一個紅點點,看了下,應該是部落格園的朋友。加了後,這位朋友問了我一個問題:   問我,這兩塊有什麼關係? 看到這段 gc 日誌,一瞬間腦子還有點懵,嗯,這個可能要翻下書了,周志明的 Java 虛擬機器那本神書裡面有講,我果斷地打

GitHub 熱點速覽 Vol.26手把手做資料庫

![](https://img2020.cnblogs.com/blog/759200/202006/759200-20200629224830448-312237694.png) 作者:HelloGitHub-**小魚乾** > 摘要:手把手帶你學知識,應該是學習新知識最友好的姿勢了。toyDB

明日之後安卓版即將公測 全面瞭解

明日之後什麼時候公測?明日之後怎麼樣?明日之後到底怎麼玩?明日之後到底肝不肝,氪不氪?明日之後能用電腦玩嗎?   明日之後是網易又一力作,前期造勢十足,吸引了萬千眼球,吊足了八方胃口,現終於公佈安卓版將於11月6日公測,雖然內測階段出了不少岔子,被丟了不少臭雞蛋爛白菜,但目前

全面瞭解網路輿情監測系統

網路輿情形成迅速,對社會影響巨大。隨著網際網路在全球範圍內的飛速發展,網路成為反映社會輿情的主要載體之一。網路環境下的輿情資訊的主要來源有:新聞評論、BBS、部落格、聚合新聞(RSS)。網路輿情表達快捷、資訊多元,方式互動,具備傳統媒體無法比擬的優勢。 由於網上的資訊量十分巨大,僅依靠人工的

明日之後安卓版即將公測 全面瞭解

明日之後什麼時候公測?明日之後怎麼樣?明日之後到底怎麼玩?明日之後到底肝不肝,氪不氪?明日之後能用電腦玩嗎? 明日之後是網易又一力作,前期造勢十足,吸引了萬千眼球,吊足了八方胃口,現終於公佈安卓版將於11月6日公測,雖然內測階段出了不少岔子,被丟了不少臭雞蛋爛白菜,