文章出處

內容概述

  1. 內存泄漏和內存管理相關基礎。
  2. Android中的內存使用。
  3. 內存分析工具和實踐。

以下內容不考慮非引用類型的數據,或者將其等同為對應的引用類型看待——一切皆對象

內存泄漏概念

不再使用的對象常駐內存,如靜態變量,或被其它還在使用的對象(生命周期更長)所引用的對象,對應內存無法回收利用。

為了避免對象無法正確、及時被釋放,需要理解:
GC如何回收對象,如何釋放對象?

對象的引用

對象的使用是通過指向它的引用被訪問的,引用被保存在引用類型變量中。

這里變量指:

類變量:靜態成員變量,成員變量也叫字段。
實例變量:非靜態成員變量。
局部變量:在方法中定義,賦值和使用。
不考慮:參數、返回值、常量。

在new一個對象后,其強引用被構造方法返回。
對象的內部類對象,也擁有this$0這樣的強引用指向它。

Java有四種引用,分別對應不同性質的引用可達性(reachable)——可達指通過此引用訪問到對應的對象。

引用的分類

強引用使用引用對應的類型變量保存,需要手動釋放——設置引用變量為null。
java的有四種引用,其它三種引用由對應的引用包裹類實現——可以認為是特殊類型的引用變量,GC在對待這些引用變量時有不同的策略:

  • 強引用(StrongReference)
    正常聲明的變量都是強引用,即便拋出OutOfMemoryError異常,也不會被回收。需要手動設置變量為null來釋放引用。

  • 軟引用(SoftReference)
    僅在內存不足時GC才會回收軟引用對象。

  • 弱引用(WeakReference)
    一旦掃描到對象僅擁有弱引用,就回收。GC運行在一個優先級很低的線程,不會那么“及時”發現。

在通過引用包裹對象get獲得實際對象時,有可能為null。可以使用一個ReferenceQueue來關聯軟引用和弱引用對象,它們在回收時其引用包裹對象被添加至此隊列。

  • 虛引用(PhantomReference)
    不對GC產生任何影響,必須有關聯的ReferenceQueue,在對象僅剩虛引用時,GC在回收它時把對應的引用包裹對象放入ReferenceQueue——通常就是用來跟蹤對象的回收,做一些清理操作。不含任何“持有”對象的概念,get永遠返回null。

GC何時回收對象[簡要]

Java有自動的內存回收機制,在合適的時候,運行時會執行GC來清理掉那些不再被使用的對象。根據內存需要,程序運行時會不定期多次執行GC。

Java判斷對象是否不再使用有多種策略,最終都是和對象的引用相關。

如果對象的引用數量為0,那么它顯然是垃圾對象。
此外,Java使用“根對象可達性”來判定對象是否有效。

在虛擬機中,有一類GC相關的對象被稱作“GC root”。
GC root通過引用變量一級級來找到堆中的每一個對象。很顯然,不同類型的引用變量,GC對待它們有不同的發現(使用其中的引用)策略。
那些最終不能從根對象引用得到的對象被認為是不可達對象,也就是可回收對象。

可見,只有強引用需要我們自己來考慮其釋放的問題。在分析內存泄漏問題時,我們主要關注對象的強引用。

對象的釋放

對象的釋放,就是對其強引用的釋放——將保存此引用的變量設置為null。另外,若對象包含內部類對象,那么內部對象的引用也要被釋放。

不同的變量它們的默認生命周期是不一樣的。

  1. 非靜態成員變量隨對象的釋放而釋放
  2. 局部變量隨方法結束釋放
  3. 靜態成員變量隨進程結束而釋放。

都可以“手動”設置為null來釋放。

方法未返回前,執行域的變量都不會釋放。需要注意一些方法中的變量的及時釋放。

void releaseObject() {
    Person p = new Person();
    p = new Person(); // 釋放
    p = null;         // 釋放
    // more code...
}

void uglyMethod() {
    Task task;
    while(!stop) {
        task = mBlockingQueue.take(); // 阻塞
        
        //一些針對task的操作。
    }
}
上面,在take()獲得下一個對象賦給task之前,task一直引用著上一個從隊列中獲得的Task對象——它無法被釋放。

引用的方向

引用指向某個對象。
A持有B的引用,那么此引用的方向從A到B。
A不可釋放,A引用B,那么B也不可釋放。反之,B引用到了不可釋放的A,對B的釋放沒有影響。

Outgoing Reference:
對于一個對象,查看它擁有的引用變量,可以知道它所引用的其它對象。

Incoming Reference:
其它對象持有的指向當前對象的引用變量。

環引用

若A和B互相引用,這兩個對象則形成一個環形引用,但不是根對象可達,環形引用是可以被正常回收的。

Android中的內存使用

  • Android程序有內存限制。
  • 頻繁的GC容易造成程序響應問題。
  • 進程自動回收:運行在后臺的程序,擁有的內存越大,越容易被回收——任務棧和進程的關系——做好數據持久化、程序狀態連續性和恢復。

對象使用的建議

Android程序偏向更輕量級的對象,更少的內存占用時間(除去必要的內存緩存),重用避免重復創建。

  • 避免使用枚舉
    使用final static int。

  • 多使用final修飾
    除非業務需要,首選final修飾,編譯器會優化。

  • 圖片
    成熟的庫(Android-Universal-Image-Loader),用多少取多少,及時釋放,緩存。

  • 軟引用和弱引用
    能滿足需要的話,代替強引用。

  • 池和對象復用
    避免對象創建,引起內存抖動。例如知道一個集合是固定大小的話,那么每次網絡請求結束后更新對象字段值,而不是clear又創建一批新對象。
    線程池——好處不多說。使用時注意因為run持久不結束,線程對象對應的字段和局部變量注意泄漏。
    Adapter中數據對象和View的復用。

  • UI操作的去噪
    快速滑動、輸入等。

  • 避免不必要的getter、setter
    僅僅是簡單的POJO,完全沒必要訪問控制器。
  • 合并handler
    handler不要離開Activity,最好的一個Activity使用一個就夠了。不要使用Handler代替回調來通信,使用第三方庫,如EventBus來解耦,handler傳遞數據很低效(不及時-它不是同步的,對象序列化)。
    handler是用來完成跨線程的通信的。
  • 及時釋放引用
    能使用局部變量的,就不要使用字段。方法中,釋放那些不使用又繼續占有的對象引用。
    四大組件對象不是由我們new的,有其明確的生命周期,在“銷毀”動作時從對象引用層面釋放該釋放的。
  • 內部類
    優先使用靜態內部類。
    匿名內部類總是默認持有外部類對象的引用。

  • 在保證速度的前提下使用文件緩存
    一些情況下甚至是必須的,如登錄狀態。
  • 使用ApplicationContext
    僅在必要的時候——如dialog——使用Activity,而且注意Activity的Context的及時釋放。

  • 使用具體類而不是接口
    例如,HashMap,變量不需要聲明為Map,這會有更好的執行速度。
    沒必要為“不存在”的擴展性做犧牲。

  • 在onDestroy中做好清理
    主要是引用的釋放,廣播的取消注冊,回調/監聽對象的解除,handler的取消投遞的消息、網絡請求的取消、動畫的停止,線程、其它異步任務和處理等。

“最佳實踐”平時多收集,原則上
對于泄漏問題,只有一點,不使用就及時把保持引用的成員變量和局部變量設置為null。重點注意回調和靜態字段。

常見的泄漏

典型大對象

  • Activity
  • 圖片、音頻、視頻文件
  • Json數據

可以從Activity開始,依次排查占用內存較大的對象的泄漏。通常,一個包含更多其它對象的大對象的釋放,順帶解決了很多對象的泄漏。

匿名內部類

網絡,語音,線程,其它異步操作,如果使用到callBack/Listener對象,應該注意這些對象的釋放。
場景:
AudioManager是全局的語音管理對象。
假設播放需要傳遞語音文件路徑并提供回調來控制UI:
在Activity中:

void onCreate() {
    AudioManager.addListener(new AudioPlayCallBack() {
        @Override
        public void startPlay() {
        }
        
        @Override
        public void stopPlay() {
        }
    });
}

void onPlayButtonClick() {
    AudioManager.startPlay(mAudioPath);
}

void onDestroy() {
    AudioManager.clearListener();
    AudioManager.stopPlay();
}

監聽(觀察者模式),廣播接收器

同回調一樣,一般的,Activity中使用Receiver或Observer對象,在onCreate中開始注冊,在onDestroy中需要解除注冊。

Service

作為四大組件之一,對象本身創建和釋放不是我們控制。使用startService和stopService、bingService和unBindService來控制組件對象的生命周期。
通常服務是一直運行在后臺的,避免在服務中保存不使用的對象。
場景:

ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mCoreService = ((CoreService.CoreServiceBinder) service).getService();
            mCoreService.registerConnectionStatusCallback(new IConnectionStatusCallback() {
                @Override
                public void connectionStatusChanged(int connectedState, String reason) {
                    // xxx
                }
            });
            // xxx
        }

        /**
        在和Service的連接“意外”中斷時執行,通常是運行Service的其它進程崩潰后引起。
        同一進程中幾乎不會發生(Service死掉了,而此處代碼還在執行...):此方法幾乎不會被執行。
        不會移除此連接。必須主動調用unbindService來解除連接。
        */
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mCoreService.unRegisterConnectionStatusCallback(); // 不執行
            mCoreService = null;// 不執行
        }
    };

上面應該在onDestroy中unbindService并移除Activity和Service對象的引用(回調匿名內部類)連接。

Handler

延遲消息被線程中的MessageQueue持有,在消息未處理前,Message對象引用handler,而handler引用Activity的事情很容易發生。

handler大多數時間也是寫為匿名內部類——這本身沒什么。
在onDestroy中:

void onDestroy() {
    handler.removeCallbacksAndMessages(null);
}

Context

Android中Context是“God Object”,它擁有很多運行時需要的全局信息。通常使用第三方庫,系統API時,需要一個
Context時,優先使用Application。如果必須用到Activity的情況,記得它和匿名內部類是一樣的,不要在三番五次的參數傳遞之后,忘記釋放。

動畫

屬性動畫必須手動stop,否則它會一直執行下去,持有Activity的mContext導致Activity對象的泄漏。

單例、全局對象

少用,注意意外的引用駐留。
簡單的:
ActivityManager管理Activity的集合,在onCreate和onDestroy時從ActivityManager中add和remove掉。
類變量如果是內部類這樣的擁有對外部類的引用:
記得釋放類變量,或者換用靜態內部類,普通類,然后提供對外部類引用的設置和解除。

總而言之:對象是有生命周期的,需要在合適的時間釋放對象的強引用。

內存分析工具

學習內存分析工具的使用,在實踐中積累內存泄漏的問題,避免錯誤的代碼。

Android Monitor

Android Studio 1.5以上版本有此功能。
可以快速查看對象個數,占用內存情況,“簡單地”分析對象引用情況。

Memory Analysis Tool

Java的內存分析工具。

Shallow vs. Retained Heap

Dominator Tree

LeakCanary

產生和發現引用泄漏

檢測目標類

運行程序,GC后dump生成hprof文件,使用MAT分析。

全面測試

在測試環境,使用LeakCanary實時監測。

討論

列表+詳情頁 詳情頁 使用SingleInstance?不允許多個頁面同時打開?

Volley Dispatcher引起的泄露。

收集踩過的坑。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()