天天看點

Android性能優化典範-管理應用記憶體

本文翻譯自google官方開發文檔管理應用記憶體

管理應用記憶體

随機存取存儲器 (RAM) 在任何軟體開發環境中都是一項寶貴資源,但在移動作業系統中,由于實體記憶體通常都有限,是以 RAM 就更寶貴了。雖然 Android 運作時 (ART) 和 Dalvik 虛拟機都執行例行的垃圾回收任務,但這并不意味着您可以忽略應用配置設定和釋放記憶體的位置和時間。您仍然需要避免引入記憶體洩漏問題(通常因在靜态成員變量中保留對象引用而引起),并在适當時間(如生命周期回調所定義)釋放所有 Reference 對象。

本頁面介紹了如何積極減少應用的記憶體使用量。如需了解 Android 作業系統如何管理記憶體,請參閱 Android 記憶體管理概覽。

監控可用記憶體和記憶體使用量

您需要先找到應用中的記憶體使用問題,然後才能修複問題。Android Studio 中的記憶體性能剖析器可以通過以下方式幫助您查找和診斷記憶體問題:

  1. 了解您的應用在一段時間内如何配置設定記憶體。記憶體分析器可以顯示實時圖表,說明應用的記憶體使用量、配置設定的 Java 對象數量以及垃圾回收事件發生的時間。
  2. 發起垃圾回收事件,并在應用運作時拍攝 Java 堆的快照。
  3. 記錄應用的記憶體配置設定情況,然後檢查所有配置設定的對象、檢視每個配置設定的堆棧軌迹,并在 Android Studio 編輯器中跳轉到相應代碼。
釋放記憶體以響應事件

如 Android 記憶體管理概覽中所述,Android 可以通過多種方式從應用中回收記憶體,或在必要時完全終止應用,進而釋放記憶體以執行關鍵任務。為了進一步幫助平衡系統記憶體并避免系統需要終止您的應用程序,您可以在

Activity

類中實作

ComponentCallbacks2

接口。借助所提供的

onTrimMemory()

回調方法,您的應用可以在處于前台或背景時監聽與記憶體相關的事件,然後釋放對象以響應訓示系統需要回收記憶體的應用生命周期事件或系統事件。

例如,您可以實作

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;
        }
    }
}
           

Android 4.0(API 級别 14)中添加了

onTrimMemory()

回調。對于早期版本,您可以使用

onLowMemory()

,此回調大緻相當于

TRIM_MEMORY_COMPLETE

事件。

檢視您應該使用多少記憶體

為了允許多個程序同時運作,Android 針對為每個應用配置設定的堆大小設定了硬性限制。裝置的确切堆大小限制因裝置總體可用的 RAM 多少而異。如果您的應用已達到堆容量上限并嘗試配置設定更多記憶體,系統就會抛出

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 類和代碼結構所使用的記憶體往往多于其他功能、類和結構。您可以在代碼中選擇效率更高的替代方案,以盡可能降低應用的記憶體使用量。

謹慎使用服務

在不需要某項服務時讓其保持運作狀态,是 Android 應用可能犯下的最嚴重的記憶體管理錯誤之一。如果您的應用需要某項服務在背景執行工作,請不要讓其保持運作狀态,除非其需要運作作業。請注意在服務完成任務後使其停止運作。否則,您可能會在無意中導緻記憶體洩漏。

在您啟動某項服務後,系統更傾向于讓此服務的程序始終保持運作狀态。這種行為會導緻服務程序代價十分高昂,因為一旦服務使用了某部分 RAM,那麼這部分 RAM 就不再可供其他程序使用。這會減少系統可以在 LRU 緩存中保留的緩存程序數量,進而降低應用切換效率。當記憶體緊張,并且系統無法維護足夠的程序以托管目前運作的所有服務時,這甚至可能導緻系統出現抖動。

您通常應該避免使用持久性服務,因為它們會對可用記憶體提出持續性的要求。我們建議您采用

JobSchedulerJobScheduler

等替代實作方式。要詳細了解如何使用

JobScheduler

排程背景程序,請參閱背景優化。

如果您必須使用某項服務,則限制此服務的生命周期的最佳方式是使用

IntentService

,它會在處理完啟動它的

intent

後立即自行結束。有關詳情,請參閱在背景服務中運作。

使用經過優化的資料容器

程式設計語言所提供的部分類并未針對移動裝置做出優化。例如,正常

HashMap

實作的記憶體效率可能十分低下,因為每個映射都需要分别對應一個單獨的條目對象。

Android 架構包含幾個經過優化的資料容器,包括

SparseArray

SparseBooleanArray

LongSparseArray

。 例如,

SparseArray

類的效率更高,因為它們可以避免系統需要對鍵(有時還對值)進行自動裝箱(這會為每個條目分别再建立 1-2 個對象)。

如果需要,您可以随時切換到原始數組以獲得非常精簡的資料結構。

謹慎對待代碼抽象

開發者往往會将抽象簡單地當做一種良好的程式設計做法,因為抽象可以提高代碼靈活性和維護性。不過,抽象的代價很高:通常它們需要更多的代碼才能執行,需要更多的時間和更多的 RAM 才能将代碼映射到記憶體中。是以,如果抽象沒有帶來顯著的好處,您就應該避免使用抽象。

針對序列化資料使用精簡版 Protobuf

協定緩沖區是 Google 設計的一種無關乎語言和平台,并且可擴充的機制,用于對結構化資料進行序列化。該機制與 XML 類似,但更小、更快也更簡單。如果您決定針對資料使用

Protobuf

,則應始終在用戶端代碼中使用精簡版

Protobuf

。正常

Protobuf

會生成極其冗長的代碼,這會導緻應用出現多種問題,例如 RAM 使用量增多、APK 大小顯著增加以及執行速度變慢。

有關詳情,請參閱 Protobuf 自述檔案中的“精簡版”部分。

避免記憶體抖動

如前所述,垃圾回收事件通常不會影響應用的性能。不過,如果在短時間内發生許多垃圾回收事件,就可能會快速耗盡幀時間。系統花在垃圾回收上的時間越多,能夠花在呈現或流式傳輸音頻等其他任務上的時間就越少。

通常,“記憶體抖動”可能會導緻出現大量的垃圾回收事件。實際上,記憶體抖動可以說明在給定時間内出現的已配置設定臨時對象的數量。

例如,您可以在 for 循環中配置設定多個臨時對象。或者,您也可以在視圖的

onDraw()

函數中建立新的

Paint

Bitmap

對象。在這兩種情況下,應用都會快速建立大量對象。這些操作可以快速消耗新生代 (

young generation

) 區域中的所有可用記憶體,進而迫使垃圾回收事件發生。

當然,您必須先在代碼中找到記憶體抖動較高的位置,然後才能進行修複。為此,您應該使用 Android Studio 中的記憶體分析器。

确定代碼中的問題區域後,請嘗試減少對性能至關重要的區域中的配置設定數量。您可以考慮将某些代碼邏輯從内部循環中移出,或将其移到基于 Factory 的配置設定結構中。

移除會占用大量記憶體的資源和庫

代碼中的某些資源和庫可能會在您不知情的情況下吞噬記憶體。APK 的總體大小(包括第三方庫或嵌入式資源)可能會影響應用的記憶體消耗量。您可以通過從代碼中移除任何備援、不必要或臃腫的元件、資源或庫,降低應用的記憶體消耗量。

縮減總體 APK 大小

您可以通過縮減應用的總體大小來顯著降低應用的記憶體使用量。位圖大小、資源、動畫幀數和第三方庫都會影響 APK 的大小。Android Studio 和 Android SDK 提供了可幫助您縮減資源和外部依賴項大小的多種工具。這些工具支援現代代碼收縮方法,例如 R8 編譯。(Android Studio 3.3 及更低版本使用 ProGuard,而不是 R8 編譯。)

要詳細了解如何縮減 APK 的總體大小,請參閱有關如何縮減應用大小的指南。

使用 Dagger 2 實作依賴注入

依賴注入架構可以簡化您編寫的代碼,并提供一個可供您進行測試及其他配置更改的自适應環境。

如果您打算在應用中使用依賴注入架構,請考慮使用

Dagger 2

。Dagger 不使用反射來掃描您應用的代碼。

Dagger

的靜态編譯時實作意味着它可以在 Android 應用中使用,而不會帶來不必要的運作時代價或記憶體消耗量。

其他使用反射的依賴注入架構傾向于通過掃描代碼中的注釋來初始化程序。此過程可能需要更多的 CPU 周期和 RAM,并可能在應用啟動時導緻出現明顯的延遲。

謹慎使用外部庫

外部庫代碼通常不是針對移動環境編寫的,在移動用戶端上運作時可能效率低下。如果您決定使用外部庫,則可能需要針對移動裝置優化該庫。在決定是否使用該庫之前,請提前規劃,并在代碼大小和 RAM 消耗量方面對庫進行分析。

即使是一些針對移動裝置進行了優化的庫,也可能因實作方式不同而導緻問題。例如,一個庫可能使用的是精簡版 Protobuf,而另一個庫使用的是 Micro Protobuf,導緻您的應用出現兩種不同的 Protobuf 實作。日志記錄、分析、圖像加載架構和緩存以及許多您意料之外的其他功能的不同實作都可能導緻這種情況。

雖然 ProGuard 可以使用适當的标記移除 API 和資源,但無法移除庫的大型内部依賴項。您所需要的這些庫中的功能可能需要較低級别的依賴項。如果存在以下情況,這就特别容易導緻出現問題:您使用某個庫中的 Activity 子類(往往會有大量的依賴項)、庫使用反射(這很常見,意味着您需要花費大量的時間手動調整 ProGuard 以使其運作)等。

此外,請避免僅針對數十個功能中的一兩個功能使用共享庫。您一定不希望産生大量您甚至根本用不到的代碼和開銷。在考慮是否使用某個庫時,請查找與您的需求十分契合的實作。否則,您可以決定自己去建立實作。

繼續閱讀