Android高效加载Bitmap

本篇博客内容:

  • 计算合适比例,读取适屏的bitmap

  • 开启工作线程,执行读取bitmap的任务

  • 在ListView或者GridView中多个Bitmap并发操作

  • 内存缓存(LruCache类)和磁盘缓存(DiskLruCache类)

  • UI线程执行ImageView加载工作线程读取到的Bitmap


众所周知,Bitmap很容易造成UI线程阻塞,消耗巨大内存。若是使用Bitmap不恰当,容易出现内存溢出:java.lang.OutofMemoryError: bitmap size exceeds VM budget 。

在开发运用加载Bitmap时,应该注意以下几个方面:

  • 最初的设计是:android设备对于单个运用程序分配16MB。后期,分配量变大。
    因此,需要按ImageView在手机屏幕中的实际大小来加载图片,节省内存。

  • Bitmap消耗很多内存。特别是从相册中获取拍照的图片,往往一个相机产生的图片能有4M,或者更多。
    因此,在获取相机产生图片时,一定要计算出合适的比例,按比例来加载图片

  • android的运用程序中UI(例如ListView,GridView,ViewPager等控件)通常会一次操作加载多张Bitmap。
    这时候为了滑动不卡顿,应该预先加载一些没在屏幕中展现的bitmap.

在实际开发中,应该做出对Bitmap以下几个步骤:

1. Loading Large Bitmaps Efficiently:

高效加载Bitmap,必定是按合适的比例去加载,以免多产生不必要的内存。这里就必须说下BitmapFactory与BitmapFactory.Options。 BitmapFactory提供将各种资源(例如Resource资源,File文件,Byte[],I/O流)解码成Bitmap的方法。 BitmapFactory.Options提供一些解码的配置,例如图片比例,图片格式等。

这里展现,采用向上取整的方式,计算压缩尺寸获取最适合ImageView的bitmap.

    /**
     * 将stream中数据转换成适合ImageView大小的bitmap
     * @param inputStream
     * @return
     */
    public Bitmap streamToBitmap(InputStream inputStream) {
        BitmapFactory.Options options = null;
        ByteArrayOutputStream outputStream = null;
        byte[] data = null;
        try {
            options = new BitmapFactory.Options();
            /*
             * inJustDecodeBounds设置为true时,
             * BitmapFactory进行解码,不会分配内存,会返回一个null的bitmap.
             *  这便可以用于获取Bitmap的实际大小
             */
            options.inJustDecodeBounds = true;

            //先将strem中的数据转成byte[]
            int length;
            data = new byte[1024];
            outputStream = new ByteArrayOutputStream();
            while ((length = inputStream.read(data)) > 0) {
                outputStream.write(data, 0, length);
            }
            //将流中数据转成byte[]
            data = outputStream.toByteArray(); 

            //将byte[]数据生成bitmap
            byteToBitmap(data, options);

            //计算出适屏比例
            options.inSampleSize = calculateInSize(options,  
                     task.getImagevViewWidth(), task.getImageViewHeight());

            options.inJustDecodeBounds = false; 

            return byteToBitmap(data, options);
        } catch (Exception e) {
            java.lang.System.gc();
            e.printStackTrace();
            return null;
        } finally {//释放资源的操作
            try {
                if (inputStream != null) {
                    inputStream.close();
                    inputStream = null;
                }
                if (outputStream != null) {
                    outputStream.close();
                    outputStream = null;
                }
                if (data != null) {
                    data = null;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }  

   /**
     * 将byte解析成bitmap
     * @param data
     * @param options
     * @return
     */
    public Bitmap byteToBitmap(byte[] data, BitmapFactory.Options options) {
        return BitmapFactory.decodeByteArray(data, 0, data.length, options);
    }


    /**
     * 采用向上取整的方式,计算压缩尺寸
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateInSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int inSampleSize = 0;
        if (reqWidth > 0) {//ImageView指定大小,按适合尺寸加载
            int heightRatio = (int) Math.ceil((options.outHeight*1.0f )/ reqHeight);
            int widthRatio = (int) Math.ceil((options.outHeight *1.0f)/ reqWidth);
           inSampleSize=   Math.max(heightRatio,widthRatio);
        }else{  //ImageView没有指定大小,按图片原始大小加载
            inSampleSize=1;
        }
        return inSampleSize;
    }

2. Processing Bitmaps Off the UI Thread:

加载大的Bitmap,不能在主线中进行。因为加载图片时,会进行I/O操作,容易阻塞线程。利用工作线程来加载Bitmap.使用线程方式:线程池,AsyncTask。

这里是线程池来执行加载图片的任务,先说线程池配置:

public class ThreadManager {
     /**
     * 线程池
     */
    private ThreadPoolExecutor bitampExecutor;
    private static final int CODE_POOL_SIZE = 8;
    /**
     * 线程池最多同时执行8个线程
     */
    private static final int maxPoolSize = 8;
    /**
     * 线程池中线程在空闲时,存留时间
     */
    private static final int KEEP_ALIVE_TIME = 1;

    private static final TimeUnit TIME_UNIT;
    /**
     * 保存执行任务的Runnable的队列
     */
    private final BlockingQueue decodeBitmapQueue;
    private final BlockingQueue bitmapTasksQueue;

     /**
     * 静态代码,来创建单例模式。避免使用synchronized 方式的单例类
     */
    static {
        TIME_UNIT = TimeUnit.SECONDS;
        ourInstance = new ThreadManager();
    }

    private ThreadManager() {
       decodeBitmapQueue = new LinkedBlockingQueue();
       decodeBitmapQueue = new LinkedBlockingQueue();
        //配置线程池
       bitampExecutor = new ThreadPoolExecutor(CODE_POOL_SIZE, maxPoolSize 
                      , KEEP_ALIVE_TIME, TIME_UNIT, decodeBitmapQueue);
   }

然后看执行Runanable实现类的配置:

public class BitmapTask_Runnable implements Runnable {
    private BitmapTask task;
    private BaseApplication context;
    public BitmapTask_Runnable(BitmapTask task) {
        this.task = task;
        this.context = BaseApplication.getBaseAppliction();
    }
    @Override
    public void run() {
        //设置线程的优先级,这里设置为后台
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        //在BitmapTask中保存线程的指针,便于终止任务线程。
        task.setCurrentThread(Thread.currentThread());
         try {
            if (Thread.interrupted()) {
                return;
            }
            //执行上一步的,读取适屏的bitmap操作
         }
         } catch (Exception e) {
            bitmap = null;
            e.printStackTrace();
        } finally {//最后将任务结果发送到UI线程,进行UI更新
            if (bitmap != null) {
                task.handleState(Constant.BITMAPFINISH);
            } else {
                task.handleState(Constant.BITMAPFARIL);
            }
            //任务完成,不再持有工作线程的指针。
            task.setCurrentThread(null);
            Thread.interrupted();
        }

}

这里用到了一个实体类,用于保存工作线程,ImageView,bitmap一致性。

//线程任务抽象类
public abstract class ThreadTask {
        abstract void recycle();
}

/**
 * 用途:
 * 一个用于任务类,用于保存ImageView指针,和后台线程,任务结果(Bitmap)
 */
public class BitmapTask extends ThreadTask {
    /**
     * 当前正在执行的工作线程
     */
    private Thread currentThread = null;
    /**
     * 线程池的操作类
     */
    private ThreadManager manager;
    /**
     * 执行任务的Runnable接口的实现类
     */
    private BitmapTask_Runnable runnable;
    /**
     * 用弱引用来保存ImageView的指针
     */
    private WeakReference imageViewWeakReference;
    /**
     * ImageView的宽和高
     */
    private int imageViewHeight;
    private int imagevViewWidth;
    /**
     * 任务结果,即ImageView要加载的数据
     */
    private Bitmap bitmap;
    /**
     * 图片资源路径
     */
    private String imageId;
    public BitmapTask() {
         manager = ThreadManager.getInstance();
        runnable = new BitmapTask_Runnable(this);
    }
    public void initTask(ImageView imageView, String imageId) {
        this.imageViewWeakReference = new WeakReference(imageView);
        this.imagevViewWidth = imageView.getWidth();
        this.imageViewHeight = imageView.getHeight();
         Log.i("imageview",imageViewHeight+" "+imagevViewWidth+" "+imageView.getMeasuredHeight());
        this.imageId = imageId;
    }

    public Runnable  getCurrentRunnable(){
        return  runnable;
    }

    public ThreadManager getThreadManager(){
        return  manager;
    }

    public Bitmap getBitmap(){
       return this.bitmap;
    }

    public void setBitmap(Bitmap bitmap){
        this.bitmap=bitmap;
    }

    public ImageView getImageView() {
        if (imageViewWeakReference != null) {
            return imageViewWeakReference.get();
        }
        return null;
    }
    public Thread getCurrentThread() {
        synchronized (manager) {
            return currentThread;
        }
    }

    /**
     *  synchronized 确保ImageView与工作线程一致性。
     * @param currentThread
     */
    public void setCurrentThread(Thread currentThread){
        synchronized (manager){
            this.currentThread=currentThread;
        }
    }
    public int getImageViewHeight() {
        return imageViewHeight;
    }
    public int getImagevViewWidth() {
        return imagevViewWidth;
    }
    public String getImageId() {
        return imageId;
    }
    public void handleState(int state) {
        manager.handeState(this, state);
    }

    /**
     * 执行任务,更新UI后,进行资源回收
     */
    @Override
    void recycle() {
        if (imageViewWeakReference != null) {
            imageViewWeakReference.clear();
            imageViewWeakReference = null;
        }
        imageViewHeight = 0;
        imagevViewWidth = 0;
        currentThread = null;
        imageId = null;
        bitmap=null;
    }
}

3. Handle Concurrency:

通常类似ListView与GridView等视图控件在使用线程加载Bitmap时,会同时带来并发的问题。
首先为了更高的效率,ListView与GridView的子Item视图会在用户滑动屏幕时被循环使用。
如果每一个子视图都触发一个线程,那么就无法确保关联的视图在结束任务时,分配的视图已经进入循环队列中,给另外一个子视图进行重用, 而且, 无法确保所有的异步任务的完成顺序和他们本身的启动顺序保持一致。

解决方式:ImageView持有线程的指针(即ImageView与持有工作线程指针的实体类绑定),在完成任务时再次读取检查。
创建一个BitmapDrawable的子类,将持有工作线程指针的实体类与其绑定,然后将其绑定在ImageView中。这样ImageView就间接绑定到了工作线程,确保了一致性。

创建一个BitmapDrawable的子类,绑定持有工作线程指针的实体类:

/**
 *
 * 用途:捆绑bitmaptask,用户处理并发问题(ListView,GridView)
 *
 * 保持ImageView与BitmapTask的一致性。
 */
public class AsyncDrawable extends BitmapDrawable {
    private WeakReference bitmapTaskWeakReference;

    /**
     *
     * @param res  资源
     * @param bitmap  预览的bitmap
     * @param task    执行任务的BitmapTask
     */
    public AsyncDrawable(Resources res, Bitmap bitmap, BitmapTask task) {
        super(res, bitmap);
        bitmapTaskWeakReference = new WeakReference(task);
    }

    /**
     * 用于检查ImageView相对应的BitmapTask
     * @return
     */
    public BitmapTask getBitmapTask() {
        if (bitmapTaskWeakReference != null) {
            return bitmapTaskWeakReference.get();
        }
        return null;
    }
}

ImageView执行Bitmap前,绑定BitmapDrawable子类。

   public static void loadBitmap(String  imageId, ImageView  iv){
       Drawable drawable= iv.getDrawable();
       if(drawable instanceof AsyncDrawable){
           AsyncDrawable  asyncDrawable=(AsyncDrawable) drawable;
           BitmapTask bitmapTask= asyncDrawable.getBitmapTask();
           if(bitmapTask!=null){
              if(!imageId.equals(bitmapTask.getImageId())){//开始加载bitmap
                  ThreadManager.getInstance().removeThread(bitmapTask,imageId);//取消先前加载的图片
                  ThreadManager.startThread(iv, imageId);//重新加载新的不同图片
              }
           }else{//asyncDrawable中的bitmaptask为空,新加载bitmap
               ThreadManager.startThread(iv, imageId);
           }
       }else{//imageview没有存有asyncdrawable
           ThreadManager.startThread(iv, imageId);
       }
    }

4.Caching Bitmaps:

说道缓存,首先想到手机的磁盘缓存。除了磁盘缓存,官方还提供了一种LruCache类来实现内存缓存
,内存缓存的好处是快速恢复。当手机屏幕旋转时,使用内存缓存能快速恢复bitmap。

LruCache类的配置,创建,添加,读取,移除key-bitmap类型数据:

   /**
     * 内存缓存,这里用于缓存Bitmap,内存缓存比磁盘缓存快速。
     * 在设备旋转时,用内存缓存恢复状态,比较快速,
     */
    private final LruCache bitmapLruCache;

    // 使用1/10的内存作为内存缓存的最大值,即LruCache的大小=app允许最大内存/10
    final int IMAGE_CACHE_SIZE = ((int) Runtime.getRuntime().maxMemory() / 1024) / 10;

    bitmapLruCache = new LruCache(IMAGE_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount() / 1024;
            }
        };  


     /**
     * 添加key-bitmap到内存缓存中。
     * @param bitmap
     * @param imageId
     */
    public void addLruCache(Bitmap bitmap, String imageId) {
        synchronized (bitmapLruCache) {
            if (bitmap != null && bitmapLruCache.get(imageId) == null) {
                bitmapLruCache.put(imageId, bitmap);
            }
        }
    }

    /**
     * 根据key从内存缓存中获取到Bitmap
     * @param imageId
     * @return
     */
    public Bitmap getBitmapFromLruCache(String imageId) {
        synchronized (bitmapLruCache) {
            return bitmapLruCache.get(imageId);
        }
    }
    /**
     * 根据key从内存缓存中移除Bitmap
     * @param imageId
     * @return
     */
    public void removeLruCahce(String imageId) {
        synchronized (bitmapLruCache) {
            if (bitmapLruCache.get(imageId) != null) {
                bitmapLruCache.remove(imageId);
            }
        }
    }

磁盘缓存,实际上是File存储。在Java se中File流是基础知识点,官方提供了DiskLruCache类,替开发者省掉了很多问题。
这里介绍DiskLruCache的操作,封装在一个操作类中:

public final class Constant {
    //允许10M bitmap值
    public static final int BITEMAPSIZE = 10 * 1024 * 1024;
    //bitmap储存的目录
    public static final String BITAMP_CACHE_FILE = "bitmapCacheFile";
    //允许的文件个数
    public static final int FILECOUNT = 1;
    //bitmap读写操作完成标志
    public static final int BITMAPFINISH = 0;
    //bitmap读写操作失败的标志
    public static final int BITMAPFARIL = FILECOUNT;

}  

public class BitmapUtils {

    public static DiskLruCache getDiskLruCache() {
        File cacheFile;
        DiskLruCache diskLruCache = null;
        try {
            cacheFile = MyUtils.getDireFile(BaseApplication.getBaseAppliction()  
                                 , Constant.BITAMP_CACHE_FILE);
            if (!cacheFile.exists()) {
                cacheFile.mkdirs();
            }
            diskLruCache = DiskLruCache.open(cacheFile, MyUtils.getAPPVerson()  
                                , Constant.FILECOUNT, Constant.BITEMAPSIZE);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return diskLruCache;
    }

    public static DiskLruCache.Editor getCacheEditor(DiskLruCache diskLruCache, String imageUrl) {
        String key = null;
        try {
            key = hashimageUrlForDisk(imageUrl);
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            return editor;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    //将bitmap写入磁盘中
    public static void writeBitmapToDisk(String imageUrl, Bitmap bitmap) {
        DiskLruCache diskLruCache = getDiskLruCache();
        BufferedOutputStream bufferedOutputStream = null;
        try {
            String key = hashimageUrlForDisk(imageUrl);
            DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
            if (snapshot == null && diskLruCache != null) {
                DiskLruCache.Editor editor = diskLruCache.edit(key);
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    bufferedOutputStream = new BufferedOutputStream(outputStream);
                    // bitmap 压缩 参数: 格式 ,压缩范围(0~100),流
                    boolean isSucess = bitmap.compress(Bitmap.CompressFormat.PNG,  
                                    100, bufferedOutputStream);
                    if (isSucess) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }
                    diskLruCache.flush();// 同步到日志
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedOutputStream != null) {
                try {
                    bufferedOutputStream.close();
                    bufferedOutputStream = null;
                } catch (Exception e) {
                }
            }
        }

    }
    //从磁盘中读取bitmap
    public static Bitmap readBitampFromDisk(String imageUrl) {
        DiskLruCache diskLruCache = getDiskLruCache();
        String key = hashimageUrlForDisk(imageUrl);
        InputStream in = null;
        Bitmap bitmap=null;
        try {
            DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
            if (snapshot != null) {
                in = snapshot.getInputStream(0);
              bitmap= BitmapFactory.decodeStream(in);
            }
        } catch (Exception e) {
            e.printStackTrace();
            bitmap=null;
        } finally {
            try {
                if (in != null) {
                    in.close();
                    in = null;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return  bitmap;

    }
    //md5加密路径,生成新的缓存需要用的key
    public static String hashimageUrlForDisk(String imageUrl) {
        String cacheimageUrl;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(imageUrl.getBytes());
            cacheimageUrl = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheimageUrl = String.valueOf(imageUrl.hashCode());
        }
        return cacheimageUrl;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }


    /**
     * 将bitmap编码成byte[]数据
     * @param bitmap
     * @return
     */
    public static byte[]  bitmapToByte(Bitmap bitmap){
        byte[] b=null;
        ByteArrayOutputStream outputStream=null;
        try{
            outputStream=new ByteArrayOutputStream();
            //将图片按100%压缩
            bitmap.compress(Bitmap.CompressFormat.PNG,100,outputStream);
            b= outputStream.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            try{
                if (outputStream!=null){
                    outputStream.close();
                }
            }catch ( Exception e){
                e.printStackTrace();
            }
        }
        return b;
    }


}

5.Displaying Bitmaps in Your UI:

Handler类提供线程间的通讯,将工作线程的执行的结果发送到绑定UI线程的Hanlder,
Handler进行ImageView加载bitmap操作。

这里,在线程池的操作类,添加绑定UI线程的Hanlder,便于更新UI

    /**
     * UI线程绑定的Handler
     */
    private final Handler uiHandler;

    private ThreadManager() {
        /**
         * 配置一个UI线程绑定的Handler,用来更新UI
         */
        uiHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case Constant.BITMAPFINISH:
                        BitmapTask task = (BitmapTask) msg.obj;
                        ImageView iv = task.getImageView();
                        if (iv != null) {
                            Drawable drawable = iv.getDrawable();
                            if (drawable instanceof AsyncDrawable) {
                                AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
                                //防止并发造成任务与imageview错乱.
                                if (task == asyncDrawable.getBitmapTask()) {
                                    iv.setImageBitmap(task.getBitmap());
                                }
                            }
                        }
                        //资源回收
                        recycle(task);
                        break;
                    default:
                        super.handleMessage(msg);
                }
            }
        };
    }  

     /**
     * 回收资源
     * @param task
     */
    public void recycle(BitmapTask task) {
        task.recycle();
        //将空闲的任务,重新添加到队列中。
        ourInstance.bitmapTasksQueue.offer(task);
    }

实际开发中,使用Bitmap思路分析:

  • 1.从缓存中读取图片:先从内存缓存中读取。
    若是从内存中读取到的Bitmap为空,则在磁盘缓存中读取。
    若是从磁盘中读取到的Bitmap为空,则从起始资源中读取

  • 2.开启工作线程,执行从起始资源中读取bitmap的任务

  • 3.计算合适比例,读取适屏的bitmap

  • 4.确保ImageView与任务线程的一致性,处理并发问题

  • 5.最后Hanlder更新UI

本项目代码:http://download.csdn.net/detail/hexingen/9672513

中文翻译资料:http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/index.html

官方资料:https://code.google.com/archive/p/android-imagedownloader/

相关知识点阅读:

  • 了解线程池的知识点,可以阅读线程池

  • 了解AsyncTask的知识点,可以阅读AsyncTask。

你可能感兴趣的:(Android,应用层开发)