第八章 性能優化 之 內存優化(一)

文章目錄

(一)Android內存泄露

1、Java內存基礎知識

1.1)JVM(java虛擬機:java程序運行環境)內存——棧、堆、方法區

(1)棧(stack)FIFO
棧中只存放基本類型和對象的引用,如:局部參數,方法變量,參數,返回值等
(2)堆(heap)
堆內存用於虛擬機啓動時存放new的對象實例、數組,由JVM垃圾回收機制管理。JVM只有一個堆區爲所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身
(3)方法區(method)
被所有的線程共享。方法區包含所有的class(類的模板,包括屬性、方法)

1.2)垃圾回收機制

(1)什麼是垃圾回收機制?

垃圾回收(Garbage Collection)是Java虛擬機(JVM)垃圾回收器提供的一種用於在空閒時間不定時回收無任何對象引用的對象佔據的內存空間的一種機制。

(2)怎樣的對象會被回收?——無任何對象引用的對象

(3)如何判斷對象是否存活——可達性分析法

由圖論引入,程序把所有引用關係看做一張有向圖,從一個結點GC Root開始,尋找對應的引用結點,找到這個結點後繼續尋找這個結點的引用結點,根頂點可以到達的對象都是有效對象,GC不會回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼我們認爲這個(這些)對象不再被引用,可以被GC回收。
Java語言中,可作爲GC Roots的對象包括下面幾種:
a) 虛擬機棧中引用的對象(棧幀中本地變量表)
b) 方法區中類靜態屬性引用的對象
c) 方法區中常量引用的對象
d) 本地方法棧中JNI(Native方法)引用的對象
e) Thread——活着的線程
在這裏插入圖片描述

1.3)Java的四種引用

(1)強引用

實際編碼中最常見的一種引用類型。常見形式如:A a = new A();等。

(2)軟引用

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

軟引用所指示的對象進行垃圾回收需要滿足如下兩個條件:
1.當其指示的對象沒有任何強引用對象指向它;
2.當虛擬機內存不足時

(3)弱引用

A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);

WeakReference不改變原有強引用對象的垃圾回收時機,一旦其指示對象沒有任何強引用對象時,此對象即進入正常的垃圾回收流程。

(4)虛引用

2、內存泄露與內存溢出

2.1)內存泄露(ML)

當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。

2.2)內存溢出(OOM)

程序向系統申請的內存空間超出了系統能給的。由於Android的每個應用程序都會用一個專用的Dalvik虛擬機實例(進程)運行,如果程序內存移除,Android只會kill該進程,不會影響其他進程使用。
當應用程序中產生的內存泄露較多,容易導致應用程序所需內存超出系統分配的內存限額,從而導致內存溢出

2.3)內存泄露本質原因

當一個對象不再被使用,本該被GC回收時,而因有另外一個正在使用的對象持有它的引用,從而導致它不能被程序回收而停留在堆內存中
故本質原因爲:
持有引用者的生命週期>被引用者的生命週期

注:由於Java存在垃圾回收機制,理應不存在內存泄露;出現內存泄露的原因僅僅是外部人爲原因=無意識地持有對象引用,使得持有引用者的生命週期>被引用者的生命週期

2.4)Android內存管理機制

1、簡介

Android內存管理機制=內存分配機制+內存回收/釋放機制

管理的內存對象包括進程、對象、變量。
管理的角色有Application Framework、Dalvik虛擬機、Linux內核。

其中 Application Framework 與 Linux內核 負責對 進程 進行內存的分配&回收
Dalvik虛擬機 負責對 對象、變量 進行內存的分配&回收

2、針對進程的內存策略

a.內存分配策略

由 ActivityManagerService 集中管理 所有進程的內存分配

b.內存回收策略
步驟1:Application Framework決定回收的進程類型

Android中的進程 是託管的;當進程空間緊張時,會 按進程優先級低->>高的順序 自動回收進程
Android將進程分爲5個優先等級,具體如下:
在這裏插入圖片描述

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

詳細過程可研究源碼ActivityManagerService.java

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

Android的對於對象、變量的內存策略同 Java

a.內存分配策略

對象 / 變量的內存分配 由程序自動 負責
共有3種:靜態分配、棧式分配、 & 堆式分配,分別面向靜態變量、局部變量 & 對象實例
在這裏插入圖片描述
實例

public class Sample {    
    // 該類的實例對象的成員變量s1、mSample1 & 指向對象存放在堆內存中
    int s1 = 0;
    Sample mSample1 = new Sample();   
    
    // 方法中的局部變量s2、mSample2存放在 棧內存
    // 變量mSample2所指向的對象實例存放在 堆內存
    public void method() {        
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
    // 變量mSample3的引用存放在棧內存中
    // 變量mSample3所指向的對象實例存放在堆內存
    // 該實例的成員變量s1、mSample1也存放在堆內存中
    Sample mSample3 = new Sample();
b.內存釋放策略

對象 / 變量(堆式分配)的內存釋放 由Java垃圾回收器(GC) / 幀棧 負責
靜態分配不需釋放、棧式分配僅 通過幀棧自動出、入棧,較簡單,故不詳細描述

Java垃圾回收器(GC)的內存釋放=垃圾回收算法,主要包括:

  • 標記-清除 算法
  • 複製算法
  • 標記-整理算法
  • 分代收集 算法
    在這裏插入圖片描述

2.5)常見內存泄露原因&解決方法

2.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;

2.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,則儘量使用Applicaiton的Context
    (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;
    }
}
  • 解決
    單例模式引用的對象的生命週期 = 應用的生命週期
    如上述實例,應傳遞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;
    }
}

2.5.3)非靜態內部類/匿名類

非靜態內部類 / 匿名類 默認持有 外部類的引用;而靜態內部類則不會
常見3種情況,分別是:非靜態內部類的實例 = 靜態、多線程、消息傳遞機制(Handler)

2.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,建議使用 Application 的 Context
2.5.3.2)多線程:AsyncTask、實現Runnable接口、繼承Thread類

多線程的使用方法 = 非靜態內部類 / 匿名類;即 線程類 屬於 非靜態內部類 / 匿名類

  • 原因
    當 工作線程正在處理任務 & 外部類需銷燬時, 由於 工作線程實例 持有外部類引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露
  • 實例
 /** 
     * 方式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:靜態內部類 不默認持有外部類的引用,從而使得 “工作線程實例 持有 外部類引用” 的引用關係 不復存在
    將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生命週期結束時,強制結束線程
    }
2.5.3.3)消息傳遞機制:Handler
  • 原因
    當Handler消息隊列 還有未處理的消息 / 正在處理消息時,存在引用關係: “未被處理 / 正處理的消息 -> Handler實例 -> 外部類”
    若出現 Handler的生命週期 > 外部類的生命週期 時(即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷燬時),將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露
  • 解決
    解決方案1:靜態內部類+弱引用
 private static class FHandler extends Handler{

        // 定義 弱引用實例
        private WeakReference<Activity> reference;

        // 在構造方法中傳入需持有的Activity實例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity實例
            reference = new WeakReference<Activity>(activity); }

        // 通過複寫handlerMessage() 從而確定更新UI的操作
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, "收到線程1的消息");
                    break;
                case 2:
                    Log.d(TAG, " 收到線程2的消息");
                    break;


            }
        }
    }

解決方案2:當外部類結束生命週期時,清空Handler內消息隊列

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 外部類Activity生命週期結束時,同時清空消息隊列 & 結束Handler生命週期
    }

2.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退出時記得停止動畫

2.5.5)其他使用

除了上述4種常見情況,還有一些日常的使用會導致內存泄露,主要包括:Context、WebView、Adapter,具體介紹如下
在這裏插入圖片描述

2.5.6)總結

在這裏插入圖片描述

2.6)內存泄露分析工具

1、MAT(Memory Analysis Tools)

簡介:一個Eclipse的 Java Heap 內存分析工具
作用:查看當前內存佔用情況
通過分析 Java 進程的內存快照 HPROF 分析,快速計算出在內存中對象佔用的大小,查看哪些對象不能被垃圾收集器回收 & 可通過視圖直觀地查看可能造成這種結果的對象

2、Heap Viewer

簡介:一個的 Java Heap 內存分析工具
作用:查看當前內存快照,可查看 分別有哪些類型的數據在堆內存總 & 各種類型數據的佔比情況

3、Allocation Tracker

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

4、Android Studio 的 Memory Monitor

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

5、LeakCanary

簡介:一個square出品的Android開源庫
作用:檢測內存泄露

(二)Android內存優化

1、介紹

優化處理 應用程序的內存使用、空間佔用
避免因不正確使用內存 & 缺乏管理,從而出現 內存泄露(ML)、內存溢出(OOM)、內存空間佔用過大 等問題,最終導致應用程序崩潰(Crash)

2、常見內存問題&優化方案

2.1內存泄露

見前 常見內存泄露原因&解決方法

2.2內存抖動

1、定義:內存大小不斷浮動的現象
2、原因:由於大量、臨時的小對象頻繁創建,導致程序頻繁地分配內存 & 垃圾回收器(GC)頻繁回收內存
垃圾收集器(GC)頻繁地回收內存會導致卡頓,甚至內存溢出(OOM)——大量、臨時的小對象頻繁創建會導致內存碎片,使得當需分配內存時,雖總體上有剩餘內存可分配,但由於這些內存不連續,導致無法模塊分配。系統則視爲內存不夠,故導致內存溢出OOM
3、優化:儘量避免頻繁創建大量、臨時的小對象

2.3圖片Bitmap相關

1、定義:Android系統分配給每個應用程序的內存有限。而圖片資源非常消耗內存(Bitmap),很多情況下,圖片所佔內存沾整個App內存的大部分。容易造成OOM
2、優化:
(1)使用完畢後釋放圖片資源
在這裏插入圖片描述
(2)根據分辨率適配&縮放圖片
在這裏插入圖片描述
(3)按需選擇合適的解碼方式
使用BitmapFactory.inPreferredConfig設置合適的解碼方式
在這裏插入圖片描述
(4)設置圖片緩存
在這裏插入圖片描述
(5)總結
在這裏插入圖片描述

2.4代碼質量 & 數量

原因:
代碼本身的質量(如 數據結構、數據類型等) & 數量(代碼量的大小)可能會導致大量的內存問題,如佔用內存大、內存利用率低等
優化:
主要從代碼總量、數據結構、數據類型、 & 數據對象引用 方面優化,具體如下
在這裏插入圖片描述

2.5常見不正確使用

2.5.1)Adapter、ListView

原因:在滑動ListView獲取最新View時,容易頻繁生成大量對象,即每次在getView中重新實例化1個View對象,不僅浪費資源、時間、也將使得內存佔用越來越大,從而使得內存泄露。
解決:ListView複用、緩存優化,具體如下:1、使用緩存convertView2、直接使用ViewHolder
備註:初始時,ListView會根據當前的屏幕布局,從Adapter中實例化一定數量的View對象,同時ListView會將這些View對象緩存起來
當向上、下滾動ListView時,原先位於最上、下的List Item的View對象會被回收,然後被用來構造新出現的最下面List Item。
a.這個構造過程由getView()方法完成,來向ListView提供每一個Item所需要的View對象
b.getView()的第二個形參View convertView = 被緩存起來的list item的View對象
c.初始化時緩存中沒有view對象,則convertView = null

2.5.2)服務Service

原因:當啓動常駐服務Service時,系統會優先保持服務在後臺不斷運行。服務Service使用的內存不能做其他事情&會減少系統的LRU緩存處理數目,最終導致App使用、切換效率低,從而導致內存應用效率低
解決:
1、儘量減少使用常駐服務Service
2、儘量使用IntentService控制Service的生命週期(當Service執行完所有任務時(intent)會自動停止)

2.5.3)依賴注入框架

原因:框架運行方式:通過註解方式掃描代碼,從而執行一系列初始化,該運行方式把一些我們不需要的大量代碼映射到內存中,被映射後的數據被分配到乾淨的內存中,很長一段時間不會使用,從而造成了內存大量浪費
解決:避免使用依賴注入框架
備註:依賴注入框架的作用:通過簡單代碼&自適應環境 即可進行有用的測試和其他配置的更改

2.5.4)多進程

原因:進程佔用內存(空進程也佔用內存IM)
解決:儘量少用多進程
備註:爲了保活&提高穩定性,很多App都會將進程拆分=多進程

2.6補充

2.6.1)內存優化的終極方案

調大 虛擬機Dalvik的堆內存大小
即 在AndroidManifest.xml的application標籤中增加一個android:largeHeap屬性(值 = true),從而通知虛擬機 應用程序需更大的堆內存
但不建議 & 不鼓勵該做法

2.6.2)額外技巧

技巧1:獲取當前可使用的內存大小
調用 ActivityManager.getMemoryClass()方法可獲取當前應用可用的內存大小(單位 = 兆)
技巧2:獲取當前的內存使用情況
在應用生命週期的任何階段,調用 onTrimMemory()獲取應用程序 當前內存使用情況(以內存級別進行識別),可根據該方法返回的內存緊張級別參數 來釋放內存
技巧3:當視圖變爲隱藏狀態時,則釋放內存
當用戶跳轉到不同的應用 & 視圖不再顯示時, 應釋放應用視圖所佔的資源

3、輔助內存優化的分析工具

見內存泄露分析工具

4、總結

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章