Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 管理Android APP的內存

管理Android APP的內存

編輯:關於Android編程

[SDK官網原文鏈接](https://developer.android.com/topic/performance/memory.html)

在任何一個軟件開發環境中,RAM都是有價值的資源,然而在物理內存受限的移動操作系統中,它顯得更有價值。盡管由Android的Dalvik虛擬機負責內存垃圾的回收,但是在什麼時候以及在哪裡分配和釋放內存都是不容忽視的問題。
為了便於GC更合理的回收APP的內存,你應該避免內存洩漏(通常由於持有全局成員的對象引用所導致),和對象引用在合適的時機釋放(它被定義在下面討論的生命周期回調)。對於大部分APP而言,
當對象離開APP活動線程的范圍之外,但沒有被系統回收的內存,將由Dalvik GC去負責處理。
本文解釋了Android如何管理應用程序進程和內存分配,以及如何主動減少內存使用。

一、Android如何管理內存

Android 沒有提供內存交換空間,但是它可以用分頁和內存映射去管理內存。這意味著你修改的任何的內存,不論是分配的新對象還是產生的映射分頁,都將駐留在RAM,不能被換出。因此完全釋放內存的唯一方式就是釋放你持有的對象的引用。但有一個例外:mmap任何文件沒有修改,例如代碼,可以調出內存如果系統要使用內存。
共享內存
為了滿足一切RAM的需要,Android可以跨進程共享RAM分頁。主要體現在一下幾種形式:
每個APP進程都是從一個現有的稱作Zygote進程fork出來的。當系統啟動,加載公共framework代碼和資源(比如:activity主題)時,Zygote進程就開始啟動了。為了開啟一個新的APP進程,系統會從Zygote進程fork一個新的進程,然後加載和運行APP代碼在這個新的進程裡。對於分配給framework代碼和資源的大部分RAM分頁,都可以在所有APP進程間共享。
大部分靜態數據被映射到同一進程。這不僅允許同一數據在不同進程間共享而且能夠在需要的時候被換出。示例的靜態數據包括:Dalvik代碼(處在用於直接映射的預鏈接的.odex文件),APP 資源(用於設計能直接映射的資源表結構和調整APK的ZIP條目),以及傳統項目元素比如.so文件的native代碼。
在許多地方,Android 跨進程分享相同動態RAM主要體現在顯示分配的內存共享區域(ashmem或者gralloc)。例如:window surface在APP和屏幕渲染之間共享內存,cursor緩存在content provider和使用者之間共享內存。

(1)APP內存的分配和回收(待完善)

下面是一些關於Android如何從你的APP分配和回收內存的事實:
每個進程的Dalvik堆都限制在一個虛擬內存的區間裡。它定義了邏輯的heap size,heap size可以根據需要增加,但是系統為每個app定義了最大size。
堆的邏輯大小和堆使用的物理內存的大小並不是一致的。當觀察APP堆時,你會發現Android會計算稱為比例設置大小(PSS)的一個值,它包括跨進程分享的髒的和干淨的頁,但是這個數量是和有多少個APP共享那個RAM成比例的。系統把PSS總量當做物理內存的占用量。

二、監控可用內存和內存使用

Android框架,Android Studio 以及Android SDK可以幫助你分析和調整你的APP內存使用,Android 框架公開了一些允許你運行時動態減少內存使用的API,Android Studio 和Android SDK提供了一些工具允許你查看APP內存使用情況。

(1)分析RAM使用情況的工具

在你解決APP內存使用問題之前,你首先應當找到問題的地方。Android Studio 和Android SDK包含了一些分析你的APP內存使用的工具。
1.設備有一個叫做DDMS工具,允許你檢查APP進程之間的內存使用。你能用這些信息去查看你的APP整體使用情況。例如,你可以強制觸發一個GC,然後查看保持在內存中對象類型,你可以使用這些信息來區分你在APP中的操作是否在內存中分配或留下過多的對象。
了解更多的如何使用DDMS工具,請參看 Using DDMS
2.在Android Studio裡的Memory Monitor會顯示在一個單獨會話過程中的內存分配情況,這個工具會顯示Java內存可用和分配推移圖,以及垃圾回收事件。當你的APP運行時,你也可以啟動垃圾回收事件和Java堆的快照。Memory Monitor 工具輸出的信息可以幫你識別過度的垃圾回收導致的APP運行緩慢時間節點。
了解更多的如何使用Memory Monitor工具,請參看 Viewing Heap Updates
3.垃圾回收事件也可以顯示在Traceview視圖裡,Traceview允許你通過時間軸和方法內部發生的概要內容來查看跟蹤日志文件。你能用這個工具來確定當GC事件發生時什麼代碼正在執行。
了解更多的如何使用Traceview viewer,請參看Profiling with Traceview and dmtracedump
4.Android Studio裡的Allocation Tracker工具會給你內存分配情況的詳細展示,它記錄了一個APP內存分配情況,以及通過簡要快照來列出所有的分配對象,你可以用這個工具來追蹤部分代碼分配了太多對象。
了解更多的如何使用Allocation Tracker,請參看 Allocation Tracker Walkthrough

(2)釋放內存響應事件

一個Android設備能夠不同數量的空閒內存上,不管設備的物理內存多大,以及用戶如何操作它。當內存有壓力時,系統會發出廣播信號,APP應當監聽這些信號,調整合適的內存運行。
你可以使用API ComponentCallbacks2來監聽這些信號,然後調整內存使用以響應app的生命周期和設備事件,onTrimMemory() 方法允許你的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()回調被新增在Android 4.0(API級別14),對於早期版本,您可以使用onLowMemory()回調為舊版本回退,大致相當於TRIM_MEMORY_COMPLETE事件。

(3)檢查你應該使用多少內存

為了允許多個運行時進程共存,Android為每個APP設定了嚴格的堆大小限制。具體堆大小限制數值和具體設備整體可用的RAM有關,如果你的APP已經達到了堆的容量,依然嘗試申請分配更多內存,系統就會拋出OOM異常。
為了避免耗盡內存,你可以查詢系統來確認當前設備還有多少可用空間,你可以通過調用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 classes,以及代碼結構相比於其他的更耗內存。你應該在你的代碼中作出有效的選擇來最小化你的使用的內存。
(1)使用較少的Service

保持一個不再需要的Service一直運行,是Android APP內存管理犯的最嚴重的問題之一。如果你的APP需要一個Service來執行後台工作,其實並不需要一直保持它運行,除非它一直在運行一個任務。
當你的Service完成任務時,記得要停止你的Service。否則,你將無意中導致內存洩漏。
當你啟動一個Service時,系統更傾向於保持你的Service進程一直運行,這一行為將導致你的Service非常昂貴,因為一個Service占用的RAM對於其他進程而言依然是不可用的,這將減少系統維持的在LRU Chache中的緩存進程的數量,那將導致APP的切換顯得低效。那甚至有可能導致系統抖動當系統內存緊張以至於不能維持足夠的進程來確保所有Service運行。
一般情況下應該避免使用持續的Service,因為他們對可用內存有持續性的需求。相反,我們建議您使用另一種JobScheduler等實現。了解更多的信息關於如何使用JobScheduler處理後台進程,參看JobScheduler。
如果你必須使用一個Service,最好的方式就是使用IntentService來限制你的Service的生命周期,IntentService可以盡可能早的結束,當處理完成你啟動的Intent。

(2)使用優化的數據容器

編程語言提供一些類在移動終端設備上並不是最優的,例如原生HashMap的實現是比較內存低效的,因為對於每一個映射都需要一個單獨的條目對象。Android框架包括幾個優化的數據容器,包括SparseArray SparseBooleanArray,LongSparseArray。例如,SparseArray是比較內存有效的,因為它避免了系統自動對key,有時可能是value的自動裝箱(int轉為Integer類型)。如果有必要,你可以切換到很瘦數據結構的原始數組。

(3)謹慎使用代碼抽象

開發者通常使用抽象編程作為一個良好的編程實踐,因為抽象代碼可以提高代碼的靈活性和可維護性。然而抽象的同時也付出了巨大的代價:那通常需要大量更多的執行代碼,更耗費時間,以及需要更多的RAM用以將代碼映射到內存中。如果你的代碼抽象並不能帶來重大好處,應當盡量避免。
例如:枚舉通常需要超過兩倍於靜態常量的內存來存儲,在Android上你應該嚴格避免使用枚舉類型。

(3)使用nano版本的序列號對象數據Protocol buffers

Protocol buffers是語言中立的,平台無關的,可擴展的機制由谷歌設計用於序列化結構化數據類似於XML,但更小、更快,更簡單。如果你決定使用protobufs數據,你應該總是使用nano版本的protobufs在客戶端代碼。常規protobufs會生成非常冗長的代碼,這可能會導致在APP中的多種問題,增大RAM使用,增加APK大小增加,執行慢。

(4)避免內存生產

如前所述,垃圾回收通常不會影響應用程序的性能。然而,許多垃圾收集的事件發生同一個短時間內可以迅速吃掉你的幀時間。系統花在垃圾回收的時間越多,相對的做其他事情(渲染或流式音頻)的時間就越少。
通常,內存的產生會導致大量垃圾回收事件的發生。實際上,大量內存的產生往往是在一段時間大量臨時對象的創建導致的。例如,你可能會在一個for循環分配多個臨時對象,或者你可能在View的OnDraw回調裡創建Paint或者Bitmap對象。 在這兩種情況下,APP會以較高的頻率產生大量的對象。這些會導致快速消耗年輕代中所有可用內存,強制垃圾回收事件的發生。當然,在你解決這個問題之前,你應當首先找到在代碼中內存高發的地方,一旦你識別出了代碼中的問題區域,應當盡量減少在這些關鍵區域的分配數量。應當考慮將其移出到內部循環外或者有可能放到用於分配數據結構的工廠類中。

四、移除內存密集型資源和庫

一些資源和庫能夠在你不知情的情況下吞噬你的內存。APK的總體大小,包括第三方庫和嵌入的資源,都會影響應用程序消耗多少內存。你可以提高你的應用程序的內存消耗通過從你的代碼中刪除任何多余的,不必要的,或者膨脹的組件,資源。

(1)減少APK的整體大小

通過減小APK的整體大小,你可以很大程度的減少你的APP所使用的內存。位圖的大小,資源,動畫幀,以及第三方庫都會增加你的APK的大小。Android Studio 和 Android SDK提供多種工具來幫助你減少資源的大小和外部依賴。如何減小APK的整體大小,請參看之前譯文。

(2)謹慎使用依賴注入框架

依賴注入框架( 例如:Guice or RoboGuice)能夠簡化你的代碼,和提供便於測試和其他配置更改的環境。然而,依賴框架對移動終端並不是一直是優化的。
例如:這些框架會在初始化進程的過程中通過掃描你的代碼來尋找注釋,這就需要大量的不必要的代碼映射到RAM中。系統分配這些映射頁面到干淨內存以便於回收它們。然而這些頁面會在內存中駐留很長一段時間才會被回收。
如果你需要在你的APP中使用一種依賴注入框架,可以考慮Dagger。例如:Dagger不會使用反射來掃描你的代碼,Dagger嚴謹的實現使得它可以在用你的代碼中,而不會增加不必要的內存使用。

(3)小心使用外部庫

外部庫代碼通常不是寫給移動開發環境的,在移動客戶端使用外部庫是效率不高的。當你決定使用一個外部庫時,你可能需要優化你的外部庫為移動設備。在你使用它之前,要在代碼的大小和內存占用方面做好前期的計劃和庫分析。
甚至一些經過移動端優化的庫也可能由於實現上的差異,導致一些的問題的產生。舉例:一個庫可能會使用nano版本的protobufs,然而另一個庫可能使用micro版本的庫,這就產生了兩種不同的實現在你的APP代碼中。這可以發生在日志記錄,分析,圖像加載框架,緩存以及其他你未能預料的事情上。
盡管ProGuard能通過使用正確的標志來移除API和資源,但是它不能移除庫本身大量的內部依賴。你可能只是對這些庫的很少部分有依賴。那就變得特別有問題,當你使用外部庫的一個Activity子類,而它會有一大片依賴項時,當你所使用的外部庫使用反射,那是常見的,但是需要你花費大量時間來手動配置ProGuard以使它工作時,等等場景。
你應當盡量避免當你依賴一個外部庫,而只是使用庫中眾多功能的一兩個而已。你可能自己也不希望引入一大堆你用不上的代碼和開銷,因此當你尋找采用外部庫時,應當采用和你需求強烈匹配的庫。否則,你可能需要創建你自己的實現。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved