【Using English】61 Android管理應用內存

隨機存取存儲器(RAM)在任何軟件開發(fā)環(huán)境中都是寶貴的資源,但是在物理內存常常受到限制的移動操作系統(tǒng)中變得更加珍貴。盡管Android運行時(ART)和Dalvik虛擬機都會例行地執(zhí)行垃圾回收,但這不意味著應用可以不顧時間和位置地分配和釋放內存。仍然要避免引入內存泄漏,通常的原因是static變量持有一個對象的引用。并且在恰當?shù)臅r機(如定義好的生命周期回調方法)所有的釋放所有Reference對象。

本文講解釋如何主動地減少應用使用的內存。關于Android系統(tǒng)如何管理內存的信息,請查看Android內存管理概述。

監(jiān)控可用內存 和 內存使用量

在修復應用內存使用量的問題之前,需要先找到它們。Android Studio 的Memory Profiler(內存分析器)可以通過以下方法幫助你發(fā)現(xiàn)且診斷內存問題:

  1. 查看應用在一段時間內如何分配內存。內存分析器會提供一個實時圖像,圖像里包括應用使用的內存,分配的java對象數(shù),以及垃圾回收觸發(fā)的時間。
  2. 發(fā)起一次垃圾回收事件,并且抓取一張應用運行時的Java堆的快照。
  3. 記錄應用的內存分配然后檢查所有分配的對象,查看每一次分配的調用棧,在Android Studio的編輯器中跳轉到相關的代碼中。

在響應事件中釋放內存

正如Android內存管理概述一文所述,Android有很多種辦法可以回收應用的內存,如果必要的話,也會終止整個應用進程來釋放資源。為了進一步幫助平衡系統(tǒng)內存,避免系統(tǒng)需要終止應用進程的情況發(fā)生,應用可以在Activity中實現(xiàn)ComponentCallback2接口。該接口中提供了onTrimMemory()回調方法,這個方法允許應用監(jiān)聽內存相關的事件,不論應用是在前臺還是后臺,并且通過釋放對象來響應表示系統(tǒng)需要回收內存的事件,這些事件包括應用生命周期的事件和系統(tǒng)事件。

例如下面的代碼,應用可以實現(xiàn)onTrimMemory()接口回調來響應不同的內存相關的事件,

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

class MainActivity : AppCompatActivity(), 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.
     */
    override fun onTrimMemory(level: Int) {

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

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.
                   The user interface has moved to the background.
                   釋放當前占用內存你的UI對象,當前UI已經移到后臺。
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            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.
                   釋放所有應用不需要的內存
                   設備處于低內存狀態(tài),這時應用還在運行。
                   該事件突出了內存相關事件的嚴重程度。
                   如果當前事件是`TRIM_MEMORY_RUNNING_CRITICAL`,那么系統(tǒng)將開始終止后臺進程
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            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.
                   
                   盡可能地釋放進程的內存。
                   應用處于`LRU`列表中,且系統(tǒng)處于低內存狀態(tài)。該事件指明了當前應用處于`LRU`列表中。
                   如果事件是`TRIM_MEMORY_COMPLETE`,應用進程將第一個被終止。
                */
            }

            else -> {
                /*
                  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.
                  
                  釋放非必要數(shù)據(jù)結構
                  應用收到了一個未識別的系統(tǒng)內存事件,把該是當做通常的低內存信息來處理。
                */
            }
        }
    }
}

onTrimMemory()回調方法在Android4.0(API level 14)被添加。更早期的系統(tǒng)版本,可以使用onLowMemory(),這個回調方法幾乎等同于TRIM_MEMORY_COMPLETE事件。

查看應用該使用多少內存

為了允許多個進程同時運行,Android對每個應用進程堆內存的大小設置了硬性的限制。這個限制的具體值會根據(jù)設備總體可用RAM的不同而變化。如果應用到達了對內存容量的極限,這時嘗試分配更多的內存,系統(tǒng)會拋出OutOfMemory錯誤。

為了避免運行時超過內存限制,應用可以查詢系統(tǒng)來獲知當前設備應用可用的最大堆內存空間。應用可以通過getMemoryInfo()方法向系統(tǒng)查詢該值。該方法會返回一個ActivityManager.MemoryInfo對象,這個對象提供了關于設備當前內存狀態(tài)的信息,包括可用的內存,總內存以及觸發(fā)系統(tǒng)終止進程的內存閾值。ActivityManager.MemoryInfo對象還會提供一個簡單的lowMemory布爾值,它表示當前設備是否內存不足。

以下的代碼片段是一個如何使用getMemoryInfo()的例子:

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see whether the device is in a low memory state.
    // 如果一個任務需要大量的內存,那么執(zhí)行之前要檢查當前應用是否處于內存不足狀態(tài)
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work ...
        // 做內存密集的工作
    }
}

// Get a MemoryInfo object for the device's current memory status.
//獲取一個代表了當前內存狀態(tài)的`MemoryInfo`對象
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

使用內存效率更高的代碼結構

一些Android特性,Java類和代碼結構會使用更多的內存。應用代碼可以選擇更高效的替代方案來縮減占用的內存。

謹慎使用Service

讓不需要的Service持續(xù)運行,這是Android內存管理中犯下的最嚴重的錯誤之一。如果應用需要一個Service以便在后臺執(zhí)行任務,除非真實需要執(zhí)行任務,否則請它停止運行。請注意在Service完成工作后使其停止運行。否則,您可能無意中造成了一起內存泄漏。

當應用啟動一個Service,系統(tǒng)會盡可能讓它持續(xù)運行。由于被Service使用的內存不再會被其他內存使用,這種策略導致Service進程是成本很高。這降低了系統(tǒng)可以在LRU緩存中維護的進程數(shù)量,從而降低應用切換效率。當內存緊張,系統(tǒng)無法維護足夠的進程來托管當前運行的所有服務時,甚至會導致系統(tǒng)出現(xiàn)內存抖動。

因為對于內存的持續(xù)性需求,通常要避免使用持續(xù)性的Service。我們推薦您使用JobScheduler等替代實現(xiàn)方案。關于如何使用JobScheduler來調度后臺進程的更多信息,請查看后臺優(yōu)化

如果應用必須使用Service,限制生命周期的最佳方案是使用IntentService,它會在完成啟動時的意圖后就停止運行。更多信息請查看在后臺服務中運行。

使用優(yōu)化過的數(shù)據(jù)容器

編程語言提供的一些類沒有針對移動設備進行優(yōu)化。例如,常規(guī)的HashMap實現(xiàn)內存效率很低,因為每一個映射都需要對應一個單獨的實體對象。

Android狂阿基包含了多個優(yōu)化過的數(shù)據(jù)容器,包括SparseArray,SparseBooleanArray,和LongSparseArray.例如,SparseArray這個類是更高效的,因為它避免了系統(tǒng)需要對鍵(有時候值也需要)自動裝箱的操作(這個操作會為每個實體創(chuàng)建額外的一個或兩個對象)。

如果必須,您可以隨時切換回原始數(shù)組已獲得非常精簡的數(shù)據(jù)結構。

謹慎對待代碼抽象

開發(fā)者會經常把抽象作為一個好的編程實踐,因為抽象可以提高代碼的靈活性和可維護性。但是,抽象帶來一個顯著的成本:它需要更多的代碼才能執(zhí)行,這需要更多的時間和RAM(空間)把代碼映射到內存。所以如果你的抽象沒有帶來顯著的好處,應該避免它們。

數(shù)據(jù)的序列化使用精簡版的Protobuf

Protocol buffers是一種跨語言、跨平臺、可擴展的機制,Google研發(fā)它用于序列化結構性數(shù)據(jù),與XML類似,但是更小,更快,更簡單。如果應用決定使用ProtoBufs來序列化數(shù)據(jù),您應該在客戶端上始終使用精簡版的ProtoBufs。常規(guī)版本的ProtoBufs會生成非常冗長的代碼,這會導致應用出現(xiàn)很多問題,如增加了RAM使用,顯著增大了APK的尺寸,執(zhí)行速度變慢。

關于精簡版本ProtoBufs的更多信息,請查看ProtoBufs自述文件。

避免內存抖動

正如之前提到的,垃圾回收時間通常不會影響應用的性能。但是,短時間內觸發(fā)多次垃圾回收事件可以消耗掉一幀的繪制時間。系統(tǒng)用在垃圾回收的時間越多,它可以用在繪制或傳輸音頻上的時間就越少。

通常,"內存抖動"會觸發(fā)大量的垃圾回收事件。實際上,內存抖動描述了給定時間內分配臨時對象的數(shù)量。

例如,您可能在for循環(huán)中分配了多個對象?;蛘咴赩iew的onDraw()方法中創(chuàng)建了新的PaintBitmap對象。在這兩種情況下,應用都會快速創(chuàng)建大量的對象。這會導致快速消耗新生代中所有的內存,強制觸發(fā)一個垃圾回收事件。

當然,在修復內存抖動之前,你需要先在代碼中找到它們。為此,您應該使用Android Studio提供的Memory Profiler

確定了代碼中的問題區(qū)域后,嘗試減少在性能的關鍵區(qū)域分配內存的數(shù)量。可以考慮把相關點從最內存的循環(huán)中移出,或許可以使用一個基于工廠模式的對象創(chuàng)建結構中。

移除占用大量內存的資源和依賴庫

一些資源和庫會在你不知情的情況下吃掉大量內存。增大apk的總體大小,包括第三方的庫或內嵌的資源可以影響應用消耗內存的大小。您可以提優(yōu)化應用的內存消耗,通過在代碼中移除所有重復的,不需要的或者臃腫的模塊。

縮減總體apk的大小

縮減apk的總體大小可以顯著地縮減應用的內存使用。位圖大小,資源,動畫幀和第三方庫都會增加apk的大小。Android Studio 和 Android SDK提供了很多工具,這些工具可以幫助您減小資源和外部庫的體積。這些工具支持現(xiàn)代的代碼裁剪方法,例如R8編譯。(Android Studio 3.3之前的版本使用Proguard代替R8編譯)

關于縮減整體apk大小的更多信息,請查看如何縮減應用大小。

依賴注入使用Dagger2

依賴注入狂阿基可以簡化代碼的編寫,并且提供了一個可用于測試和配飾更改的自適應環(huán)境。

如果您打算在應用中使用依賴注入框架,請考慮使用Dagger2。Dagger并沒有使用反射來掃描代碼。Dagger的靜態(tài),編譯時實現(xiàn)方案意味著它在Android應用中不會增加運行時的成本和內存使用。

其他由反射實現(xiàn)的依賴注入框架傾向于掃描代碼中的注解來初始化進程。這個進程會要求更多的CPU時間片和RAM,并且會導致一個應用啟動明顯的延遲。

謹慎使用外部庫

外部庫的代碼通常沒有針對移動環(huán)境優(yōu)化,用于移動客戶端可能很低效。當您決定使用一個外部庫時,您可能需要針對移動環(huán)境優(yōu)化該庫。在決定使用這個庫之前,要提前規(guī)劃,并且分析這個庫在代碼大小和內存消耗方面的表現(xiàn)。

即使是針對移動場景做過優(yōu)化的庫,也可能因為實現(xiàn)方式的不同而導致問題。例如,一個庫使用了精簡版的ProtoBufs,當另一個庫使用了Micro ProtoBufs,導致應用內實現(xiàn)了兩種ProtoBufs。在日志記錄,統(tǒng)計工具,圖片加載框架,緩存以及很多想象不到的方面,都可能發(fā)生這種多個實現(xiàn)的問題。

盡管Proguard可以通過恰當?shù)臉撕瀬硪瞥鼳PI和資源,但是不能移除一個庫的大型內部依賴項。在用這些庫的時候,您需要的功能可能需要低級別的依賴項。下面的情況中,這會造成顯著的問題:當您從一個庫中使用一個Activity子類(往往需要大量的依賴項)時這個庫使用了反射(這很常見,也意味著您需要花很多時間手動調整Proguard規(guī)則來讓它正常工作)等等。

另外,避免使用只用到一兩個功能的共享庫,但是它提供了十幾個功能。您一定不希望引入大量的用不到的代碼。當您考慮是否使用一個庫時,請尋找與你需求最匹配的實現(xiàn)。否則,您可以考慮創(chuàng)建自己的實現(xiàn)。

original

About 【Using English】

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容