文章出處:http://developer.android.com/training/articles/memory.html#YourApphtml
Random-access memory(RAM)在任何軟件開發環境都是稀有資源,在移動操做系統物理內存有限的狀況下將顯得更加珍貴.雖然Android的Dalvik虛擬機優化了內存回收機制,但咱們也要關注你的app的內存分配合和釋放java
爲了垃圾回收器能回收你係統的內存,你應該避免引發內存泄露(一般由全局成員hold了對象引用),並且要在合適的時間點(如生命週期回調時,這將在後面章節進一步討論)釋放被引用的對象。android
對於大多數的app來講,Dalvik虛擬機的垃圾回收器幫你進行了內存回收和管理:當響應的對象超出應用範圍時回收和釋放。c++
本文描述了Android如何管理app進程和內存分配,以及當你進行android開發時如何減小內存使用。至於Java裏通用的資源釋放方式請參看其餘相關文檔。若是你須要分析你的app的內存使用狀況,請參考 Investigating Your RAM Usage。shell
How Android Manages Memory編程
Android內存模型並無交換空間(swap space)的概念,而是使用分頁(paging)和內存映射(memory-mapping)管理內存,這意味着無論是分配新的對象仍是使用已有的映射頁api
這些內存仍然被佔據在RAM裏而不能被扇出。所以徹底釋放你app內存的惟一方式是釋放對象引用以便於能被垃圾回收器回收。這有個例外:當加載沒有修改的文件例如代碼進入RAM時,該文件佔用的內存空間能被RAM回收。緩存
Sharing Memory安全
爲了知足每一個app對RAM的須要,Android在進程間共享RAM分頁。一般遵循下面方式實現:網絡
1.每個app進程被一個存在的進程(Zygote)調起。當系統啓動、加載android框架代碼和資源(例如activity themes)時Zygote進程啓動。爲了開啓一個新的app進程,系統調用Zygote進程在一個新的進程里加載和運行app的代碼。這使得框架代碼和資源能分配比較多的RAM內存分頁,同時也使全部的app進程同享這片內存區。
2.大多數靜態數據被映射到進程中。這不只讓一樣的數據在進程間共享,也容許在須要的時候被調出。靜態數據包括:Dalvik代碼(指pre-linked的.ODEX文件),應用程序的資源(被組織成資源映射表的結構,在打包成apk時優化和對齊res資源)及native代碼如.so文件。
3.一些狀況下,Android使用顯示分配的同享內存區域(經過ashmem或者aralloc分配)在進程間共享相同的動態RAM。例如:Window surfaces在app和screen compositor之間使用共享內存,遊標緩存在content provider和client之間共享內存。
因爲共享內存的大量使用,須要特別關注你的app使用了多少內存。分析你的app內存使用狀況的技術將在Investigating Your RAM Usage.裏討論。
Allocating and Reclaiming App Memory
如下是一些android分配和回收內存的考量:
1.android會爲每一個進程在dalvik虛擬機裏分別開闢heap空間。同時定義了每一個heap的邏輯大小,之後能夠按需增加(固然增加到系統爲每一個app定義的最大size爲止)。
2.heap的邏輯大小和實際使用的物理內存大小是不相同的。當觀察你的app的heap的時候,你會看到一個叫作Proportional Set Size (PSS)的值,它是經過共享給其餘進程的page頁大小計算出來的值。但這僅僅是共享給其餘app內存的一個百分比,系統認爲PPS總大小是你的app所佔用的物理內存大小。更多的關於PPS信息,請參考Investigating Your RAM Usage。
3.android不會進行碎片整理以釋放heap空間,android只會壓縮棧底未被使用的邏輯heap空間。但這並不意味着heap空間的物理內存不能被壓縮。垃圾回收以後,Dalvik遍歷heap,找到不被使用的pages,將這些pages返回給內核。所以,大塊成對的分配和釋放應該能回收全部(或幾乎全部)使用的物理內存。然而,從較小的分配回收內存可能效率低得多,由於用於小分配的頁仍然能夠被引用和共享,還沒有被釋放。
Restricting App Memory
爲了實現多任務功能,Android限制了分配給每一個app的heap大小,上限大小在各個設備之間差異較大,取決於該設備的整體可用的有效RAM。若是你的app內存使用已到達了heap的容量,Dalvik將試着分配更多的內存。直到產生OutOfMemoryError。
你可能想要知道你的手機設置給每一個app到底有多大的heap空間。例如,想知道緩存多少數據是安全的。你能夠經過調用ActivityManager裏的getMemoryClass()查詢系統這個數字,它將返回一個以Mb爲單位的整數,標識你的應用程序的heap大小。下面章節將詳細討論,請參見 Check how much memory you should use
Switching Apps
Android不是用交換空間(swape space)在app之間切換。Android按最近使用(LRU)策略在緩存中保存後臺進程(用戶不可見進程)。例如,當用戶開啓一個app時,系統爲這個app產生一個進程。但當用戶將這個app退到後臺時,這個進程並無退出。系統持有這個進程緩存。當用戶又從新進入該app時,該進程將被重用,以便於更快的app切換。
若是你的app有緩存進程,它佔用了一些當前它並不須要的內存。這將制約你的系統的總體性能。所以,當系統內存吃緊時,系統將按LRU原則殺死後臺進程。同時也考量哪些進程最佔用內存。
爲了讓你的app進程更長的緩存在內存而不被殺死,參見When to release your references章節的建議。
當app進程從前臺到後臺時怎麼被緩存以及android決定殺死哪個後臺進程,更多的信息請參考Processes and Threads(http://developer.android.com/guide/components/processes-and-threads.html)
How Your App Should Manage Memory
你應該在整個開發階段都考慮RAM的因素,包括開始開發以前的設計階段。你能設計多種實現方式,選擇其中最節省內存最有效的方式去編寫代碼。當你開發和實現你的app時,你應該使用下面的技術確保你的app能更有效的使用內存
慎用Service
若是你的app須要一個Servcie運行後臺任務時,當執行完該任務後就中止該服務。特別注意不要你的service任務已完成,而不去中止它。
當你使用一個Service時,系統會盡可能的保證該Service運行。這就使得進程佔用了一部份內存。而該部份內存不能被釋放。這致使了系統在LRU緩存區緩存進程數的減小。這也使得app切換更耗時,當系統內存緊張時它甚至能致使系統宕機,並殺死後臺正在運行的service。
限制你Service生命週期的最好的方式是使用IntentSerivce。當IntentService處理完開啓它的Intent時,它會自會關閉。更多的信息,請閱讀Running in a Background Service
當一個service不須要而還在後臺運行時,這是最消耗內存的內存管理錯誤。所以要慎用服務,當服務完成後臺任務時要記得關閉。若是不這樣作,因爲RAM的限制,你的app運行將變得很是卡,用戶也將發現app錯誤的行爲,最後卸載你的應用。
Release memory when your user interface becomes hidden
當用戶跳到一個不一樣的app,而使你的app的UI不可見時,你應該釋放那些僅僅被你的UI使用的資源。及時的釋放你的UI資源能顯著的增長你的緩存進程容量。這對用戶體驗有顯著的影響。
爲了接受到用戶退出的UI的通知,你須要實如今你的Activity裏實現onTrimMemory回調。你應該使用該方法監聽TRIM_MEMORY_UI_HIDDEN級別的UI隱藏,TRIM_MEMORY_UI_HIDDEN代表你的UI如今被隱藏了,你應該僅僅釋放被你的UI使用的資源。
注意在TRIM_MEMROY_UI_HIDDEN級別下,只有當你的應用進程的全部UI組件相對於用戶不可見時纔回調onTrimMemory()。這不一樣於Activity的onStop()方法回調。onStop在activity實例不可見時纔回調。這發生在app裏從一個activity跳到另外一個activity時。所以,雖然這時你應該重寫onStop方法,在該方法裏作釋放資源(例如網絡鏈接、註銷廣播等)的工做。但這時你不該該OnStop裏作釋放UI資源工做。這確保了若是用戶從activity返回到先前的activity時,你的UI資源是仍然有效的,以便於快速resume你的activity。
Release memory as memory becomes tight
在app的任何生命週期階段,onTrimMemory() 回調方法均可以告訴你你的設備的內存何時愈來愈低,。你能夠根據該方法傳遞的內存緊張級別參數來釋放資源.
TRIM_MEMORY_RUNNING_CRITICAL
應用處於運行狀態而且認爲不能被殺掉, 而設備可使用的內存比較低, 這時系統級會按LRU策略殺掉一些其它的緩存應用.
TRIM_MEMORY_RUNNING_LOW
應用處於運行狀態而且認爲不能被殺掉, 而設備可使用的內存很是低, 能夠把你的application不用的資源釋放一些已提升系統性能(這會會直接影響到你的程序的性能)
TRIM_MEMORY_RUNNING_CRITICAL
應用處於運行狀態可是系統已經把LRU緩存裏的大多數進程給殺掉了, 你必須釋放掉不是很是關鍵的資源, 若是系統不能回收足夠的運行內存, 系統會清除全部緩存應用而且會把正在活動的應用殺掉(例如正在運行的Service).
還有,當你的app進程被系統緩存時,你可能會在onTrimMemory()裏收到下面的幾個內存級別:
TRIM_MEMORY_BACKGROUND
系統處於低內存的運行狀態中而且你的應用剛進入LRU緩存. 雖然你的應用不會處於被殺的高風險中, 可是系統已經開始清除緩存列表中的其它應用, 因此你必須釋放容易恢復的資源使你的應用繼續存留在列表中以便用戶再次回到你的應用時能快速恢復
TRIM_MEMORY_MODERATE
系統處於低內存的運行狀態中而且你的應用處於緩存應用列表的中部. 若是系統運行內存有限, 你的應用有被殺掉的風險.
TRIM_MEMORY_COMPLETE
系統處於低內存的運行狀態中若是系統如今沒有回收足夠的內存,你的應用將會第一個被殺掉. 你必須釋放掉全部非關鍵的資源從而恢復的應用.由於 onTrimMemory() 是在android API 14中加入的, 因此低版本可使用 onLowMemory() 方法替代, 該方法大體至關於 TRIM_MEMORY_COMPLETE 事件.
注意: 當系統開始清除緩存應用列表中的應用時, 雖然系統的主要工做機制是自下而上, 可是也會經過殺掉消費大內存的應用從而使系統得到更多的內存,因此在緩存應用列表中消耗更少的內存將會有更大的機會留存下來以便用戶再次使用時進行快速恢復.
Check how much memory you should use
前面提到, 不一樣的android設備系統擁有的運行內存各自都不一樣, 從而不一樣的應用堆內存的限制大小也不同. 你能夠經過調用 ActivityManager 中的 getMemoryClass() 函數以兆爲單位獲取當前應用可用的內存大小, 若是你想獲取超過最大限度的內存則會發生 OutOfMemoryError .
特別地, 能夠在 manifest 文件中的 <application> 標籤中設置 largeHeap 屬性的值爲 "true"時, 當前應用就能夠獲取到系統分配的最大堆內存. 若是你設置了該值, 能夠經過 ActivityManager getLargeMemoryClass() 函數獲取最大的堆內存。然而, 提供給app獲取最大的heap的能力是由於確實有小部分應用須要消耗大的堆內存(好比大照片編輯應用). 歷來不要僅僅是由於你的app內存溢出了就簡單的請求最大的heap,但內存溢出時,你要作的是快速定位內存泄露點並恢復它,只有當你的內存確實須要很大的heap空間並且不存在內存泄露時,你才須要設置largeHeap 屬性的值爲 "true",即便這種狀況下,你也應該儘量的避免這種需求. 由於使用大量內存後, 當你切換不一樣的應用或者執行其它相似的操做時, 長時間的內存回收會致使系統的性能降低
從而漸漸的會損害整個系統的用戶體驗.
另外, 大內存在不一樣的設備並不相同. 當app跑在有運行內存限制的設備上時, 大內存和正常的堆內存是同樣的. 那便是設置largeHeap 屬性的值爲 "true可能並不起做用,因此若是你設置了largeHeap 屬性的值爲 "true, 你也應該調用 getMemoryClass() 函數查看正常的堆內存的大小而且儘量使內存使用狀況維護在正常堆內存之下.
Avoid wasting memory with bitmaps
當你加載 bitmap 時, 須要根據當前設備的分辨率加載相應分辨率的bitmap進入內存,若是下載下來的原圖分辨率比設備分辨率高則要壓縮它. 要當心bitmap的分辨率增長後所佔用的內存也要進行相應的增長(平方級increase2的增加), 由於它是根據x和y的大小來增長內存佔用的.
注意: 在 Android 2.3.x(api level 10)如下, 不管圖片的分辨率多大 bitmap 對象在內存中始終顯示相同大小, 實際的像素數據被存儲在底層 native 的內存中(c++內存). 由於內存分析工具沒法跟蹤 native 的內存狀態,因此調試 bitmap 內存分配變得很是困難. 然而, 從 Android 3.0(api level 11)開始, bitmap 對象的內存數據開始在應用程序所在Dalvik虛擬機堆內存中進行分配, 提升了回收機率和調試的可能性. 若是你在老版本中發現 bitmap 對象佔用的內存大小始終同樣時, 切換到3.0或以上系統的設備來進行調試
更多的使用bitmaps的提示, 參閱Managing Bitmap Memory.
Use optimized data containers
利用 Android 框架優化後的數據容器, 好比 SparseArray, SparseBooleanArray 和 LongSparseArray. 傳統的 HashMap 在內存上的實現十分的低效,由於它須要爲 HashMap 中每一項在內存中創建映射關係. 另外, SparseArray類很是高效由於它避免了對key和value的自動封箱.
Be aware of memory overhead
在你開發你的app應用時,各個階段都要很謹慎的考慮所使用的語言和庫帶來的內存上的成本和開銷. 一般狀況下, 表面上看起來可有可無的代碼可能帶來巨大的開銷, 下面是一些例子:
1.相比於靜態常量,枚舉會有超過其兩倍以上的內存開銷,在 android 中你須要嚴格避免使用枚舉
2.java 中的每一個類(包含匿名內部類)大約使用500個字節
3.每一個類實例在內存(RAM)中佔用12到16個字節
4.在 HashMap 中放入一個數據時, 須要爲額外的其它項分配內存, 總共佔用32個字節
app裏這裏或者那裏雖然幾個bytes(app裏可能被設計成類或者對象)的很快的增長都會增長內存消耗。這會增長你分析堆內存的複雜度。你不容易意識到是許多小對象佔用了你大量內存。
Be careful with code abstractions
咱們都知道使用抽象是一個好的編程實踐,由於抽象提升了代碼的靈活性和可維護性。然而,抽象也產生了很大的開銷:由於爲了抽象代碼能執行,一般地須要至關大的更多代碼。爲了映射進內存須要更多的時間和和更多的內存。所以,若是你的app不是必須使用抽象,你應該避免使用抽象。
Use nano protobufs for serialized data
Protocol Buffers 是 Google 公司開發的一種跨平臺的、與語言無關的數據描述語言,相似於XML可以將結構化數據序列化. 可是它更小, 更快, 更簡單. 若是你決定使用它序列化你的數據, 你必須在你的客戶端代碼中一直使用nano protocol buffer, 由於正常的 protocol buffer 會產生極其冗餘的代碼, 在你的應用會引發不少問題: 增長了使用的內存, 增長了apk文件的大小, 執行速度較慢以及會把一些提示符號打入dex 包中.
For more information, see the "Nano version" section in the protobuf readme.
Avoid dependency injection frameworks
使用像 Guice 和 RoboGuice 依賴注入框架會有很大的吸引力, 由於它可使咱們的代碼更加簡潔和提供自適應的環境用來進行有用的測試和進行其它配置的更改.然而這些框架經過註解的方式掃描你的代碼來執行一系列的初始化, 這會把一些咱們不須要的大量的代碼映射到內存中. 被映射後的數據會被分配到乾淨的內存中, 放入到內存中後很長一段時間都不會使用, 這樣形成了內存大量的浪費.
Be careful about using external libraries
許多的外部依賴庫每每不是在移動環境下寫出來的, 這樣當在移動環境中使用這些庫時就會很是低效. 因此當你決定使用一個外部庫時, 你就要承擔外部庫帶來的移植問題和維護負擔.在項目計劃前期就要分析該類庫的受權條件, 代碼量, 內存的佔用再來決定是否使用該庫.
即便專門設計用於 android 的庫也有潛在的風險, 由於每一個庫作的事情都不同. 例如, 一個庫可能使用的是 nano protobuf 另一個庫使用的是 micro protobuf, 如今在你的應用中有兩個不一樣 protobuf 的實現. 這將會有不一樣的日誌, 分析, 圖片加載框架, 緩存, 等全部你不可預知的事情的發生. Proguard 不會給你優化這些, 由於全部低級別的 api 依賴須要你依賴的庫裏所包含的特徵. 當你使用從外部庫繼承的 activity 時尤爲會成爲一個問題(由於這每每產生大量的依賴). 庫要使用反射(這是常見的由於你要花許多時間去調整ProGuard使它工做)等.也要當心不要陷入使用幾十個依賴庫去實現一兩個特性的陷阱; 不要引入大量不須要使用的代碼. 一天結束時, 當你沒有發現符合你要求的實現時, 最好的方式是建立一個屬於本身的實現.
Optimize overall performance
如何優化你的app的總體性能,能夠參考文檔:Best Practices for Performance.該文檔裏對於如何優化你的CPU性能也給出些tips,裏面的一些tips也將有助於你優化你的內存。例如:減小布局文件裏的view層次
You should also read about optimizing your UI with the layout debugging tools and take advantage of the optimization suggestions provided by the lint tool.
Use ProGuard to strip out any unneeded code
代碼混淆工具 ProGuard 經過去除沒有用的代碼和經過語義模糊來重命名類, 字段和方法來縮小, 優化和混淆你的代碼. 使用它能使你的代碼更簡潔, 更少許的RAM映射頁.
Use zipalign on your final APK
若是構建apk後你沒有作後續的任何處理(包括根據你的證書進行簽名), 你必須運行 zipalign 工具爲你的apk進行優化, 若是不這樣作會致使你的應用使用更多的內存,zipalign以後像資源這樣的東西不會再從apk中映射(mmap)入內存.
注意:goole play store 不接受沒有進行zipalign的apk
Analyze your RAM usage
一旦你的apk已build到一個相對穩定的版本,你應該在你的app的各個生命週期階段分析你的應用的RAM使用狀況。
For information about how to analyze your app, read Investigating Your RAM Usage.
使用多進程
一種更高級的技術能管理應用中的內存, 分離組件技術能把單進程內存劃分爲多進程內存. 該技術必定要謹慎的使用而且大多數的應用都不會跑多進程, 由於若是你操做不當反而會浪費更多的內存而不是減小內存.它主要用於後臺和前臺能各自負責不一樣業務的應用程序
當你構建一個音樂播放器應用而且長時間從一個 service 中播放音樂時使用多進程處理對你的應用來講更恰當. 若是整個應用只有一個進程, 當前用戶卻在另一個應用或服務中控制播放時, 卻爲了播放音樂而運行着許多不相關的用戶界面會形成許多的內存浪費. 像這樣的應用能夠分隔爲兩個進程:一個進程負責 UI 工做, 另一個則在後臺服務中運行其它的工做.
在各個應用的 manifest 文件中爲各個組件申明 android:process 屬性就能夠分隔爲不一樣的進程.例如你能夠指定你一運行的服務從主進程中分隔成一個新的進程來並取名爲"background"(固然名字能夠任意取).
<service android:name=".PlaybackService"
android:process=":background" />
進程名字必須以冒號開頭":"以確保該進程屬於你應用中的私有進程.
在你決定建立一個新的進程以前必須理解對這樣作內存的影響. 爲了說明每一個進程的影響, 一個基本空進程會佔用大約1.4兆的內存, 下面的堆內存信息說明這一點
adb shell dumpsys meminfo com.example.android.apis:empty
** MEMINFO in pid 10172 [com.example.android.apis:empty] **
Pss Pss Shared Private Shared Private Heap Heap Heap
Total Clean Dirty Dirty Clean Clean Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------ ------
Native Heap 0 0 0 0 0 0 1864 1800 63
Dalvik Heap 764 0 5228 316 0 0 5584 5499 85
Dalvik Other 619 0 3784 448 0 0
Stack 28 0 8 28 0 0
Other dev 4 0 12 0 0 4
.so mmap 287 0 2840 212 972 0
.apk mmap 54 0 0 0 136 0
.dex mmap 250 148 0 0 3704 148
Other mmap 8 0 8 8 20 0
Unknown 403 0 600 380 0 0
TOTAL 2417 148 12480 1392 4832 152 7448 7299 148
注意: 上面關鍵的數據是 private dirty 和 private clean 兩項, 第一項主要使用了大約是1.4兆的非分頁內存(分佈在Dalvik heap, native分配, book-keeping, 和庫的加載), 另外執行業務代碼使用150kb的內存.
空進程的內存佔用是至關顯著的, 當你的應用加入了許多業務後會增加得更加迅速. 下面的例子是使用activity顯示一些文字, 當前進程的內存使用情況的分析.
** MEMINFO in pid 10226 [com.example.android.helloactivity] **
Pss Pss Shared Private Shared Private Heap Heap Heap
Total Clean Dirty Dirty Clean Clean Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------ ------
Native Heap 0 0 0 0 0 0 3000 2951 48
Dalvik Heap 1074 0 4928 776 0 0 5744 5658 86
Dalvik Other 802 0 3612 664 0 0
Stack 28 0 8 28 0 0
Ashmem 6 0 16 0 0 0
Other dev 108 0 24 104 0 4
.so mmap 2166 0 2824 1828 3756 0
.apk mmap 48 0 0 0 632 0
.ttf mmap 3 0 0 0 24 0
.dex mmap 292 4 0 0 5672 4
Other mmap 10 0 8 8 68 0
Unknown 632 0 412 624 0 0
TOTAL 5169 4 11832 4032 10152 8 8744 8609 134
這個比上面多花費了3倍的內存, 只是在 界面 上顯示一些簡單的文字, 用了大約4兆. 從這裏能夠得出一個很重要的結論:若是你的想在應用中使用多進程, 只能有一個進程來負責 UI 的工做, 在其它進程中不能出現任何 UI的工做, 不然會迅速提升內存的使用率(尤爲當你加載 bitmap 資源和其它資源時). 一旦加入了UI的繪製工做就不可能會減小內存的使用了.
另外, 當你的應用超過一個進程時, 保持代碼的緊湊很是重要, 由於如今由相同實現形成的沒必要要的內存開銷會複製到每個進程中, 會形成內存浪費更嚴重的情況出現. 例如, 你使用了枚舉, 不一樣的進程在內存中都會建立和初始化這些常量.而且你全部的抽象適配器和其它臨時的開銷也會和前面同樣被複制過來.
另外要關心的問題是多進程之間的依賴關係. 例如, 當應用中運行默認的進程須要爲UI進程提供內容, 後臺進程的代碼爲進程自己提供內容還要留在內存中爲UI運行提供支持, 若是你的目標是在一個擁有重量級的UI進程的應用裏擁有一個獨立運行的後臺進程, 那麼你在UI進程中則不能直接依賴它, 而要在UI進程使用 service 處理它.