Android應用內存管理

jopen 9年前發布 | 19K 次閱讀 Android Android開發 移動開發

      內存在任何軟件開發環境中都是非常寶貴的資源,尤其是在手機操作系統中。盡管Dalvik虛擬機會通過gc來自動回收資源,但是這并不意味這你可以忽略應用內存的分配和釋放,一些被引用的無用對象是不會被gc釋放的。 
         Android沒有為內存提供交換空間,但是它使用內存分頁和內存映射來管理內存。這意味這任何你修改的內存,不論是分配新對象或者修改映射頁,都會保 留在內存中。所以唯一的釋放app內存的方法就是釋放對象的引用,是該對象可以被gc回收。但是有個例外,任何被映射進內存的沒有修改的文件,比如代碼, 如果系統其他地方需要其所占的內存則可以被調度。

  • 共享內存
    由于本身的需求,Android嘗試在不同的進程間共享內存,一般通過下面幾種方式:
    1.每一個app進程都是由Zygote進程復制出來的,Zygote進程是在系統啟動和加載framework代碼和資源時起來的。當新啟動一個進程 時,系統會復制Zygote進程,然后在新的進程中執行app的代碼。這就允許framework代碼和資源占用的大部分內存被所有的app進程共享。

    2.許多靜態數據被映射到一個進程中,這不僅可以在需要同樣數據的進程之間共享(在Android中,每個應用程序中儲存的數據文件都會被多個進程訪問: 安裝程序會讀取應用程序的manifest文件來處理與之相關的權限問題;Home應用程序會讀取資源文件來獲取應用程序的名和圖標;系統服務會因為很多 種原因讀取資源),當需要的話其所占的內存也可以被調度給其他地方使用。例如:Dalvik代碼(存在于可以被直接映射的預編譯后的odex文件 中),app資源(通過結構化設計資源表和對齊apk中的文件),普通的工程元素比如.so中的本地代碼。
    3.在許多地方,Android通過共享內存區域實現進程間數據共享(ashmem或者gralloc)。比如,window surface在app和screen compositor之間使用共享內存,cursor buffers在content provider和client之間使用共享內存。
在開發App時可以使用以下技術使App內存使用更加有效率
  • 謹慎使用services
如果你的app需要一個service在后臺完成工作,不要一直保持service在后臺運行除非它確實在執行工作,要確保工作完成后成功停止service。
當你運行一個service時,系統會選擇一直保持service所在的進程運行,這就使這個進程非常耗費資源,因為service使用的內存不能被其他任何地方使用和調度,這也會減少系統在LRU cache中緩存的進程數,使app之間的切換效率變低。
最后的方式來限制service的生命周期是使用IntentService,它會在處理完intent之后關閉自己。
留下一個不需要的service一直在運行是在Android中最糟糕的內存管理錯誤之一,
    所以不要妄想使用一個service來保持app一直在運行,這樣做不僅會增加app因為內存不足而出問題的風險,而且用戶還會發現這些表現不好的app并卸載它們。
        
  • 當用戶界面隱藏時釋放內存
        當用戶切換到另一個app你的UI不再可見時,你應該釋放只被您的UI使用的任何資源。在這種情況下釋放UI資源可以明顯地增加系統緩存進程的能力,從而提高用戶體驗。
        當用戶離開您的UI時,Activity的onTrimMemory()方法會執行,你應該使用這個方法去監聽 TRIM_MEMORY_UI_HIDDEN,它表示你的UI隱藏了,這時應該釋放UI使用的資源。需要注意的是你的app只有在所有的UI組件都不可見 的情況在才會執行onTrimMemory() TRIM_MEMORY_UI_HIDDEN回調,這與Activity的onStop()是不同的,onStop()會在切換到另一個activity 時也調用,所以你可以在onStop回調中釋放資源比如網絡連接或者注銷廣播,但一般不應該釋放UI資源,這保證了當你通過back鍵返回時,你的上一個 activity的UI資源是有效的,可以迅速的進行切換。


  • 在內存不足的情況下釋放內存
在app的生命周期中,onTrimMemory()回調在設備的內存不足的情況下也會被調用,你應該根據下面的等級來進行相應的資源釋放
TRIM_MEMORY_RUNNING_MODERATE
你的app在運行,而且沒有被考慮殺掉,但是設備在低內存的情況下運行,系統已經開始殺掉LRU cache中的進程
TRIM_MEMORY_RUNNING_LOW
你的app在運行,而且沒有被考慮殺掉,但是設備在更低內存的情況下運行,所以你應該釋放不使用的資源來提高系統的表現(它會直接影響你的app的表現)
     TRIM_MEMORY_RUNNING_CRITICAL
        你的app在運行,但是系統已經殺掉了LRU cache中的大部分進程,所以你應該馬上釋放所有不重要的資源。如果系統不能回收足夠的內存,它將會情況所有LRU cache中的進程,并且開始殺掉那些系統應該保持存活的進程,比如那些有正在運行的service的進程。
        同樣,如果你的app進程目前正在被緩存,你可以從onTrimMemory()方法中收到下列級別的通知:
     TRIM_MEMORY_BACKGROUND
        系統在低內存的環境下運行,你的進程接近LRU列表的開頭。盡管你的進程被殺掉的風險不是很高,但是系統可能已經準備殺掉LRU cache中的進程。你應該釋放一些容易恢復的資源,保證你的進程仍然存在于這個列表中,用戶可以很快的返回你的app。
    TRIM_MEMORY_MODERATE
        系統在低內存的環境下運行,你的進程解決LRU列表的中間位置,如果系統進一步內存吃緊的話,你的進程可能會被殺掉。
    TRIM_MEMORY_COMPLETE
        系統在低內存的環境下運行,你的進程在系統的首選kill列表中,你應該釋放所有對于返回app時不重要的東西。
    
  • 避免Bitmaps耗費內存
     當加載一個bitmap,在內存中只保存對當前設備屏幕所需要的尺寸,如果原圖比所需的大,就進行縮放。要記住bitmap尺寸的增加會導致相應的內存增加。
  • 使用優化過的數據容器
     使用經過優化過的數據容器,比如SparseArray,SparseBooleanArray,LongSparseArray。通常的 HashMap實現內存使用是相當低效的,因為它對每一個映射都使用entry對象,另外,SparseArray類更加有效,因為它避免了自動對key 和value的auto-boxing(將原始類型封裝為對象類型,比如把int類型封裝成Integer類型)


  • 注意內存開銷
    了解你所使用語言與庫的內存消耗,當設計應用的時候要時刻明確這些信息,表明看起來正常的可能會有巨大的開銷,例如下面的幾種情況:
     1.枚舉的內存消耗是靜態常量的兩倍以上,你應該嚴格控制在Android上使用枚舉。
     2.每一個java類包括匿名內部類需要大概500字節
     3.每一個類對象消耗12到16字節
     4.在HashMap中存放一個簡單對象需要一個額外消耗32字節左右的entry對象
   可能最后app的內存消耗主要是各個地方的小內存累加起來的。
  • 小心代碼的抽象
    開發者們經常會使用抽象只是因為它是一個良好的編程實踐,因為抽象可以提高代碼的靈活性和可維護性。但是抽象可能帶來明顯的消耗:通常它們需要相當數量的 代碼來執行,需要更多的時間和更多的內存來映射代碼進內存。所以如果你的抽象沒有明顯的好處,你應該避免使用它們。

  • 使用nano protobufs序列化數據
     Protocol buffers是一個由google設計的語言中立,平臺中立,可擴展機制,用來序列化結構數據.像xml,但更小,更快,更簡單.如果你決定使用 protobufs,你應該在客戶端代碼使用nano protobufs。普通的protobufs生成非常冗長的代碼,可能會給app帶來各自問題:增加內存占用,apk體積增長,執行慢,并且很快會達到 dex文件的限制。


  • 避免依賴注入框架
    使用依賴注入框架比如Guice或者RoboGuice可能是非常吸引人的,因為它們能簡化代碼,并且提供自適應的測試和配置環境,但是,這些框架需要執 行許多進程的初始化工作(通過掃描代碼中的注解),這會導致明顯的被映射進內存的代碼增加,盡管有些你并不需要。這些映射的頁被分配到clean memory,所以Android可以回收它們,但是這些pages可能會在內存中存放很長時間才會被回收。

  • 小心使用外部庫
    外部庫通常不是為移動環境寫的,在移動客戶端上使用可能會導致低效。至少,當你決定要用一個外部庫時,你應該承擔起移植維護并為移動優化的工作。在決定使用前,為這些工作作計劃,分析庫大小,內存占用。
    即使為Android設計的庫,也可能會帶來風險,因為每個庫做不同的事情。比如,一個庫使用nano protobufs另一個庫使用micro protobufs。現在你有兩種不同的protobuf實現。這些是不可預料的。ProGuard救不了你,因為這些都是你需要的庫的特性需要的低級別 的依賴。當你從庫中使用一個Activity的子類(會有大片的依賴)或使用反射或其他,更容易出問題。
    也要小心不要掉入為幾十個特性中的一兩個特性使用一個共享庫的陷阱。你不需要增加一大堆你不會用的代碼開銷。找了很久也沒找到一個與你的需求非常符合的現有實現,自己創建實現也許是最好的。


  • 使用ProGuard剔除不需要的代碼
    ProGuard工具通過移除沒有用到的代碼,重命名類,方法,變量為語義模糊的名稱來縮減,優化和混淆你的代碼。使用Proguard會使你的代碼更加緊湊,使用更少的內存來映射。
在最終的Apk上面使用zipalign
    如果你編譯系統生成的apk做處理(包括簽名),你必須使用zipalign工具來讓apk重新對齊。不這樣做的話會使你的app消耗更多的內存,因為資源文件可能不需要被映射。Google Play不接受沒有被zipaligned的apk

  • 使用多個進程
        如果合適,把app的組件分到不同的進程中可能會對內存有幫助。這個技術必須小心使用,大部分的app不應該運行在多個進程中,因為如果使用不當的話很容 易就會增加而不是減少app的內存,它對于后臺工作像前臺工作一樣多的app是有用的,可以對后臺和前臺的工作分開管理。
        一個多進程的例子就是當因為播放器在后臺使用service長時間播放音樂時,如果所有的app都在一個進程中,那么給activity UI分配的內存就必須在音樂播放期間被保持,即使用戶目前在其他的app,services在后臺控制播放。像這樣的app可以被分為兩個進程:一個UI 進程,一個后臺service進程。
        當你決定要創建一個新的進程,你應該理解新進程占用的內存。一個沒有做任何事情的空進程占的內存大概為2.4m,當你開始在進程中工作的時候內存數據會明 顯增長,比如當顯示一個顯示一些文字activity的時候,內存會增長到5m左右。當你把應用分成多個進程,只有一個進程應該和UI有關,其他進程應該 避免UI顯示,因為這會顯著增加進程的內存,當UI繪制出來后就很難減少內存的使用。
        另外,當使用多個進程時,你應該保證代碼的簡潔,因為任何公共實現不必要的內存消耗,都會在每個進程中都復制一份。例如如果你使用枚舉,每一個進程會重復創建和初始化這些常量,耗費的內存也會加倍。
        另外需要注意的問題是進程之間的依賴,舉個例子,如果在UI進程中有一個contentProvider,同時在后臺進程中使用這個 contentProvider也會保持UI進程持續存在內存中。如果你的目標是有一個后臺進程可以獨立與重量級的UI進程,后臺進程不能依賴運行在UI 進程的content provider或者service。
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!