App內存佔用優化

RAM(Random-access memory)在任何軟件開發中都是非常寶貴的資源,移動操作系統由於其物理內存的侷限性更是如此。儘管ART(Android Runtime)與Dalvik虛擬機會執行常規的垃圾回收,但這並不意味着可以忽略App中的內存分配與釋放。我們應當避免引起內存泄露,如持有靜態成員變量而導致無法釋放,應當在應用的生命週期回調中釋放掉所有的引用。

本文主要介紹如何減少App中的內存使用。

監控可用內存及內存使用狀況

Android 框架與Android Studio可以幫助我們來分析和調整App的內存使用,其中Android框架提供了一些API來幫助App在運行時動態減少內存佔用,Android Studio包括一些工具來查看內存的使用情況。

RAM使用分析工具

在優化內存問題之前,需要先找到這些問題,Android Studio及Android SDK提供了幾個工具用來分析App中的內存使用:

  1. Android Studio中的Memory Monitor

    該工具可以顯示一個會話過程中的內存分配情況,有一個可視化的圖形界面,可以看到Java內存隨時間的變化情況以及GC事件。當App運行時,可以啓動GC操作並且獲取Java Heap的快照。該工具的輸出可以幫助我們定位哪裏容易導致頻繁的垃圾回收,從而導致應用程序變慢。

  2. Android Studio中的Allocation Tracker工具

    該工具記錄了一個App的內存分配情況並在分析快照中列出了所有分配的對象。可以使用此工具找到分配過多對象的部分代碼。

響應回調釋放內存

不同的Android設備或不同的用戶操作會導致不同的內存佔用狀況,Android系統在遇到內存壓力的情況下會發出信號預警,App需要監聽這些信號來調整內存的使用。

可以使用ComponentCallbacks2 API來監聽回調以調整內存使用狀態。onTrimMemory()可以允許App監聽內存相關的事件,無論App是在前臺運行還是在後臺運行。下面是一個示例,通過實現Activity的onTrimMemory()方法來監聽內存相關的回調。

import android.content.ComponentCallbacks2;
// Other import statements ...

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code ...

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that was raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event was raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory that your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                   begin killing background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                   the first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app received an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

onTrimMemory()回調是在Android4.0(API Level 14)添加的,對於之前的版本,可以使用onLowMemory()回調替代,它大致相當於TRIM_MEMORY_COMPLETE事件。

檢測應該使用多少內存

Android爲了支持多進程,因此爲每個App佔用的內存做了限制。由於不同設備的RAM大小不同,因此分配給每個App的Heap大小也會有差異。當App已經到達了特定的Heap限制,如果再進行內存分配的話,就會拋出 OutOfMemoryError異常。

爲了避免內存溢出,我們可以通過getMemoryInfo()查詢當前設備上還有多少可用的內存空間,該方法返回一個ActivityManager.MemoryInfo對象,它包含了設備當前的內存狀態,如可用內存、總內存以及內存閾值(當可用內存低於該閾值時,系統就會殺死部分進程)等信息。ActivityManager.MemoryInfo有一個叫做lowMemory的布爾屬性,表示設備是否處於低內存狀態。

下面是一個使用getMemoryInfo()的示例:

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work ...
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

使用高效、內存佔用少的代碼結構

一些Android特性、Java類、代碼結構會使用更多的內存,可通過使用更高效的代碼結構來節省內存。

有節制地使用Service

讓一個不再需要的Service保持運行是Android開發中最糟糕的內存管理錯誤之一。如果App需要Service來執行後臺任務,需要在它完成任務時終止它,否則可能導致內存泄露。

當啓動一個Service時,系統會保持該服務所在的進程,這樣該服務佔用的內存將不能被其他進程使用。同時系統通過LRU Cache緩存的的進程數量也將減少,從而降低進程間切換的效率。當內存緊張或系統無法爲當前所運行的Service提供足夠的進程時還會發生系統抖動。

應當避免使用持久運行的Service,因爲它們對內存有持續的需求,建議使用JobScheduler

如果必須使用Service,可以使用IntentService來限制其生命週期,IntentService會在處理完任務之後終止。

使用優化的數據容器

一些編程語言提供的類可能並未針對移動設備做優化,例如通用的HashMap實現是比較低效的,因爲每一個映射都需要一個單獨的Entry對象。

Android框架提供了一些優化過的數據容器,如SparseArraySparseBooleanArrayLongSparseArray。例如SparseArray更加高效,是因爲它避免了對key及一些value的自動裝箱操作。

謹慎使用代碼抽象

開發者經使用抽象來簡化編程,因爲抽象可以提高代碼的靈活性,也更方便維護。但是抽象會帶來明顯的內存消耗:抽象一般來說需要執行更多的代碼、需要更多的時間以及RAM空間來將代碼映射到內存。所以如果抽象不能帶來明顯的好處,應當避免使用代碼抽象。

例如,枚舉佔用的內存通常是靜態常量的兩倍,需要嚴格避免在Android中使用枚舉。

使用Nano版本protobufs序列化數據

Protocol buffers是Google出品的獨立於平臺及語言的、可拓展的結構化數據序列化技術,它類似於XML,但更加輕量級、快速、簡潔。如果決定使用protobufs來序列化數據,應該在客戶端選擇使用Nano版本,因爲常規版本的protobufs會生成極其冗長的代碼,從而導致App端出現各種問題,如內存溢出、APK大小增加、執行速度變慢等。

Nano版本Protobufs的相關參考:protobuf readme

避免內存泄露

垃圾回收通常不會影響應用的性能,但是短時間內的垃圾收集將會佔用幀時間,垃圾回收佔用的時間越多,用到其他事情上的時間就越少。

通常,內存泄露會導致頻繁地垃圾回收事件的發生,在實踐中,內存泄露描述了給定時間內分配的臨時對象的數量。

例如,可能在for循環中分配多個臨時對象,或者在View的onDraw()方法中創建多個Paint、Bitmap對象。上述情況下,App會快速創建大量對象,從而迅速消耗掉新生代中的內存,導致GC的發生。

我們需要找到內存泄露的地方並進行修復,如將實例化操作移出for循環,不要在onDraw()這種頻繁調用的方法中創建對象。

移除內存密集型的資源和庫

代碼中的一些資源及Library可能會在我們不知情的情況下吞噬內存。一個APK中,第三方庫或者嵌入的資源會影響到App佔用的內存總量。可以通過移除冗餘的資源、臃腫的組件及不必要的Library來優化內存消耗。

減小APK大小

通過減小APK的大小可以明顯減低App對內存的佔用。Bitmap大小、資源、動畫幀圖像以及第三方庫影響APK的大小,Android Studio及Android SDK提供了一些工具用來減少資源大小及外部依賴。

更多關於APK的瘦身方案,可參考Reduce APK Size

使用Dagger2實現依賴注入

依賴注入框架可以簡化代碼併爲測試及其他配置變化提供適配環境。

如果打算在App中使用依賴注入框架的話,建議使用Dagger2。Dagger沒有使用反射,它的靜態、編譯時實現意味着它不需要運行時成本及內存消耗。

其他使用反射的依賴注入框架需要掃描代碼來尋找註解,這個過程可能需要更多的CPU週期和RAM,並可能導致應用程序啓動的明顯滯後。

謹慎使用外部Library

第三方的Library代碼可能並非爲移動環境所寫,當應用到移動客戶端時可能導致性能降低。當決定使用一個第三方庫時,需要針對移動環境做優化,另外需要分析Library的代碼大小及內存佔用情況,最後再決定是否應用該Library。

哪怕是一些針對移動環境做過優化的Library因爲不同的實現也可能導致一些問題,如一種情況使用了Nano版的protobufs,另一種情況使用了Micro版的protobufs,不同的Library實現有可能導致意想不到的問題。

儘管Proguard可以移除無效的API及資源,但是它無法移除一個Library大的內部依賴。這些庫中的功能可能需要較低的依賴項,例如當使用一個庫提供的Activity子類時,可能會引入大量的依賴。

需要避免只使用一個庫中的有限的功能而引入庫的情況,我們不希望引入大量不需要的代碼。當決定是否使用一個Library時,儘可能高度匹配我們的需求,否則,可以考慮自己實現。

參考文獻Manage Your App’s Memory

發佈了115 篇原創文章 · 獲贊 143 · 訪問量 47萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章