Android面試一天一題(Day 29:內存泥潭(下))

上一節有介紹了一些和內存相關的基礎知識,這一節就講一下怎么發現和處理內存問題。對于我們來說,最容易發現的內存問題當然是OOM(OutOfMemoryError),應用直接Crash,日志也會很清晰的標明哪個對象OOM了。這個解決起來也不難,常見的Bitmap OOM相信大家也知道怎么處理。

相對于OOM較麻煩一點的就是內存泄露(Memory Leak),每次就露那么一點,就像溫水煮青蛙一樣,很難發現有變化。內存泄漏也是造成應用程序OOM的主要原因之一!我們知道Android系統為每個應用程序分配的內存有限,而當一個應用中產生的內存泄漏比較多時,之后我們再申請新的內存時會及其容易產生OOM。

內存泄漏指的是進程中某些對象(垃圾對象)已經沒有使用價值了,但是它們卻可以直接或間接地引用到GC roots導致無法被GC回收。無用的對象占據著內存空間,使得實際可使用內存變小,形象地說法就是內存泄漏了。

面試題:如何檢測內存泄露,如何進行內存優化?

Android系統為每一個應用程序都設置了一個硬性的Dalvik Heap Size最大限制閾值,這個閾值在不同的設備上會因為RAM大小不同而各有差異。如果你的應用占用內存空間已經接近這個閾值,此時再嘗試分配內存的話,很容易引起OOM。

較簡單的查看一個應用的內存使用情況可以通過DDMS的Heap視圖查看:


在Android Studio上也可以通過Memory Monitor查看內存中Dalvik Heap的實時變化:



注意:GC過于頻繁容易出現內存抖動,這也是造成應用卡頓的常見原因。

也可以通過命行的方式查看:

adb shell dumpsys meminfo <package_name|pid> [-d]

具體的數值意義可以查看官網的說明:https://developer.android.com/studio/profile/investigate-ram.html

MAT內存分析工具

詳細的內存使用情況,可以通過Android Studio的Android Monitor界面,在Memory那欄有上幾個小圖標,點擊有一個向下箭頭的圖標會自動生成并打開的HPROF視圖。

不過用他來分析內存泄露還不是很智能,我們可以借助第三方工具,常見的工具就是MAT了(Memory Analyzer Tool),下載地址 http://eclipse.org/mat/downloads.php,這里我們需要下載獨立版的MAT(之前在使用Ecelipse開發Android應用時,我們常常會使用它的插件版本)。

注意:Android Monitor生成的HPROF文件為Dalvik虛擬機格式的,需要轉成J2SE虛擬機格式的,否則MAT工具中無法打開。轉換的方式也很簡單,Android Studio自帶了,直接在“Captures”->"Heap Snapshot"選中剛剛生成的".hprof"文件,然后鼠標右鍵選擇“Export to standard .hprof”可以在MAT上使用了。

MAT的具體使用方式,網上很多,大家可以自己搜一下。這里就提一下用它怎么能快速查找到內存泄露的點,比如通過“Dominator Tree”的"Path To GC Roots"的排除虛引用/弱引用/軟引用等的引用鏈,因為被虛引用/弱引用/軟引用的對象可以直接被GC給回收,我們要看的就是某個我們已經不需要使用的對象否還存在強引用鏈。比如,我們已退出一個Activity(onDestroy方法也被執行了),但在Path To GC Roots中卻發現這個Activity對象還被有一個引用鏈,那么就可以確認這個Activity對像就產生了內存泄漏。一般來說,從它的引用鏈上也可以直觀地看出是誰在引用它。

除了上面介紹了MAT檢測內存泄露, 有一個叫LeakCanary工具大家也可以嘗試一下。項目地址:https://github.com/square/leakcanaryLeakCanary會檢測應用的內存回收情況,如果發現有垃圾對象沒有被回收,就會去分析當前的內存快照,也就是上邊MAT用到的.hprof文件,找到對象的引用鏈,并顯示在頁面上。這款插件的好處就是,可以在手機端直接查看內存泄露的地方,可以輔助我們檢測內存泄露。

開發中如何避免內存泄漏

這點我比較喜歡問面試者,希望面試者能羅列出一些他自己遇到過的情況。通常來說,Activity的泄漏是內存泄漏里面最嚴重的問題,它占用的內存多(它里面有N多資源的引用),影響比較明顯。下面就示例兩種錯誤的引用方式。

錯誤的單例模式

public class Singleton {
    private static Singleton instance;
    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

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

這是一個非線程安全的單例模式,instance作為靜態對象,其生命周期要長于普通的對象,其中也包含Activity,假如Activity A去getInstance獲得instance對象,傳入this,常駐內存的Singleton保存了你傳入的Activity A對象,并一直持有,即使Activity被銷毀掉,但因為它的引用還存在于一個Singleton中,就不可能被GC掉,這樣就導致了內存泄漏。

View持有Activity引用

public class MainActivity extends Activity {
    private static Drawable mDrawable;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = new ImageView(this);
        mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
        iv.setImageDrawable(mDrawable);
    }
}

有一個靜態的Drawable對象當ImageView設置這個Drawable時,ImageView保存了mDrawable的引用,而ImageView傳入的this是MainActivity的mContext,因為被static修飾的mDrawable是常駐內存的,MainActivity是它的間接引用,MainActivity被銷毀時,也不能被GC掉,所以造成內存泄漏。

其實避免Activity的泄漏的方式可以總結為:不要讓生命周期長于Activity的對象持有到Activity的引用。

在開發中,我們也可以給一些初級的工程師相關的建議,如:

  1. 注意單例模式和靜態變量是否會持有對Context的引用;
  1. 注意監聽器的注銷;(在Android程序里面存在很多需要register與unregister的監聽器,我們需要確保在合適的時候及時unregister那些監聽器。)
  2. 不要在Thread或AsyncTask中的引用Activity;

小結

內存泄漏檢測并不屬于一個經常會做的事情,所以上面寫的一些東西難免會有一些錯誤。不過我認為在面試中,更關注的是面試者做何去發現和解決這個問題,然后是否會對遇到過的問題有一個總結,至于細節上的東西在真正做的時候會直接得到工具或LOG的反饋,不一定非常記得很清楚的人才說明他會這個東西。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容