search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

管理App的內存

管理App的內存。對於任何軟體來說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_HIDDEN的onTrimMemory僅在當你的進程對於用戶也是隱藏時,它不同於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『(這個名字可以隨設置為自己喜歡的隨便什麼名字)

進程名字應該以「:」開頭保證這個進程是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



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦