Android合理管理內存

有很多朋友都問過我,怎樣才能寫出高性能的應用程序,如何避免程序出現OOM,或者當程序內存佔用太高的時候該怎麼樣去排查。確實,一個優秀的應用程序,不只僅要功能完成得好,性能問題也應該處理得恰到好處。爲此,我也是閱讀了很多Android官方給出的高性能編程建議,那麼從本篇文章開始,我就準備開始寫一個全新系列的博文,來把這些建議進行整理和分析,幫助你們可以寫出更加出色的應用程序。php

內存(RAM)對於任何一個軟件開發環境都是種很是珍貴的資源,而對於移動操做系統來說的話,則會顯得更加珍貴,由於手機的硬件條件相對於PC畢竟是比較落後的。儘管Android系統的虛擬機擁有自動回收垃圾的機制,但這並不表明咱們就能夠忽視應該在何時分配和釋放內存。java

爲了使垃圾回收器能夠正常釋放程序所佔用的內存,在編寫代碼的時候就必定要注意儘可能避免出現內存泄漏的狀況(一般都是因爲全局成員變量持有對象引用所致使的),而且在適當的時候去釋放對象引用。對於大多數的應用程序而言,後面其它的事情就能夠都交給垃圾回收器去完成了,若是一個對象的引用再也不被其它對象所持有,那麼系統就會將這個對象所分配的內存進行回收。android

咱們在開發軟件的時候應當自始至終都把內存的問題充分考慮進去,這樣的話才能開發出更加高性能的軟件。而內存問題也並非無規律可行的,Android系統給咱們提出了不少內存優化的建議技巧,只要按照這些技巧來編寫程序,就可讓咱們的程序在內存性能發麪表現得至關不錯,下面咱們就來一一學習一下這些技巧。程序員

節制地使用Service

若是應用程序當中須要使用Service來執行後臺任務的話,請必定要注意只有當任務正在執行的時候才應該讓Service運行起來。另外,當任務執行完以後去中止Service的時候,要當心Service中止失敗致使內存泄漏的狀況。正則表達式

當咱們啓動一個Service時,系統會傾向於將這個Service所依賴的進程進行保留,這樣就會致使這個進程變得很是消耗內存。而且,系統能夠在LRU cache當中緩存的進程數量也會減小,致使切換應用程序的時候耗費更多性能。嚴重的話,甚至有可能會致使崩潰,由於系統在內存很是吃緊的時候可能已沒法維護全部正在運行的Service所依賴的進程了。算法

爲了可以控制Service的生命週期,Android官方推薦的最佳解決方案就是使用IntentService,這種Service的最大特色就是當後臺任務執行結束後會自動中止,從而極大程度上避免了Service內存泄漏的可能性。編程

讓一個Service在後臺一直保持運行,即便它並不執行任何工做,這是編寫Android程序時最糟糕的作法之一。因此Android官方極度建議開發人員們不要過於貪婪,讓Service在後臺一直運行,這不只可能會致使手機和程序的性能很是低下,並且被用戶發現了以後也有可能直接致使咱們的軟件被卸載(我我的就會這麼作)。數組

當界面不可見時釋放內存

當用戶打開了另一個程序,咱們的程序界面已經再也不可見的時候,咱們應當將全部和界面相關的資源進行釋放。在這種場景下釋放資源可讓系統緩存後臺進程的能力顯著增長,所以也會讓用戶體驗變得更好。緩存

那麼咱們如何才能知道程序界面是否是已經不可見了呢?其實很簡單,只須要在Activity中重寫onTrimMemory()方法,而後在這個方法中監聽TRIM_MEMORY_UI_HIDDEN這個級別,一旦觸發了以後就說明用戶已經離開了咱們的程序,那麼此時就能夠進行資源釋放操做了,以下所示:網絡

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. @Override  

  2. public void onTrimMemory(int level) {  

  3.     super.onTrimMemory(level);  

  4.     switch (level) {  

  5.     case TRIM_MEMORY_UI_HIDDEN:  

  6.         // 進行資源釋放操做  

  7.         break;  

  8.     }  

  9. }  

注意onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回調只有當咱們程序中的全部UI組件所有不可見的時候纔會觸發,這和onStop()方法仍是有很大區別的,由於onStop()方法只是當一個Activity徹底不可見的時候就會調用,好比說用戶打開了咱們程序中的另外一個Activity。所以,咱們能夠在onStop()方法中去釋放一些Activity相關的資源,好比說取消網絡鏈接或者註銷廣播接收器等,可是像UI相關的資源應該一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)這個回調以後纔去釋放,這樣能夠保證若是用戶只是從咱們程序的一個Activity回到了另一個Activity,界面相關的資源都不須要從新加載,從而提高響應速度。

當內存緊張時釋放內存

除了剛纔講的TRIM_MEMORY_UI_HIDDEN這個回調,onTrimMemory()方法還有不少種其它類型的回調,能夠在手機內存下降的時候及時通知咱們。咱們應該根據回調中傳入的級別來去決定如何釋放應用程序的資源:

  • TRIM_MEMORY_RUNNING_MODERATE    表示應用程序正常運行,而且不會被殺掉。可是目前手機的內存已經有點低了,系統可能會開始根據LRU緩存規則來去殺死進程了。

  • TRIM_MEMORY_RUNNING_LOW    表示應用程序正常運行,而且不會被殺掉。可是目前手機的內存已經很是低了,咱們應該去釋放掉一些沒必要要的資源以提高系統的性能,同時這也會直接影響到咱們應用程序的性能。

  • TRIM_MEMORY_RUNNING_CRITICAL    表示應用程序仍然正常運行,可是系統已經根據LRU緩存規則殺掉了大部分緩存的進程了。這個時候咱們應當儘量地去釋聽任何沒必要要的資源,否則的話系統可能會繼續殺掉全部緩存中的進程,而且開始殺掉一些原本應當保持運行的進程,好比說後臺運行的服務。

以上是當咱們的應用程序正在運行時的回調,那麼若是咱們的程序目前是被緩存的,則會收到如下幾種類型的回調:

  • TRIM_MEMORY_BACKGROUND    表示手機目前內存已經很低了,系統準備開始根據LRU緩存來清理進程。這個時候咱們的程序在LRU緩存列表的最近位置,是不太可能被清理掉的,但這時去釋放掉一些比較容易恢復的資源可以讓手機的內存變得比較充足,從而讓咱們的程序更長時間地保留在緩存當中,這樣當用戶返回咱們的程序時會感受很是順暢,而不是經歷了一次從新啓動的過程。

  • TRIM_MEMORY_MODERATE    表示手機目前內存已經很低了,而且咱們的程序處於LRU緩存列表的中間位置,若是手機內存還得不到進一步釋放的話,那麼咱們的程序就有被系統殺掉的風險了。

  • TRIM_MEMORY_COMPLETE    表示手機目前內存已經很低了,而且咱們的程序處於LRU緩存列表的最邊緣位置,系統會最優先考慮殺掉咱們的應用程序,在這個時候應當儘量地把一切能夠釋放的東西都進行釋放。

避免在Bitmap上浪費內存

當咱們讀取一個Bitmap圖片的時候,有一點必定要注意,就是千萬不要去加載不須要的分辨率。在一個很小的ImageView上顯示一張高分辨率的圖片不會帶來任何視覺上的好處,但卻會佔用咱們至關多寶貴的內存。須要僅記的一點是,將一張圖片解析成一個Bitmap對象時所佔用的內存並非這個圖片在硬盤中的大小,可能一張圖片只有100k你以爲它並不大,可是讀取到內存當中是按照像素點來算的,好比這張圖片是1500*1000像素,使用的ARGB_8888顏色類型,那麼每一個像素點就會佔用4個字節,總內存就是1500*1000*4字節,也就是5.7M,這個數據看起來就比較恐怖了。

至於如何去壓縮圖片,以及更多在圖片方面節省內存的技術,你們能夠去參考我以前寫的一篇博客 Android高效加載大圖、多圖解決方案,有效避免程序OOM 。

使用優化過的數據集合

Android API當中提供了一些優化事後的數據集合工具類,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用這些API可讓咱們的程序更加高效。傳統Java API中提供的HashMap工具類會相對比較低效,由於它須要爲每個鍵值對都提供一個對象入口,而SparseArray就避免掉了基本數據類型轉換成對象數據類型的時間。

知曉內存的開支狀況

咱們還應當清楚咱們所使用語言的內存開支和消耗狀況,而且在整個軟件的設計和開發當中都應該將這些信息考慮在內。可能有一些看起來無關痛癢的寫法,結果卻會致使很大一部分的內存開支,例如:

  • 使用枚舉一般會比使用靜態常量要消耗兩倍以上的內存,在Android開發當中咱們應當儘量地不使用枚舉。

  • 任何一個Java類,包括內部類、匿名類,都要佔用大概500字節的內存空間。

  • 任何一個類的實例要消耗12-16字節的內存開支,所以頻繁建立實例也是會必定程序上影響內存的。

  • 在使用HashMap時,即便你只設置了一個基本數據類型的鍵,好比說int,可是也會按照對象的大小來分配內存,大概是32字節,而不是4字節。所以最好的辦法就是像上面所說的同樣,使用優化過的數據集合。

謹慎使用抽象編程

許多程序員都喜歡各類使用抽象來編程,認爲這是一種很好的編程習慣。固然,這一點不能否認,由於的抽象的編程方法更加面向對象,並且在代碼的維護和可擴展性方面都會有所提升。可是,在Android上使用抽象會帶來額外的內存開支,由於抽象的編程方法須要編寫額外的代碼,雖然這些代碼根本執行不到,可是卻也要映射到內存當中,不只佔用了更多的內存,在執行效率方面也會有所下降。固然這裏我並非提倡你們徹底不使用抽象編程,而是謹慎使用抽象編程,不要認爲這是一種很酷的編程方式而去肆意使用它,只在你認爲有必要的狀況下才去使用。

儘可能避免使用依賴注入框架

如今有不少人都喜歡在Android工程當中使用依賴注入框架,好比說像Guice或者RoboGuice等,由於它們能夠簡化一些複雜的編碼操做,好比能夠將下面的一段代碼:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. class AndroidWay extends Activity {   

  2.     TextView name;   

  3.     ImageView thumbnail;   

  4.     LocationManager loc;   

  5.     Drawable icon;   

  6.     String myName;   

  7.   

  8.     public void onCreate(Bundle savedInstanceState) {   

  9.         super.onCreate(savedInstanceState);   

  10.         setContentView(R.layout.main);  

  11.         name      = (TextView) findViewById(R.id.name);   

  12.         thumbnail = (ImageView) findViewById(R.id.thumbnail);   

  13.         loc       = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);   

  14.         icon      = getResources().getDrawable(R.drawable.icon);   

  15.         myName    = getString(R.string.app_name);   

  16.         name.setText( "Hello, " + myName );   

  17.     }   

  18. }   

簡化成這樣的一種寫法:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. @ContentView(R.layout.main)  

  2. class RoboWay extends RoboActivity {   

  3.     @InjectView(R.id.name)             TextView name;   

  4.     @InjectView(R.id.thumbnail)        ImageView thumbnail;   

  5.     @InjectResource(R.drawable.icon)   Drawable icon;   

  6.     @InjectResource(R.string.app_name) String myName;   

  7.     @Inject                            LocationManager loc;   

  8.   

  9.     public void onCreate(Bundle savedInstanceState) {   

  10.         super.onCreate(savedInstanceState);   

  11.         name.setText( "Hello, " + myName );   

  12.     }   

  13. }  

看上去確實十分誘人,咱們甚至能夠將findViewById()這一類的繁瑣操做所有省去了。可是這些框架爲了要搜尋代碼中的註解,一般都須要經歷較長的初始化過程,而且還可能將一些你用不到的對象也一併加載到內存當中。這些用不到的對象會一直佔用着內存空間,可能要過好久以後纔會獲得釋放,相較之下,也許多敲幾行看似繁瑣的代碼纔是更好的選擇。

使用ProGuard簡化代碼

ProGuard相信你們都不會陌生,不少人都會使用這個工具來混淆代碼,可是除了混淆以外,它還具備壓縮和優化代碼的功能。ProGuard會對咱們的代碼進行檢索,刪除一些無用的代碼,而且會對類、字段、方法等進行重命名,重命名以後的類、字段和方法名都會比原來簡短不少,這樣的話也就對內存的佔用變得更少了。

使用多個進程

這個技巧其實並非很是建議使用,但它確實是一種能夠幫助咱們節省和管理內存的高級技巧。若是你要使用它的話必定要謹慎使用,由於絕大多數的應用程序都不該該在多個進程當中運行的,一旦使用不當,它甚至會增長額外的內存而不是幫咱們節省內存。這個技巧比較適用於那些須要在後臺去完成一項獨立的任務,和前臺的功能是能夠徹底區分開的場景。

這裏舉一個比較適合去使用多進程技巧的場景,好比說咱們正在作一個音樂播放器軟件,其中播放音樂的功能應該是一個獨立的功能,它不須要和UI方面有任何關係,即便軟件已經關閉了也應該能夠正常播放音樂。若是此時咱們只使用一個進程,那麼即便用戶關閉了軟件,已經徹底由Service來控制音樂播放了,系統仍然會將許多UI方面的內存進行保留。在這種場景下就很是適合使用兩個進程,一個用於UI展現,另外一個則用於在後臺持續地播放音樂。

想要實現多進程的功能也很是簡單,只須要在AndroidManifest文件的應用程序組件中聲明一個android:process屬性就能夠了,好比說咱們但願播放音樂的Service能夠運行在一個單獨的進程當中,就能夠這樣寫:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. <service android:name=".PlaybackService"  

  2.          android:process=":background" />  

這裏指定的進程名是background,你也能夠將它改爲任意你喜歡的名字。須要注意的是,進程名的前面都應該加上一個冒號,表示該進程是一個當前應用程序的私有進程。

雖然說如今的手機內存都已經很是大了,可是咱們你們都知道,系統是不可能將全部的內存都分配給咱們的應用程序的。沒錯,每一個程序都會有可以使用的內存上限,這被稱爲堆大小(Heap Size)。不一樣的手機,堆大小也不盡相同,隨着如今硬件設備不斷提升,堆大小也已經由Nexus One時的32MB,變成了Nexus 5時的192MB。若是你們想要知道本身手機的堆大小是多少,能夠調用以下代碼:

[java] view plaincopy

  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);  

  2. int heapSize = manager.getMemoryClass();  

結果是以MB爲單位進行返回的,咱們在開發應用程序時所使用的內存不能超出這個限制,不然就會出現OutOfMemoryError。所以,好比說咱們的程序中須要緩存一些數據,就能夠根據堆大小來決定緩存數據的容量。

下面咱們來討論一下Android的GC操做,GC全稱是Garbage Collection,也就是所謂的垃圾回收。Android系統會在適當的時機觸發GC操做,一旦進行GC操做,就會將一些再也不使用的對象進行回收。那麼哪些對象會被認爲是再也不使用,而且能夠被回收的呢?咱們來看下面一張圖:


上圖當中,每一個藍色的圓圈就表明一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關係。這些對象有些是處於活動狀態的,而有些就已經再也不被使用了。那麼GC操做會從一個叫做Roots的對象開始檢查,全部它能夠訪問到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經再也不被使用了,以下圖所示:


能夠看到,目前全部黃色的對象仍然會被系統繼續保留,而藍色的對象就會在GC操做當中被系統回收掉了,這大概就是Android系統一次簡單的GC流程。

那麼何時會觸發GC操做呢?這個一般都是由系統去決定的,咱們通常狀況下都不須要主動通知系統應該去GC了(雖然咱們確實能夠這麼作,下面會講到),可是咱們仍然能夠去監聽系統的GC過程,以此來分析咱們應用程序當前的內存狀態。那麼怎樣才能去監聽系統的GC過程呢?其實很是簡單,系統每進行一次GC操做時,都會在LogCat中打印一條日誌,咱們只要去分析這條日誌就能夠了,日誌的基本格式以下所示:

[plain] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>  

注意這裏我仍然是以dalvik虛擬機來進行說明,art狀況下打印的內容也是基本相似的。。

首先第一部分GC_Reason,這個是觸發此次GC操做的緣由,通常狀況下一共有如下幾種觸發GC操做的緣由:

  • GC_CONCURRENT:   當咱們應用程序的堆內存快要滿的時候,系統會自動觸發GC操做來釋放內存。

  • GC_FOR_MALLOC:   當咱們的應用程序須要分配更多內存,但是現有內存已經不足的時候,系統會進行GC操做來釋放內存。

  • GC_HPROF_DUMP_HEAP:   當生成HPROF文件的時候,系統會進行GC操做,關於HPROF文件咱們下面會講到。

  • GC_EXPLICIT:   這種狀況就是咱們剛纔提到過的,主動通知系統去進行GC操做,好比調用System.gc()方法來通知系統。或者在DDMS中,經過工具按鈕也是能夠顯式地告訴系統進行GC操做的。

接下來第二部分Amount_freed,表示系統經過此次GC操做釋放了多少內存。

而後Heap_stats中會顯示當前內存的空閒比例以及使用狀況(活動對象所佔內存 / 當前程序總內存)。

最後Pause_time表示此次GC操做致使應用程序暫停的時間。關於這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3以前GC操做是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。雖然說這個阻塞的過程並不會很長,也就是幾百毫秒,可是用戶在使用咱們的程序時仍是有可能會感受到略微的卡頓。而自2.3以後,GC操做改爲了併發的方式進行,就是說GC的過程當中不會影響到應用程序的正常運行,可是在GC操做的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,用戶已是徹底沒法察覺到了。

下面是一次GC操做在LogCat中打印的日誌:


能夠看出,和咱們上面所介紹的格式是徹底一致的,最後的暫停時間31ms+7ms,一次就是GC開始時的暫停時間,一次是結束時的暫停時間。另外能夠根據進程id來區分這是哪一個程序中進行的GC操做,那麼從上圖就能夠看出這條GC日誌是屬於24699這個程序的。

那麼這是使用dalvik運行環境時所打印的GC日誌,而自Android 4.4版本以後加入了art運行環境,在art中打印GC日誌基本和dalvik是相同的,以下圖所示:


相信沒有什麼難理解的地方吧,art中只是內容顯示的格式有了稍許變化,打印的主體內容仍然是不變的。

好的,經過日誌的方式咱們能夠簡單瞭解到系統的GC工做狀況,可是若是咱們想要更加清楚地實時知曉當前應用程序的內存使用狀況,只經過日誌就有些力不從心了,咱們須要經過DDMS中提供的工具來實現。

打開DDMS界面,在左側面板中選擇你要觀察的應用程序進程,而後點擊Update Heap按鈕,接着在右側面板中點擊Heap標籤,以後不停地點擊Cause GC按鈕來實時地觀察應用程序內存的使用狀況便可,以下圖所示:


接着繼續操做咱們的應用程序,而後繼續點擊Cause GC按鈕,若是你發現反覆操做某一功能會致使應用程序內存持續增高而不會降低的話,那麼就說明這裏頗有可能發生內存泄漏了。

好了,討論完了GC,接下來咱們討論一下Android中內存泄漏的問題。你們須要知道的是,Android中的垃圾回收機制並不能防止內存泄漏的出現,致使內存泄漏最主要的緣由就是某些長存對象持有了一些其它應該被回收的對象的引用,致使垃圾回收器沒法去回收掉這些對象,那也就出現內存泄漏了。好比說像Activity這樣的系統組件,它又會包含不少的控件甚至是圖片,若是它沒法被垃圾回收器回收掉的話,那就算是比較嚴重的內存泄漏狀況了。

下面咱們來模擬一種Activity內存泄漏的場景,內部類相信你們都有用過,若是咱們在一個類中又定義了一個非靜態的內部類,那麼這個內部類就會持有外部類的引用,以下所示:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class MainActivity extends ActionBarActivity {  

  2.   

  3.     @Override  

  4.     protected void onCreate(Bundle savedInstanceState) {  

  5.         super.onCreate(savedInstanceState);  

  6.         setContentView(R.layout.activity_main);  

  7.         LeakClass leakClass = new LeakClass();  

  8.     }  

  9.   

  10.     class LeakClass {  

  11.   

  12.     }  

  13.     ......  

  14. }  

目前來看,代碼仍是沒有問題的,由於雖然LeakClass這個內部類持有MainActivity的引用,可是隻要它的存活時間不會長於MainActivity,就不會阻止MainActivity被垃圾回收器回收。那麼如今咱們來將代碼進行以下修改:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class MainActivity extends ActionBarActivity {  

  2.   

  3.     @Override  

  4.     protected void onCreate(Bundle savedInstanceState) {  

  5.         super.onCreate(savedInstanceState);  

  6.         setContentView(R.layout.activity_main);  

  7.         LeakClass leakClass = new LeakClass();  

  8.         leakClass.start();  

  9.     }  

  10.   

  11.     class LeakClass extends Thread {  

  12.   

  13.         @Override  

  14.         public void run() {  

  15.             while (true) {  

  16.                 try {  

  17.                     Thread.sleep(60 * 60 * 1000);  

  18.                 } catch (InterruptedException e) {  

  19.                     e.printStackTrace();  

  20.                 }  

  21.             }  

  22.         }  

  23.     }  

  24.     ......  

  25. }  

這下就有點不太同樣了,咱們讓LeakClass繼承自Thread,而且重寫了run()方法,而後在MainActivity的onCreate()方法中去啓動LeakClass這個線程。而LeakClass的run()方法中運行了一個死循環,也就是說這個線程永遠都不會執行結束,那麼LeakClass這個對象就一直不能獲得釋放,而且它持有的MainActivity也將沒法獲得釋放,那麼內存泄露就出現了。

如今咱們能夠將程序運行起來,而後不斷地旋轉手機讓程序在橫屏和豎屏之間切換,由於每切換一次Activity都會經歷一個從新建立的過程,而前面建立的Activity又沒法獲得回收,那麼長時間操做下咱們的應用程序所佔用的內存就會愈來愈高,最終出現OutOfMemoryError。

下面我貼出一張不斷切換橫豎屏時GC日誌打印的結果圖,以下所示:


能夠看到,應用程序所佔用的內存是在不斷上升的。最可怕的是,這些內存一旦升上去了就永遠不會再降下來,直到程序崩潰爲止,由於這部分泄露的內存一直都沒法被垃圾回收器回收掉。

那麼經過上面學習的GC日誌以及DDMS工具這兩種方式,如今咱們已經能夠比較輕鬆地發現應用程序中是否存在內存泄露的現象了。可是若是真的出現了內存泄露,咱們應該怎麼定位到具體是哪裏出的問題呢?這就須要藉助一個內存分析工具了,叫作Eclipse Memory Analyzer(MAT)。咱們須要先將這個工具下載下來,下載地址是:http://eclipse.org/mat/downloads.php。這個工具分爲Eclipse插件版和獨立版兩種,若是你是使用Eclipse開發的,那麼可使用插件版MAT,很是方便。若是你是使用Android Studio開發的,那麼就只能使用獨立版的MAT了。

下載好了以後下面咱們開始學習如何去分析內存泄露的緣由,首先仍是進入到DDMS界面,而後在左側面板選中咱們要觀察的應用程序進程,接着點擊Dump HPROF file按鈕,以下圖所示:


點擊這個按鈕以後須要等待一段時間,而後會生成一個HPROF文件,這個文件記錄着咱們應用程序內部的全部數據。可是目前MAT仍是沒法打開這個文件的,咱們還須要將這個HPROF文件從Dalvik格式轉換成J2SE格式,使用hprof-conv命令就能夠完成轉換工做,以下所示:

[plain] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. hprof-conv dump.hprof converted-dump.hprof  

hprof-conv命令文件存放於<Android Sdk>/platform-tools目錄下面。另外若是你是使用的插件版的MAT,也能夠直接在Eclipse中打開生成的HPROF文件,不用通過格式轉換這一步。

好的,接下來咱們就能夠來嘗試使用MAT工具去分析內存泄漏的緣由了,這裏須要提醒你們的是,MAT並不會準確地告訴咱們哪裏發生了內存泄漏,而是會提供一大堆的數據和線索,咱們須要本身去分析這些數據來去判斷究竟是不是真的發生了內存泄漏。那麼如今運行MAT工具,而後選擇打開轉換事後的converted-dump.hprof文件,以下圖所示:


MAT中提供了很是多的功能,這裏咱們只要學習幾個最經常使用的就能夠了。上圖最中央的那個餅狀圖展現了最大的幾個對象所佔內存的比例,這張圖中提供的內容並很少,咱們能夠忽略它。在這個餅狀圖的下方就有幾個很是有用的工具了,咱們來學習一下。

Histogram能夠列出內存中每一個對象的名字、數量以及大小。

Dominator Tree會將全部內存中的對象按大小進行排序,而且咱們能夠分析對象之間的引用結構。

通常最經常使用的就是以上兩個功能了,那麼咱們先從Dominator Tree開始學起。

如今點擊Dominator Tree,結果以下圖所示:


這張圖包含的信息很是多,我來帶着你們一塊兒解析一下。首先Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存,所以從上圖中看,前兩行的Retained Heap是最大的,咱們分析內存泄漏時,內存最大的對象也是最應該去懷疑的。

另外你們應該能夠注意到,在每一行的最左邊都有一個文件型的圖標,這些圖標有的左下角帶有一個紅色的點,有的則沒有。帶有紅點的對象就表示是能夠被GC Roots訪問到的,根據上面的講解,能夠被GC Root訪問到的對象都是沒法被回收的。那麼這就說明全部帶紅色的對象都是泄漏的對象嗎?固然不是,由於有些對象系統須要一直使用,原本就不該該被回收。咱們能夠注意到,上圖當中全部帶紅點的對象最右邊都有寫一個System Class,說明這是一個由系統管理的對象,並非由咱們本身建立並致使內存泄漏的對象。

那麼上圖中就沒法看出內存泄漏的緣由了嗎?確實,內存泄漏原本就不是這麼容易找出的,咱們還須要進一步進行分析。上圖當中,除了帶有System Class的行以外,最大的就是第二行的Bitmap對象了,雖然Bitmap對象如今不能被GC Roots訪問到,但不表明着Bitmap所持有的其它引用也不會被GC Roots訪問到。如今咱們能夠對着第二行點擊右鍵 -> Path to GC Roots -> exclude weak references,爲何選擇exclude weak references呢?由於弱引用是不會阻止對象被垃圾回收器回收的,因此咱們這裏直接把它排除掉,結果以下圖所示:


能夠看到,Bitmap對象通過層層引用以後,到了MainActivity$LeakClass這個對象,而後在圖標的左下角有個紅色的圖標,就說明在這裏能夠被GC Roots訪問到了,而且這是由咱們本身建立的Thread,並非System Class了,那麼因爲MainActivity$LeakClass能被GC Roots訪問到致使不能被回收,致使它所持有的其它引用也沒法被回收了,包括MainActivity,也包括MainActivity中所包含的圖片。

經過這種方式,咱們就成功地將內存泄漏的緣由找出來了。這是Dominator Tree中比較經常使用的一種分析方式,即搜索大內存對象通向GC Roots的路徑,由於內存佔用越高的對象越值得懷疑。

接下來咱們再來學習一下Histogram的用法,回到Overview界面,點擊Histogram,結果以下圖所示:


這裏是把當前應用程序中全部的對象的名字、數量和大小所有都列出來了,須要注意的是,這裏的對象都是隻有Shallow Heap而沒有Retained Heap的,那麼Shallow Heap又是什麼意思呢?就是當前對象本身所佔內存的大小,不包含引用關係的,好比說上圖當中,byte[]對象的Shallow Heap最高,說明咱們應用程序中用了不少byte[]類型的數據,好比說圖片。能夠經過右鍵 -> List objects -> with incoming references來查看具體是誰在使用這些byte[]。

那麼經過Histogram又怎麼去分析內存泄漏的緣由呢?固然其實也能夠用和Dominator Tree中比較類似的方式,即分析大內存的對象,好比上圖中byte[]對象內存佔用很高,咱們經過分析byte[],最終也是能找到內存泄漏所在的,可是這裏我準備使用另一種更適合Histogram的方式。你們能夠看到,Histogram中是能夠顯示對象的數量的,那麼好比說咱們如今懷疑MainActivity中有可能存在內存泄漏,就能夠在第一行的正則表達式框中搜索「MainActivity」,以下所示:


能夠看到,這裏將包含「MainActivity」字樣的全部對象所有列出了出來,其中第一行就是MainActivity的實例。可是你們有沒有注意到,當前內存中是有11個MainActivity的實例的,這太不正常了,經過狀況下一個Activity應該只有一個實例纔對。其實這些對象就是因爲咱們剛纔不斷地橫豎屏切換所產生的,由於橫豎屏切換一次,Activity就會經歷一個從新建立的過程,可是因爲LeakClass的存在,以前的Activity又沒法被系統回收,那麼就出現這種一個Activity存在多個實例的狀況了。

接下來對着MainActivity右鍵 -> List objects -> with incoming references查看具體MainActivity實例,以下圖所示:


若是想要查看內存泄漏的具體緣由,能夠對着任意一個MainActivity的實例右鍵 -> Path to GC Roots -> exclude weak references,結果以下圖所示:


能夠看到,咱們再次找到了內存泄漏的緣由,是由於MainActivity$LeakClass對象所致使的。

好了,這大概就是MAT工具最經常使用的一些用法了,固然這裏還要提醒你們一句,工具是死的,人是活的,MAT也沒有辦法保證必定能夠將內存泄漏的緣由找出來,仍是須要咱們對程序的代碼有足夠多的瞭解,知道有哪些對象是存活的,以及它們存活的緣由,而後再結合MAT給出的數據來進行具體的分析,這樣纔有可能把一些隱藏得很深的問題緣由給找出來。

 咱們在平時寫代碼時還要注意一些編碼規範,這樣不只可以在微觀層面提高程序必定的性能,也可讓咱們的代碼變得更加專業,下面就讓咱們來一塊兒學習一下這些技巧。

避免建立沒必要要的對象

建立對象歷來都不該該是一件隨意的事情,由於建立一個對象就意味着垃圾回收器須要回收一個對象,而這兩步操做都是須要消耗時間的。雖然說建立一個對象的代價確實很是小,而且Android 2.3版本當中又增長了併發垃圾回收器機制(詳見 Android最佳性能實踐(二)——分析內存的使用狀況),這讓GC操做時的停頓時間也變得難以察覺,可是這些理由都不足以讓咱們能夠肆意地建立對象,須要建立的對象咱們天然要建立,可是沒必要要的對象咱們就應該儘可能避免建立。

下面來看一些咱們能夠避免建立對象的場景:

  • 若是咱們有一個須要拼接的字符串,那麼能夠優先考慮使用StringBuffer或者StringBuilder來進行拼接,而不是加號鏈接符,由於使用加號鏈接符會建立多餘的對象,拼接的字符串越長,加號鏈接符的性能越低。

  • 在沒有特殊緣由的狀況下,儘可能使用基本數據類來代替封裝數據類型,int比Integer要更加高效,其它數據類型也是同樣。

  • 當一個方法的返回值是String的時候,一般能夠去判斷一下這個String的做用是什麼,若是咱們明確地知道調用方會將這個返回的String再進行拼接操做的話,能夠考慮返回一個StringBuffer對象來代替,由於這樣能夠將一個對象的引用進行返回,而返回String的話就是建立了一個短生命週期的臨時對象。

  • 正如前面所說,基本數據類型要優於對象數據類型,相似地,基本數據類型的數組也要優於對象數據類型的數組。另外,兩個平行的數組要比一個封裝好的對象數組更加高效,舉個例子,Foo[]和Bar[]這樣的兩個數組,使用起來要比Custom(Foo,Bar)[]這樣的一個數組高效得多。

固然上面所說的只是一些表明性的例子,咱們所要遵照的一個基本原則就是儘量地少建立臨時對象,越少的對象意味着越少的GC操做,同時也就意味着越好的程序性能和用戶體驗。

靜態優於抽象

若是你並不須要訪問一個對象中的某些字段,只是想調用它的某個方法來去完成一項通用的功能,那麼能夠將這個方法設置成靜態方法,這會讓調用的速度提高15%-20%,同時也不用爲了調用這個方法而去專門建立對象了,這樣還知足了上面的一條原則。另外這也是一種好的編程習慣,由於咱們能夠放心地調用靜態方法,而不用擔憂調用這個方法後是否會改變對象的狀態(靜態方法內沒法訪問非靜態字段)。

對常量使用static final修飾符

咱們先來看一下在一個類的最頂部定義以下代碼:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. static int intVal = 42;  

  2. static String strVal = "Hello, world!";  

編譯器會爲上述代碼生成一個初始化方法,稱爲<clinit>方法,該方法會在定義類第一次被使用的時候調用。而後這個方法會將42的值賦值到intVal當中,並從字符串常量表中提取一個引用賦值到strVal上。當賦值完成後,咱們就能夠經過字段搜尋的方式來去訪問具體的值了。

可是咱們還能夠經過final關鍵字來對上述代碼進行優化:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. static final int intVal = 42;  

  2. static final String strVal = "Hello, world!";  

通過這樣修改以後,定義類就再也不須要一個<clinit>方法了,由於全部的常量都會在dex文件的初始化器當中進行初始化。當咱們調用intVal時能夠直接指向42的值,而調用strVal時會用一種相對輕量級的字符串常量方式,而不是字段搜尋的方式。

另外須要你們注意的是,這種優化方式只對基本數據類型以及String類型的常量有效,對於其它數據類型的常量是無效的。不過,對於任何常量都是用static final的關鍵字來進行聲明仍然是一種很是好的習慣。

使用加強型for循環語法

加強型for循環(也被稱爲for-each循環)能夠用於去遍歷實現Iterable接口的集合以及數組,這是jdk 1.5中新增的一種循環模式。固然除了這種新增的循環模式以外,咱們仍然還可使用原有的普通循環模式,只不過它們之間是有效率區別的,咱們來看下面一段代碼:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. static class Counter {  

  2.     int mCount;  

  3. }  

  4.   

  5. Counter[] mArray = ...  

  6.   

  7. public void zero() {  

  8.     int sum = 0;  

  9.     for (int i = 0; i < mArray.length; ++i) {  

  10.         sum += mArray[i].mCount;  

  11.     }  

  12. }  

  13.   

  14. public void one() {  

  15.     int sum = 0;  

  16.     Counter[] localArray = mArray;  

  17.     int len = localArray.length;  

  18.     for (int i = 0; i < len; ++i) {  

  19.         sum += localArray[i].mCount;  

  20.     }  

  21. }  

  22.   

  23. public void two() {  

  24.     int sum = 0;  

  25.     for (Counter a : mArray) {  

  26.         sum += a.mCount;  

  27.     }  

  28. }  

能夠看到,上述代碼當中咱們使用了三種不一樣的循環方式來對mArray中的全部元素進行求和。其中zero()方法是最慢的一種,由於它是把mArray.length寫在循環當中的,也就是說每循環一次都須要從新計算一次mArray的長度。而one()方法則相對快得多,由於它使用了一個局部變量len來記錄數組的長度,這樣就省去了每次循環時字段搜尋的時間。two()方法在沒有JIT(Just In Time Compiler)的設備上是運行最快的,而在有JIT的設備上運行效率和one()方法不相上下,惟一須要注意的是這種寫法須要JDK 1.5以後才支持。

可是這裏要跟你們提一個特殊狀況,對於ArrayList這種集合,本身手寫的循環要比加強型for循環更快,而其餘的集合就沒有這種狀況。所以,對於咱們來講,默認狀況下能夠都使用加強型for循環,而遍歷ArrayList時就仍是使用傳統的循環方式吧。

多使用系統封裝好的API

Java語言當中其實給咱們提供了很是豐富的API接口,咱們在編寫程序時若是可使用系統提供的API就應該儘可能使用,系統提供的API完成不了咱們須要的功能時才應該本身去寫,由於使用系統的API在不少時候比咱們本身寫的代碼要快得多,它們的不少功能都是經過底層的彙編模式執行的。

好比說String類當中提供的好多API都是擁有極高的效率的,像indexOf()方法和一些其它相關的API,雖然說咱們經過本身編寫算法也可以完成一樣的功能,可是效率方面會和這些方法差的比較遠。這裏舉個例子,若是咱們要實現一個數組拷貝的功能,使用循環的方式來對數組中的每個元素一一進行賦值固然是可行的,可是若是咱們直接使用系統中提供的System.arraycopy()方法將會讓執行效率快9倍以上。

避免在內部調用Getters/Setters方法

咱們平時寫代碼時都被告知,必定要使用面向對象的思惟去寫代碼,而面向對象的三大特性咱們都知道,封裝、多態和繼承。其中封裝的基本思想就是不要把類內部的字段暴漏給外部,而是提供特定的方法來容許外部操做相應類的內部字段,從而在Java語言當中就出現了Getters/Setters這種封裝技巧。

然而在Android上這個技巧就再也不是那麼的受推崇了,由於字段搜尋要比方法調用效率高得多,咱們直接訪問某個字段可能要比經過getters方法來去訪問這個字段快3到7倍。不過咱們確定不能僅僅由於效率的緣由就將封裝這個技巧給拋棄了,編寫代碼仍是要按照面向對象思惟的,可是咱們能夠在能優化的地方進行優化,好比說避免在內部調用getters/setters方法。

那什麼叫作在內部調用getters/setters方法呢?這裏我舉一個很是簡單的例子:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class Calculate {  

  2.       

  3.     private int one = 1;  

  4.       

  5.     private int two = 2;  

  6.   

  7.     public int getOne() {  

  8.         return one;  

  9.     }  

  10.   

  11.     public int getTwo() {  

  12.         return two;  

  13.     }  

  14.       

  15.     public int getSum() {  

  16.         return getOne() + getTwo();  

  17.     }  

  18. }  

能夠看到,上面是一個Calculate類,這個類的功能很是簡單,先將one和two這兩個字段進行了封裝,而後提供了getOne()方法獲取one字段的值,提供了getTwo()方法獲取two字段的值,還提供了一個getSum()方法用於獲取總和的值。

這裏咱們注意到,getSum()方法當中的算法就是將one和two的值相加進行返回,可是它獲取one和two的值的方式也是經過getters方法進行獲取的,其實這是一種徹底沒有必要的方式,由於getSum()方法自己就是Calculate類內部的方法,它是能夠直接訪問到Calculate類中的封裝字段的,所以這種寫法在Android上是不推崇的,咱們能夠進行以下修改:

[java] view plaincopy在CODE上查看代碼片派生到個人代碼片

  1. public class Calculate {  

  2.       

  3.     private int one = 1;  

  4.       

  5.     private int two = 2;  

  6.   

  7.     ......  

  8.       

  9.     public int getSum() {  

  10.         return one + two;  

  11.     }  

  12. }  

改爲這種寫法以後,咱們就避免了在內部調用getters/setters方法,而對於外部而言Calculate類仍然是具備很好的封裝性的。

相關文章
相關標籤/搜索