Android,合理管理記憶體

來源:互聯網
上載者:User

標籤:

[-]

  1. 節制地使用Service
  2. 當介面不可見時釋放記憶體
  3. 當記憶體緊張時釋放記憶體
  4. 避免在Bitmap上浪費記憶體
  5. 使用最佳化過的資料集合
  6. 知曉記憶體的開支情況
  7. 謹慎使用抽象編程
  8. 盡量避免使用依賴注入架構
  9. 使用ProGuard簡化代碼
  10. 使用多個進程

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/42238627

有不少朋友都問過我,怎樣才能寫出高效能的應用程式,如何避免程式出現OOM,或者當程式記憶體佔用過高的時候該怎麼樣去排查。確實,一個優秀的應用程式,不僅僅要功能完成得好,效能問題也應該處理得恰到好處。為此,我也是閱讀了不少Android官方給出的高效能編程建議,那麼從本篇文章開始,我就準備開始寫一個全新系列的博文,來把這些建議進行整理和分析,協助大家能夠寫出更加出色的應用程式。

注意本系列文章的內容基本源於Android Doc,如果想要閱讀更加詳細的關於效能方面的資料,可以直接去閱讀Android官方文檔。

記憶體(RAM)對於任何一個軟體開發環境都是種非常珍貴的資源,而對於移動作業系統來講的話,則會顯得更加珍貴,因為手機的硬體條件相對於PC畢竟是比較落後的。儘管Android系統的虛擬機器擁有自動回收垃圾的機制,但這並不代表我們就可以忽視應該在什麼時候分配和釋放記憶體。

為了使記憶體回收行程可以正常釋放程式所佔用的記憶體,在編寫代碼的時候就一定要注意盡量避免出現記憶體流失的情況(通常都是由於全域成員變數持有對象引用所導致的),並且在適當的時候去釋放對象引用。對於大多數的應用程式而言,後面其它的事情就可以都交給記憶體回收行程去完成了,如果一個對象的引用不再被其它對象所持有,那麼系統就會將這個對象所分配的記憶體進行回收。

我們在開發軟體的時候應當自始至終都把記憶體的問題充分考慮進去,這樣的話才能開發出更加高效能的軟體。而記憶體問題也並不是無規律可行的,Android系統給我們提出了很多記憶體最佳化的建議技巧,只要按照這些技巧來編寫程式,就可以讓我們的程式在記憶體效能發麵表現得相當不錯,下面我們就來一一學習一下這些技巧。

節制地使用Service

如果應用程式當中需要使用Service來執行背景工作的話,請一定要注意只有當任務正在執行的時候才應該讓Service運行起來。另外,當任務執行完之後去停止Service的時候,要小心Service停止失敗導致記憶體流失的情況。

當我們啟動一個Service時,系統會傾向於將這個Service所依賴的進程進行保留,這樣就會導致這個進程變得非常消耗記憶體。並且,系統可以在LRU cache當中緩衝的進程數量也會減少,導致切換應用程式的時候耗費更多效能。嚴重的話,甚至有可能會導致崩潰,因為系統在記憶體非常吃緊的時候可能已無法維護所有正在啟動並執行Service所依賴的進程了。

為了能夠控制Service的生命週期,Android官方推薦的最佳解決方案就是使用IntentService,這種Service的最大特點就是當背景工作執行結束後會自動停止,從而極大程度上避免了Service記憶體流失的可能性。關於IntentService更加詳細的用法講解,可以參考《第一行代碼——Android》的9.5.2節。

讓一個Service在後台一直保持運行,即使它並不執行任何工作,這是編寫Android程式時最糟糕的做法之一。所以Android官方極度建議開發人員們不要過於貪婪,讓Service在後台一直運行,這不僅可能會導致手機和程式的效能非常低下,而且被使用者發現了之後也有可能直接導致我們的軟體被卸載(我個人就會這麼做)。

當介面不可見時釋放記憶體

當使用者開啟了另外一個程式,我們的程式介面已經不再可見的時候,我們應當將所有和介面相關的資源進行釋放。在這種情境下釋放資源可以讓系統緩衝後台進程的能力顯著增加,因此也會讓使用者體驗變得更好。

那麼我們如何才能知道程式介面是不是已經不可見了呢?其實很簡單,只需要在Activity中重寫onTrimMemory()方法,然後在這個方法中監聽TRIM_MEMORY_UI_HIDDEN這個層級,一旦觸發了之後就說明使用者已經離開了我們的程式,那麼此時就可以進行資源釋放操作了,如下所示:

[java]  view plain copy
  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就避免掉了基礎資料型別 (Elementary Data Type)轉換成對象資料類型的時間。

知曉記憶體的開支情況

我們還應當清楚我們所使用語言的記憶體開支和消耗情況,並且在整個軟體的設計和開發當中都應該將這些資訊考慮在內。可能有一些看起來無關痛癢的寫法,結果卻會導致很大一部分的記憶體開支,例如:

  • 使用枚舉通常會比使用靜態常量要消耗兩倍以上的記憶體,在Android開發當中我們應當儘可能地不使用枚舉。
  • 任何一個Java類,包括內部類、匿名類,都要佔用大概500位元組的記憶體空間。
  • 任何一個類的執行個體要消耗12-16位元組的記憶體開支,因此頻繁建立執行個體也是會一定程式上影響記憶體的。
  • 在使用HashMap時,即使你只設定了一個基礎資料型別 (Elementary Data Type)的鍵,比如說int,但是也會按照對象的大小來分配記憶體,大概是32位元組,而不是4位元組。因此最好的辦法就是像上面所說的一樣,使用最佳化過的資料集合。
謹慎使用抽象編程

許多程式員都喜歡各種使用抽象來編程,認為這是一種很好的編程習慣。當然,這一點不可否認,因為的抽象的編程方法更加物件導向,而且在代碼的維護和可擴充性方面都會有所提高。但是,在Android上使用抽象會帶來額外的記憶體開支,因為抽象的編程方法需要編寫額外的代碼,雖然這些代碼根本執行不到,但是卻也要映射到記憶體當中,不僅佔用了更多的記憶體,在執行效率方面也會有所降低。當然這裡我並不是提倡大家完全不使用抽象編程,而是謹慎使用抽象編程,不要認為這是一種很酷的編程方式而去肆意使用它,只在你認為有必要的情況下才去使用。

盡量避免使用依賴注入架構

現在有很多人都喜歡在Android工程當中使用依賴注入架構,比如說像Guice或者RoboGuice等,因為它們可以簡化一些複雜的編碼操作,比如可以將下面的一段代碼:

[java]  view plain copy
  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 plain copy
  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 plain copy
  1. <service android:name=".PlaybackService"  
  2.          android:process=":background" />  

這裡指定的進程名是background,你也可以將它改成任意你喜歡的名字。需要注意的是,進程名的前面都應該加上一個冒號,表示該進程是一個當前應用程式的私人進程。

遵循以上的所有編程建議,我們就可以讓應用程式記憶體的使用變得更加合理化。但這隻是第一步而已,為了要讓程式擁有最佳效能,我們要學習的東西還有很多,下篇文章當中將會介紹如何分析記憶體的使用方式,感興趣的朋友請繼續閱讀 Android最佳效能實踐(二)——分析記憶體的使用方式 。

Android,合理管理記憶體

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.