天天看點

五個次元:Android記憶體管理、優化

  RAM對于軟體開發環境而言是有價值的資源,但它對受限于實體記憶體限制的作業系統具有更大的價值。即使Android Runtime和Dalvik virtual machein執行正常的垃圾回收,但這并不意味着你可以忽略app在何時何地指派和釋放記憶體。你仍然需要去避免産生記憶體洩露。比如長期持有靜态成員變量常常引起記憶體洩露。我們應該在合适的時間,比如在生命周期回調函數處釋放一些引用對象,來避免記憶體洩露的發生。

  這篇文章将闡述怎樣在app中主動的降低記憶體消耗。關于java程式設計中清理資源的一般實踐,請參照其他關于資源引用管理的書籍或者線上文檔。如果你想分析一個運作中app的記憶體情況,請閱讀文章“Tools for analyzing RAM usage”。如果你想知道AndroidRuntime和Dalvik virtual machine如何管理記憶體,請參閱文章“Overview of Android Memory Management”。

(一)監控記憶體的使用情況:

  Androidframework和Android Studio能夠幫助你分析和調整app的記憶體使用情況。Android framework 暴露了幾個在app運作時動态降低記憶體消耗的API。Android Studio 提供了幾個觀察app記憶體使用情況的工具。

分析記憶體使用的工具:

  想要解決app産生的記憶體問題,我們首先得知道記憶體問題由什麼引起。Android Studio提供了如下幾個分析記憶體的工具:

  1.“MemoryMonitor”展示app 在一個單獨回話過程中如何指派記憶體。這個工具會實時繪制包括垃圾回收事件等可用記憶體和已占用記憶體的模拟圖像。在程式運作時,你僅僅能通過初始化一個垃圾回收事件,擷取Java 堆快照。這個Memory Monitor 的輸出圖像能幫你定位到app在什麼地方因産生過多的垃圾回收事件導緻程式變慢。

關于Memory  Monitor的具體使用方法,請參閱“ViewingHeap Updates”

  2.“AllocationTracker”能夠讓我們詳細的跟蹤到app如何配置設定記憶體。這個工具能夠記錄app的記憶體配置設定情況,而且能夠列出所有被配置設定的對象以及分析快照。用這個工具你能夠追蹤到配置設定太多對象的代碼。

  關于AllocationTracker的具體使用方法,請參閱“Allocation Tracker Walkthrough”

(二)在合适的時間釋放記憶體     

  Android裝置在運作時的可用記憶體會根據實體記憶體總量以及使用者操作方法不斷變化。當系統記憶體有壓力的情況下會發送信号來通知程式。app應監聽這些通知,并依據監聽結果來調整記憶體使之使用适度。

用APIComponentCallbacks2 來響應app生命周期事件和 裝置事件,根據監聽相關信号的結果調整記憶體的使用。方法onTrimMemory()能夠監聽app運作在前台或背景時候的相關記憶體事件。

在Activity中,實作回調方法onTrimMemory()來監聽這些記憶體變化的事件,如下代碼片段:

import android.content.ComponentCallbacks2;

// Other import statements ...

publicclassMainActivityextendsAppCompatActivity

    implementsComponentCallbacks2{

    // Other activity code ...

    /**

     * Release memory when the UI becomes hidden or whensystem resources become low.

     * @param level the memory-related event that wasraised.

     */

    publicvoid onTrimMemory(int level){

        // Determine which lifecycle or systemevent was raised.

        switch(level){

            caseComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*

                  Release any UI objects that currently hold memory.

                   Theuser interface has moved to the background.

                */

                break;

            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:

            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:

            caseComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

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

                   Thedevice is running low on memory while the app is running.

                   Theevent raised indicates the severity of the memory-related event.

                   Ifthe event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will

                   beginkilling background processes.

            caseComponentCallbacks2.TRIM_MEMORY_BACKGROUND:

            caseComponentCallbacks2.TRIM_MEMORY_MODERATE:

            caseComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                  Release as much memory as the process can.

                   Theapp is on the LRU list and the system is running low on memory.

                   Theevent raised indicates where the app sits within the LRU list.

                   Ifthe event is TRIM_MEMORY_COMPLETE, the process will be one of

                   thefirst to be terminated.

            default:

                  Release anynon-critical data structures.

                  The appreceived an unrecognized memory level value

                  from thesystem. Treat this as a generic low-memory message.

        }

    }

備注:回調方法onTrimMemory()在Android4.0中添加。更早的版本,能用回調方法onLowMemory()作為替代,大約和 TRIM_MEMORY_COMPLETE 事件相同。

(三)檢查你需要用到的記憶體:

  為了支援多程序,android為每一個app設定了一個堆記憶體上限。不同裝置為app配置設定的具體上限值,取決于RAM的總大小。如果程式到達了這個上限值還想占用更多的記憶體,系統将會抛出異常 OutOfMemoryError。

  為了避免記憶體溢出,我們可以查詢目前裝置為app指定的記憶體上限值。我們可以通過調用方法getMemoryInfo()來查詢目前系統的這個屬性值。這個方法将會傳回一個包含可用記憶體、總記憶體、記憶體臨界值(如果系統可用記憶體低于這個臨界值将會殺掉部分程序)等目前裝置狀态的ActivityManager.MemoryInfo對象。ActivityManager.MemoryInfo也提供了一個boolean類型的字段lowMemory,記錄目前裝置是否運作在低記憶體情況下。

下面的代碼片段展示了一個使用geMemoryInfo()方法的例子:

public void doSomethingMemoryIntensive(){

(四)使用高效記憶體的代碼結構

    一些android特性、java類、代碼結構趨向于用更多的記憶體。我們可以通過選擇更高效的替代方案來使我們app用最少的記憶體。

(1)少用Services:

 在不必要的時候運作一個服務,是app最糟糕的記憶體管理錯誤之一。如果app需要運作一個服務在背景執行工作,除非有必要的任務,否則不要一直運作。當Service完成它的任務後一定要記得停這個Service。否則,可能一不留神就産生一個記憶體洩露。

 開啟一個Service後,系統喜歡為這個service開啟一個程序使其持續運作。這種情況使Service非常耗費資源,因為其他程序無法使用被service所在程序占用的RAM資源。系統緩存在LRU catche的程序數量減少後,使app運作效率降低。更有甚者,當記憶體緊張,系統無法維持足夠的程序支援所有service的時候,service可能被銷毀。

 通常情況下我們應該避免長期使用service,因為它們會一直占用記憶體。我們推薦使用可供選擇的類比如JobScheduler 去排程背景程序。

 關于如何使用JobScheduler排程背景程序,參閱“BackgroundOptimizations”

(2)使用最優化的資料容器:

  程式設計語言提供的部分類對于移動裝置不是最優化的。例如一般的HashMap實作能使很多記憶體低效率工作,因為它需要為每一個映射生成一個單獨的條目對象。

Androidframework包含幾個最優化的資料容器,包括SparseArray,SparseBooleanArray,LongSparseArray。例如 AparseArray之是以更高效,是由于它們避免系統的需要,對key(有時候也對value)進行自動裝箱。

如果有必要,我們可以總是選擇原始的數組作為最精煉的資料結構。

(3)謹慎使用抽象代碼:

  開發者通常僅僅把抽象當成一種好的程式設計實踐,因為抽象可以提升代碼的靈活性和可維護性。然而,抽象伴随着巨大的資源耗費而來:通常它們會需要更多的時間和更多的記憶體,需要執行相當數量的代碼,花費更多的時間和記憶體。是以如果抽象不能帶來巨大好處,我們應該盡可能去避免。

例如:枚舉需要消耗的記憶體通常是靜态變量的兩倍多。我們應該嚴格避免在android上使用枚舉。

(4)使用nanoprotobufs序列化資料

    Protocol buffers 是一個谷歌設計的具有語言中立、平台中立、可擴充、用來序列化結構資料的類似于XML的機制,但其比XML快,比XML小,比XML簡單。如果你決定使用protobufs處理資料,那麼我們應該在用戶端代碼中一直使用nano protobufs。普通的protobufs通常生成非常多的代碼,這樣會引起app産生很多問題,比如占用更多記憶體、APK尺寸大幅度增加、運作緩慢。

更多運作,請參閱文章"protobuf readme"中"Nanoversion"的段落。

(5)避免記憶體抖動

  就像上文提醒到的,GC事件通常不會影響app的性能。然而一些發生在短時間内的GC事件能迅速耗盡你的幀像時間。系統花費在GC上的時間越多,那麼它處理其他諸如渲染或者音頻流的事件。

  通常情況下下,記憶體抖動能引起大量的的GC事件。在實踐中,記憶體抖動描繪的是,在一個給定時間内,配置設定臨時對象的數目。

  例如,你可能通過for循環配置設定多個臨時對象。或者在一個View的onDraw方法中建立一個Paint或者Bitmap對象。如上兩個例子,app迅速地建立了大量的大體積對象。年青一代的這些行為将會迅速的消耗所有的可用記憶體,強制一個垃圾回收事件産生。

  當然,在你修複記憶體抖動之前,需要先發現導緻記憶體抖動發生的代碼所在地方。請用在文章"Analyze your RAM usage"中讨論的工具。

  一旦你識别代碼出現問題的區域,試着降低處于性能臨界區域的配置數目。考慮移除内循環的東西,或者将它們移到基于配置設定結構的 Factory(參閱文章:)。

(五)降低記憶體占有量——累贅的資源和庫

  一些資源和庫在你的代碼能在你不知道的情況下貪婪的消耗記憶體。apk的大小、包括第三方庫以及嵌入的資源、能夠影響app消耗的記憶體量。我們能通過移除代碼中多餘的、不必要的、臃腫的元件、資源或者庫的方式來改善app的記憶體消耗。

(1)降低apk大小

  我們能通過降低apk大小的方式顯著的降低app的記憶體使用情況。bitmap尺寸、資源、動畫的幀、第三方庫都能導緻apk變大。android sdk 和 android studio 提供了多種工具來幫助我們降低資源和外部依賴的尺寸。

  關于降低apk大小的更多資訊,參閱"ReduceAPK Size"。

(2)如果需要依賴注入,建議使用Dagger

  依賴注入架構能夠簡化你寫的代碼以及為測試和測試改變提供一個适配的環境。如果你打算在app中用一個依賴注入架構,考慮使用Dagger2(參閱https://google.github.io/dagger/).

Dagger不會用反射掃描app代碼。它是靜态的、編譯時實作的架構,這意味着在運作時沒有不必要的運作時花費或者記憶體使用。

  其他的那些依賴注入架構通過掃描代碼和注解,運用反射去初始化程序。這個産生的程序會需要大量的CPU周期以及記憶體,能在app啟動時引起一個顯而易見的延遲。

(3)謹慎使用依賴庫:

  外部庫的代碼通常不是為手機環境所寫,使其在手機用戶端上工作時可能是很低效的。當我們決定用一個依賴庫,可能需要針對移動裝置進行優化。在決定真正使用這個依賴庫之前,應預先做好針對移動裝置優化的計劃,依據代碼尺寸和記憶體大小進行分析。

  即使一些移動優化的庫也能由于不同的實作引起問題。例如,當一個庫使用了micro protobufs而一個庫使用了nano protobufs,這樣app中有兩個不同的 protobuf實作。當問題發生時,原因可能是logging,analytics,p_w_picpath loading frameworks,caching或者一些其他我們無法預料的事情的不同實作導緻的。

  盡管ProGuard(參閱:https://developer.android.com/studio/build/shrink-code.html)能根據正确的标記移除APIs和資源,但是它不能移除一個庫大型的依賴。你在庫中想要的特性可能需要低版本的依賴....

  翻譯整理,更多内容,敬請期待....