由于比较多的朋友加我qq询问我问题,我把代码就放在github上吧,谢谢大家的支持
github地址:https://github.com/washpan/SimpleImageLoader
安卓在加载大图的时候经常会出现oom的错误,给大家分享我的一些处理经验。
大家可以参考android官网的例子bitmapfun,里面有很多地方可以借鉴。
http://developer.android.com/training/displaying-bitmaps/index.html
1. 为了快速加载图片,我们需要将其缓存一部分到内存中,这里我们选用LruCache,我重写sizeOf方法和entryRemoved。
继承LruCache实现我们自己的cache类
package com.washpan.bitmaptool; import android.support.v4.util.LruCache; /** * @author washpan [email protected] 用于预读Bitmap对象的LruCache * */ public class BitmapLruCache extends LruCache<String, SafeBitmap> { /** * 删除时是否释放 * */ public boolean isRecycleWhenRemove = false; public BitmapLruCache(int maxSize) { super(maxSize); } /** * 如果是释放操作判断是否需要释放,标记释放位 * */ @Override protected void entryRemoved(boolean evicted, String key, SafeBitmap oldValue, SafeBitmap newValue) { super.entryRemoved(evicted, key, oldValue, newValue); if (evicted) { if (null != oldValue && isRecycleWhenRemove) { if (null != oldValue.bitmap && oldValue.bitmap.isRecycled()) { oldValue.bitmap.recycle(); } } } } /** * 这里计算Bitmap的大小 * */ @Override protected int sizeOf(String key, SafeBitmap value) { return BitmapDecodeTool.sizeOfBitmap(value.bitmap, value.config); } }
2. 为了控制释放,编写一个含有Bitmap对象的数据结构,该结构包含了图片类型,方便计算占用空间大小。
SafeBitmap类代码
package com.washpan.bitmaptool; import android.graphics.Bitmap; /** * @author washpan [email protected] * 用于记录Bitmap的数据结构 * */ public class SafeBitmap { /** * Bitmap对象 * */ public Bitmap bitmap; /** * Bitmap的格式 * */ public Bitmap.Config config; }
BitmapDecodeTool代码
package com.washpan.bitmaptool; import java.io.File; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.os.Build; import android.os.Debug; /** * @author washpan [email protected] 用于解码bitmap的工具类 * */ public class BitmapDecodeTool { public static synchronized Bitmap decodeBitmap(String filename, int width, int height, int maxMultiple,Bitmap.Config config,boolean isScale) { //只加载基础信息,并不真正解码图片 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 这里进行假解码,获取宽高等信息,此时返回的bitmap是null的,因为并不真正进行解码 BitmapFactory.decodeFile(filename, options); if (options.outWidth < 1 || options.outHeight < 1) { String fn = filename; File ft = new File(fn); if (ft.exists()) { ft.delete(); return null; } } options.inPreferredConfig = config; //计算缩放率 options.inSampleSize = calculateOriginal(options, width, height, maxMultiple); // 设置为false开始真的解码图片 options.inJustDecodeBounds = false; Bitmap bm1 = null; try { bm1 = BitmapFactory.decodeFile(filename, options); } catch (Error e) { //发生错误进行再次压缩 Runtime.getRuntime().runFinalization(); try { Thread.sleep(600); options.inSampleSize += 1; options.inJustDecodeBounds = false; options.inDither = true; options.inPreferredConfig = null; try { bm1 = BitmapFactory.decodeFile(filename, options); } catch (Error e2) { //还错,继续压缩并且在sdcard中建立缓存区 Runtime.getRuntime().runFinalization(); try { Thread.sleep(600); options.inSampleSize += 1; //内存不足的情况下尝试在sdcard开辟空间存储内存 options.inTempStorage = new byte[12 * 1024]; options.inJustDecodeBounds = false; options.inDither = true; options.inPreferredConfig = null; try { bm1 = BitmapFactory.decodeFile(filename, options); } catch (Error e4) { //实在不行了返回null,解码失败 Runtime.getRuntime().runFinalization(); bm1 = null; } } catch (InterruptedException e3) { } } } catch (InterruptedException e1) { } } if (bm1 == null) { return null; } if (!isScale) { return bm1; } //等比缩放 int queryWidth = width; int queryHeight = height; int resWidth = bm1.getWidth(); int resHeight = bm1.getHeight(); float scaleWidth = ((float) queryWidth) / resWidth; float scaleHeight = ((float) queryHeight) / resHeight; Bitmap bm; try { if (scaleWidth >= 1 && scaleHeight >= 1) { bm = bm1; } else if (scaleHeight >= 1 && scaleWidth < 1) { int cutH = resHeight; int cutW = queryWidth * cutH / queryHeight; int cutX = resWidth / 2 - cutW / 2; bm = Bitmap.createBitmap(bm1, cutX, 0, cutW, cutH); } else if (scaleWidth >= 1 && scaleHeight < 1) { int cutW = resWidth; int cutH = queryHeight * cutW / queryWidth; bm = Bitmap.createBitmap(bm1, 0, 0, cutW, cutH); } else { float scale = scaleHeight < scaleWidth ? scaleWidth : scaleHeight; Matrix matrix = new Matrix(); matrix.postScale(scale, scale); bm = Bitmap.createBitmap(bm1, 0, 0, resWidth, resHeight, matrix, true); } if(null !=bm1 && !bm1.isRecycled()){ bm1.recycle(); } } catch (Exception e) { bm = bm1; } return bm; } /** * 计算缩放比例 * */ private static int calculateOriginal(BitmapFactory.Options options, int reqWidth, int reqHeight, int maxMultiple) { int inSampleSize = 1; final int height = options.outHeight; final int width = options.outWidth; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float) height / (float) reqHeight); } else { inSampleSize = Math.round((float) width / (float) reqWidth); } final float totalPixels = width * height; final float totalReqPixelsCap = (reqWidth * reqHeight * maxMultiple); //按允许的倍数计算缩放倍数 while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { inSampleSize++; } //检测是否有足够的内存对缩放倍数进行缩放,不行则继续缩放 while(!CheckBitmapFitsInMemory(reqWidth*maxMultiple/inSampleSize, reqHeight*maxMultiple/inSampleSize, options.inPreferredConfig)){ inSampleSize++; } } return inSampleSize; } /** * 按照宽高计算bitmap所占内存大小 * */ public static long GetBitmapSize(long bmpwidth, long bmpheight, Bitmap.Config config) { return bmpwidth * bmpheight * getBytesxPixel(config); } /** * 计算bitmap所占空间,单位bytes * */ @SuppressLint("NewApi") public static int sizeOfBitmap(Bitmap bitmap, Bitmap.Config config) { int size = 1; // 3.1或者以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { size = bitmap.getByteCount() * getBytesxPixel(config)>>10; } // 3.1以下 else { size = bitmap.getRowBytes() * bitmap.getHeight() * getBytesxPixel(config)>>10; } return size; } /** * 按照不同格式计算所占字节数 * */ public static int getBytesxPixel(Bitmap.Config config) { int bytesxPixel = 1; // 3.1或者以上 switch (config) { case RGB_565: case ARGB_4444: bytesxPixel = 2; break; case ALPHA_8: bytesxPixel = 1; break; case ARGB_8888: bytesxPixel = 4; break; } return bytesxPixel; } /** * 当前空闲的堆内存 * */ public static long FreeMemory() { return Runtime.getRuntime().maxMemory() - Debug.getNativeHeapAllocatedSize(); } /** * 检测当前是否有足够的内存进行读取bitmap * */ public static boolean CheckBitmapFitsInMemory(long bmpwidth, long bmpheight, Bitmap.Config config) { return (GetBitmapSize(bmpwidth, bmpheight, config) < FreeMemory()); } }
3.编写加载器,最好不要用线程池进行图片加载,当2个或者以上的线程(如果是多核cpu)同时加载大图的时候,oom异常的几率会加大,并且很难控制,例子中我是起一个线程在锁队列中一个一个任务进行执行。
BitmapLoader 代码
package com.washpan.bitmaptool; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.LinkedBlockingDeque; import android.graphics.Bitmap; import android.widget.ImageView; /** * @author washpan [email protected] 用于加载bitmap的装载器 * */ public class BitmapLoader { /** * 当快速滑动时不加载 * */ public static final Object LOCKWHENSCROLL = new Object(); private BitmapLruCache cache = null; private boolean isPause = false; private String path = ""; private int defaultWidth = 100; private int defaultHeight = 100; private boolean isStop = false; private Thread loadingThread = null; private ArrayList<LoadBitmapListener> listeners = new ArrayList<BitmapLoader.LoadBitmapListener>(); /** * 通过view找到正在执行的task,像listView重用item时快速滑动时旧的view所对应的task如果未启动就没有必要让其再启动了,因为会很耗资源 * */ private HashMap<ImageView, LoadingTask> views = new HashMap<ImageView, LoadingTask>(); /** * 加载锁队列 * */ private LinkedBlockingDeque<LoadingTask> tasks = new LinkedBlockingDeque<BitmapLoader.LoadingTask>(); class LoadingTask implements Runnable { private String url = ""; private ImageView view = null; public boolean isWorking = false; public LoadingTask(String url, ImageView view) { this.url = url; this.view = view; } public boolean isStop = false; @Override public void run() { // 用于快速滑动时锁住加载操作 synchronized (LOCKWHENSCROLL) { while (isPause) { try { LOCKWHENSCROLL.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } isWorking = true; // 先从本地加载 SafeBitmap safeBitmap = loadLocal(url); if (null != safeBitmap && null != safeBitmap.bitmap) { // 放入缓存中 synchronized (cache) { cache.put(NameUtils.generateKey(url), safeBitmap); } synchronized (listeners) { for (LoadBitmapListener l : listeners) { l.onFinish(view, safeBitmap.bitmap); } } } // 从网络加载 else { HttpDownloadPic dowload = new HttpDownloadPic(); File cacheFile = new File(path, NameUtils.generateKey(url)); try { dowload.downloadPic(cacheFile, url); if (null != cacheFile && cacheFile.exists()) { safeBitmap = loadLocal(url); if (null != safeBitmap && null != safeBitmap.bitmap) { // 放入缓存中 synchronized (cache) { cache.put(NameUtils.generateKey(url), safeBitmap); } synchronized (listeners) { for (LoadBitmapListener l : listeners) { l.onFinish(view, safeBitmap.bitmap); } } } } else { synchronized (listeners) { for (LoadBitmapListener l : listeners) { l.onError(view); } } } } catch (IOException e) { synchronized (listeners) { for (LoadBitmapListener l : listeners) { l.onError(view); } } }finally{ synchronized (views) { views.remove(view); } } } } } public void setPause(boolean isPause) { this.isPause = isPause; if (!isPause) { synchronized (LOCKWHENSCROLL) { LOCKWHENSCROLL.notifyAll(); } } } public BitmapLoader(BitmapLruCache cache, String path, int defaultWidth, int defaultHeight) { this.cache = cache; this.path = path; this.defaultWidth = defaultWidth; this.defaultHeight = defaultHeight; loadingThread = new Thread(new Worker()); loadingThread.start(); } public class Worker implements Runnable { public void run() { while (!isStop) { LoadingTask task = null; task = tasks.poll(); if (null != task && !task.isStop) { task.run(); } } } } public interface LoadBitmapListener { void onFinish(final ImageView view, final Bitmap bitmap); void onError(final ImageView view); } public synchronized void loadBitmap(final ImageView view, final String url, final LoadBitmapListener listener) { // 停掉之前的任务 synchronized (views) { LoadingTask oldTask = views.get(view); if (null != oldTask && !oldTask.isWorking) { oldTask.isStop = true; } } synchronized (listeners) { listeners.add(listener); } LoadingTask task = new LoadingTask(url, view); synchronized (views) { views.put(view, task); } try { tasks.put(task); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 从本地加载图片 * */ private SafeBitmap loadLocal(String url) { SafeBitmap safeBitmap = new SafeBitmap(); safeBitmap.bitmap = BitmapDecodeTool.decodeBitmap(path + File.separator + NameUtils.generateKey(url), defaultWidth, defaultHeight, 3, Bitmap.Config.RGB_565, false); safeBitmap.config = Bitmap.Config.RGB_565; return safeBitmap; } public SafeBitmap getCahe(String key) { return this.cache.get(key); } public void cleanCache(boolean isRecycle) { this.cache.isRecycleWhenRemove = isRecycle; this.cache.evictAll(); this.isStop = true; synchronized (views) { views.clear(); } synchronized (listeners) { listeners.clear(); } } }
4. 如果使用ListView等列表控件快速滑动时请不要进行加载,否则oom也很容易出现。
完整源码下载地址
http://download.csdn.net/detail/anuojieyanjiu/5331190
优化后的版本下载地址,以下的为修改后的代码。
http://download.csdn.net/detail/anuojieyanjiu/5331505
发现如果要显示默认图片,比如加载图片时的loading页面,最好只解码一次,以后直接使用该bitmap对象,而不要每次使用的时候都进行decode,那样内存会消耗很大。
上个版本由于及时标记了bitmap的recycle标记位,有时候会发生recycled bitmap use的异常,修复之后的版本下载地址
http://download.csdn.net/detail/anuojieyanjiu/5331961
如果还不放心,可以关掉某个activity的硬件加速,
AndroidManifest.xml中某个activity中配置以下信息即可关闭硬件加速
android:hardwareAccelerated="false"
如果你关闭掉了硬件加速,那么释放缓存可以不用那么及时,例子中的BigImageActivity类的destroyItem方法中的
//硬件加速关闭的情况下以下代码可以屏蔽掉 String url = TestData.imageUrls[position]; SafeBitmap safeBitmap = loader.getCahe(NameUtils.generateKey(url)); if(null !=safeBitmap && null !=safeBitmap.bitmap&& !safeBitmap.bitmap.isRecycled()){ safeBitmap.bitmap.recycle(); loader.removeCache(NameUtils.generateKey(url)); System.runFinalization(); }
这几行代码都可以注释掉
关闭硬件加速后的版本下载地址
http://download.csdn.net/detail/anuojieyanjiu/5332383