这篇文章其实也可以起另外一个标题:android 如何高效的展示图片
原文地址:http://developer.android.com/training/displaying-bitmaps/index.html
学习和使用常见的技术去处理和加载图片,能让你的用户界面快速响应,并且能避免动不动就超出了受限内存。可能你一不小心,就会导致内存消耗完毕从而crash
,抛出
java.lang.OutofMemoryError: bitmap size exceeds VM budget,在应用程序中加载图片是个棘手的问题,主要包括以下几个原因:
1.移动设备通常有约束的系统资源,
android 设备中可能在一个应用里只有16M的可用内存。Android兼容定义文档(CDD),3.7节,指出了在不同屏幕大小和密度所需的最小应用程序内存。应用程序应该优化过并且在这个最小的内存限制之下执行。当然,请记住许多设备配置更高的限制。 2.图片需要消耗大量的内存,尤其是丰富的图像比如照片。例如,摄像头拍照的Galaxy Nexus 2592 x1936像素(像素)。 如果位图配置使用ARGB 8888(默认从Android 2.3起)然后加载这个图像到内存需要大约19 mb的内存(2592 * 1936 * 4字节),在一些设备会立即消耗完每个应用拥有的内存。
3.android App 的UI 可能需要频繁的将多个图片一次性加载 ,比如 ListView,GridView,ViewPager 一般是在屏幕上包含多张图片,同时在屏幕之外也可能即将需要展示(也许就在你滑动的一瞬间就显示了)。
内容分为五个部分,分别是:
1,高效的加载大图片:在不会超过单个应用限制内存这一条例下进行decode bitmap.
2, 异步加载图片:通过
AsyncTask
异步处理图片,同时也提一下如何处理并发的情况。3. 缓存图片:通过内存和磁盘缓存,来提高UI的响应速度和流程性。
4. 内存管理:如何管理位图内存来最大化你的应用程序的性能。
4. 在你的UI上展示图片:用例子说明如何在ViewPager和GridView中 使用后台线程来和图片缓存来加载图片。
一、高效的加载大图片
在decode一张Bitmap之前,先检查这张图片的尺寸(除非你明确的知道这张图片的尺寸,并且确定一定不会产生内存溢出),可以设置BitmapFactory.Options对象options的属性inJustDecodeBounds为true,因为他不会去开辟内存生成一张图片,却能够知道图片的宽度和高度BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;
在知道宽度和高度后,可根据需要生成采样率(说明一点:如果生成的采样率是2的幂,如2,4,8,16...那么解码一张图片将会更快更高效)public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // Calculate ratios of height and width to requested height and width final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // Choose the smallest ratio as inSampleSize value, this will guarantee // a final image with both dimensions larger than or equal to the // requested height and width. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
在知道采样率后,就可以生成图片了(这时要设置inSampleSize=获取的采样率,inJustDecodeBounds=false)public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
在获取图片后,就可以设置UI组件的图片相关属性(这里是举例,通常在UI中需要异步回调进行设置,而不是直接在UI线程中设置,如果需要在SD卡中读取,或者网络读取的话,会因为耗时导致阻塞UI线程,从而产出ANR):mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
二、异步加载图片class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference<ImageView>(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
注意:这里的ImageView 使用弱引用的目的 是为了确保AsyncTask不会阻止ImageView或者它引用的资源被系统回收
所以要异步加载一张图片很简单了,调用如下即可:public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
上面主要是针对普通的View ,但是ListView,GridView,ViewPage等这些滚动时有重用自己的child view又该怎么办呢?因为如果每一个child View 都触发一个AsyncTask的话,就无法
保证一种情况:AsyncTask已经完成,但与之相关连的child View却没有被回收,转而被重用了,你这样设置的话,显示的图片不是会错了么。。。就是顺序无法保证一致。这种情况下的解决方案是:ImageView存储一个最近的AsyncTask的引用,并且在完成的时候再次判断一下,不就可以了吗!
仿照AsyncTask存储一个ImageView软应用的方法,我们可以自定义一个Drawable,也存储一个AsyncTask的软应用,使用了相同的思想
static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } }在执行AsyncTask之前,我们new AsyncDrawable 绑定到ImageView
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); } }绑定之前,先清掉以前的Task即可:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true; }private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; }
所以在你的BitmapWorkerTask 中onPostExecute() 需要再次检查当前的task是不是ImageView相匹配的Task
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } } }
这个时候你就可以在ListView,GridView的getView()方法中执行loadBitmap
方法即可了。
三、缓存图片
a.内存缓存。就是使用我们常说的LRU进行缓存了。
这里注意一点:在过去,很流行使用软应用和弱应用来缓存图片,但是现在已经不推荐使用了,因为从Android 2.3(API级别9)以后,垃圾收集器是更积极收集软/弱引用,这使得它们相当无效,
Android 3.0(API级别11) 之前,支持数据的位图存储在本地内存(而不是虚拟机内存),并且不是显示方式释放,可能导致应用程序超过其内存限制和崩溃。
如何为LruCahce选定一个合适的大小,需要考虑一系列的因素,如:
1.内存是如何加强你的activity/application.
2.有多少图片需要马上显示,有多少图片准备显示。
3.设备的尺寸和密度是什么,高密度设备在缓存相同数量的图片需要的更大的缓存
4.图片的尺寸和配置是什么,需要消耗多少的内存。
5.你是否访问频繁?如果频繁访问,你可能需要将某些图片内存常驻,或者使用多个LruCache(不同场合不同大小不同定义)
6.需要平衡数量与质量。
所以没有一个特定的大小或者适合所有的解决方案,得自己去分析APP,综合想出一个解决方案,缓存太小,导致额外的开销,缓存太大,容易再次使内存溢出或者留下很少的内存供给其它的用途。
这里举个例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache. final int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than // number of items. return bitmap.getByteCount() / 1024; } }; ... } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }所以之前的loadBitmap方法,使用LRUCache,就可以先check一下:
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } else { mImageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(mImageView); task.execute(resId); } }
同时BitmapWorkerTask也需要更新LRUCache:class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } ... }b.DISK卡缓存
注意:如果访问特别的频繁,ContentProvider可能是一个合适的地方存储缓存的图片。
DiskLruCache的例子:
private DiskLruCache mDiskLruCache; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override protected void onCreate(Bundle savedInstanceState) { ... // Initialize memory cache ... // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); new InitDiskCacheTask().execute(cacheDir); ... } class InitDiskCacheTask extends AsyncTask<File, Void, Void> { @Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir = params[0]; mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); mDiskCacheStarting = false; // Finished initialization mDiskCacheLock.notifyAll(); // Wake any waiting threads } return null; } } class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { final String imageKey = String.valueOf(params[0]); // Check disk cache in background thread Bitmap bitmap = getBitmapFromDiskCache(imageKey); if (bitmap == null) { // Not found in disk cache // Process as normal final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100)); } // Add final bitmap to caches addBitmapToCache(imageKey, bitmap); return bitmap; } ... } public void addBitmapToCache(String key, Bitmap bitmap) { // Add to memory cache as before if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } // Also add to disk cache synchronized (mDiskCacheLock) { if (mDiskLruCache != null && mDiskLruCache.get(key) == null) { mDiskLruCache.put(key, bitmap); } } } public Bitmap getBitmapFromDiskCache(String key) { synchronized (mDiskCacheLock) { // Wait while disk cache is started from background thread while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); } catch (InterruptedException e) {} } if (mDiskLruCache != null) { return mDiskLruCache.get(key); } } return null; } // Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
内存缓存检查在UI线程中,Disk卡缓存检查在非UI线程中,但是图片完成后,内存缓存和Disk缓存都需要添加。
举个例子,当Configuration 改变时候,如横竖屏切换。
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { ... RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(getFragmentManager()); mMemoryCache = retainFragment.mRetainedCache; if (mMemoryCache == null) { mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { ... // Initialize cache here as usual } retainFragment.mRetainedCache = mMemoryCache; } ... } class RetainFragment extends Fragment { private static final String TAG = "RetainFragment"; public LruCache<String, Bitmap> mRetainedCache; public RetainFragment() {} public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); if (fragment == null) { fragment = new RetainFragment(); } return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
四、内存管理
主要讲的是促进垃圾回收期回收,和使图片重用。
先讲下关于版本的一些变化知识:
1.在Android上Android 2.2(API级别8)和低版本时,当垃圾收集发生时,您的应用程序的线程被暂停,这导致滞后,降低性能。Android 2.3增加了并发垃圾收集,这意味着内存位图不再被引用不久后就会被回收。
2.在安卓2.3.3(API级别10)和低版本,位图像素数据存储在Native内存。它是独立于位图本身(存储在Dalvik堆)。像素数据在Native内存不是显式的方式释放,可能导致应用程序超过其内存限制和崩溃。在Android 3.0(API级别11),像素数据连同相关的位图存储在Dalvik堆。
2.3.3以及以前的优化:
通过引用计数的方式来判断图片是否可以回收了。
private int mCacheRefCount = 0; private int mDisplayRefCount = 0; ... // Notify the drawable that the displayed state has changed. // Keep a count to determine when the drawable is no longer displayed. public void setIsDisplayed(boolean isDisplayed) { synchronized (this) { if (isDisplayed) { mDisplayRefCount++; mHasBeenDisplayed = true; } else { mDisplayRefCount--; } } // Check to see if recycle() can be called. checkState(); } // Notify the drawable that the cache state has changed. // Keep a count to determine when the drawable is no longer being cached. public void setIsCached(boolean isCached) { synchronized (this) { if (isCached) { mCacheRefCount++; } else { mCacheRefCount--; } } // Check to see if recycle() can be called. checkState(); } private synchronized void checkState() { // If the drawable cache and display ref counts = 0, and this drawable // has been displayed, then recycle. if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); }
3.0以及以后版本的优化:
3.0以后引进了
BitmapFactory.Options.inBitmap
这个属性,如果option设置了这个属性的话,当load一张图片的时候,它将尝试去复用一张已经存在的图片:就是复用之前那种图片的内存,而不用频繁的去开辟/回收内存,从而提高了效率。
当然是有条件的:复用图片的大小必须和新生成的图片大小一致(确保所占用的内存一致)
HashSet<SoftReference<Bitmap>> mReusableBitmaps; private LruCache<String, BitmapDrawable> mMemoryCache; // If you're running on Honeycomb or newer, create // a HashSet of references to reusable bitmaps. if (Utils.hasHoneycomb()) { mReusableBitmaps = new HashSet<SoftReference<Bitmap>>(); } mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) { // Notify the removed entry that is no longer being cached. @Override protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) { if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { // The removed entry is a recycling drawable, so notify it // that it has been removed from the memory cache. ((RecyclingBitmapDrawable) oldValue).setIsCached(false); } else { // The removed entry is a standard BitmapDrawable. if (Utils.hasHoneycomb()) { // We're running on Honeycomb or later, so add the bitmap // to a SoftReference set for possible use with inBitmap later. mReusableBitmaps.add (new SoftReference<Bitmap>(oldValue.getBitmap())); } } } .... }public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight, ImageCache cache) { final BitmapFactory.Options options = new BitmapFactory.Options(); ... BitmapFactory.decodeFile(filename, options); ... // If we're running on Honeycomb or newer, try to use inBitmap. if (Utils.hasHoneycomb()) { addInBitmapOptions(options, cache); } ... return BitmapFactory.decodeFile(filename, options); }private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { // inBitmap only works with mutable bitmaps, so force the decoder to // return mutable bitmaps. options.inMutable = true; if (cache != null) { // Try to find a bitmap to use for inBitmap. Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { // If a suitable bitmap has been found, set it as the value of // inBitmap. options.inBitmap = inBitmap; } } } // This method iterates through the reusable bitmaps, looking for one // to use for inBitmap: protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { Bitmap bitmap = null; if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator(); Bitmap item; while (iterator.hasNext()) { item = iterator.next().get(); if (null != item && item.isMutable()) { // Check to see it the item can be used for inBitmap. if (canUseForInBitmap(item, options)) { bitmap = item; // Remove from reusable set so it can't be used again. iterator.remove(); break; } } else { // Remove from the set if the reference has been cleared. iterator.remove(); } } } return bitmap; }private static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; // Returns true if "candidate" can be used for inBitmap re-use with // "targetOptions". return candidate.getWidth() == width && candidate.getHeight() == height; }
微博:http://weibo.com/u/3209971935