android內存優化

寫在最前:html

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

因此我將本文定義爲一個工具類的文章,若是你在ANDROID開發中遇到關於內存問題,或者立刻要參加面試,或者就是單純的學習或複習一下內存相關知識,都歡迎閱讀。(本文最後我會盡可能列出所參考的文章)。android


OOM:git


內存泄露能夠引起不少的問題:github

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:


關於內存簡介,和Reckon(內存計算)的內容請看上一篇文章:ANDROID內存優化(大彙總——上)



Reduce :


Reduce的意思就是減小,直接減小內存的使用是最有效的優化方式。

下面來看看有哪些方法能夠減小內存使用:


Bitmap

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


圖片顯示:

咱們須要根據需求去加載圖片的大小。

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

只有當用戶點擊具體條目想看詳細信息的時候,這時另啓動一個fragment/activity/對話框等等,去顯示整個圖片


圖片大小:

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

[java] view plaincopyprint?

  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 plaincopyprint?

  1. publicstaticBitmapreadBitMap(Contextcontext, intresId) {  

  2.     BitmapFactory.Optionsopt = newBitmapFactory.Options();  

  3.     opt.inPreferredConfig = Bitmap.Config.RGB_565;  

  4.     opt.inPurgeable = true;  

  5.     opt.inInputShareable = true;  

  6.     //獲取資源圖片   

  7.     InputStreamis = context.getResources().openRawResource(resId);  

  8.     returnBitmapFactory.decodeStream(is, null, opt);  

  9. }  


圖片回收:

使用Bitmap事後,就須要及時的調用Bitmap.recycle()方法來釋放Bitmap佔用的內存空間,而不要等Android系統來進行釋放。

下面是釋放Bitmap的示例代碼片斷。

[java] view plaincopyprint?

  1. // 先判斷是否已經回收  

  2. if(bitmap != null && !bitmap.isRecycled()){  

  3.     // 回收而且置爲null  

  4.     bitmap.recycle();  

  5.     bitmap = null;  

  6. }  

  7. System.gc();  


捕獲異常:

通過上面這些優化後還會存在報OOM的風險,因此下面須要一道最後的關卡——捕獲OOM異常:

[java] view plaincopyprint?

  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錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。

軟引用(SoftReference)
只有內存不夠時纔回收,經常使用於緩存;當內存達到一個閥值,GC就會去回收它;

弱引用(WeakReference)   

弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。 


虛引用(PhantomReference)   

"虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。  


軟引用和弱引用的應用實例:

注意:對於SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap緩存方案,如今已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更着重於對軟/弱引用的回收,因此下面的內容能夠選擇忽略。

在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大並且聲明週期較長的對象時候,能夠儘可能應用軟引用和弱引用技術。

下面以使用軟引用爲例來詳細說明(弱引用的使用方式與軟引用是相似的):

假設咱們的應用會用到大量的默認圖片,並且這些圖片不少地方會用到。若是每次都去讀取圖片,因爲讀取文件須要硬件操做,速度較慢,會致使性能較低。因此咱們考慮將圖片緩存起來,須要的時候直接從內存中讀取。可是,因爲圖片佔用內存空間比較大,緩存不少圖片須要不少的內存,就可能比較容易發生OutOfMemory異常。這時,咱們能夠考慮使用軟引用技術來避免這個問題發生。

首先定義一個HashMap,保存軟引用對象。

[java] view plaincopyprint?

  1. private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();  

再來定義一個方法,保存Bitmap的軟引用到HashMap。

[java] view plaincopyprint?

  1. public void addBitmapToCache(String path) {  

  2.        // 強引用的Bitmap對象  

  3.        Bitmap bitmap = BitmapFactory.decodeFile(path);  

  4.        // 軟引用的Bitmap對象  

  5.        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);  

  6.        // 添加該對象到Map中使其緩存  

  7.        imageCache.put(path, softBitmap);  

  8.    }  

獲取的時候,能夠經過SoftReference的get()方法獲得Bitmap對象。

[java] view plaincopyprint?

  1. public Bitmap getBitmapByPath(String path) {  

  2.         // 從緩存中取軟引用的Bitmap對象  

  3.         SoftReference<Bitmap> softBitmap = imageCache.get(path);  

  4.         // 判斷是否存在軟引用  

  5.         if (softBitmap == null) {  

  6.             return null;  

  7.         }  

  8.         // 取出Bitmap對象,若是因爲內存不足Bitmap被回收,將取得空  

  9.         Bitmap bitmap = softBitmap.get();  

  10.         return bitmap;  

  11.     }  

使用軟引用之後,在OutOfMemory異常發生以前,這些緩存的圖片資源的內存空間能夠被釋放掉的,從而避免內存達到上限,避免Crash發生。

須要注意的是,在垃圾回收器對這個Java對象回收前,SoftReference類所提供的get方法會返回Java對象的強引用,一旦垃圾線程回收該Java對象以後,get方法將返回null。因此在獲取軟引用對象的代碼中,必定要判斷是否爲null,以避免出現NullPointerException異常致使應用崩潰。


到底何時使用軟引用,何時使用弱引用呢?

我的認爲,若是隻是想避免OutOfMemory異常的發生,則可使用軟引用。若是對於應用的性能更在乎,想盡快回收一些佔用內存比較大的對象,則可使用弱引用。

還有就是能夠根據對象是否常用來判斷。若是該對象可能會常用的,就儘可能用軟引用。若是該對象不被使用的可能性更大些,就能夠用弱引用。

另外,和弱引用功能相似的是WeakHashMap。WeakHashMap對於一個給定的鍵,其映射的存在並不阻止垃圾回收器對該鍵的回收,回收之後,其條目從映射中有效地移除。WeakHashMap使用ReferenceQueue實現的這種機制。


其餘小tips:

對常量使用static final修飾符

讓咱們來看看這兩段在類前面的聲明:

static int intVal = 42;
static String strVal = "Hello, world!";
編譯器會生成一個叫作clinit的初始化類的方法,當類第一次被使用的時候這個方法會被執行。方法會將42賦給intVal,而後把一個指向類中常量表 的引用賦給strVal。當之後要用到這些值的時候,會在成員變量表中查找到他們。 下面咱們作些改進,使用「final」關鍵字:

static final int intVal = 42;
static final String strVal = "Hello, world!";

如今,類再也不須要clinit方法,由於在成員變量初始化的時候,會將常量直接保存到類文件中。用到intVal的代碼被直接替換成42,而使用strVal的會指向一個字符串常量,而不是使用成員變量。

將一個方法或類聲明爲final不會帶來性能的提高,可是會幫助編譯器優化代碼。舉例說,若是編譯器知道一個getter方法不會被重載,那麼編譯器會對其採用內聯調用。

你也能夠將本地變量聲明爲final,一樣,這也不會帶來性能的提高。使用「final」只能使本地變量看起來更清晰些(可是也有些時候這是必須的,好比在使用匿名內部類的時候)。

靜態方法代替虛擬方法

若是不須要訪問某對象的字段,將方法設置爲靜態,調用會加速15%到20%。這也是一種好的作法,由於你能夠從方法聲明中看出調用該方法不須要更新此對象的狀態。


減小沒必要要的全局變量

儘可能避免static成員變量引用資源耗費過多的實例,好比Context

由於Context的引用超過它自己的生命週期,會致使Context泄漏。因此儘可能使用Application這種Context類型。 你能夠經過調用Context.getApplicationContext()或 Activity.getApplication()輕鬆獲得Application對象。 

避免建立沒必要要的對象

最多見的例子就是當你要頻繁操做一個字符串時,使用StringBuffer代替String。

對於全部全部基本類型的組合:int數組比Integer數組好,這也歸納了一個基本事實,兩個平行的int數組比 (int,int)對象數組性能要好不少。

整體來講,就是避免建立短命的臨時對象。減小對象的建立就能減小垃圾收集,進而減小對用戶體驗的影響。


避免內部Getters/Setters

在Android中,虛方法調用的代價比直接字段訪問高昂許多。一般根據面嚮對象語言的實踐,在公共接口中使用Getters和Setters是有道理的,但在一個字段常常被訪問的類中宜採用直接訪問。

避免使用浮點數

一般的經驗是,在Android設備中,浮點數會比整型慢兩倍。


使用實體類比接口好

假設你有一個HashMap對象,你能夠將它聲明爲HashMap或者Map:

Map map1 = new HashMap();
HashMap map2 = new HashMap();

哪一個更好呢?

按照傳統的觀點Map會更好些,由於這樣你能夠改變他的具體實現類,只要這個類繼承自Map接口。傳統的觀點對於傳統的程序是正確的,可是它並不適合嵌入式系統。調用一個接口的引用會比調用實體類的引用多花費一倍的時間。若是HashMap徹底適合你的程序,那麼使用Map就沒有什麼價值。若是有些地方你不能肯定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(固然公共API是一個例外:一個好的API經常會犧牲一些性能)


避免使用枚舉

枚舉變量很是方便,但不幸的是它會犧牲執行的速度和並大幅增長文件體積。

使用枚舉變量可讓你的API更出色,並能提供編譯時的檢查。因此在一般的時候你毫無疑問應該爲公共API選擇枚舉變量。可是當性能方面有所限制的時候,你就應該避免這種作法了。


for循環

訪問成員變量比訪問本地變量慢得多,以下面一段代碼:

[java] view plaincopyprint?

  1. for(int i =0; i < this.mCount; i++)  {}  

永遠不要在for的第二個條件中調用任何方法,以下面一段代碼:

[java] view plaincopyprint?

  1. for(int i =0; i < this.getCount(); i++) {}  

對上面兩個例子最好改成:

[java] view plaincopyprint?

  1. int count = this.mCount; / int count = this.getCount();  

  2. for(int i =0; i < count; i++)  {}  

在java1.5中引入的for-each語法。編譯器會將對數組的引用和數組的長度保存到本地變量中,這對訪問數組元素很是好。 可是編譯器還會在每次循環中產生一個額外的對本地變量的存儲操做(以下面例子中的變量a),這樣會比普通循環多出4個字節,速度要稍微慢一些:

[java] view plaincopyprint?

  1. for (Foo a : mArray) {  

  2.     sum += a.mSplat;  

  3. }  


瞭解並使用類庫

選擇Library中的代碼而非本身重寫,除了一般的那些緣由外,考慮到系統空閒時會用匯編代碼調用來替代library方法,這可能比JIT中生成的等價的最好的Java代碼還要好。

當你在處理字串的時候,不要吝惜使用String.indexOf()String.lastIndexOf()等特殊實現的方法。這些方法都是使用C/C++實現的,比起Java循環快10到100倍。

System.arraycopy方法在有JIT的Nexus One上,自行編碼的循環快9倍。

android.text.format包下的Formatter類,提供了IP地址轉換、文件大小轉換等方法;DateFormat類,提供了各類時間轉換,都是很是高效的方法。

TextUtils類,對於字符串處理Android爲咱們提供了一個簡單實用的TextUtils類,若是處理比較簡單的內容不用去思考正則表達式不妨試試這個在android.text.TextUtils的類

高性能MemoryFile類,不少人抱怨Android處理底層I/O性能不是很理想,若是不想使用NDK則能夠經過MemoryFile類實現高性能的文件讀寫操做。MemoryFile適用於哪些地方呢?對於I/O須要頻繁操做的,主要是和外部存儲相關的I/O操做,MemoryFile經過將 NAND或SD卡上的文件,分段映射到內存中進行修改處理,這樣就用高速的RAM代替了ROM或SD卡,性能天然提升很多,對於Android手機而言同時還減小了電量消耗。該類實現的功能不是不少,直接從Object上繼承,經過JNI的方式直接在C底層執行。



Reuse:


Reuse重用,減小內存消耗的重要手段之一。

核心思路就是將已經存在的內存資源從新使用而避免去建立新的,最典型的使用就是緩存(Cache池(Pool)


Bitmap緩存:


Bitmap緩存分爲兩種:

一種是內存緩存,一種是硬盤緩存。


內存緩存(LruCache):

以犧牲寶貴的應用內存爲代價,內存緩存提供了快速的Bitmap訪問方式。系統提供的LruCache類是很是適合用做緩存Bitmap任務的,它將最近被引用到的對象存儲在一個強引用的LinkedHashMap中,而且在緩存超過了指定大小以後將最近不常使用的對象釋放掉。

注意之前有一個很是流行的內存緩存實現是SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap緩存方案,然而如今已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更着重於對軟/弱引用的回收,這使得上述的方案至關無效。


硬盤緩存(DiskLruCache):

一個內存緩存對加速訪問最近瀏覽過的Bitmap很是有幫助,可是你不能侷限於內存中的可用圖片。GridView這樣有着更大的數據集的組件能夠很輕易消耗掉內存緩存。你的應用有可能在執行其餘任務(如打電話)的時候被打斷,而且在後臺的任務有可能被殺死或者緩存被釋放。一旦用戶從新聚焦(resume)到你的應用,你得再次處理每一張圖片。

在這種狀況下,硬盤緩存能夠用來存儲Bitmap並在圖片被內存緩存釋放後減少圖片加載的時間(次數)。固然,從硬盤加載圖片比內存要慢,而且應該在後臺線程進行,由於硬盤讀取的時間是不可預知的。

注意:若是訪問圖片的次數很是頻繁,那麼ContentProvider可能更適合用來存儲緩存圖片,例如Image Gallery這樣的應用程序。

更多關於內存緩存和硬盤緩存的內容請看Google官方教程https://developer.android.com/develop/index.html


圖片緩存的開源項目:

對於圖片的緩存如今都傾向於使用開源項目,這裏我列出幾個我搜到的:


1. Android-Universal-Image-Loader 圖片緩存

目前使用最普遍的圖片緩存,支持主流圖片緩存的絕大多數特性。
項目地址:https://github.com/nostra13/Android-Universal-Image-Loader

 

2. picasso square開源的圖片緩存
項目地址:https://github.com/square/picasso
特色:(1)能夠自動檢測adapter的重用並取消以前的下載
(2)圖片變換
(3)能夠加載本地資源
(4)能夠設置佔位資源
(5)支持debug模式

 

3. ImageCache 圖片緩存,包含內存和Sdcard緩存
項目地址:https://github.com/Trinea/AndroidCommon
特色:

(1)支持預取新圖片,支持等待隊列
(2)包含二級緩存,可自定義文件名保存規則
(3)可選擇多種緩存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13種)或自定義緩存算法
(4)可方便的保存及初始化恢復數據
(5)支持不一樣類型網絡處理
(6)可根據系統配置初始化緩存等


4. Android 網絡通訊框架Volley

項目地址:https://android.googlesource.com/platform/frameworks/volley

咱們在程序中須要和網絡通訊的時候,大致使用的東西莫過於AsyncTaskLoader,HttpURLConnection,AsyncTask,HTTPClient(Apache)等,在2013年的Google I/O發佈了Volley。Volley是Android平臺上的網絡通訊庫,能使網絡通訊更快,更簡單,更健壯。

特色:

(1)JSON,圖像等的異步下載;

(2)網絡請求的排序(scheduling)

(3)網絡請求的優先級處理

(4)緩存

(5)多級別取消請求

(6)和Activity和生命週期的聯動(Activity結束時同時取消全部網絡請求)


Adapter適配器

在Android中Adapter使用十分普遍,特別是在list中。因此adapter是數據的 「集散地」 ,因此對其進行內存優化是頗有必要的。

下面算是一個標準的使用模版:

主要使用convertView和ViewHolder來進行緩存處理

[java] view plaincopyprint?

  1. @Override  

  2. public View getView(int position, View convertView, ViewGroup parent) {  

  3.     ViewHolder vHolder = null;  

  4.     //若是convertView對象爲空則建立新對象,不爲空則複用    

  5.     if (convertView == null) {  

  6.         convertView = inflater.inflate(..., null);  

  7.         // 建立 ViewHodler 對象    

  8.         vHolder = new ViewHolder();  

  9.         vHolder.img= (ImageView) convertView.findViewById(...);  

  10.         vHolder.tv= (TextView) convertView.findViewById(...);  

  11.         // 將ViewHodler保存到Tag中(Tag能夠接收Object類型對象,因此任何東西均可以保存在其中)  

  12.         convertView.setTag(vHolder);  

  13.     } else {  

  14.         //當convertView不爲空時,經過getTag()獲得View    

  15.         vHolder = (ViewHolder) convertView.getTag();  

  16.     }  

  17.     // 給對象賦值,修改顯示的值    

  18.     vHolder.img.setImageBitmap(...);  

  19.     vHolder.tv.setText(...);  

  20.     return convertView;  

  21. }  

  22. //將顯示的View 包裝成類    

  23. static class ViewHolder {  

  24.     TextView tv;  

  25.     ImageView img;  

  26. }  


池(PooL)

對象池:

對象池使用的基本思路是:將用過的對象保存起來,等下一次須要這種對象的時候,再拿出來重複使用,從而在必定程度上減小頻繁建立對象所形成的開銷。 並不是全部對象都適合拿來池化――由於維護對象池也要形成必定開銷。對生成時開銷不大的對象進行池化,反而可能會出現「維護對象池的開銷」大於「生成新對象的開銷」,從而使性能下降的狀況。可是對於生成時開銷可觀的對象,池化技術就是提升性能的有效策略了。


線程池:

線程池的基本思想仍是一種對象池的思想,開闢一塊內存空間,裏面存放了衆多(未死亡)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成後線程對象歸池,這樣能夠避免反覆建立線程對象所帶來的性能開銷,節省了系統的資源。

好比:一個應用要和網絡打交道,有不少步驟須要訪問網絡,爲了避免阻塞主線程,每一個步驟都建立個線程,在線程中和網絡交互,用線程池就變的簡單,線程池是對線程的一種封裝,讓線程用起來更加簡便,只須要創一個線程池,把這些步驟像任務同樣放進線程池,在程序銷燬時只要調用線程池的銷燬函數便可。


java提供了ExecutorServiceExecutors類,咱們能夠應用它去創建線程池。

一般能夠創建以下4種:

[java] view plaincopyprint?

  1. /** 每次只執行一個任務的線程池 */  

  2. ExecutorService singleTaskExecutor =  Executors.newSingleThreadExecutor();  

  3.   

  4. /** 每次執行限定個數個任務的線程池 */  

  5. ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);  

  6.   

  7. /** 全部任務都一次性開始的線程池 */  

  8. ExecutorService allTaskExecutor = Executors.newCachedThreadPool();  

  9.   

  10. /** 建立一個可在指定時間裏執行任務的線程池,亦可重複執行 */  

  11. ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);  


更多關於線程池的內容我推薦這篇文章:http://www.xuanyusong.com/archives/2439

注意:

要根據狀況適度使用緩存,由於內存有限。

能保存路徑地址的就不要存放圖片數據,不常用的儘可能不要緩存,不用時就清空。

相關文章
相關標籤/搜索