Android代碼內存優化建議-Android官方篇

前言

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

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

正文

本文原文來自Android開發者官網Managing Your App's Memory章節中的
How Your App Should Manage Memory部分。是Android官方幫助應用開發者更好地管理應用的內存而寫的。作爲一個應用程序開發者,你需要在你開發應用程序的時時刻刻都考慮內存問題。

1. 節制地使用Service

如果應用程序當中需要使用Service來執行後臺任務的話,請一定要注意只有當任務正在執行的時候才應該讓Service運行起來。另外,當任務執行完之後去停止Service的時候,要小心Service停止失敗導致內存泄漏的情況。

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

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

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

2. 當界面不可見時釋放內存

當用戶打開了另外一個程序,我們的程序界面已經不再可見的時候,我們應當將所有和界面相關的資源進行釋放。在這種場景下釋放資源可以讓系統緩存後臺進程的能力顯著增加,因此也會讓用戶體驗變得更好。
那麼我們如何才能知道程序界面是不是已經不可見了呢?其實很簡單,只需要在Activity中重寫onTrimMemory()方法,然後在這個方法中監聽TRIM_MEMORY_UI_HIDDEN這個級別,一旦觸發了之後就說明用戶已經離開了我們的程序,那麼此時就可以進行資源釋放操作了,如下所示:

@Override  
public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 進行資源釋放操作  
        break;  
    }  
}

注意onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回調只有當我們程序中的所有UI組件全部不可見的時候纔會觸發,這和onStop()方法還是有很大區別的,因爲onStop()方法只是當一個Activity完全不可見的時候就會調用,比如說用戶打開了我們程序中的另一個Activity。因此,我們可以在onStop()方法中去釋放一些Activity相關的資源,比如說取消網絡連接或者註銷廣播接收器等,但是像UI相關的資源應該一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)這個回調之後纔去釋放,這樣可以保證如果用戶只是從我們程序的一個Activity回到了另外一個Activity,界面相關的資源都不需要重新加載,從而提升響應速度。

3.當內存緊張時釋放內存

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

3.1 應用程序正在運行時

TRIM_MEMORY_RUNNING_MODERATE 表示應用程序正常運行,並且不會被殺掉。但是目前手機的內存已經有點低了,系統可能會開始根據LRU緩存規則來去殺死進程了。
TRIM_MEMORY_RUNNING_LOW 表示應用程序正常運行,並且不會被殺掉。但是目前手機的內存已經非常低了,我們應該去釋放掉一些不必要的資源以提升系統的性能,同時這也會直接影響到我們應用程序的性能。
TRIM_MEMORY_RUNNING_CRITICAL 表示應用程序仍然正常運行,但是系統已經根據LRU緩存規則殺掉了大部分緩存的進程了。這個時候我們應當儘可能地去釋放任何不必要的資源,不然的話系統可能會繼續殺掉所有緩存中的進程,並且開始殺掉一些本來應當保持運行的進程,比如說後臺運行的服務。

3.2 應用程序被緩存

TRIM_MEMORY_BACKGROUND 表示手機目前內存已經很低了,系統準備開始根據LRU緩存來清理進程。這個時候我們的程序在LRU緩存列表的最近位置,是不太可能被清理掉的,但這時去釋放掉一些比較容易恢復的資源能夠讓手機的內存變得比較充足,從而讓我們的程序更長時間地保留在緩存當中,這樣當用戶返回我們的程序時會感覺非常順暢,而不是經歷了一次重新啓動的過程。
TRIM_MEMORY_MODERATE 表示手機目前內存已經很低了,並且我們的程序處於LRU緩存列表的中間位置,如果手機內存還得不到進一步釋放的話,那麼我們的程序就有被系統殺掉的風險了。
TRIM_MEMORY_COMPLETE 表示手機目前內存已經很低了,並且我們的程序處於LRU緩存列表的最邊緣位置,系統會最優先考慮殺掉我們的應用程序,在這個時候應當儘可能地把一切可以釋放的東西都進行釋放。

因爲onTrimMemory()是在API14才加進來的,所以如果要支持API14之前的話,則可以考慮 onLowMemory()這個方法,它粗略的相等於onTrimMemory()回調的TRIM_MEMORY_COMPLETE事件。

注意:當系統安裝LRU cache殺進程的時候,儘管大部分時間是從下往上按順序殺,有時候系統也會將佔用內存比較大的進程納入被殺範圍,以儘快得到足夠的內存。所以你的應用在LRU list中佔用的內存越少,你就越能避免被殺掉,當你恢復的時候也會更快。

4. 檢查你應該使用多少的內存

正如前面提到的,每一個Android設備都會有不同的RAM總大小與可用空間,因此不同設備爲app提供了不同大小的heap限制。你可以通過調用getMemoryClass()來獲取你的app的可用heap大小。如果你的app嘗試申請更多的內存,會出現OutOfMemory的錯誤。

在一些特殊的情景下,你可以通過在manifest的application標籤下添加largeHeap=true的屬性來聲明一個更大的heap空間。如果你這樣做,你可以通過getLargeMemoryClass()來獲取到一個更大的heap size。

然而,能夠獲取更大heap的設計本意是爲了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因爲你需要使用大量的內存而去請求一個大的heap size。只有當你清楚的知道哪裏會使用大量的內存並且爲什麼這些內存必須被保留時纔去使用large heap. 因此請儘量少使用large heap。使用額外的內存會影響系統整體的用戶體驗,並且會使得GC的每次運行時間更長。在任務切換時,系統的性能會變得大打折扣。

另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。

5. 避免在Bitmap上浪費內存

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

6. 使用優化過的數據集合

利用Android Framework裏面優化過的容器類,例如SparseArraySparseBooleanArray, 與 LongSparseArray。 通常的HashMap的實現方式更加消耗內存,因爲它需要一個額外的實例對象來記錄Mapping操作。另外,SparseArray更加高效在於他們避免了對key與value的autobox自動裝箱,並且避免了裝箱後的解箱。

7. 知曉內存的開支情況

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

  • 使用枚舉通常會比使用靜態常量要消耗兩倍以上的內存,在Android開發當中我們應當儘可能地不使用枚舉。
  • 任何一個Java類,包括內部類、匿名類,都要佔用大概500字節的內存空間。
  • 任何一個類的實例要消耗12-16字節的內存開支,因此頻繁創建實例也是會一定程序上影響內存的。
  • 在使用HashMap時,即使你只設置了一個基本數據類型的鍵,比如說int,但是也會按照對象的大小來分配內存,大概是32字節,而不是4字節。因此最好的辦法就是像上面所說的一樣,使用優化過的數據集合。

8. 謹慎使用抽象編程

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

9. 爲序列化的數據使用nano protobufs

Protocol buffers是由Google爲序列化結構數據而設計的,一種語言無關,平臺無關,具有良好擴展性的協議。類似XML,卻比XML更加輕量,快速,簡單。如果你需要爲你的數據實現協議化,你應該在客戶端的代碼中總是使用nano protobufs。通常的協議化操作會生成大量繁瑣的代碼,這容易給你的app帶來許多問題:增加RAM的使用量,顯著增加APK的大小,更慢的執行速度,更容易達到DEX的字符限制。

關於更多細節,請參考protobuf readme的"Nano version"章節。

10. 儘量避免使用依賴注入框架

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

class AndroidWay extends Activity {   
    TextView name;   
    ImageView thumbnail;   
    LocationManager loc;   
    Drawable icon;   
    String myName;   

    public void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
        setContentView(R.layout.main);  
        name      = (TextView) findViewById(R.id.name);   
        thumbnail = (ImageView) findViewById(R.id.thumbnail);   
        loc       = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);   
        icon      = getResources().getDrawable(R.drawable.icon);   
        myName    = getString(R.string.app_name);   
        name.setText( "Hello, " + myName );   
    }   
}

簡化成這樣的一種寫法:

@ContentView(R.layout.main)  
class RoboWay extends RoboActivity {   
    @InjectView(R.id.name)             TextView name;   
    @InjectView(R.id.thumbnail)        ImageView thumbnail;   
    @InjectResource(R.drawable.icon)   Drawable icon;   
    @InjectResource(R.string.app_name) String myName;   
    @Inject                            LocationManager loc;   

    public void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
        name.setText( "Hello, " + myName );   
    }   
}

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

11. 謹慎使用external libraries

很多External library的代碼都不是爲移動網絡環境而編寫的,在移動客戶端則顯示的效率不高。至少,當你決定使用一個external library的時候,你應該針對移動網絡做繁瑣的porting與maintenance的工作。

即使是針對Android而設計的library,也可能是很危險的,因爲每一個library所做的事情都是不一樣的。例如,其中一個lib使用的是nano protobufs, 而另外一個使用的是micro protobufs。那麼這樣,在你的app裏面就有2種protobuf的實現方式。這樣的衝突同樣可能發生在輸出日誌,加載圖片,緩存等等模塊裏面。

同樣不要陷入爲了1個或者2個功能而導入整個library的陷阱。如果沒有一個合適的庫與你的需求相吻合,你應該考慮自己去實現,而不是導入一個大而全的解決方案。

12. 優化整體性能

官方有列出許多優化整個app性能的文章:Best Practices for Performance. 這篇文章就是其中之一。有些文章是講解如何優化app的CPU使用效率,有些是如何優化app的內存使用效率。

你還應該閱讀optimizing your UI來爲layout進行優化。同樣還應該關注lint工具所提出的建議,進行優化。

13. 使用ProGuard來剔除不需要的代碼

ProGuard能夠通過移除不需要的代碼,重命名類,域與方法等方對代碼進行壓縮,優化與混淆。使用ProGuard可以是的你的代碼更加緊湊,這樣能夠使用更少mapped代碼所需要的RAM。

14. 對最終的APK使用zipalign

在編寫完所有代碼,並通過編譯系統生成APK之後,你需要使用zipalign對APK進行重新校準。如果你不做這個步驟,會導致你的APK需要更多的RAM,因爲一些類似圖片資源的東西不能被mapped。

Notes::Google Play不接受沒有經過zipalign的APK。

15. 分析你的RAM使用情況

一旦你獲取到一個相對穩定的版本後,需要分析你的app整個生命週期內使用的內存情況,並進行優化,更多細節請參考Investigating Your RAM Usage.

16. 使用多進程

如果合適的話,有一個更高級的技術可以幫助你的app管理內存使用:通過把你的app組件切分成多個組件,運行在不同的進程中。這個技術必須謹慎使用,大多數app都不應該運行在多個進程中。因爲如果使用不當,它會顯著增加內存的使用,而不是減少。當你的app需要在後臺運行與前臺一樣的大量的任務的時候,可以考慮使用這個技術。

一個典型的例子是創建一個可以長時間後臺播放的Music Player。如果整個app運行在一個進程中,當後臺播放的時候,前臺的那些UI資源也沒有辦法得到釋放。類似這樣的app可以切分成2個進程:一個用來操作UI,另外一個用來後臺的Service.

你可以通過在manifest文件中聲明'android:process'屬性來實現某個組件運行在另外一個進程的操作。

<service android:name=".PlaybackService"
         android:process=":background" />

更多關於使用這個技術的細節,請參考原文,鏈接如下。
http://developer.android.com/training/articles/memory.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章