Android性能優(yōu)化(六)--穩(wěn)定--內(nèi)存--內(nèi)存泄漏

1 簡介

  • 即 ML (Memory Leak)
  • 指 程序在申請內(nèi)存后,當(dāng)該內(nèi)存不需再使用 但 卻無法被釋放 & 歸還給 程序的現(xiàn)象

2 對應(yīng)用程序的影響

  • 容易使得應(yīng)用程序發(fā)生內(nèi)存溢出,即 OOM
  • oom
  • OOM = Out Of Memory
  • Android系統(tǒng)為每個應(yīng)用程序分配的內(nèi)存有限,應(yīng)用程序所需的內(nèi)存 超出了系統(tǒng)為其分配的內(nèi)存限額的現(xiàn)象
  • 當(dāng)應(yīng)用程序中產(chǎn)生的內(nèi)存泄漏較多,容易導(dǎo)致應(yīng)用程序所需內(nèi)存超出圍棋分配的內(nèi)存限額,導(dǎo)致內(nèi)存溢出,導(dǎo)致Crash。

3 發(fā)生內(nèi)存泄露的本質(zhì)原因

  • 本該回收的對象,因為某些原因而不能被回收,從而繼續(xù)停留在堆內(nèi)存中。
  • 當(dāng)1個對象已不需再被使用,本該被GC回收時,而因有另外一個正在使用的對象持有它的引用,從而導(dǎo)致它不能被程序回收而停留在堆內(nèi)存中
  • 本質(zhì):無意識地持有對象引用,使得 持有引用者的生命周期 > 被引用者的生命周期

4 Android 內(nèi)存管理機制

4.1簡介

  • 定義
    Android內(nèi)存管理 = 內(nèi)存分配 + 內(nèi)存回收(釋放)
  • 機制說明
  • 1.管理的內(nèi)容對象
    進(jìn)程--對象--變量
  • 2.管理的角色
    Android系統(tǒng)可分為3個層次:Application Framwork,Dalvik虛擬機,Linux內(nèi)核
    其中
    負(fù)責(zé)進(jìn)程內(nèi)存的角色:Application Framwork,Linux內(nèi)核
    負(fù)責(zé)對象、變量內(nèi)存的角色:Dalvik虛擬機
  • 總結(jié)
  • Android內(nèi)存管理 = 對 進(jìn)程、對象、變量進(jìn)行內(nèi)存分配 & 回收
  • 分別由Application Framwork,Dalvik虛擬機,Linux內(nèi)核負(fù)責(zé)

4.2 針對進(jìn)程的內(nèi)存策略

a. 內(nèi)存分配策略
由 ActivityManagerService 集中管理 所有進(jìn)程的內(nèi)存分配
b. 內(nèi)存回收策略

  • 步驟1:Application Framework 決定回收的進(jìn)程類型

Android中的進(jìn)程 是托管的;當(dāng)進(jìn)程空間緊張時,會 按進(jìn)程優(yōu)先級低->>高的順序 自動回收進(jìn)程


944365-97d76f93f0fdd7f6.png
  • 步驟2:Linux 內(nèi)核真正回收具體進(jìn)程
  • ActivityManagerService 對 所有進(jìn)程進(jìn)行評分(評分存放在變量adj中)

  • 更新評分到Linux 內(nèi)核

  • 由Linux 內(nèi)核完成真正的內(nèi)存回收

  • 有興趣可研究系統(tǒng)源碼ActivityManagerService.java

4.3 針對對象、變量的內(nèi)存策略

Android的對于對象、變量的內(nèi)存策略同 Java

5 常見的內(nèi)存泄露原因 & 解決方案

  • 常見引發(fā)內(nèi)存泄露原因主要有:
  • 1.集合類
  • 2.Static關(guān)鍵字修飾的成員變量
  • 3.非靜態(tài)內(nèi)部類 / 匿名類
  • 4.資源對象使用后未關(guān)閉

5.1 集合類

  • 內(nèi)存泄露原因
    集合類 添加元素后,仍引用著 集合元素對象,導(dǎo)致該集合元素對象不可被回收,從而 導(dǎo)致內(nèi)存泄漏
  • 實例演示
// 通過 循環(huán)申請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個集合中有許多元素,故最簡單的方法 = 清空集合對象 & 設(shè)置為null
 // 釋放objectList
objectList.clear();
objectList=null;

5.2 Static 關(guān)鍵字修飾的成員變量

  • tatic 關(guān)鍵字修飾的成員變量的生命周期
  • 加載:java虛擬機在加載類的過程中為靜態(tài)變量分配內(nèi)存。
  • 類變量:static變量在內(nèi)存中只有一個,存放在方法區(qū),屬于類變量,被所有實例所共享
  • 銷毀:類被卸載時,靜態(tài)變量被銷毀,并釋放內(nèi)存空間。static變量的生命周期取決于類的生命周期
  • 當(dāng)程序執(zhí)行完,也就是該類的所有對象都已經(jīng)被回收,或者加載類的ClassLoader已經(jīng)被回收,那么該類就會從jvm的方法區(qū)卸載,即生命期終止。
  • 泄露原因
  • 若使被 Static 關(guān)鍵字修飾的成員變量 引用耗費資源過多的實例(如Context),則容易出現(xiàn)該成員變量的生命周期 > 引用實例生命周期的情況
  • 當(dāng)引用實例需結(jié)束生命周期銷毀時,會因靜態(tài)變量的持有而無法被回收,從而出現(xiàn)內(nèi)存泄露
  • 實例講解
public class ClassName {
 // 定義1個靜態(tài)變量
 private static Context mContext;
 //...
// 引用的是Activity的context
 mContext = context; 

// 當(dāng)Activity需銷毀時,由于mContext = 靜態(tài) & 生命周期 = 應(yīng)用程序的生命周期,故 Activity無法被回收,從而出現(xiàn)內(nèi)存泄露

}
  • 解決方案

1.盡量避免 Static 成員變量引用資源耗費過多的實例(如 Context)
若需引用 Context,則盡量使用Applicaiton的Context

  • 2.使用 弱引用(WeakReference) 代替 強引用 持有實例
  • 單例模式--靜態(tài)成員變量有個非常典型的例子
  • 單例模式 由于其靜態(tài)特性
  • 若1個對象已不需再使用 而單例對象還持有該對象的引用,那么該對象將不能被正?;厥?從而 導(dǎo)致內(nèi)存泄漏
  • 實例演示
// 創(chuàng)建單例時,需傳入一個Context
// 若傳入的是Activity的Context,此時單例 則持有該Activity的引用
// 由于單例一直持有該Activity的引用(直到整個應(yīng)用生命周期結(jié)束),即使該Activity退出,該Activity的內(nèi)存也不會被回收
// 特別是一些龐大的Activity,此處非常容易導(dǎo)致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;
    }
}

  • 解決方案
private SingleInstanceClass(Context context) {        
        this.mContext = context.getApplicationContext(); // 傳遞的是Application 的context
    }

5.3 非靜態(tài)內(nèi)部類 / 匿名類

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

5.3.1 非靜態(tài)內(nèi)部類的實例 = 靜態(tài)

  • 泄露原因
  • 若 非靜態(tài)內(nèi)部類所創(chuàng)建的實例 = 靜態(tài),會因 非靜態(tài)內(nèi)部類默認(rèn)持有外部類的引用 而導(dǎo)致外部類無法釋放,最終 造成內(nèi)存泄露
  • 外部類中 持有 非靜態(tài)內(nèi)部類的靜態(tài)對象
  • 實例演示
// 背景:
   a. 在啟動頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源,會在Activity內(nèi)部創(chuàng)建一個非靜態(tài)內(nèi)部類的單例
   b. 每次啟動Activity時都會使用該單例的數(shù)據(jù)

public class TestActivity extends AppCompatActivity {  
    
    // 非靜態(tài)內(nèi)部類的實例的引用
    // 注:設(shè)置為靜態(tài)  
    public static InnerClass innerClass = null; 
   
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   

        // 保證非靜態(tài)內(nèi)部類的實例只有1個
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非靜態(tài)內(nèi)部類的定義    
    private class InnerClass {        
        //...
    }
}

// 造成內(nèi)存泄露的原因:
    // a. 當(dāng)TestActivity銷毀時,因非靜態(tài)內(nèi)部類單例的引用(innerClass)的生命周期 = 應(yīng)用App的生命周期、持有外部類TestActivity的引用
    // b. 故 TestActivity無法被GC回收,從而導(dǎo)致內(nèi)存泄漏
  • 解決方案
  • 1.將非靜態(tài)內(nèi)部類設(shè)置為:靜態(tài)內(nèi)部類
  • 2.該內(nèi)部類抽取出來封裝成一個單例
  • 3.盡量 避免 非靜態(tài)內(nèi)部類所創(chuàng)建的實例 = 靜態(tài)
    若需使用Context,建議使用 Application 的 Context

5.3.2 多線程:AsyncTask、實現(xiàn)Runnable接口、繼承Thread類

  • 場景:多線程的使用方法 = 非靜態(tài)內(nèi)部類 / 匿名類;即 線程類 屬于 非靜態(tài)內(nèi)部類 / 匿名類
  • 泄露原因
    當(dāng) 工作線程正在處理任務(wù) & 外部類需銷毀時, 由于 工作線程實例 持有外部類引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 內(nèi)存泄露
  • 1.多線程主要使用的是:AsyncTask、實現(xiàn)Runnable接口 & 繼承Thread類
  • 2.前3者內(nèi)存泄露的原理相同,此處主要以繼承Thread類 為例說明
  • 實例演示
/** 
     * 方式1:新建Thread子類(內(nèi)部類)
     */  
        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);

            // 通過創(chuàng)建的內(nèi)部類 實現(xiàn)多線程
            new MyThread().start();

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

   /** 
     * 方式2:匿名Thread內(nèi)部類
     */ 
     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);

        // 通過匿名內(nèi)部類 實現(xiàn)多線程
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執(zhí)行了多線程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }
}


/** 
  * 分析:內(nèi)存泄露原因
  */ 
  // 工作線程Thread類屬于非靜態(tài)內(nèi)部類 / 匿名內(nèi)部類,運行時默認(rèn)持有外部類的引用
  // 當(dāng)工作線程運行時,若外部類MainActivity需銷毀
  // 由于此時工作線程類實例持有外部類的引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 內(nèi)存泄露
  • 解決方案
    造成內(nèi)存泄露的原因有2個關(guān)鍵條件:
    1. 存在 ”工作線程實例 持有外部類引用“ 的引用關(guān)系
  • 2.工作線程實例的生命周期 > 外部類的生命周期,即工作線程仍在運行 而 外部類需銷毀
// 共有2個解決方案:靜態(tài)內(nèi)部類 & 當(dāng)外部類結(jié)束生命周期時,強制結(jié)束線程
// 具體描述如下

   /** 
     * 解決方式1:靜態(tài)內(nèi)部類
     * 原理:靜態(tài)內(nèi)部類 不默認(rèn)持有外部類的引用,從而使得 “工作線程實例 持有 外部類引用” 的引用關(guān)系 不復(fù)存在
     * 具體實現(xiàn):將Thread的子類設(shè)置成 靜態(tài)內(nèi)部類
     */  
        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);

            // 通過創(chuàng)建的內(nèi)部類 實現(xiàn)多線程
            new MyThread().start();

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

   /** 
     * 解決方案2:當(dāng)外部類結(jié)束生命周期時,強制結(jié)束線程
     * 原理:使得 工作線程實例的生命周期 與 外部類的生命周期 同步
     * 具體實現(xiàn):當(dāng) 外部類(此處以Activity為例) 結(jié)束生命周期時(此時系統(tǒng)會調(diào)用onDestroy()),強制結(jié)束線程(調(diào)用stop())
     */ 
     @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部類Activity生命周期結(jié)束時,強制結(jié)束線程
    }

5.3.3 消息傳遞機制:Handler

  • 1.若由于Handler = 非靜態(tài)內(nèi)部類 / 匿名內(nèi)部類(2種使用方式),故又默認(rèn)持有外部類的引用(即MainActivity實例)
  • 2.在Handler消息隊列 還有未處理的消息 / 正在處理消息時,消息隊列中的Message持有Handler實例的引用
  • 上述的引用關(guān)系會一直保持,直到Handler消息隊列中的所有消息被處理完畢
  • 若出現(xiàn) Handler的生命周期 > 外部類的生命周期 時(即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀時),將使得外部類無法被垃圾回收器(GC)回收,從而造成 內(nèi)存泄露
  • 解決方案1:靜態(tài)內(nèi)部類+弱引用
  • 靜態(tài)內(nèi)部類 不默認(rèn)持有外部類的引用,從而使得 “未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關(guān)系 的引用關(guān)系 不復(fù)存在。
    使用WeakReference弱引用持有Activity實例
public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";
    private Handler showhandler;

    // 主線程創(chuàng)建時便自動創(chuàng)建Looper & 對應(yīng)的MessageQueue
    // 之后執(zhí)行Loop()進(jìn)入消息循環(huán)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //1. 實例化自定義的Handler類對象->>分析1
        //注:
            // a. 此處并無指定Looper,故自動綁定當(dāng)前線程(主線程)的Looper、MessageQueue;
            // b. 定義時需傳入持有的Activity實例(弱引用)
        showhandler = new FHandler(this);

        // 2. 啟動子線程1
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定義要發(fā)送的消息
                Message msg = Message.obtain();
                msg.what = 1;// 消息標(biāo)識
                msg.obj = "AA";// 消息存放
                // b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
                showhandler.sendMessage(msg);
            }
        }.start();

        // 3. 啟動子線程2
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // a. 定義要發(fā)送的消息
                Message msg = Message.obtain();
                msg.what = 2;// 消息標(biāo)識
                msg.obj = "BB";// 消息存放
                // b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
                showhandler.sendMessage(msg);
            }
        }.start();

    }

    // 分析1:自定義Handler子類
    // 設(shè)置為:靜態(tài)內(nèi)部類
    private static class FHandler extends Handler{

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

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

        // 通過復(fù)寫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:當(dāng)外部類結(jié)束生命周期時,清空Handler內(nèi)消息隊列
  • 不僅使得 “未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關(guān)系 不復(fù)存在
  • 同時 使得 Handler的生命周期(即 消息存在的時期) 與 外部類的生命周期 同步
@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 外部類Activity生命周期結(jié)束時,同時清空消息隊列 & 結(jié)束Handler生命周期
    }

5.4 資源對象使用后未關(guān)閉

  • 泄露原因
  • 對于資源的使用(如 廣播BraodcastReceiver、文件流File、數(shù)據(jù)庫游標(biāo)Cursor、圖片資源Bitmap等),若在Activity銷毀時無及時關(guān)閉 / 注銷這些資源,則這些資源將不會被回收,從而造成內(nèi)存泄漏
  • 解決方案
  • 在Activity銷毀時 及時關(guān)閉 / 注銷資源
// 對于 廣播BraodcastReceiver:注銷注冊
unregisterReceiver()

// 對于 文件流File:關(guān)閉流
InputStream / OutputStream.close()

// 對于數(shù)據(jù)庫游標(biāo)cursor:使用后關(guān)閉游標(biāo)
cursor.close()

// 對于 圖片資源Bitmap:Android分配給圖片的內(nèi)存只有8M,若1個Bitmap對象占內(nèi)存較多,當(dāng)它不再被使用時,應(yīng)調(diào)用recycle()回收此對象的像素所占用的內(nèi)存;最后再賦為null 
Bitmap.recycle();
Bitmap = null;

// 對于動畫(屬性動畫)
// 將動畫設(shè)置成無限循環(huán)播放repeatCount = “infinite”后
// 在Activity退出時記得停止動畫

5.5 日常的使用會導(dǎo)致內(nèi)存泄露

5.5.1 webview

不再使用WebView對象后無銷毀,導(dǎo)致占用的內(nèi)存長期無法被回收,從而造成內(nèi)存泄漏

  • 解決方案
    通過多線程在不使用WebView對象時進(jìn)行銷毀
  • 如:
  • 1.為WebView開啟另一個進(jìn)程
  • 2.通過AIDL與主線程進(jìn)行通信,WebView所在的進(jìn)程可根據(jù)業(yè)務(wù)需要,選擇在合適的時機銷毀,從而達(dá)到內(nèi)存完整釋放

5.5.2 Adapter

  • 在滑動ListView獲取最新View時,容易頻繁生成大量對象
  • 即每次都在getView()中重新實例化1個View對象
  • 不僅浪費資源、時間,也將使得內(nèi)存占用越來越大,從而使得內(nèi)存泄漏
  • 解決方案
  • 使用緩存的convertView
  • 直接使用ViewHolder

6 輔助分析內(nèi)存泄露的工具

  • MAT(Memory Analysis Tools)
  • Heap Viewer
  • Allocation Tracker
  • Android Studio 的 Memory Monitor
  • LeakCanary
  • Lint

6.1 MAT(Memory Analysis Tools)

  • 一個Eclipse的 Java Heap 內(nèi)存分析工具
  • 通過分析 Java 進(jìn)程的內(nèi)存快照 HPROF 分析,快速計算出在內(nèi)存中對象占用的大小,查看哪些對象不能被垃圾收集器回收 & 可通過視圖直觀地查看可能造成這種結(jié)果的對象
    MAT使用教程

6.2 Heap Viewer

  • 定義:一個的 Java Heap 內(nèi)存分析工具
  • 作用:查看當(dāng)前內(nèi)存快照
  • 可查看 分別有哪些類型的數(shù)據(jù)在堆內(nèi)存總 & 各種類型數(shù)據(jù)的占比情況
    Android性能專項測試之Heap Viewer工具

6.3 Allocation Tracker

  • 簡介:一個內(nèi)存追蹤分析工具
  • 作用:追蹤內(nèi)存分配信息,按順序排列
    AllocationTracker

6.4 Memory Monitor

  • 簡介:一個 Android Studio 自帶 的圖形化檢測內(nèi)存工具
  • 作用:跟蹤系統(tǒng) / 應(yīng)用的內(nèi)存使用情況。核心功能如下


    944365-2c16e53599c5a310.png

AndroidStudio Memory Monitor使用介紹

6.5 LeakCanary

7 總結(jié)

944365-fe3b3dce5ed610b9.png

參考

https://mp.weixin.qq.com/s/hoR_PFzmRR4UrF8ckgOSbQ?

http://www.itdecent.cn/p/97fb764f2669

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容