標籤:

提升Android下內存的使用意識和排查能力

內存問題一直是大型App的開發人員比較頭痛的問題,特別是像手淘這種超級的App,App中到處都是帶有圖片和視頻的界面,而且這些功能都是由不同的團隊甚至不同的事業部開發的,要整體上去管控和排查內存的問題變得相當的複雜。之前,我們多個線上版本都存在著嚴重的Activity等內存泄漏和不合理內存使用。這不是偶然,一個很重要的原因就是我們很多的開發測試人員側重業務開發,忽略內存和性能,而且沒有站在全局性的角度去考慮資源的使用。認為我自己的模塊多緩存一些就會加快速度,以空間換時間看似正確,但是在手淘這樣的超級App中是不可取的,需要嚴格限制,否則不要說裡面幾百個模塊,有幾十個模塊這樣來做,其結果都會是災難性的,不但沒有加快速度,反而會拖慢速度以及帶來很多穩定性問題。

經過一年多的更新,現在的Android Studio所帶的工具已經相當的成熟。以前我們還停留在使用MAT來分析內存,但是現在Android Studio的內存分析工具已經相當的強大,已經完全可以拋開MAT來實現更為直觀的內存檢測。我想,作為一個大型App的開發人員和測試人員,掌握這些技能都是必不可少的,也是提高整個App質量的關鍵所在。

在使用工具分析內存之前,我們需要了解一下內存回收上的一些策略,否則很多時候排查到的可能都不是真正的問題。

1、沒有強引用不代表這塊內存就會馬上回收

我們知道,Java的內存回收如果有強引用存在,那麼這個對象的內存不會回收。那麼這個對象的引用如果不存在,是不是這塊內存就會回收呢?答案是否定的,VM有自己的回收策略,不會因為一個對象的引用為空了就立馬對它進行回收。也就是說,回收策略需要達到一定的觸發閾值。我們可以看一個Demo,寫如下的分配對象的方法:

void onButtonClick(){ for (int i = 0; i < 1000; i++) { View view = new ImageView(this); } }

在內存充足的情況下,我們點擊按鈕4次,執行了4遍該函數,這個時候可以看到堆內存呈現了4次增長。如下圖所示:

靜置在那半個小時,內存仍然會維持現狀,VM並不會來執行實際的GC。我們可以Dump內存看看內存中的對象:

我們可以看到,4000個ImageView對象仍然毫髮無損的在內存中殘留,系統沒有回收其內存。不管是Dalvik還是ART環境,包括最新的Android N,都可能出現這樣的情況,具體是否每次都保持(等量)增長等還要看手機內存剩餘情況和手機的GC策略。所以,我們在檢測內存佔用或者內存泄漏之前,一定要執行工具自帶的GC觸發功能,否則結果會不準確。明明沒有泄漏或者沒有佔用,而Dump出來的堆中提示佔用很大或者泄漏。 通過Memory Monitor工具,我們可以看到其引用的情況。點擊其中一個ImageView,我們看下它的引用情況:

可以看到,其引用路徑是被一個FinalizerReference所持有。該對象已經標記為紅色,表明我們自己的代碼中已經沒有其他引用持有該對象,狀態是等待被回收。

通過手動觸發GC,我們可以來主動回收這塊內存,點擊如下圖所示的按鈕,觸發GC。

當然,這裡還有一個問題存在,因為剛才創建的屬於Finalizer對象,該對象前面的文章已經分析過,需要至少兩次GC才能真正回收其內存。所以,第一次觸發GC的時候,我們可以看一下內存的變化。

第一次觸發GC後,內存只是部分的下降,這個時候Finalize鏈表中的對象被回收,但是ImageView還沒有回收(在不同Rom和Android 版本下可能會存在少許差異,在Dalvik下的差異會更大一些)。我們可以看一下堆內存:

該對象的引用鏈路全部標記為紅色,已經沒有強引用指向該對象,剛才的FinalizerReference已經執行了清理。但是第一次GC只是執行了Finalizer清理,而沒有真正的回收這部分對象。所以還需要再一次的觸發GC,再次執行GC後,我們可以看到堆內存下降,這些對象被回收了。

我們可以通過Dump堆內存來驗證是否已經回收,如下圖所示,對內存中已經沒有了剛才的ImageView對象,確實已經被回收了。

通過前面的分析我們可以知道,並不是引用釋放了,內存就會回收。在實際使用手機淘寶的過程中,我們也可以觀察堆內存的變化,在退出一個界面的時候很可能是不會有GC的,堆內存不會變化。如果這個時候用MAT工具去Dump內存,那結果很多是不夠正確的。而如果後續再做其他操作,引發GC後,才會使得結果更加準確,當然我們可以手動的去觸發GC。

2、調用GC時的策略變化

在前面的文章中已經提到過,代碼調用GC,只是告訴VM需要GC,但是是否真正的去執行GC還是要看VM的配置和是否達到閾值。前面也提到,在執行手動GC的時候,Dalvik和ART下會有比較明顯的差異。Android 5.0開始增加了更多的GC方式,到了6.0,7.0在GC方面有了更多的優化。特別是在執行Finalizer對象方面,Android 5.0開始回收就沒有那麼快,單次執行GC,並不會導致失去引用的Finalizer對象進行完全回收,如果要更好的回收Finalizer對象,需要執行System.runFinalization()方法來強制回收Finalizer對象。

我們將測試代碼後面加上主動調用GC的代碼,如下:

void onButtonClick(){ for (int i = 0; i < 1000; i++) { View view = new ImageView(this); } System.gc(); }

在不同的Android版本上看下執行效果,點擊按鈕執行多次。在Android 4.4.4版本的設備上,內存基本已經回收,如果將1000次修改為10次,偶爾可以看到有ImageView已經沒有引用存在,但是仍然沒有回收。如下圖所示:

從這裡可以看出來,在該版本下,大部分的對象在沒有強引用後,調用System.gc()就會被回收。我們再看下ART上的情況,在ART下,卻發生了不一致的表現。在調用後沒有進行GC。查看堆內存,我們也可以看到,4000個ImageView仍然存在,並未執行GC。

看來在ART後,這部分的GC策略做了調整,System.gc()沒有內存回收。我們可以看下源碼,在Android 4.4.4下的源碼:

/** * Indicates to the VM that it would be a good time to run the * garbage collector. Note that this is a hint only. There is no guarantee * that the garbage collector will actually be run. */ public static void gc() { Runtime.getRuntime().gc(); }

我們可以看到,在調用gc函數後,直接調用了運行時的gc。再來看下5.0上的代碼:

/** * Indicates to the VM that it would be a good time to run the * garbage collector. Note that this is a hint only. There is no guarantee * that the garbage collector will actually be run. */ public static void gc() { boolean shouldRunGC; synchronized(lock) { shouldRunGC = justRanFinalization; if (shouldRunGC) { justRanFinalization = false; } else { runGC = true; } } if (shouldRunGC) { Runtime.getRuntime().gc(); } } /** * Whether or not we need to do a GC before running the finalizers. */ private static boolean runGC; /** * If we just ran finalization, we might want to do a GC to free the finalized objects. * This lets us do gc/runFinlization/gc sequences but prevents back to back System.gc(). */ private static boolean justRanFinalization; /** * Provides a hint to the VM that it would be useful to attempt * to perform any outstanding object finalization. */ public static void runFinalization() { boolean shouldRunGC; synchronized(lock) { shouldRunGC = runGC; runGC = false; } if (shouldRunGC) { Runtime.getRuntime().gc(); } Runtime.getRuntime().runFinalization(); synchronized(lock) { justRanFinalization = true; } }

從源碼我們可以發現,System.gc()函數,已經有了變化,從5.0開始,多了一些判斷條件。是否執行gc,是依賴於justRanFinalization變數,而變數justRanFinalization在runFinalization後才會變為true。也就是說,直接調用System.gc()方法並沒有調用Runtime.getRuntime().gc(),只是做了一個標記將runGC變數設置為true。

在runFinalization函數中,如果標記了runGC的,會先執行一次gc,然後清理Finalizer對象,並標記為依據清理過了Finalizer對象。這樣在下次gc調用的時候,就會真正執行一次gc,以回收Finalizer對象。

在Android ART的設備上,我們將調用gc的方式做下變更:

void onButtonClick(){ for (int i = 0; i < 2000; i++) { View view = new ImageView(this); } Runtime.getRuntime().gc(); }

這裡直接調用了Runtime.getRuntime().gc()。這個時候內存回收確實會有變化。但是Finalizer對象仍然可能存在,在Android 5.0的時候會回收一部分,但是從6.0開始,單次調用gc,Finalizer對象卻不一定回收。如下圖所示,雖然所有引用鏈路已經不復存在,但是內存仍然沒有回收:

那麼執行2次gc是否就都能把內存回收了呢?我們修改下代碼:

void onButtonClick(){ for (int i = 0; i < 2000; i++) { View view = new ImageView(this); } Runtime.getRuntime().gc(); Runtime.getRuntime().gc(); }

這裡連續2次調用了gc,按理是可以回收Finalizer對象的,但是由於兩次調用gc的間隔太短,而Finalizer對象是由專門的線程執行回收的,所以也不一定能完全回收。這個和線程的調度情況有關係。例如執行上面代碼可能出現的結果是部分回收:

如果想要全部回收,可以在中間停頓一些間隔,或者增加System.runFinalization()方法的調用。這樣就能將當前可以回收的內存基本都回收了。我們在Android Studio的觸發GC的按鈕,也是通過BinderInternal$GcWatcher等代碼來執行內存回收的。當然,在實際的業務代碼中,不要主動調用gc,這樣可能會導致額外的內存回收開銷,在檢測代碼中,如果需要檢測內存,最好按照gc,runFinalization,gc的順序去回收內存後,再做內存的Dump分析。

3、使用Android Studio的Memory分析內存佔用

Memory Monitor工具比起MAT一個很大的優勢就是可以非常簡單的查看內存佔用,而且可以迅速找到自己的模塊所佔用的堆內存,所以希望開發和測試人員都能夠遷移到Android Studio所帶的工具上來。如何查看內存佔用?真的是非常的簡單,而且可以找到很細小的內存佔用情況。

例如,這裡自定義一個類,然後創建該類,代碼如下:

public class MyThread extends Thread{ @Override public void run() { View view = new View(MainActivity.this); view = null; } } void onButtonClick(){ new MyThread().start(); }

這裡只是創建了一個該對象,但是也很容易可以跟蹤到該對象的內存情況。通過Memory Monitor的【Dump Java Heap】按鈕可以把當前堆內存顯示出來,如下圖所示:

這裡是默認查看方式,我們可以切換到以包名的形式查看。這樣就可以很容易的找到我們自己的代碼了。如下所示:

切換查看方式後,我們可以很容易的就找到自己所寫的代碼內存佔用情況。這裡可以看到實例的個數,佔用內存的大小等情況。 例如在打開手機淘寶,簡單操作一會後再來觀察內存佔用情況。按照包名查看後,我們很容易就可以看到整個堆內存的佔用情況,如下圖所示:

通過上圖我們很容易看到,在com.taobao包名下佔用了近240多M的內存。繼續往下看,聚划算模塊,圖片庫模塊佔了大頭。點擊ju模塊,展開後,又可以看到該包名下的內存佔用:

通過上圖我們可以清晰的看到,在ju包名下的內存分布情況。

所以,在內存佔用的檢查上,Android Studio已經給我們提供了強大的工具,各個開發和測試人員已經可以很簡單的查看到自己開發的模塊佔據的內存大小,然後對內存的使用做一些改進等措施。這裡也可以通過右側的Instance窗口檢查各個實例的引用以及排查不合理的內存強制佔用。

4、排查內存泄漏

Memory Dump下來後,我們可以檢查Java堆的Activity內存泄漏和重複的String。很多人還習慣於MAT分析工具,其實Memory Monitor已經包含了這個功能,而且使用非常簡單。 首先dump內存,如前面分析的那樣,在右側可以看到【Analyzer Tasks】按鈕。

然後,我們點擊該按鈕,就可以看到分析泄漏的工具。

這裡可以只勾選 檢測泄漏Activity選項,然後選擇執行。這樣就可以看到泄漏的Activity了。

通過引用指向,我們可以比較容易的判斷出不該持有的強引用關係,而且該工具從上到下的排序,已經做了初步的判斷。

當然在檢測泄漏和佔用之前,需要點擊2次GC的按鈕,這樣結果才會相對準確。

對於Android Studio提供的內存分析工具,使用起來非常簡單,會比Mat工具要快捷,排查問題也更加容易。所以Android的開發和測試人員,應該儘可能的都遷移到該工具上來,並能夠熟練的掌握內存分析工具,這樣才能讓自己開發的模塊質量更加的優秀。

以上主要是針對Android Studio 2中的使用方式,在今年的Android Studio 3 Preview版本中,內存這塊的分析工具更加強大,可以在面板上直接看到更細粒度的內存佔用,不僅僅是Java的對內存了。

對於需要更細粒度和更全面的分析一些內存的細節,本文所涉及的內存知識還是不夠的,還需要了解Linux下的內存機制以及Android下的一些內存機制,例如按頁分配,共享內存,GPU內存等等。

推薦閱讀:

國產手機什麼時候才能用上龍芯處理器?
【非國行先鋒測試】HTC U11 國行版刷國際版 Android 8.0(Oreo)系統
谷歌「不作惡」的信條被媒體曲解或誇大了嗎?
安卓模擬器哪家比較好?
為什麼日本 Android 應用很少使用 Material Design ?

TAG:Android |