/** * LocalImageLoader.java * ImageChooser * * Created by likebamboo on 2014-4-22 * Copyright (c) 1998-2014 http://likebamboo.github.io/ All rights reserved. */ package com.likebamboo.imagechooser.loader; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Point; import android.os.Handler; import android.os.Message; import android.support.v4.util.LruCache; import android.text.TextUtils; import android.widget.ImageView; import com.likebamboo.imagechooser.ICApplication; import com.likebamboo.imagechooser.utils.DeviceUtil; import java.io.File; import java.io.FileInputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /** * <p> * 本地图片加载器,采用的是异步解析本地图片,使用单例模式,线程池加载图片 * <p> * 自己写图片加载调度算法: * <p> * 问题描述:线程池虽然可以同时加载好几张图片,但是当加载任务超过线程池的最大线程数时,加载任务仍然会表显出一定的顺序性。 * <p> * 这样就导致了一个问题,当我滑动到一个列表的底部时,可能需要等很久才看到图片加载出来。 那么可不可以想一种方法优先加载可视化区域的图片呢? * <p> * 于是,我决定自己维护一个请求列表,根据请求的不同优先级加载图片,具体是这样的。 * <p> * 首先判断图片是否在内存缓存中,如果有,直接从内存中取出图片。如果没有,构建一个图片请求对象,加入到请求列表。 * (如果列表中原来有这个请求,删除原来的,将新的加入到列表末尾,这样可以保证该请求的优先级高) * * <pre> * 线程池按照如下的规则处理图片请求列表: * <p> * 1.如果请求列表中的请求数量小于线程池中空闲的线程数,顺序的将请求分配给线程池中空闲线程(这时候请求列表中的所有请求同时得到执行) * <p> * 2.如果请求列表中的请求数量大于线程池中空闲的线程数,将空闲的线程的分配给优先级高的请求(优先级根据请求的先后顺序来定,后请求的优先级高) * <p> * 当某个请求被执行完后,从请求列表中删除请求任务,同时,如果请求列表中还有未处理完的任务,继续按照上述规则处理请求。 * * @author likebamboo */ public class LocalImageLoader { /** * 内存Lru缓存 */ private LruCache<String, Bitmap> mMemoryCache = null; /** * 单例 */ private static LocalImageLoader mInstance = new LocalImageLoader(); /** * 创建一个固定线程数的线程池 */ private ThreadPoolExecutor mThreadPool = (ThreadPoolExecutor)Executors.newFixedThreadPool(3); /** * 图片请求列表,用于调度 */ private ArrayList<ImageRequest> mImagesList = new ArrayList<ImageRequest>(); /** * 处于正在请求状态的请求列表 */ private ArrayList<ImageRequest> mOnLoadingList = new ArrayList<ImageRequest>(); /** * 是否正处于调度状态 */ private boolean onDispath = false; private LocalImageLoader() { // 获取应用程序的最大内存 final int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024); // 用最大内存的1/4来存储图片 final int cacheSize = maxMemory / 4; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { // 获取每张图片的大小 @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; } /** * 通过此方法来获取实例 * * @return */ public static LocalImageLoader getInstance() { return mInstance; } /** * 加载本地图片,对图片不进行裁剪 * * @param path * @param mCallBack * @return */ public Bitmap loadImage(final String path, final ImageCallBack mCallBack) { return this.loadImage(path, null, mCallBack); } /** * 此方法来加载本地图片,这里的mPoint是用来封装ImageView的宽和高,我们会根据ImageView控件的大小来缩放Bitmap * * @param path * @param point * @param callBack * @return */ @SuppressLint("HandlerLeak") public Bitmap loadImage(final String path, final Point point, final ImageCallBack callBack) { // 先获取内存中的Bitmap Bitmap bitmap = getBitmapFromMemCache(path); // 若该Bitmap不在内存缓存中,则将其加入到调度任务列表中 if (bitmap == null) { addImageRequest(new ImageRequest(path, point, callBack)); } return bitmap; } /** * 添加图片请求任务 */ private void addImageRequest(ImageRequest item) { if (null == item || TextUtils.isEmpty(item.getPath())) { return; } synchronized (mImagesList) { mImagesList.remove(item); mImagesList.add(item); } // 如果当前不处于调度状态,开始调度 if (!onDispath) { dispatch(); } } /** * 删除图片请求任务 * * @param path */ private void removeImageRequest(ImageRequest item) { if (item == null || TextUtils.isEmpty(item.getPath())) { return; } synchronized (mImagesList) { mImagesList.remove(item); if (mImagesList.size() > 0) { dispatch(); } else { // 没有请求了,中止调度 onDispath = false; } } } /** * 任务调度 */ private void dispatch() { // 开始调度 onDispath = true; // 如果当前线程池已满 ,不再处理请求任务 if (mThreadPool.getActiveCount() >= mThreadPool.getCorePoolSize()) { return; } // 空闲线程的数量 int spareThreads = mThreadPool.getCorePoolSize() - mThreadPool.getActiveCount(); // 如果请求列表中数量小于空闲的线程数,顺序处理请求 synchronized (mImagesList) { if (mImagesList.size() < spareThreads) { for (ImageRequest item : mImagesList) { execute(item); } } else { // 否则从后面(优先级高)开始处理请求 for (int i = mImagesList.size() - 1; i >= mImagesList.size() - spareThreads; i--) { execute(mImagesList.get(i)); } } } } /** * 执行加载图片任务 * * @param request */ private void execute(final ImageRequest request) { // 如果该图片正在请求,忽略之。 if (mOnLoadingList.contains(request)) { return; } final ImageHandler handler = new ImageHandler(this, request); mThreadPool.execute(new Runnable() { @Override public void run() { mOnLoadingList.add(request); Point size = request.getSize(); if (size == null || size.x == 0 || size.y == 0) { size = DeviceUtil.getDeviceSize(ICApplication.getContext()); } // 先获取图片的缩略图 Bitmap mBitmap = decodeThumbBitmapForFile(request.getPath(), size.x, size.y, false); Message msg = handler.obtainMessage(); msg.obj = mBitmap; handler.sendMessage(msg); mOnLoadingList.remove(request); // 将图片加入到内存缓存 addBitmapToMemoryCache(request.getPath(), mBitmap); } }); } /** * 往内存缓存中添加Bitmap * * @param key * @param bitmap */ private void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null && bitmap != null) { synchronized (mMemoryCache) { mMemoryCache.put(key, bitmap); } } } /** * 根据key来获取内存中的图片 * * @param key * @return */ private Bitmap getBitmapFromMemCache(String key) { Bitmap bitmap = null; // 先从硬引用缓存中获取 synchronized (mMemoryCache) { bitmap = mMemoryCache.get(key); if (bitmap != null) { // 找到该Bitmap之后,将其移到LinkedHashMap的最前面,保证它在LRU算法中将被最后删除。 mMemoryCache.remove(key); mMemoryCache.put(key, bitmap); return bitmap; } } return null; } /** * 根据View(主要是ImageView)的宽和高来获取图片的缩略图 * * @param path * @param viewWidth * @param viewHeight * @param isHighQuality 是否质量高 * @return */ private Bitmap decodeThumbBitmapForFile(String path, int viewWidth, int viewHeight, boolean isHighQuality) { File f = new File(path); if (!f.exists()) { return null; } BitmapFactory.Options options = new BitmapFactory.Options(); // 设置为true,表示解析Bitmap对象,该对象不占内存 options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); // 设置缩放比例 options.inSampleSize = computeScale(options, viewWidth, viewHeight); if (!isHighQuality) { // PS:为了减少内存,可以将缩放比例调的更大一些,这样就不会导致系统频繁GC的情况了 options.inSampleSize += options.inSampleSize / 2 + 2; } // 图片质量 options.inPreferredConfig = Config.RGB_565; // 设置为false,解析Bitmap对象加入到内存中 options.inJustDecodeBounds = false; options.inPurgeable = true; options.inInputShareable = true; // 获取资源图片 try { return BitmapFactory.decodeStream(new FileInputStream(f), null, options); } catch (Exception e) { e.printStackTrace(); return null; } } /** * <p> * 该方法来自google的图片缓存Demo * <p> * 请查看:http://developer.android.com/training/displaying-bitmaps/index.html * <p> * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} * object when decoding bitmaps using the decode* methods from * {@link BitmapFactory}. This implementation calculates the closest * inSampleSize that will result in the final decoded bitmap having a width * and height equal to or larger than the requested width and height. This * implementation does not ensure a power of 2 is returned for inSampleSize * which can be faster when decoding but results in a larger bitmap which * isn't as useful for caching purposes. * * @param options An options object with out* params already populated (run * through a decode* method with inJustDecodeBounds==true * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @return The value to be used for inSampleSize */ private int computeScale(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; // This offers some additional logic in case the image has a strange // aspect ratio. For example, a panorama may have a much larger // width than height. In these cases the total pixels might still // end up being too large to fit comfortably in memory, so we should // be more aggressive with sample down the image (=larger // inSampleSize). final float totalPixels = width * height; // Anything more than 2x the requested pixels we'll sample down // further final float totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { inSampleSize++; } } return inSampleSize; } private static class ImageHandler extends Handler { private final WeakReference<LocalImageLoader> mActivity; private final WeakReference<ImageRequest> mRequest; public ImageHandler(LocalImageLoader activity, ImageRequest request) { mActivity = new WeakReference<LocalImageLoader>(activity); mRequest = new WeakReference<ImageRequest>(request); } @Override public void handleMessage(Message msg) { ImageRequest request = mRequest.get(); if (request != null) { LocalImageLoader activity = mActivity.get(); if (activity != null) { activity.removeImageRequest(request); } request.getCallBack().onImageLoader((Bitmap)msg.obj, request.getPath()); } } } /** * The default implementation of ImageListener which handles basic functionality * of showing a default image until the network response is received, at which point * it will switch to either the actual image or the error image. * @param view The imageView that the listener is associated with. * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. */ public static ImageCallBack getImageListener(final ImageView view, final Object tag, final int defaultImageResId, final int errorImageResId) { if (view != null) { view.setImageResource(defaultImageResId); view.setTag(tag); } return new ImageCallBack() { @Override public void onImageLoader(Bitmap bitmap, String path) { if (view == null || !("" + path).equals(view.getTag())) { return; } if (bitmap != null) { view.setImageBitmap(bitmap); } else { view.setImageResource(errorImageResId); } } }; } /** * 加载本地图片的回调接口 */ public interface ImageCallBack { /** * 当子线程加载完了本地的图片,将Bitmap和图片路径回调在此方法中 * * @param bitmap * @param path */ public void onImageLoader(Bitmap bitmap, String path); } }