管理App的記憶體

來源:互聯網
上載者:User

標籤:

https://developer.android.com/training/articles/memory.html#Android

對於任何軟體來說RAM都是一個非常重要的資源,但是由於實體記憶體總是有限的,所以記憶體對於手機作業系統來說也更加重要。儘管Android的Dalvik虛擬機器會執行GC,但是仍然不允許忽略應該在什麼時候,什麼地方分配和釋放記憶體為了記憶體回收行程能夠回收app的記憶體,需要避免記憶體泄露(通常是由全域變數持有對象引用引起)和在合適的時候釋放引用的對象(如下面會說到的生命週期的回調)對於大部分的app,當應用活動線程相應的對象離開了範圍時,Dalvik記憶體回收行程會回收分配的記憶體這篇文章解釋了Android是怎麼管理app進程和記憶體配置的,和Android開發時應該主動的減少記憶體的使用,用Java編程時更多關於清理記憶體的一般實踐可以參考其它書本或者線上文檔關於管理資源引用的說明,如果你已經建立了一個工程並且正在尋找應該怎樣分析你應用的記憶體,可以參考 [調查應用記憶體情況](https://developer.android.com/studio/profile/investigate-ram.html)
Android是怎麼管理記憶體的?

Android不提供記憶體的互換,但是它可以用分頁和記憶體映射來管理記憶體,這意味著你修改的任何記憶體,不論是分配新對象還是映射頁,都會常駐記憶體而不會被移除,因此從app完全釋放記憶體的唯一方式就是釋放你可能持有的對象引用,使得記憶體回收行程可以正常回收。這樣就導致了一種潛在的異常:當系統記憶體吃緊,任何被映射但是沒有被修改的檔案,如代碼,會被移除記憶體

共用記憶體

為了適配Android對記憶體的需求,Android通過進程共用記憶體頁,它可以通過如下方式:

  • 每一個app進程都是從已經存在的Zygote進程forked出來,Zygote進程會在系統啟動和載入通用的framework代碼和資源(如activity的主題)時啟動,所以為了開啟一個app進程,系統會fork Zygote進程然後在新的進程載入運行app的代碼。這樣就需要分配給framework代碼和資源大部分記憶體需要被所有的進程共用

  • 大部分待用資料會被映射到進程中,這樣不僅可以使得相同的資料在進程間共用同時當必要的時候被移除,比如待用資料包括:Dalvik代碼(用於直接映射的預連結的.odex檔案),app資源檔(通過設計一個可以被直接映射的資源表或對齊APK的zip entries)和傳統的工程元素如.so的本地檔案

  • 很多時候,Android通過顯示的記憶體配置(ashmem或gralloc)實現在進程間共用動態記憶體,比如,系統surface在應用和screen compositor間共用記憶體,cursor buffers在content provider和用戶端間共用記憶體

由於共用記憶體的大量使用,需要格外注意應用記憶體的使用,旁徵博引的決定應用記憶體使用量的討論在 調查應用記憶體情況

申請和回收記憶體

下面是一些關於Android是怎麼分配和回收應用的執行個體

  • 每個進程的Dalvik堆棧被限制在一個虛擬記憶體範圍內,它定義了邏輯的堆棧大小,它可以隨著它的需要自增長(但是只能增長到系統分配給每個app的上限)

  • 邏輯堆棧大小不同於堆棧使用的實體記憶體大小,當檢查應用堆棧的時候,Android會計算一個叫做Proportional Set Size(PSS)的值,這個值包含被其他進程共用的髒和乾淨的頁,但是它也是按有多少apps共用記憶體按比例分配的。總的PSS大小才是系統認為的你的app實體記憶體大小,更多PSS的說明,參考調查應用記憶體情況

限制應用的記憶體

為了保證多任務運行環境,Android設定了每一個app的堆大小,準確的堆大小由於每個裝置可用記憶體不一樣所以也各有差別,一旦應用達到了堆棧上限,並且試圖申請更多的記憶體,會收到系統OutOfMemoryError錯誤

一些情況下,你可能需要查詢系統來確定你在裝置上可用記憶體的準確數值,比如,為了確定應該緩衝多少資料是安全的,可以調用getMemoryClass查詢系統得到這個資料,它會返回app可用記憶體的一個以兆為單位的整型數值,下面將會討論,檢查應該用多少記憶體

切換apps

當使用者切換app的時候,Android不是切換記憶體空間,而是把沒有在forground的進程app切換到一個LRU的緩衝中,比如,當使用者第一次啟動app時,會給它建立一個進程,但是當使用者離開app時,進程並沒有停止,而是系統緩衝了這個進程,所以當使用者稍候返回這個app時,進程可以快速被重用

如果app有一個緩衝的進程,並且它持有了現在不需要的記憶體,即使使用者沒有在用,它也會影響系統的整體效能,所以,當系統記憶體吃緊時,它會殺死LRU緩衝隊列中最近最少使用的進程,但是也會考慮殺死記憶體佔用最多的進程,為了保證進程在後台被儲存的盡量長,聽從下面的建議,關於應該什麼時候釋放引用

更多關於當app沒有處於forground進程是怎麼被緩衝和Android是怎麼決定殺死哪個進程的參考 進程和線程

app應該怎麼管理記憶體

在開發的所有階段都要考慮RAM的限制,包括在開發之前app的設計,有很多方式可以讓你設計和編寫代碼得到更高效率的結果,儘管應用在聚集越來越多相同的技術

請設計和實現app的時候盡量的遵循下面建議使得記憶體更加高效

慎用services

如果app需要service在後台執行任務,不要讓它一直運行除非它確實在執行任務,小心處理當它工作結束時由於沒有關閉導致的service的泄露

當你開啟一個service時,系統會盡量保持service的進程正常運行,這就導致了這個進程代價非常昂貴,因為這個service的RAM不可以被別的任何東西使用和替換,也導致了系統儲存在LRU cache隊列的進程數量會減少,使得app切換效率降低,甚至當記憶體緊張時會導致系統抖動,並且系統可能無法保持足夠的進程來承載當前正在運行所有services

為了限制service的生命最好的方式是使用IntentService,它會結束自己當完成了開啟它的intent時,更多資訊可以參考 運行一個後台服務

android app容易犯的最糟糕的記憶體管理的錯誤就是讓一個已經不需要的service一直運行,所以不要為了你app的持續運行通過保持一個持續啟動並執行service實現,由於記憶體的限制這樣不僅會增加你app低效率啟動並執行風險,還會導致使用者發現了這樣的不好的行為時卸載app

當你的使用者介面已經不在前台時釋放記憶體

當使用者切換到其它app,你的UI已經不再可見時,應用釋放僅僅你的UI佔用的所有資源,釋放UI資源可以增加系統緩衝進程的能力,並且可以很直接的影響經驗品質

為了通知什麼時候使用者離開了你的UI,實現Activity的onTrimMomory()回調,用這個方法監聽TRIM_MEMORY_UI_HIDDEN,它表明了你的UI是隱藏的,你應該釋放只是你的UI佔用的資源

需要注意的是,app接收TRIM_MEMORY_UI_HIDDENonTrimMemory()僅在當你的進程對於使用者也是隱藏時,它不同於onStop()回調,onStop()回調是在Activity執行個體隱藏時也就是使用者切換到app的另外一個activity時. 因此儘管你可以實現onStop()執行個體釋放activity的資源就像網路連接或者取消註冊broadcast receivers,但是你不應該釋放你的UI資源除非接收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN),這樣就確保了如果使用者從另外一個activity返回時,你的UI資源仍舊可用從而使得activity可以快速可見

記憶體緊張時釋放記憶體

對應app生命週期的各個階段,onTrimMemory()回調告訴了我們什麼時候裝置處於低記憶體的狀態,我們應該在收到onTrimMemory()時進一步的釋放記憶體資源

  • TRIM_MEMORY_RUNNING_MODERATE

app正在運行並且系統沒有考慮殺死它,但是裝置運行記憶體吃緊,系統正在殺死LRU cache的進程

  • TRIM_MEMORY_RUNNING_LOW

應用正在運行並且系統沒有考慮殺死它,但是裝置運行記憶體吃緊,所以應該釋放沒有用的資源來提高系統運行效率(因為直接影響了app的效率)

  • TRIM_MEMORY_RUNNING_CRITICAL

應用正在運行,但是系統已經殺死了LRU cache的大部分進程,所以應該釋放所有沒有在臨界的資源。如果系統不能回收足夠的記憶體,它會清理所有的LRU cache的進程和一些系統傾向保持的進程,比如那些保持service的進程

同樣的,當app進程當前正在被緩衝的時候,你可能會收到onTrimMemory()的下面的這些回調

  • TRIM_MEMORY_BACKGROUND

系統記憶體吃緊並且你的進程處於LRU列表的前面,儘管你的app進程沒有被系統殺死那麼高的風險,但是系統仍然可能正在殺死LRU cache的進程,你應該釋放一些容易恢複的資源以便於你的進程可以保持在LRU列表中,並且當使用者返回app的時候你可以快速恢複

  • TRIM_MEMORY_MODERATE

系統運行記憶體吃緊並且你的進程處於LRU列表的中部,系統開始進一步的釋放記憶體,你的進程有幾率被殺死

  • TRIM_MEMORY_COMPLETE

系統運行記憶體吃緊並且如果系統沒有恢複記憶體的話你的進程是第一個被殺死的,你應該釋放所有不嚴重影響你app恢複的資源

由於onTrimMemory()回調是在API 14之後添加的,在低版本可以用onLowMemory()回調,低版本的回調相當於TRIM_MEMORY_COMPLETE事件

注意:當系統開始在LRU列表殺進程時,儘管它是自上而下的工作,它同樣會考慮是哪個進程消耗了更多的記憶體並且如果殺死哪個會提供給系統更多的記憶體,因此在LRU列表你消耗越少的記憶體你越有更多的機會保持在列表中,並且更容易被使用者快速恢複

檢查應該用多少記憶體

就像上面提到的,每一個Android裝置對系統來說有不同的RAM大小,這也導致了對於每個app不同的堆棧大小,可以調用getMemoryClass()來獲得app以兆為單位的可用堆大小。如果app試圖申請多於可用的記憶體大小,系統會報OutOfMemoryError錯誤

在特殊的情況下,可以在manifest的標籤設定largeHeap屬性為true申請一個較大的堆大小,如果這麼做的話,可以調用getLargeMemoryClass()來獲得大致的large堆大小值。

然而,申請大堆的能力僅用於一些可以證明需要更多RAM的app(如一個大圖編輯app).不要僅僅是因為你把記憶體耗盡了所以去申請更大的堆記憶體,僅僅應該在你確切的知道你的記憶體被分配在什麼時候在哪兒並且為什麼它必須被保持。然而即使你可以證明你的app是正當的使用large heap,你應該避免任何時候需要擴充的時候都去申請它,使用擴充的記憶體會損害使用者整體的體驗,因為記憶體回收行程會花費更長的時間,系統執行也會變慢如任務切換或者其他一些通用的執行

此外,large heap在不同的裝置上也不一樣,當運行在一個記憶體吃緊的裝置時,large heap可能跟正常的heap大小一樣,所以即使你申請了large heap,你應該調用getMemoryClass()檢查正常的heap大小並且盡量的低於那個限制

避免bitmap的記憶體浪費

當你載入一個bitmap的時候,只需要保持當前螢幕解析度的在記憶體中,如果bitmap是一個更高解析度時去縮放它,要知道的是bitmap解析度的增長代表著記憶體的增長,因為X和Y的尺寸都在增加

注意:在Android2.3.x(api level 10)以下,bitmap對象總是在app堆出現相同的大小忽略圖片解析度(實際的像素儲存在本地記憶體中)。這導致調試bitmap的記憶體配置非常的困難,因為大部分的堆分析工具無法看到本地記憶體配置。然而,Android 3.0(api level 11)之後,bitmap的像素資料是被app的Davlik堆分配的,提高了記憶體回收和調試效率。因此如果你的app使用bitmaps並且你無法發現為什麼你的app在一些老裝置上正在使用一些記憶體,可以切換到Android3.0以上的裝置debug調試

更多的一些關於bitmap處理的 參考管理bitmap記憶體

使用最佳化過的data容器

利用Android framework最佳化過的容易,如SpareArray,SpareBooleanArray,LongSparseArray.通常HashMap實現是非常消耗記憶體的,因為它需要為每一個映射建立entry對象,但是SparseArray類卻非常高效,因為他們避免系統對key和一些value的自動裝箱(它會建立了一個新的對象或兩個entry)並且不用擔心當它是有意義的資料時轉換為原始的arrays

注意記憶體開銷

瞭解你用的語言和連結庫的開銷,當設計app時從開始到結束都要記著這件事,通常,表面的一些看起來無害的事可能實際上會開銷非常大比如:

  • Enums通常需要比靜態變數超過兩倍的記憶體,在Android中應該嚴格避免使用enums

  • 每一個Java類(包括抽象內部類)使用大約500 byte的代碼

  • 每一個類執行個體花費12-16bytes的記憶體

  • 把一個單個entry放到HashMap中需要另外分配一個32bytes的entry對象(詳細可以看 最佳化資料容器)

A few bytes here and there quickly add up—app designs that are class- or object-heavy will suffer from this overhead。這會導致你處於一種尷尬的位置:在堆分析器裡看到很多小對象佔用著你的記憶體

注意抽象代碼的使用

通常情況下,開發人員會把抽象認為是良好的代碼實踐,因為抽象可以提高程式的靈活性和可維護性,然而,抽象卻有很大的成本:通常它們需要更多執行的代碼,需要更多時間和記憶體使得代碼映射到記憶體。因此如果抽象沒有帶來顯著的好處的話應該避免使用它

為序列化得資料提供nano protobufs

protocol buffers 是google設計的一種語言無關,平台無關,可擴充的序列化得結構語言-如XML,但是更小、更快、更簡單。如果你決定用protocol buffer的資料,應該在用戶端代碼中中使用nano protobufs。普通的protobufs會產生特別冗長的代碼,而這些會導致app各種各樣的問題:增加記憶體佔用、apk大小增長、執行速度變慢和快速達到dex限制

更多資訊,請查看 protobuf readme

避免依賴注入架構

使用依賴注入架構如Guice或者RoboGuice可能非常吸引人,因為它們可以使得你們寫的代碼簡單並且提供自適應的環境用於測試或者其它配置的變化。然而,這些架構通過掃描你的代碼的注釋會產生大量的處理流,這需要將大量的代碼映射到記憶體中。儘管你並不需要。這些映射頁會被分配到乾淨的記憶體以便Android可以回收它們,但是直到它在記憶體中存在很長一段時間之後才會被回收。

謹慎使用第三方庫

很多第三方庫不是為行動裝置環境寫的,所以如果我們把它們用於我們的用戶端就會非常的低效。至少當你決定用一個第三方庫的時候應該要考慮到你將對這些庫有重要的移植和維護的負擔。在決定用之前提前計劃並且分析這些庫的代碼大小和記憶體佔用

即使是專門為Android設計的庫也存在潛在的風險,因為每一個庫在代碼編寫上可能完全不同,比如一個庫可能使用nano protobufs但是另外一個庫卻用的是micro protobufs,那麼現在在你的app中就會有兩種不同的protobuf實現。同樣會出現的比如對log、分析、圖片載入、緩衝和其它你想不到的不同的實現。ProGuard也解救不了你,因為這些都是依賴底層庫需要的功能。當你使用一個庫的Activity的子類的時候這個問題會變的更嚴重(意味著會有很多的依賴),當依賴庫有反射的話(這是很常見的,意味著你將會花費大量的時間調整ProGuard使得庫能正常使用)等等

要小心不要落入只使用一個共用庫的一兩個功能但是其它功能都沒用的陷阱。因為你不想要引入你甚至都沒用的大量的代碼和記憶體開銷。最後,如果沒有一個跟你的需求完全符合的現存的實現的話,最好的辦法是自己實現

使用ProGuard去除無用的代碼

ProGuard的工具通過移除無用的代碼和用語意模糊的名字重新命名類名、欄位名、方法名實現壓縮、最佳化、混淆代碼的目的。使用ProGuard可以使代碼更緊湊,使用更少的記憶體映射頁

對最終的APK使用zipalign

如果你需要對系統編譯產生的apk做任何處理的話(包括使用你的生產認證簽名),必須要做的就是對apk進行zipalign對齊,如果不執行zipalign的話你的app會需要更多的記憶體,因為資源檔沒辦法從APK中被映射

注意: google play store不接受沒有zipaligned的apk

分析記憶體佔用

一旦你的app達到了一個相對穩定的狀態,開始分析你的app在整個生命週期記憶體佔用了多少記憶體。關於怎麼分析app相關資訊,請閱讀調查應用記憶體情況

使用多進程

如果對於你的app適用的話,一個先進的技術可以協助你管理你app的記憶體,將app的組件劃分為不同的進程。這種技術通常來說非常有用,但是大部分的apps不應該多進程運行,因為一旦操作不當它可以很容易的使得app記憶體增加而不是減少。對於app來說在前後台運行重要的工作並且把這些操作區分開來是非常相當有用的

有一個例子是適合多進程操作的,比如一個音樂播放器,需要在一個service播放音樂很長一段時間。如果整個app在一個進程啟動並執行話,那麼為activity UI分配很多資源必須和播放音樂保持的時間一樣長,儘管使用者已經切換到了另外一個app但是service仍舊在控制播放。這樣的app最好用兩個進程,一個用於UI,另外一個用於後台service的持續運行

你可以在manifest檔案為組件設定android:process屬性來設定一個單獨的進程。比如,可以設定service應該單獨運行一個進程而不是在主進程可以聲明一個新的進程比如’background‘(這個名字可以隨設定為自己喜歡的隨便什麼名字)

<service android:name=".PlayService"         android:progress=":background">

進程名字應該以“:”開頭保證這個進程是app的私人進程

在建立一個進程之前,需要瞭解對記憶體的影響。為了說明每一個進程的影響,需要知道的是一個空進程什麼都不做需要消耗1.4MB的記憶體,像下面展示的資訊

注意:更多關於這些資料應該怎麼讀取的參考調查應用記憶體情況.這裡邊關鍵的資料是Private Dirty和Private Clean Memory,它展示了這個進程消耗了大約1.4M為non-pageable記憶體(分配了Dalvik堆,native分配,庫儲存和載入)和150K用於要執行的代碼的映射

對於一個空進程來說這些記憶體佔用是非常重要的,因為當你要在那個進程做一些事的時候它可以快速反應。比如,這是一個僅用於顯示一個包含一些文本的activity的進程記憶體佔用

這個進程大約佔用了三倍的大小4M,僅僅是在UI上展示了一些文本。這就匯出了一個重要的結論:如果你想把你的app設計為多個進程,應該只有一個進程用於UI,其它進程應該避免使用任何UI,因為進程這將會導致記憶體的快速增長(尤其是當你開始載入bitmap資源和其它資源的時候)。當UI繪製的時候減少記憶體佔用會變得非常困難

此外,當運行多個進程時,保持代碼整潔需要比平時更加重要,因為任何通用實現的沒必要的記憶體開銷都會在每一個進程中複製一份。比如,如果你用enums(儘管不應該使用enums).所有的記憶體需要建立和初始化這些常量,在每一個進程中複製,並且任何適配器和臨時的其它抽象開銷也同樣會被複製

多進程還需要考慮的是它們之間存在的依賴關係。比如,如果你的app在預設進程運行著content provider同時承載著UI,然後使用那個content provider的代碼運行在一個後台進程,它需要你的UI進程儲存在記憶體中。如果你的目標是有一個後台進程可以獨立運行於一個重量級的UI進程。它就不能依賴於UI進程執行的content provider和service

管理App的記憶體

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.