Android——自定义简化版ImageLoader
来源:互联网 发布:genesis软件 编辑:程序博客网 时间:2024/06/06 05:42
在上一篇我们已经先后介绍了 Bitmap 的高效加载方式、LruCache 以及 DiskLruCache,那么我们就动手来写一个简化版的 ImageLoader 吧!!!
一般来说,一个优秀的 ImageLoader 应该具备如下功能:
- 图片的同步加载
- 图片的异步加载
- 图片的压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
声明一下,这里实现的 ImageLoader 并不是为了写一个框架,而是纯粹的加深下三级缓存以及图片的高效加载而写的一个小Demo。
1、图片压缩功能的实现
图片压缩在上一篇博客中已经做了介绍,这里就不在废话了,为了有良好的设计风格,这里单独抽象了一个类似于完成图片的压缩功能,这个类叫 ImageResizer,它的实现如下:
public class ImageResizer { private static final String TAG = "ImageResizer"; public ImageResizer() { } /** * 根据指定的资源文件以及指定的宽/高进行等比例缩放 * @param res * @param resId * @param reqWidth * @param reqHeight * @return */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } /** * 根据文件流的文件描述符以及指定的宽/高进行等比例缩放 * @param fileDescriptor * @param reqWidth * @param reqHeight * @return * */ public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); Log.e("00000000", reqWidth + "=====" + reqHeight + "======" + options.inSampleSize); options.inJustDecodeBounds = false; return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); } /** * 根据指定的宽/高进行 2 的指数缩放 * * @param options * @param reqWidth * @param reqHeight * @return */ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (reqHeight > 0 || reqHeight > 0) { if (width > reqHeight && height > reqHeight) { final int halfWidth = width / 2; final int halfHeight = height / 2; while (halfWidth / inSampleSize >= reqWidth && halfHeight / inSampleSize >= reqHeight) { inSampleSize *= 2; } } } return inSampleSize; }}
2、内存缓存的实现
public class MemoryCache { private static MemoryCache instance; private LruCache<String, Bitmap> mMemoryCache; private MemoryCache() { //获取当前进程的可用内存 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { //完成bitmap对象大小的计算 return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; } public static MemoryCache getInstance() { if (instance == null) { synchronized (MemoryCache.class) { if (instance == null) { instance = new MemoryCache(); } } } return instance; } public void addBitmapToMemoryCache(String uri, Bitmap bitmap) { String key = MD5.hashKeyFormUrl(uri); mMemoryCache.put(key, bitmap); } public Bitmap getBitmapFromMemoryCache(String key) { return mMemoryCache.get(key); }}
没什么好说的,略过…
3、磁盘缓存的实现
public class DiskCache { private static DiskCache instance; private final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //可缓存的大小 private DiskLruCache mDiskLruCache; public static final int DISK_CACHE_INDEX = 0; private final int IO_BUFFER_SIZE = 8 * 1024; //缓冲流的大小 private DiskCache(Context context) { File diskCacheDir = FilePath.getDiskCacheDir(context, "bitmap"); if (!diskCacheDir.exists()) { diskCacheDir.mkdirs(); } //判断当前sdk缓存目录可用大小是否满足我们设置的缓存大小 if (FilePath.getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); } catch (IOException e) { e.printStackTrace(); } } } public static DiskCache getInstance(Context context) { if (instance == null) { synchronized (DiskCache.class) { if (instance == null) { instance = new DiskCache(context); } } } return instance; } public Bitmap get(String uri) throws IOException { FileDescriptor fileDescriptor = getFileDescriptor(uri); if (fileDescriptor != null) { return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, 0, 0); } else { return null; } } public Bitmap get(String uri, int reqWidth, int reqHeight) throws IOException { FileDescriptor fileDescriptor = getFileDescriptor(uri); if (fileDescriptor != null) { return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight); } else { return null; } } public void edit(String uri) throws IOException { String key = MD5.hashKeyFormUrl(uri); DiskLruCache.Editor editor = null; //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null if (mDiskLruCache != null) { editor = mDiskLruCache.edit(key); } if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(uri, outputStream)) { editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } } public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.connect(); InputStream inputStream = urlConnection.getInputStream(); in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b = 0; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (Exception e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } private FileDescriptor getFileDescriptor(String uri) throws IOException { //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null if (mDiskLruCache != null) { String key = MD5.hashKeyFormUrl(uri); Bitmap bitmap = null; FileDescriptor fileDescriptor = null; DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DiskCache.DISK_CACHE_INDEX); fileDescriptor = fileInputStream.getFD(); } return fileDescriptor; } else { return null; } }}
在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需要的大小,一般是指用户的手机空间不足了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。
磁盘缓存的添加以及读取功能稍微复杂一些,这里在简单说明一下。磁盘缓存的添加需要通过 Editor 来完成,Editor 提供了 commit 和 abort 方法来提交和撤销对文件系统的写操作,具体实现请看 DiskCache 的 edit 方法。磁盘缓存的读取需要通过 Snapshot 来完成,通过 Snapshot 可以得到磁盘缓存对象对应的 FileInputStream,但是 FileInputStream 无法便捷地进行压缩,所以通过 FileDescriptor 来加载压缩后的图片,最后将加载后的 bitmap 添加到内存缓存中,具体实现请参看 DiskCache 的 get 方法
4、同步加载
public Bitmap loadBitmap(String uri, ImageView imageView) { Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromDiskCache(uri, imageView); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromHttp(uri, imageView); if (bitmap != null) { return bitmap; } return downloadBitmapFromUrl(uri); }
从 loadBitmap 的实现可以看出,其工作过程遵循如下几步:首先尝试从内存缓存中读取图片,接着尝试冲磁盘缓存中读取图片,最后才从网络中拉取图片。另外这个方法不能在主线程中调用,否则就抛出异常。这个执行环境的检测是在各种缓存获取方法中实现的,通过检测当前线程的 Looper 是否为主线程的 Looper 来判断当前线程是否是主线程,如果是主线程就直接抛出异常终止程序。如下所示:
if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network form UI Thread."); }
5、异步加载
public void bindBitmap(final String uri, final ImageView imageView) { imageView.setTag(TAG_KEY_URI, uri); Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap = loadBitmap(uri, imageView); if (bitmap != null) { LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); }
从 bindBitmap 的实现来看,bindBitmap 方法会尝试从内存缓存中获取图片,如果获取成功就直接返回结果,否则会在线程池中去调用 loadBitmap 方法,当图片加载成功后再将图片、图片的地址以及需要绑定的 ImageView 封装成一个 LoaderResult 对象,然后再通过 mMainHandler 向主线程发送一个消息,这样就可以在主线程中给 ImageView 设置图片了,之所以通过 Handler 来中转是因为子线程无法访问 UI。
bindBitm 中用到了线程池和 Handler,这里看一下它们的实现,首先看线程池 THREAD_POOL_EXECUTOR 的实现,如下所示。可以看出它的核心线程数为当前设备的 CPU 核心数 +1,最大容量为 CPU 核心数的 2 倍 +1,线程闲置超时时长为 10 秒,关于线程池的详细介绍我们在 Android——线程和线程池 已经详细介绍过。
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; //核心线程数量 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //最大线程数量 private static final int KEEP_ALIVE = 10; //存活时间 private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(); //等待队列 public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
之所以采用线程池是有原因的,首先肯定不能采用普通的线程去做这个事,线程池的好处上篇博客已经说明。如果直接采用普通的线程去加载图片,随着列表的滑动这可能会产生大量的线程,这样并不利于整体效率的提升。另外一点,这里也没有选择采用 AsyncTask,AsyncTask 封装了线程池和 Handler,按道理它应该适合 ImageLoader 的场景。从 Android——线程和线程池 我们对 AsyncTask 的分析可以知道,AsyncTask 在 3.0 的低版本和高版本上具有不同的表现,在 3.0 以上的版本 AsyncTask 无法实现并发的效果,这显然是不能接受的,因为 ImageLoader 就是需要并发特性,虽然可以通过改造 AsyncTask 或者使用 AsyncTask 的 executeOnExecutor 方法的形式来执行异步任务,但是这总归是不太自然的实现f方式。鉴于以上两点原因,这里选择线程池和 Handler 来提供 ImageLoader 的并发能力和访问 UI 的能力。
分析完线程池的选择,下面看一下 Handler 的实现,如下所示。ImageLoader 直接采用 主线程的 Looper 来构造 Handler 对象,这就使得 ImageLoader 可以在非主线程中构造了。另外为了解决由于 View 复用导致的列表错位这一问题,在给 ImageView 设置图片之前都会检查它的 url 有没有发生改变,如果发生改变就不在给它设置图片,这样就解决了列表错位的问题。
private Handler mMainHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; String uri = (String) imageView.getTag(TAG_KEY_URI); if (uri.equals(result.uri)) { imageView.setImageBitmap(result.bitmap); } else { Log.w(TAG, "set image bitmap,but url has changed, ignored!"); } } };
到此为止,ImageLoader 的细节都已经做了全面的分析,下面是 ImageLoader的完整代码:
public class ImageLoader { private static ImageLoader instance; private Context mContext; private final int TAG_KEY_URI = R.id.imageloader_uri; private final String TAG = "ImageLoader"; private int MESSAGE_POST_RESULT = 1; private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; //核心线程数量 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //最大线程数量 private static final int KEEP_ALIVE = 10; //存活时间 private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(); //等待队列 public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); //更新 ImageView private Handler mMainHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; String uri = (String) imageView.getTag(TAG_KEY_URI); if (uri.equals(result.uri)) { imageView.setImageBitmap(result.bitmap); } else { Log.w(TAG, "set image bitmap,but url has changed, ignored!"); } } }; private ImageLoader() { } public static ImageLoader getInstance() { if (instance == null) { synchronized (ImageLoader.class) { if (instance == null) { instance = new ImageLoader(); } } } return instance; } public void init(Context context) { this.mContext = context; } /** * 异步加载 * @param uri * @param imageView */ public void bindBitmap(final String uri, final ImageView imageView) { imageView.setTag(TAG_KEY_URI, uri); Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap = loadBitmap(uri, imageView); if (bitmap != null) { LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); } /** * 同步加载 * @param uri * @param imageView * @return */ public Bitmap loadBitmap(String uri, ImageView imageView) { Bitmap bitmap = loadBitmapFromMemCache(uri); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromDiskCache(uri, imageView); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromHttp(uri, imageView); if (bitmap != null) { return bitmap; } return downloadBitmapFromUrl(uri); } /** * 从网络获取bitmap * @param urlString * @return */ private Bitmap downloadBitmapFromUrl(String urlString) { if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network form UI Thread."); } return DownLoad.downLoadBitmapFromUrl(urlString); } /** * 从网络获取bitmap,并添加到磁盘缓存 * @param uri * @param imageView * @return */ private Bitmap loadBitmapFromHttp(String uri, ImageView imageView) { if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network form UI Thread."); } try { DiskCache.getInstance(mContext).edit(uri); return loadBitmapFromDiskCache(uri, imageView); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 从磁盘缓存中获取bitmap * @param uri * @param imageView * @return */ private Bitmap loadBitmapFromDiskCache(String uri, ImageView imageView) { if (Looper.myLooper() == Looper.getMainLooper()) { throw new RuntimeException("can not visit network form UI Thread."); } Bitmap bitmap = null; try { bitmap = DiskCache.getInstance(mContext).get(uri, imageView.getWidth(), imageView.getHeight()); if (bitmap != null) { MemoryCache.getInstance().addBitmapToMemoryCache(uri, bitmap); } } catch (IOException e) { e.printStackTrace(); } return bitmap; } /** * 从内存缓存中获取bitmap * @param uri * @return */ private Bitmap loadBitmapFromMemCache(String uri) { final String key = MD5.hashKeyFormUrl(uri); return MemoryCache.getInstance().getBitmapFromMemoryCache(key); } /** * 返回结果的封装 */ private static class LoaderResult { public ImageView imageView; public String uri; public Bitmap bitmap; public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) { this.imageView = imageView; this.uri = uri; this.bitmap = bitmap; } }}
其他相关类:
//MD5public class MD5 { public static String hashKeyFormUrl(String url){ String cacheKey = url; try { MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(url.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return cacheKey; } 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(); }}
//路径获取相关类 FilePathpublic class FilePath { public static File getDiskCacheDir(Context context, String uniqueName) { boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); final String cachePath; if (externalStorageAvailable) { cachePath = context.getExternalCacheDir().getParent(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } public static long getUsableSpace(File path){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){ return path.getUsableSpace(); } final StatFs statFs = new StatFs(path.getPath()); return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong(); }}
源码下载地址:
http://download.csdn.net/detail/akaic/9659585
- Android——自定义简化版ImageLoader
- Imageloader简化版
- Android 自定义 ImageLoader
- Android开发-自定义view-简化版自定义开关
- Android 图片加载 —— ImageLoader
- 自定义图片..简化版
- Android之加载网络图片—ImageLoader
- android图片缓存实现(自定义ImageLoader)
- 自定义ImageLoader
- 自定义ImageLoader
- 自定义ImageLoader
- ImageLoader—loadImage()
- Android-View自定义属性-简化写法
- android graphic(16)—fence(简化)
- android SlidingMenu简化版
- Android应用开发:ImageLoader小陷阱——同一个URI
- Android——universal-imageloader开源库的使用
- Android图片加载库—Universal-ImageLoader用法详解
- java进阶学习路day10
- 修改centos等linux的hostname-永久生效
- 72. Edit Distance , LeetCode
- Linux 关机重启命令 shutdown reboot init
- 【ps】修复老照片思路
- Android——自定义简化版ImageLoader
- 基于邻接表的有回溯最小堆优化的单源最短路Dijkstra算法
- 最近一段时间要做的事情就是它了——Java服务器端的相关技术
- 7-Java类加载和对象创建过程
- 【LeetCode】64. Minimum Path Sum
- Java实现四舍五入的方法
- MySQL 列-如何选择类型
- 【题(注意)】【图论】NKOJ3056 导航
- 【PS】制作水彩画效果