編輯:關於Android編程
我們知道Android系統分配給每個應用程序的內存是有限的,Bitmap作為消耗內存大戶,我們對Bitmap的管理稍有不當就可能引發OutOfMemoryError,而Bitmap對象在不同的Android版本中存在一些差異,今天就給大家介紹下這些差異,並提供一些在使用Bitmap的需要注意的地方。
在Android2.3.3(API 10)及之前的版本中,Bitmap對象與其像素數據是分開存儲的,Bitmap對象存儲在Dalvik heap中,而Bitmap對象的像素數據則存儲在Native Memory(本地內存)中或者說Derict Memory(直接內存)中,這使得存儲在Native Memory中的像素數據的釋放是不可預知的,我們可以調用recycle()方法來對Native Memory中的像素數據進行釋放,前提是你可以清楚的確定Bitmap已不再使用了,如果你調用了Bitmap對象recycle()之後再將Bitmap繪制出來,就會出現Canvas: trying to use a recycled bitmap錯誤,而在Android3.0(API 11)之後,Bitmap的像素數據和Bitmap對象一起存儲在Dalvik heap中,所以我們不用手動調用recycle()來釋放Bitmap對象,內存的釋放都交給垃圾回收器來做,也許你會問,為什麼我在顯示Bitmap對象的時候還是會出現OutOfMemoryError呢?
在說這個問題之前我順便提一下,在Android2.2(API 8)之前,使用的是Serial垃圾收集器,從名字可以看出這是一個單線程的收集器,這裡的”單線程的意思並不僅僅是使用一個CPU或者一條收集線程去收集垃圾,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,Android2.3之後,這種收集器就被代替了,使用的是並發的垃圾收集器,這意味著我們的垃圾收集線程和我們的工作線程互不影響。
簡單的了解垃圾收集器之後,我們對上面的問題舉一個簡單的例子,假如系統啟動了垃圾回收線程去收集垃圾,而此時我們一下子產生大量的Bitmap對象,此時是有可能會產生OutOfMemoryError,因為垃圾回收器首先要判斷某個對象是否還存活(JAVA語言判斷對象是否存活使用的是根搜索算法 GC Root Tracing),然後利用垃圾回收算法來對垃圾進行回收,不同的垃圾回收器具有不同的回收算法,這些都是需要時間的, 發生OutOfMemoryError的時候,我們要明確到底是因為內存洩露(Memory Leak)引發的還是內存溢出(Memory overflow)引發的,如果是內存洩露我們需要利用工具(比如MAT)查明內存洩露的代碼並進行改正,如果不存在洩露,換句話來說就是內存中的對象確實還必須活著,那我們可以看看是否可以通過某種途徑,減少對象對內存的消耗,比如我們在使用Bitmap的時候,應該根據View的大小利用BitmapFactory.Options計算合適的inSimpleSize來對Bitmap進行相對應的裁剪,以減少Bitmap對內存的使用,如果上面都做好了還是存在OutOfMemoryError(一般這種情況很少發生)的話,那我們只能調大Dalvik heap的大小了,在Android 3.1以及更高的版本中,我們可以在AndroidManifest.xml的application標簽中增加一個值等於“true”的android:largeHeap屬性來通知Dalvik虛擬機應用程序需要使用較大的Java Heap,但是我們也不鼓勵這麼做。
在Android 2.3及以下管理Bitmap
從上面我們知道,在Android2.3及以下我們推薦使用recycle()方法來釋放內存,我們在使用ListView或者GridView的時候,該在什麼時候去調用recycle()呢?這裡我們用到引用計數,使用一個變量(dispalyRefCount)來記錄Bitmap顯示情況,如果Bitmap繪制在View上面displayRefCount加一, 否則就減一, 只有在displayResCount為0且Bitmap不為空且Bitmap沒有調用過recycle()的時候,我們才需求對該Bitmap對象進行recycle(),所以我們需要用一個類來包裝下Bitmap對象,代碼如下
package com.example.bitmap; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; public class RecycleBitmapDrawable extends BitmapDrawable { private int displayResCount = 0; private boolean mHasBeenDisplayed; public RecycleBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); } /** * @param isDisplay */ public void setIsDisplayed(boolean isDisplay){ synchronized (this) { if(isDisplay){ mHasBeenDisplayed = true; displayResCount ++; }else{ displayResCount --; } } checkState(); } /** * 檢查圖片的一些狀態,判斷是否需要調用recycle */ private synchronized void checkState() { if (displayResCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } /** * 判斷Bitmap是否為空且是否調用過recycle() * @return */ private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); } }除了上面這個RecycleBitmapDrawable之外呢,我們還需要一個自定義的ImageView來控制什麼時候顯示Bitmap以及什麼時候隱藏Bitmap對象
package com.example.bitmap; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.widget.ImageView; public class RecycleImageView extends ImageView { public RecycleImageView(Context context) { super(context); } public RecycleImageView(Context context, AttributeSet attrs) { super(context, attrs); } public RecycleImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void setImageDrawable(Drawable drawable) { Drawable previousDrawable = getDrawable(); super.setImageDrawable(drawable); //顯示新的drawable notifyDrawable(drawable, true); //回收之前的圖片 notifyDrawable(previousDrawable, false); } @Override protected void onDetachedFromWindow() { //當View從窗口脫離的時候,清除drawable setImageDrawable(null); super.onDetachedFromWindow(); } /** * 通知該drawable顯示或者隱藏 * * @param drawable * @param isDisplayed */ public static void notifyDrawable(Drawable drawable, boolean isDisplayed) { if (drawable instanceof RecycleBitmapDrawable) { ((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed); } else if (drawable instanceof LayerDrawable) { LayerDrawable layerDrawable = (LayerDrawable) drawable; for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); } } } }這個自定類也比較簡單,重寫了setImageDrawable()方法,在這個方法中我們先獲取ImageView上面的圖片,然後通知之前顯示在ImageView的Drawable不在顯示了,Drawable會判斷是否需要調用recycle(),當View從Window脫離的時候會回調onDetachedFromWindow(),我們在這個方法中回收顯示在ImageView的圖片,具體的使用方法
ImageView imageView = new ImageView(context); imageView.setImageDrawable(new RecycleBitmapDrawable(context.getResource(), bitmap));只需要用RecycleBitmapDrawable包裝Bitmap對象,然後設置到ImageView上面就可以啦,具體的內存釋放我們不需要管,是不是很方便呢?這是在Android2.3以及以下的版本管理Bitmap的內存。
在Android 3.0及以上管理Bitmap
由於在Android3.0及以上的版本中,Bitmap的像素數據也存儲在Dalvik heap中,所以內存的管理就直接交給垃圾回收器了,我們並不需要手動的去釋放內存,而今天講的主要是BitmapFactory.Options.inBitmap的這個字段,假如這個字段被設置了,我們在解碼Bitmap的時候,他會去重用inBitmap設置的Bitmap,減少內存的分配和釋放,提高了應用的性能,然而在Android 4.4之前,BitmapFactory.Options.inBitmap設置的Bitmap必須和我們需要解碼的Bitmap的大小一致才行,在Android4.4以後,BitmapFactory.Options.inBitmap設置的Bitmap大於或者等於我們需要解碼的Bitmap的大小就OK了,我們先假設一個場景,還是在使用ListView,GridView去加載大量的圖片,為了提高應用的效率,我們通常會做相對應的內存緩存和硬盤緩存,這裡我們只說內存緩存,而內存緩存官方推薦使用LruCache, 注意LruCache只是起到緩存數據作用,並沒有回收內存。一般我們的代碼會這麼寫
package com.example.bitmap; import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.os.Build.VERSION_CODES; import android.support.v4.util.LruCache; public class ImageCache { private final static int MAX_MEMORY = 4 * 102 * 1024; private LruCache上面只是一些事例性的代碼,將從LruCache中移除的BitmapDrawable對象的弱引用保存在一個set中,然後從set中獲取滿足BitmapFactory.Options.inBitmap條件的Bitmap對象用來提高解碼Bitmap性能,使用如下mMemoryCache; private Set > mReusableBitmaps; private void init() { if (hasHoneycomb()) { mReusableBitmaps = Collections .synchronizedSet(new HashSet >()); } mMemoryCache = new LruCache (MAX_MEMORY) { /** * 當保存的BitmapDrawable對象從LruCache中移除出來的時候回調的方法 */ @Override protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) { if (hasHoneycomb()) { mReusableBitmaps.add(new SoftReference (oldValue .getBitmap())); } } }; } /** * 從mReusableBitmaps中獲取滿足 能設置到BitmapFactory.Options.inBitmap上面的Bitmap對象 * @param options * @return */ protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { Bitmap bitmap = null; if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { synchronized (mReusableBitmaps) { final Iterator > iterator = mReusableBitmaps .iterator(); Bitmap item; while (iterator.hasNext()) { item = iterator.next().get(); if (null != item && item.isMutable()) { if (canUseForInBitmap(item, options)) { bitmap = item; iterator.remove(); break; } } else { iterator.remove(); } } } } return bitmap; } /** * 判斷該Bitmap是否可以設置到BitmapFactory.Options.inBitmap上 * * @param candidate * @param targetOptions * @return */ @TargetApi(VERSION_CODES.KITKAT) public static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) { // 在Anroid4.4以後,如果要使用inBitmap的話,只需要解碼的Bitmap比inBitmap設置的小就行了,對inSampleSize // 沒有限制 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // 在Android // 4.4之前,如果想使用inBitmap的話,解碼的Bitmap必須和inBitmap設置的寬高相等,且inSampleSize為1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; } /** * 獲取每個像素所占用的Byte數 * * @param config * @return */ public static int getBytesPerPixel(Config config) { if (config == Config.ARGB_8888) { return 4; } else if (config == Config.RGB_565) { return 2; } else if (config == Config.ARGB_4444) { return 2; } else if (config == Config.ALPHA_8) { return 1; } return 1; } @TargetApi(VERSION_CODES.HONEYCOMB) public static boolean hasHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } }
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); ... BitmapFactory.decodeFile(filename, options); ... // If we're running on Honeycomb or newer, try to use inBitmap. if (ImageCache.hasHoneycomb()) { options.inMutable = true; if (cache != null) { Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { options.inBitmap = inBitmap; } } } ... return BitmapFactory.decodeFile(filename, options); }
通過這篇文章你是不是對Bitmap對象有了更進一步的了解,在應用加載大量的Bitmap對象的時候,如果你做到上面幾點,我相信應用發生OutOfMemoryError的概率會很小,並且性能會得到一定的提升,我經常會看到一些同學在評價一個圖片加載框架好不好的時候,比較片面的以自己使用過程中是否發生OutOfMemoryError來定論,當然經常性的發生OutOfMemoryError你應該先檢查你的代碼是否存在問題,一般一些比較成熟的框架是不存在很嚴重的問題,畢竟它也經過很多的考驗才被人熟知的,今天的講解就到這裡了,有疑問的同學可以在下面留言!
今天我們研究一下如何在Android手機上顯示GIF動態圖片 首先需要在src目錄下新建一個自定義的View,代碼如下: import a
前言 之前的文章有介紹ActivityGroup,不少人問嵌套使用的問題,同樣的需求在Fragment中也存在,幸好在最新的Android support 包
今天學習了Android開發中比較難的一個環節,就是斷點續傳下載,很多人看到這個標題就感覺頭大,的確,如果沒有良好的邏輯思維,這塊的確很難搞明白。下面我就將自己學到的知
我們學的Android 數據持久化的技術包括文件存儲、SharedPreferences 存儲、以及數據庫存儲。不知道你有沒有發現,使用這些持久化技術所保存的數據都只能在
本菜開源的一個自己寫的Demo,希望能給Androider們有所幫助,