Android性能優化之內存洩漏,你想要的這裡都有!

2022-01-04 我的Android開源之旅

連結:https://www.jianshu.com/p/e719e0c397e5
聲明:本文已獲Harvey_Specter授權發表,轉發等請聯繫原作者授權

前言

在 Android 中,內存洩露的現象十分常見;而內存洩露導致的後果會使得應用Crash
本文 全面介紹了內存洩露的本質、原因 & 解決方案,最終提供一些常見的內存洩露分析工具,希望你們會喜歡。

目錄 1.png1. 簡介

即 ML (Memory Leak)
指 程序在申請內存後,當該內存不需再使用 但 卻無法被釋放 & 歸還給 程序的現象

2. 對應用程式的影響

容易使得應用程式發生內存溢出,即 OOM
內存溢出 簡介:

1.png3. 發生內存洩露的本質原因

具體描述

1.jpg4. 儲備知識:Android 內存管理機制

4.1 簡介

1.png

下面,將針對回收 進程、對象 、變量的內存分配 & 回收進行詳細講解

4.2 針對進程的內存策略
a. 內存分配策略
由 ActivityManagerService 集中管理 所有進程的內存分配
b. 內存回收策略
步驟1:Application Framework  決定回收的進程類型
Android中的進程 是託管的;當進程空間緊張時,會 按進程優先級低->>高的順序 自動回收進程
Android將進程分為5個優先等級,具體如下:

1.png

步驟2:Linux 內核真正回收具體進程
ActivityManagerService 對 所有進程進行評分(評分存放在變量adj中)
更新評分到Linux 內核
由Linux 內核完成真正的內存回收

此處僅總結流程,這其中的過程複雜,有興趣的讀者可研究系統源碼ActivityManagerService.java

4.2 針對對象、變量的內存策略

下面,將詳細講解內存分配 & 內存釋放策略
a. 內存分配策略

1.png

註:用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. 內存釋放策略

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

1.png

具體介紹如下

1.png5. 常見的內存洩露原因 & 解決方案

常見引發內存洩露原因主要有:

集合類

Static關鍵字修飾的成員變量

非靜態內部類 / 匿名類

資源對象使用後未關閉

下面,我將詳細介紹每個引發內存洩露的原因

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 關鍵字修飾的成員變量

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

// 當Activity需銷毀時,由於mContext = 靜態 & 生命周期 = 應用程式的生命周期,故 Activity無法被回收,從而出現內存洩露
}

儘量避免 Static 成員變量引用資源耗費過多的實例(如 Context)

若需引用 Context,則儘量使用Applicaiton的Context

使用 弱引用(WeakReference) 代替 強引用 持有實例

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

實例演示:

// 創建單例時,需傳入一個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;
    }
}

如上述實例,應傳遞Application的Context,因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 非靜態內部類 / 匿名類

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回收,從而導致內存洩漏

將非靜態內部類設置為:靜態內部類(靜態內部類默認不持有外部類的引用)

該內部類抽取出來封裝成一個單例

儘量 避免 非靜態內部類所創建的實例 = 靜態

若需使用Context,建議使用 Application 的 Context

5.3.2 多線程:AsyncTask、實現Runnable接口、繼承Thread類

多線程主要使用的是:AsyncTask、實現Runnable接口 & 繼承Thread類
前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)回收,從而造成 內存洩露

存在 」工作線程實例 持有外部類引用「 的引用關係

工作線程實例的生命周期 > 外部類的生命周期,即工作線程仍在運行 而 外部類需銷毀
解決方案的思路 = 使得上述任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
Android 內存洩露:詳解 Handler 內存洩露的原因與解決方案
https://www.jianshu.com/p/031515d8a7ca

5.4 資源對象使用後未關閉

// 對於 廣播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 其他使用

1.png

5.6 總結
下面,我將用一張圖總結Android中內存洩露的原因 & 解決方案

1.png6. 輔助分析內存洩露的工具

MAT(Memory Analysis Tools)

Heap Viewer

Allocation Tracker

Android Studio 的 Memory Monitor

LeakCanary

6.1 MAT(Memory Analysis Tools)

通過分析 Java 進程的內存快照 HPROF 分析,快速計算出在內存中對象佔用的大小,查看哪些對象不能被垃圾收集器回收 & 可通過視圖直觀地查看可能造成這種結果的對象

具體使用:MAT使用攻略

6.2 Heap Viewer

定義:一個的 Java Heap 內存分析工具
作用:查看當前內存快照

可查看 分別有哪些類型的數據在堆內存總 & 各種類型數據的佔比情況

具體使用:Heap Viewer使用攻略

6.3 Allocation Tracker

簡介:一個內存追蹤分析工具
作用:追蹤內存分配信息,按順序排列
具體使用:Allocation Tracker使用攻略

6.4 Memory Monitor

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

1.png

具體使用:Android Studio 的 Memory Monitor使用攻略

6.5 LeakCanary

簡介:一個square出品的Android開源庫 ->>下載地址
作用:檢測內存洩露
具體使用:https://www.liaohuqiu.net/cn/posts/leak-canary/

7. 總結

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

性能優化推薦閱讀

Android性能優化之躺著就能APK瘦身

相關焦點

  • Android 性能優化之內存洩漏,使用MAT&LeakCanary解決問題
    ,內存這個小妮子比較調皮,每個月總有那麼幾次洩漏或者溢出(OOM),這篇文章所講的是內存溢出,這裡要注意,內存溢出和內存洩漏是兩個概念,這點大家要清楚,當然,內存洩漏過多會導致內存洩漏,至於什麼是內存洩漏呢,大家都知道我們的內存回收機制是GC,所以用一句話來概括:GC回收機制所無法回收的垃圾對象。
  • Android性能優化:帶你全面實現內存優化
    本文主要講解性能優化中的內存優化,希望你們會喜歡目錄1.LeakCanary具體使用看內存洩漏那篇文章。總結本文主要講解內存優化的相關知識,總結如下:很多APP開發者,不在意一點一滴的內存洩漏。等到項目做大了,內存洩漏越來越多了,因內存洩漏而出現不可意料的問題或者內存警告,甚至APP閃退。這個時候他們就不得不去解決內存洩漏的問題了。
  • Android性能優化--內存優化
    本文來自Dotry投稿,連結:https://www.jianshu.com/p/38b627adaecd上一篇文章關於Android性能優化--啟動優化探討了啟動優化相關的知識點,在本篇將介紹內存優化的相關優化。
  • 最全的Android內存優化技巧
    本文主要介紹性能優化的一些手段,但是為了便於理解以及融會貫通,建議先了解Android內存管理機制,本文將從四個角度來介紹內存優化技巧減小對象的內存佔用儘量減少新分配出來的對象佔用內存的大小,使用更加輕量的對象1.
  • 高頻面試點:Android性能優化之內存優化(上篇)
    眾所周知,內存優化可以說是性能優化中最重要的優化點之一,可以說,如果你沒有掌握系統的內存優化方案,就不能說你對Android的性能優化有過多的研究與探索。本篇,筆者將帶領大家一起來系統地學習Android中的內存優化。
  • Android性能優化:手把手帶你全面實現內存優化
    最近有想換工作的同學們,可參考《5月技術崗位內推|RN開發招聘啦》,再往下看,一篇關於性能優化的好文章,很值得去學習。前言在 Android開發中,性能優化策略十分重要本文主要講解性能優化中的內存優化,希望你們會喜歡目錄
  • 幾乎是史上最全最實用的Android性能全面分析與優化方案研究
    藉助性能優化工具分析解決問題性能優化指標性能問題分類1、渲染問題: 過度繪製、布局冗雜2、內存問題: 內存浪費(內存管理)、內存洩漏3、功耗問題: 耗電性能優化原則和方法2、內存內存大小:峰值越低越好,需要優化前後做對比內存洩漏:需要用工具檢查對比優化前後3、功耗一、渲染問題先來看看造成應用UI卡頓的常見原因都有哪些?
  • Android性能優化典範
    Google近期在Udacity上發布了Android性能優化的在線課程,分別從渲染,運算與內存,電量幾個方面介紹了如何去優化性能,這些課程是Google之前在Youtube上發布的Android性能優化典範專題課程的細化與補充。
  • Android 內存洩漏探討
    這裡修復的方法是:不要在類初始時初始化靜態成員。可以考慮lazy初始化。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此對象的生命周期你有責任管理起來。避免 override finalize()1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。
  • ...既然妹紙不在家,剛好最近一直在為項目做內存洩漏的優化工作,那...
    結果因為妹紙公司臨時有事,她不得不回公司一趟…然後我也只能宅家裡了,既然妹紙不在家,剛好最近一直在為項目做內存洩漏的優化工作,那就來寫一點個人總結好了。什麼是內存洩漏對於不同的語言平臺來說,進行標記回收內存的算法是不一樣的,像Android(Java)則採用GC-Root的標記回收算法。
  • android內存優化總結
    之前做過公司產品的內存優化,不過時間有一段時間了,可能記憶不全,歡迎大家添加補充,有錯誤之處也方便指出。
  • Android性能優化總結
    這是來自一位粉絲「MeloDev」的投稿,講真,我這裡投稿的不少,但是只有我自己覺得很不錯的才會通過,這篇文章我覺得對大家有用,而且性能優化也算是我面試必問的一個話題了,所以這裡推薦給大家。寫在前面公司給了我一周的時間去學習Android性能的優化,參考了張明雲老師的一片文章,並且用公司的實際項目進行測試(附有截圖),還進行了一些知識點,注意事項以及很多網址連結的補充,希望這篇博文能讓做性能測試的朋友們少走一些彎路。文中沒有貼出大段代碼,但是幾乎所有的知識點都有連結,點進去就能看你想看的。轉載註明出處。
  • 【乾貨】Android內存洩漏分析實戰和心得-面試常考點
    對於C++來說,內存洩漏就是new出來的對象沒有delete,俗稱野指針;對於Java來說,就是new出來的Object 放在Heap上無法被GC回收;本文通過QQ和Qzone中內存洩漏實例來講android中內存洩漏分析解法和編寫代碼應注意的事項
  • Google 發布 Android 性能優化典範 - OSCHINA - 中文開源技術交流...
    課程專題不僅僅介紹了Android系統中有關性能問題的底層工作原理,同時也介紹了如何通過工具來找出性能問題以及提升性能的建議。主要從三個方面展開,Android的渲染機制,內存與GC,電量優化。下面是對這些問題和建議的總結梳理。0)Render Performance大多數用戶感知到的卡頓等性能問題的最主要根源都是因為渲染性能。
  • Java內存洩漏、性能優化、宕機死鎖的N種姿勢
    導讀本文介紹Java諸多優化實例:第一,排查堆上、堆外內存洩露;第二,使用arthas、jaeger、tcpdump、jstack做性能優化
  • Android性能優化典範(一)
    11) Performance Cost of Memory Leaks雖然Java有自動回收的機制,可是這不意味著Java中不存在內存洩漏的問題,而內存洩漏會很容易導致嚴重的性能問題。內存洩漏指的是那些程序不再使用的對象無法被GC識別,這樣就導致這個對象一直留在內存當中,佔用了寶貴的內存空間。
  • Android 輕鬆解決內存洩漏
    虛引用(Phantom Reference):任何時候都可以被 GC 回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為 GC 回收 Object 的標誌。
  • Android性能優化典範(三)
    4) The price of ENUMs 在StackOverFlow等問答社區常常出現關於在Android系統裡面使用枚舉類型的性能討論,關於這一點,Android官方的Training課程裡面有下面這樣一句話:
  • Android性能優化—內存分析工具MAT的使用(二)
    我們通過Android性能優化—內存分析工具MAT的使用(一)大概了解了Android到MAT的轉換過程。現在我們熟悉下MAT 中常規的使用。
  • 前端性能優化之利用 Chrome Dev Tools 性能分析
    如果你認真看了本文,一定能學會分析,沒學會,你來找我~上圖是 Chrome Dev Tools 的一個截圖,其中,我認為能用於進行頁面性能快速分析的主要是圖中圈出來的幾個模塊功能,這裡簡單介紹一下:Network : 頁面中各種資源請求的情況,這裡能看到資源的名稱