1. 程式人生 > >Android記憶體優化彙總

Android記憶體優化彙總

寫在最前:

本文的思路主要借鑑了2014年AnDevCon開發者大會的一個演講PPT,加上把網上搜集的各種記憶體零散知識點進行彙總、挑選、簡化後整理而成。

所以我將本文定義為一個工具類的文章,如果你在ANDROID開發中遇到關於記憶體問題,或者馬上要參加面試,或者就是單純的學習或複習一下記憶體相關知識,都歡迎閱讀。(本文最後我會盡量列出所參考的文章)。


記憶體簡介:
RAM(random access memory)隨機存取儲存器。說白了就是記憶體。

一般Java在記憶體分配時會涉及到以下區域:

暫存器(Registers):

速度最快的儲存場所,因為暫存器位於處理器內部我們在程式中無法控制

棧(Stack):存放基本型別的資料和物件的引用,但物件本身不存放在棧中,而是存放在堆中

堆(Heap):堆記憶體用來存放由new建立的物件和陣列。在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收器(GC)來管理。

靜態域(static field):  靜態儲存區域就是指在固定的位置存放應用程式執行時一直存在的資料,Java在記憶體中專門劃分了一個靜態儲存區域來管理一些特殊的資料變數如靜態的資料變數

常量池(constant pool):虛擬機器必須為每個被裝載的型別維護一個常量池。常量池就是該型別所用到常量的一個有序集和,包括直接常量(string,integer和floating point常量)和對其他型別,欄位和方法的符號引用。

非RAM儲存:硬碟等永久儲存空間

堆疊特點對比:

由於篇幅原因,下面只簡單的介紹一下堆疊的一些特性。

:當定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當該變數退出該作用域後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。

:當堆中的new產生陣列和物件超出其作用域後,它們不會被釋放,只有在沒有引用變數指向它們的時候才變成垃圾,不能再被使用。即使這樣,所佔記憶體也不會立即釋放,而是等待被垃圾回收器收走。這也是Java比較佔記憶體的原因。


存取速度比堆要快,僅次於暫存器。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。

:堆是一個執行時資料區,可以動態地分配記憶體大小,因此存取速度較慢。也正因為這個特點,堆的生存期不必事先告訴編譯器,而且Java的垃圾收集器會自動收走這些不再使用的資料。


:棧中的資料可以共享, 它是由編譯器完成的,有利於節省空間。

例如:需要定義兩個變數int a = 3;int b = 3;

編譯器先處理int a = 3;首先它會在棧中建立一個變數為a的引用,然後查詢棧中是否有3這個值,如果沒找到,就將3存放進來,然後將a指向3。接著處理int b = 3;在建立完b的引用變數後,因為在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。這時,如果再a=4;那麼編譯器會重新搜尋棧中是否有4值,如果沒有,則將4存放進來,並讓a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

:例如上面棧中a的修改並不會影響到b, 而在堆中一個物件引用變數修改了這個物件的內部狀態,會影響到另一個物件引用變數。

記憶體耗用名詞解析:

VSS - Virtual Set Size 虛擬耗用記憶體(包含共享庫佔用的記憶體)

RSS - Resident Set Size 實際使用實體記憶體(包含共享庫佔用的記憶體)

PSS - Proportional Set Size 實際使用的實體記憶體(比例分配共享庫佔用的記憶體)

USS - Unique Set Size 程序獨自佔用的實體記憶體(不包含共享庫佔用的記憶體)

一般來說記憶體佔用大小有如下規律:VSS >= RSS >= PSS >= USS


OOM:


記憶體洩露可以引發很多的問題:

1.程式卡頓,響應速度慢(記憶體佔用高時JVM虛擬機器會頻繁觸發GC)

2.莫名消失(當你的程式所佔記憶體越大,它在後臺的時候就越可能被幹掉。反之記憶體佔用越小,在後臺存在的時間就越長)

3.直接崩潰(OutOfMemoryError)

ANDROID記憶體面臨的問題:

1.有限的堆記憶體,原始只有16M

2.記憶體大小消耗等根據裝置,作業系統等級,螢幕尺寸的不同而不同

3.程式不能直接控制

4.支援後臺多工處理(multitasking)

5.執行在虛擬機器之上


5R:

本文主要通過如下的5R方法來對ANDROID記憶體進行優化:

1.Reckon(計算)

首先需要知道你的app所消耗記憶體的情況,知己知彼才能百戰不殆

2.Reduce(減少)

消耗更少的資源

3.Reuse(重用)

當第一次使用完以後,儘量給其他的使用

5.Recycle(回收)

返回資源

4.Review(檢查)

回顧檢查你的程式,看看設計或程式碼有什麼不合理的地方。


Reckon (計算):

瞭解自己應用的記憶體使用情況是很有必要的。如果當記憶體使用過高的話就需要對其進行優化,因為更少的使用記憶體可以減少ANDROID系統終止我們的程序的機率,也可以提高多工執行效率和體驗效果。

下面從系統記憶體(system ram)和堆記憶體(heap)兩個方面介紹一些檢視和計算記憶體使用情況的方法:

System Ram(系統記憶體):

觀察和計算系統記憶體使用情況,可以使用Android提供給我們的兩個工具procstatsmeminfo。他們一個側重於後臺的記憶體使用,另一個是執行時的記憶體使用。

Process Stats: 

Android 4.4 KitKat 提出了一個新系統服務,叫做procstats。它將幫助你更好的理解你app在後臺(background)時的記憶體使用情況。 Procstats可以去監視你app在一段時間的行為,包括在後臺運行了多久,並在此段時間使用了多少記憶體。從而幫助你快速的找到應用中不效率和不規範的地方去避免影響其performs,尤其是在低記憶體的裝置上執行時。 你可以通過adb shell命令去使用procstats(adb shell dumpsys procstats --hours 3),或者更方便的方式是執行Process Stats開發者工具(在4.4版本的手機中點選Settings > Developer options > Process Stats)
點選單個條目還可以檢視詳細資訊
meminfo:
Android還提供了一個工具叫做meminfo。 它是根據PSS標準 (Proportional Set Size——實際實體記憶體)計算每個程序的記憶體使用並且按照重要程度排序。 你可以通過命令列去執行它:( adb shell dumpsys meminfo)或者使用在裝置上點選Settings > Apps > Running(與Procstats不用,它也可以在老版本上執行)
更多關於Procstatsmeninfo的介紹可以參考我翻譯的一篇文章:Process Stats:瞭解你的APP如何使用記憶體

Heap(堆記憶體):

在程式中可以使用如下的方法去查詢記憶體使用情況

ActivityManager#getMemoryClass()

查詢可用堆記憶體的限制

3.0(HoneyComb)以上的版本可以通過largeHeap=“true”來申請更多的堆記憶體(不過這算作“作弊”)

ActivityManager#getMemoryInfo(ActivityManager.MemoryInfo) 得到的MemoryInfo中可以檢視如下Field的屬性:
availMem:表示系統剩餘記憶體 lowMemory:它是boolean值,表示系統是否處於低記憶體執行 hreshold:它表示當系統剩餘記憶體低於好多時就看成低記憶體執行

android.os.Debug#getMemoryInfo(Debug.MemoryInfo memoryInfo)

得到的MemoryInfo中可以檢視如下Field的屬性:

dalvikPrivateDirty :  The private dirty pages used by dalvik。 dalvikPss   The proportional set size for dalvik. dalvikSharedDirty  The shared dirty pages used by dalvik. nativePrivateDirty  The private dirty pages used by the  native heap . nativePss  The proportional set size for the native heap. nativeSharedDirty   The shared dirty pages used by the  native heap. otherPrivateDirty  The private dirty pages used by everything else. otherPss   The proportional set size for everything else. otherSharedDirty   The shared dirty pages used by everything else.

dalvik是指 dalvik所使用的記憶體 native是被native堆使用的記憶體。應該指使用C\C++在堆上分配的 記憶體 other:是指除 dalvik和 native使用的記憶體。但是具體是指什麼呢?至少包括在C\C++分配的非堆記憶體,比如分配在棧上的記憶體。 private:是指私有的。非共享的。 share:是指共享的記憶體 PSS 實際使用的實體記憶體(比例分配共享庫佔用的記憶體)  PrivateDirty它是指非共享的,又不能換頁出去( can not be paged to disk )的記憶體的大小。比如Linux為了提高分配記憶體速度而緩衝的小物件,即使你的程序結束,該記憶體也不會釋放掉,它只是又重新回到緩衝中而已。 SharedDirty:參照 PrivateDirty我認為 它應該是指共享的,又不能換頁出去( can not be paged to disk )的記憶體的大小。比如Linux為了提高分配記憶體速度而緩衝的小物件,即使所有共享它的程序結束,該記憶體也不會釋放掉,它只是又重新回到緩衝中而已。

android.os.Debug#getNativeHeapSize()

返回的是當前程序native堆本身總的記憶體大小

android.os.Debug#getNativeHeapAllocatedSize()

返回的是當前程序native堆中已使用的記憶體大小

android.os.Debug#getNativeHeapFreeSize()

返回的是當前程序native堆中已經剩餘的記憶體大小


Memory Analysis Tool(MAT):

通常記憶體洩露分析被認為是一件很有難度的工作,一般由團隊中的資深人士進行。不過,今天我們要介紹的 MAT(Eclipse Memory Analyzer)被認為是一個“傻瓜式“的堆轉儲檔案分析工具,你只需要輕輕點選一下滑鼠就可以生成一個專業的分析報告。

如下圖:


關於詳細的MAT使用我推薦下面這篇文章:使用 Eclipse Memory Analyzer 進行堆轉儲檔案分析


Reduce :


Reduce的意思就是減少,直接減少記憶體的使用是最有效的優化方式。

下面來看看有哪些方法可以減少記憶體使用:


Bitmap Bitmap是記憶體消耗大戶,絕大多數的OOM崩潰都是在操作Bitmap時產生的,下面來看看幾個處理圖片的方法:


圖片顯示:

我們需要根據需求去載入圖片的大小。

例如在列表中僅用於預覽時載入縮圖(thumbnails )。

只有當用戶點選具體條目想看詳細資訊的時候,這時另啟動一個fragment/activity/對話方塊等等,去顯示整個圖片


圖片大小:

直接使用ImageView顯示bitmap會佔用較多資源,特別是圖片較大的時候,可能導致崩潰。 
使用BitmapFactory.Options設定inSampleSize, 這樣做可以減少對系統資源的要求。 
屬性值inSampleSize表示縮圖大小為原始圖片大小的幾分之一,即如果這個值為2,則取出的縮圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4。 

[java]  view plain  copy  print ?
  1. BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();  
  2. bitmapFactoryOptions.inJustDecodeBounds = true;  
  3. bitmapFactoryOptions.inSampleSize = 2;  
  4. // 這裡一定要將其設定回false,因為之前我們將其設定成了true    
  5. // 設定inJustDecodeBounds為true後,decodeFile並不分配空間,即,BitmapFactory解碼出來的Bitmap為Null,但可計算出原始圖片的長度和寬度    
  6. options.inJustDecodeBounds = false;  
  7. Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);  


圖片畫素:

Android中圖片有四種屬性,分別是:
ALPHA_8:每個畫素佔用1byte記憶體 
ARGB_4444:每個畫素佔用2byte記憶體 
ARGB_8888:每個畫素佔用4byte記憶體 (預設)
RGB_565:每個畫素佔用2byte記憶體 
Android預設的顏色模式為ARGB_8888,這個顏色模式色彩最細膩,顯示質量最高。但同樣的,佔用的記憶體也最大。 所以在對圖片效果不是特別高的情況下使用RGB_565(565沒有透明度屬性),如下: [java]  view plain  copy  print ?
  1. public static Bitmap readBitMap(Context context, int resId) {  
  2.     BitmapFactory.Options opt = new BitmapFactory.Options();  
  3.     opt.inPreferredConfig = Bitmap.Config.RGB_565;  
  4.     opt.inPurgeable = true;  
  5.     opt.inInputShareable = true;  
  6.     //獲取資源圖片   
  7.     InputStream is = context.getResources().openRawResource(resId);  
  8.     return BitmapFactory.decodeStream(is, null, opt);  
  9. }  

圖片回收:

使用Bitmap過後,就需要及時的呼叫Bitmap.recycle()方法來釋放Bitmap佔用的記憶體空間,而不要等Android系統來進行釋放。

下面是釋放Bitmap的示例程式碼片段。

[java]  view plain  copy  print ?
  1. // 先判斷是否已經回收  
  2. if(bitmap != null && !bitmap.isRecycled()){  
  3.     // 回收並且置為null  
  4.     bitmap.recycle();  
  5.     bitmap = null;  
  6. }  
  7. System.gc();  

捕獲異常:

經過上面這些優化後還會存在報OOM的風險,所以下面需要一道最後的關卡——捕獲OOM異常:

[java]  view plain  copy  print ?
  1. Bitmap bitmap = null;  
  2. try {  
  3.     // 例項化Bitmap  
  4.     bitmap = BitmapFactory.decodeFile(path);  
  5. catch (OutOfMemoryError e) {  
  6.     // 捕獲OutOfMemoryError,避免直接崩潰  
  7. }  
  8. if (bitmap == null) {  
  9.     // 如果例項化失敗 返回預設的Bitmap物件  
  10.     return defaultBitmapMap;  
  11. }  

修改物件引用型別:


引用型別:

引用分為四種級別,這四種級別由高到低依次為:強引用>軟引用>弱引用>虛引用。

強引用(strong reference)
如:Object object=new Object(),object就是一個強引用了。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式