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, 而在堆中一個物件引用變數修改了這個物件的內部狀態,會影響到另一個物件引用變數。
OOM:
記憶體洩露可以引發很多問題:
1.程式卡頓,響應速度慢(記憶體佔用高時,JVM虛擬機器會頻繁觸發GC)
2.莫名消失(當你的程式所佔記憶體越大,它在後臺的存在的時間就越短。記憶體佔用越小,在後臺存在的時間就越長)
3.直接崩潰(OutOfMemoryError)
記憶體面臨的問題:
1.有限的記憶體堆,原始只有16M
2.記憶體大小消耗等根據裝置,作業系統等級,螢幕尺寸的不同而不同
3.程式不能直接控制
4.支援後臺多工處理
5.執行在虛擬機器之上
Bitmap:
Bitmap是記憶體消耗大戶,絕大多數的OOM崩潰都是操作Bitmap時產生的。
圖片顯示:
比如列表中僅顯示縮圖,點選具體條目時啟動另一個頁面顯示整個圖片。
圖片大小:
直接使用ImageView顯示bitmap會佔用較多資源,特別是圖片較大的時候,可能會導致崩潰
使用BitmapFactory.Options設定inSampleSize,這樣做可以減少對系統資源的要求。
屬性值inSampleSize表示縮圖大小為原始圖片的幾分之一,即如果為2,則提出的縮圖的寬和高就是原始圖片的二分之一,圖片大小為原始大小的四分之一。
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);
圖片畫素:
android中圖片有四種屬性:
ALPHA_8:每個畫素佔用1byte記憶體
ARGB_4444:每個畫素佔用2byte記憶體
ARGB_8888:每個畫素佔用4byte記憶體 (預設)
RGB_565:每個畫素佔用2byte記憶體
android預設的顏色模式為ARGB_8888,這個顏色顯示質量最高,佔用記憶體最大,所以在對圖片效果要求不是特別高的情況下可以使用RGB_565(565沒有透明度屬性)
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佔用的記憶體空間,不要等系統來進行釋放。
// 先判斷是否已經回收
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;
}
重用
核心思路就是將已經存在的記憶體資源重新使用而避免去建立新的,最典型的使用就是快取(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來進行快取處理
@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;
}
池(PooL)
物件池:
物件池使用的基本思路是:將用過的物件儲存起來,等下一次需要這種物件的時候,再拿出來重複使用,從而在一定程度上減少頻繁建立物件所造成的開銷。 並非所有物件都適合拿來池化――因為維護物件池也要造成一定開銷。對生成時開銷不大的物件進行池化,反而可能會出現“維護物件池的開銷”大於“生成新物件的開銷”,從而使效能降低的情況。但是對於生成時開銷可觀的物件,池化技術就是提高效能的有效策略了。
執行緒池:
執行緒池的基本思想還是一種物件池的思想,開闢一塊記憶體空間,裡面存放了眾多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆建立執行緒物件所帶來的效能開銷,節省了系統的資源。
比如:一個應用要和網路打交道,有很多步驟需要訪問網路,為了不阻塞主執行緒,每個步驟都建立個執行緒,線上程中和網路互動,用執行緒池就變的簡單,執行緒池是對執行緒的一種封裝,讓執行緒用起來更加簡便,只需要創一個執行緒池,把這些步驟像任務一樣放進執行緒池,在程式銷燬時只要呼叫執行緒池的銷燬函式即可。
java提供了ExecutorService和Executors類,我們可以應用它去建立執行緒池。
通常可以建立如下4種:
/** 每次只執行一個任務的執行緒池 */
ExecutorService singleTaskExecutor = Executors.newSingleThreadExecutor();
/** 每次執行限定個數個任務的執行緒池 */
ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);
/** 所有任務都一次性開始的執行緒池 */
ExecutorService allTaskExecutor = Executors.newCachedThreadPool();
/** 建立一個可在指定時間裡執行任務的執行緒池,亦可重複執行 */
ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);
注意:
要根據情況適度使用快取,因為記憶體有限。
能儲存路徑地址的就不要存放圖片資料,不經常使用的儘量不要快取,不用時就清空。