ANDROID記憶體優化(大彙總——中)
轉載請註明本文出自大苞米的部落格(http://blog.csdn.net/a396901990),謝謝支援!
寫在最前:
本文的思路主要借鑑了2014年AnDevCon開發者大會的一個演講PPT,加上把網上搜集的各種記憶體零散知識點進行彙總、挑選、簡化後整理而成。
所以我將本文定義為一個工具類的文章,如果你在ANDROID開發中遇到關於記憶體問題,或者馬上要參加面試,或者就是單純的學習或複習一下記憶體相關知識,都歡迎閱讀。(本文最後我會盡量列出所參考的文章)。
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:
關於記憶體簡介,和Reckon(記憶體計算)的內容請看上一篇文章:ANDROID記憶體優化(大彙總——上)
Reduce :
Reduce的意思就是減少,直接減少記憶體的使用是最有效的優化方式。
下面來看看有哪些方法可以減少記憶體使用:
圖片顯示:
我們需要根據需求去載入圖片的大小。
例如在列表中僅用於預覽時載入縮圖(thumbnails )。
只有當使用者點選具體條目想看詳細資訊的時候,這時另啟動一個fragment/activity/對話方塊等等,去顯示整個圖片
圖片大小:
直接使用ImageView顯示bitmap會佔用較多資源,特別是圖片較大的時候,可能導致崩潰。
使用BitmapFactory.Options設定inSampleSize, 這樣做可以減少對系統資源的要求。
屬性值inSampleSize表示縮圖大小為原始圖片大小的幾分之一,即如果這個值為2,則取出的縮圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4。
- BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();
- bitmapFactoryOptions.inJustDecodeBounds = true;
- bitmapFactoryOptions.inSampleSize = 2;
- // 這裡一定要將其設定回false,因為之前我們將其設定成了true
- // 設定inJustDecodeBounds為true後,decodeFile並不分配空間,即,BitmapFactory解碼出來的Bitmap為Null,但可計算出原始圖片的長度和寬度
- options.inJustDecodeBounds = false;
- Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);
圖片畫素:
ALPHA_8:每個畫素佔用1byte記憶體
ARGB_4444:每個畫素佔用2byte記憶體
ARGB_8888:每個畫素佔用4byte記憶體 (預設)
RGB_565:每個畫素佔用2byte記憶體
- publicstaticBitmapreadBitMap(Contextcontext, intresId) {
- BitmapFactory.Optionsopt = newBitmapFactory.Options();
- opt.inPreferredConfig = Bitmap.Config.RGB_565;
- opt.inPurgeable = true;
- opt.inInputShareable = true;
- //獲取資源圖片
- InputStreamis = context.getResources().openRawResource(resId);
- returnBitmapFactory.decodeStream(is, null, opt);
- }
圖片回收:
使用Bitmap過後,就需要及時的呼叫Bitmap.recycle()方法來釋放Bitmap佔用的記憶體空間,而不要等Android系統來進行釋放。
下面是釋放Bitmap的示例程式碼片段。
- // 先判斷是否已經回收
- if(bitmap != null && !bitmap.isRecycled()){
- // 回收並且置為null
- bitmap.recycle();
- bitmap = null;
- }
- System.gc();
捕獲異常:
經過上面這些優化後還會存在報OOM的風險,所以下面需要一道最後的關卡——捕獲OOM異常:
- Bitmap bitmap = null;
- try {
- // 例項化Bitmap
- bitmap = BitmapFactory.decodeFile(path);
- } catch (OutOfMemoryError e) {
- // 捕獲OutOfMemoryError,避免直接崩潰
- }
- if (bitmap == null) {
- // 如果例項化失敗 返回預設的Bitmap物件
- return defaultBitmapMap;
- }
修改物件引用型別:
引用型別:
引用分為四種級別,這四種級別由高到低依次為:強引用>軟引用>弱引用>虛引用。
強引用(strong reference)
如:Object object=new Object(),object就是一個強引用了。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。
軟引用(SoftReference)
只有記憶體不夠時才回收,常用於快取;當記憶體達到一個閥值,GC就會去回收它;
弱引用(WeakReference)
弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它 所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。
虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
軟引用和弱引用的應用例項:
注意:對於SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap快取方案,現在已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更著重於對軟/弱引用的回收,所以下面的內容可以選擇忽略。
在Android應用的開發中,為了防止記憶體溢位,在處理一些佔用記憶體大而且宣告週期較長的物件時候,可以儘量應用軟引用和弱引用技術。
下面以使用軟引用為例來詳細說明(弱引用的使用方式與軟引用是類似的):
假設我們的應用會用到大量的預設圖片,而且這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取檔案需要硬體操作,速度較慢,會導致效能較低。所以我們考慮將圖片快取起來,需要的時候直接從記憶體中讀取。但是,由於圖片佔用記憶體空間比較大,快取很多圖片需要很多的記憶體,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟引用技術來避免這個問題發生。
首先定義一個HashMap,儲存軟引用物件。
- private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
- public void addBitmapToCache(String path) {
- // 強引用的Bitmap物件
- Bitmap bitmap = BitmapFactory.decodeFile(path);
- // 軟引用的Bitmap物件
- SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
- // 新增該物件到Map中使其快取
- imageCache.put(path, softBitmap);
- }
- public Bitmap getBitmapByPath(String path) {
- // 從快取中取軟引用的Bitmap物件
- SoftReference<Bitmap> softBitmap = imageCache.get(path);
- // 判斷是否存在軟引用
- if (softBitmap == null) {
- return null;
- }
- // 取出Bitmap物件,如果由於記憶體不足Bitmap被回收,將取得空
- Bitmap bitmap = softBitmap.get();
- return bitmap;
- }
需要注意的是,在垃圾回收器對這個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
避免建立不必要的物件
最常見的例子就是當你要頻繁操作一個字串時,使用StringBuffer代替String。
對於所有所有基本型別的組合:int陣列比Integer陣列好,這也概括了一個基本事實,兩個平行的int陣列比 (int,int)物件陣列效能要好很多。
總體來說,就是避免建立短命的臨時物件。減少物件的建立就能減少垃圾收集,進而減少對使用者體驗的影響。
避免使用浮點數
通常的經驗是,在Android裝置中,浮點數會比整型慢兩倍。
使用實體類比介面好
假設你有一個HashMap物件,你可以將它宣告為HashMap或者Map:
Map map1 = new HashMap(); HashMap map2 = new HashMap();
哪個更好呢?
按照傳統的觀點Map會更好些,因為這樣你可以改變他的具體實現類,只要這個類繼承自Map介面。傳統的觀點對於傳統的程式是正確的,但是它並不適合嵌入式系統。呼叫一個介面的引用會比呼叫實體類的引用多花費一倍的時間。如果HashMap完全適合你的程式,那麼使用Map就沒有什麼價值。如果有些地方你不能確定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(當然公共API是一個例外:一個好的API常常會犧牲一些效能)
避免使用列舉
列舉變數非常方便,但不幸的是它會犧牲執行的速度和並大幅增加檔案體積。
使用列舉變數可以讓你的API更出色,並能提供編譯時的檢查。所以在通常的時候你毫無疑問應該為公共API選擇列舉變數。但是當效能方面有所限制的時候,你就應該避免這種做法了。
for迴圈
訪問成員變數比訪問本地變數慢得多,如下面一段程式碼:
- for(int i =0; i < this.mCount; i++) {}
永遠不要在for的第二個條件中呼叫任何方法,如下面一段程式碼:
- for(int i =0; i < this.getCount(); i++) {}
對上面兩個例子最好改為:
- int count = this.mCount; / int count = this.getCount();
- for(int i =0; i < count; i++) {}
- for (Foo a : mArray) {
- sum += a.mSplat;
- }
瞭解並使用類庫
選擇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:
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.html1. 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)可根據系統配置初始化快取等
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewHolder vHolder = null;
- //如果convertView物件為空則建立新物件,不為空則複用
- if (convertView == null) {
- convertView = inflater.inflate(..., null);
- // 建立 ViewHodler 物件
- vHolder = new ViewHolder();
- vHolder.img= (ImageView) convertView.findViewById(...);
- vHolder.tv= (TextView) convertView.findViewById(...);
- // 將ViewHodler儲存到Tag中(Tag可以接收Object型別物件,所以任何東西都可以儲存在其中)
- convertView.setTag(vHolder);
- } else {
- //當convertView不為空時,通過getTag()得到View
- vHolder = (ViewHolder) convertView.getTag();
- }
- // 給物件賦值,修改顯示的值
- vHolder.img.setImageBitmap(...);
- vHolder.tv.setText(...);
- return convertView;
- }
- //將顯示的View 包裝成類
- static class ViewHolder {
- TextView tv;
- ImageView img;
- }
物件池:
物件池使用的基本思路是:將用過的物件儲存起來,等下一次需要這種物件的時候,再拿出來重複使用,從而在一定程度上減少頻繁建立物件所造成的開銷。 並非所有物件都適合拿來池化――因為維護物件池也要造成一定開銷。對生成時開銷不大的物件進行池化,反而可能會出現“維護物件池的開銷”大於“生成新物件的開銷”,從而使效能降低的情況。但是對於生成時開銷可觀的物件,池化技術就是提高效能的有效策略了。
執行緒池的基本思想還是一種物件池的思想,開闢一塊記憶體空間,裡面存放了眾多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆建立執行緒物件所帶來的效能開銷,節省了系統的資源。
比如:一個應用要和網路打交道,有很多步驟需要訪問網路,為了不阻塞主執行緒,每個步驟都建立個執行緒,線上程中和網路互動,用執行緒池就變的簡單,執行緒池是對執行緒的一種封裝,讓執行緒用起來更加簡便,只需要創一個執行緒池,把這些步驟像任務一樣放進執行緒池,在程式銷燬時只要呼叫執行緒池的銷燬函式即可。
java提供了ExecutorService和Executors類,我們可以應用它去建立執行緒池。
通常可以建立如下4種:
- /** 每次只執行一個任務的執行緒池 */
- ExecutorService singleTaskExecutor = Executors.newSingleThreadExecutor();
- /** 每次執行限定個數個任務的執行緒池 */
- ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);
- /** 所有任務都一次性開始的執行緒池 */
- ExecutorService allTaskExecutor = Executors.newCachedThreadPool();
- /** 建立一個可在指定時間裡執行任務的執行緒池,亦可重複執行 */
- ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);
要根據情況適度使用快取,因為記憶體有限。
能儲存路徑地址的就不要存放圖片資料,不經常使用的儘量不要快取,不用時就清空。
寫在最後:
我準備將文章分為上、中、下三部分。
在最後一篇文章中會將剩餘的Recycle,Review總結完。
因為記憶體知識很零散,而且我也是現學現賣,所以為了儘可能的蒐集更多的資料和保證內容的準確性更新速度可能慢點。因為中間還要寫點別的爛七八糟的。
寫這篇文章的目的就是想弄一個大彙總,將零散的記憶體知識點總結一下,如果有錯誤、不足或建議都希望告訴我。
參考文章:
解析Android開發優化之:軟引用與弱引用的應用(http://www.jb51.net/article/36627.htm)android記憶體洩露優化總結(http://blog.csdn.net/imain/article/details/8560986)
Android 記憶體優化(http://blog.csdn.net/awangyunke/article/details/20380719)
Android開發優化之——對Bitmap的記憶體優化(http://blog.csdn.net/arui319/article/details/7953690)
關於android效能,記憶體優化(http://www.cnblogs.com/zyw-205520/archive/2013/02/17/2914190.html)
Android研究院之應用開發執行緒池的經典使用(http://www.xuanyusong.com/archives/2439)
相關文章
- Android記憶體優化Android記憶體優化
- Android Note - 記憶體優化Android記憶體優化
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- Android 效能優化之記憶體優化Android優化記憶體
- 淺談Android記憶體優化Android記憶體優化
- Android記憶體優化全解析Android記憶體優化
- Android記憶體優化之圖片優化Android記憶體優化
- [譯] Android效能優化:APK瘦身方式大彙總Android優化APK
- android 關於記憶體優化的一些總結Android記憶體優化
- Android記憶體洩漏監控和優化技巧總結Android記憶體優化
- Android 是如何管理 App 記憶體的 — Android 記憶體優化第二彈AndroidAPP記憶體優化
- android效能評測與優化-記憶體Android優化記憶體
- android記憶體管理機制與優化Android記憶體優化
- Android深度效能優化--記憶體優化(一篇就夠)Android優化記憶體
- Redis 記憶體優化神技,小記憶體儲存大資料Redis記憶體優化大資料
- Android效能優化,Startalk會話頁GIF記憶體優化實踐Android優化會話記憶體
- 分析並優化 Android 應用記憶體佔用優化Android記憶體
- Android系統Bitmap記憶體分配原理與優化Android記憶體優化
- 關於redis記憶體分析,記憶體優化Redis記憶體優化
- Android效能優化:手把手帶你全面實現記憶體優化Android優化記憶體
- GC那些事兒–Android記憶體優化第一彈GCAndroid記憶體優化
- Android 專案中對於記憶體優化的幾個細節點Android記憶體優化
- 記憶體優化相關記憶體優化
- 1.記憶體優化(一)記憶體洩漏記憶體優化
- 實踐App記憶體優化:如何有序地做記憶體分析與優化APP記憶體優化
- JNI記憶體管理及優化記憶體優化
- mariadb 記憶體佔用優化記憶體優化
- iOS圖片記憶體優化iOS記憶體優化
- App記憶體優化-實踐APP記憶體優化
- Redis-記憶體優化(一)Redis記憶體優化
- Android記憶體優化(四)解析Memory Monitor、Allocation Tracker和Heap DumpAndroid記憶體優化
- Android常見記憶體洩漏總結Android記憶體
- psi 跟Android記憶體最佳化Android記憶體
- win10怎麼優化記憶體 win10系統記憶體優化的方法Win10優化記憶體
- 2.記憶體優化(二)優化分析記憶體優化
- iOS 使用Instruments優化記憶體效能iOS優化記憶體
- Linux 效能優化之 記憶體 篇Linux優化記憶體
- MongoDB記憶體使用分析和優化MongoDB記憶體優化
- HBase記憶體配置及JVM優化記憶體JVM優化