在android开发过程图片加载和显示基本上是每个项目中都会包含的功能,这就导致每个项目里面ImageLoader是标配。当然我们在使用的过程中有很多牛逼的(性能好,使用简单方便)开源框架可供挑选。但是如果自己手动实现一个高效的ImageLoader那给自己的技术树里面又添加了一个靓丽的枝干。ok,接下来我们一起来分析和探讨一下高效ImageLoder的实现。
一般来说,优秀的ImageLoader都具有以下几个共性:
虽然我们可能做不到优秀,但是我们也得往这个目标和方向上使劲。所以接下来的实现中,我们也会尽力去做到这些。
前两年很多同行使用软引用和弱引用来实现图片的多级缓存,但是现在使用软引用和弱引用已经变得不再可靠,它主要存在以下几点弊端和风险
/** * 图片异步加载 */ public void bind(String resource,ImageView imageView,int reqWidth,int reqHeight){ } /** *图片同步加载 */ public Bitmap load(String resource,int reqWidth,int reqHeight){ Bitmap bitmap = null; return bitmap; }两种加载方式的实现,我们接下来一步一步地写。图片使用和加载的效率有高到低的顺序为: 内存缓存 >SD卡缓存>网络请求
<span style="white-space:pre"> </span>/** * 内存缓存大小 */ private int memoryCacheSize; /** * SD卡缓存大小 */ private long diskCacheSize; /** * SD卡缓存路径 */ private String diskCachePath;好了,有个这个配置类,我们可以编写我们的初始化配置方法了:
/** * 初始化方法 * * @param config 图片加载框架相关配置 * @param context 上下文 */ public void init(ImgLoaderConfig config, Context context) { if (null == context) { throw new IllegalArgumentException("the context could not is null"); } if (config.getMemoryCacheSize() <= 0) { int maxMemory = (int) Runtime.getRuntime().maxMemory() / 1024; memoryCacheSize = maxMemory / 8; } else { memoryCacheSize = config.getMemoryCacheSize(); } if (config.getDiskCacheSize() <= 0) { diskCacheSize = DISK_CACHE_SIZE; } else { diskCacheSize = config.getDiskCacheSize(); } mMemoryCache = new LruCache<String, Bitmap>(memoryCacheSize) { @Override protected int sizeOf(String key, Bitmap value) { //计算bitmap所占内存,使用高版本api时可以使用 value.getByteCount(); return value.getRowBytes() * value.getHeight() / 1024; } }; if (TextUtils.isEmpty(config.getDiskCachePath())) { diskCachePath = context.getCacheDir().getAbsolutePath(); } else { diskCachePath = config.getDiskCachePath(); } File file = new File(diskCachePath); if (!file.exists()) { file.mkdirs(); } try { mDiskLruCache = DiskLruCache.open(file, 1, 1, diskCacheSize); } catch (IOException e) { e.printStackTrace(); } }
/** * 将bitmap加入内存缓存中 * * @param key 缓存的key * @param bitmap 待缓存的bitmap */ private void addImg2Memory(String key, Bitmap bitmap) { if (null == mMemoryCache) { throw new IllegalArgumentException("the memoryCache could not be null"); } if (null == mMemoryCache.get(key)) { mMemoryCache.put(key, bitmap); } } /** * 根据key从内存缓存中获取bitmap * * @param key 缓存的key * @return 缓存的bitmap */ private Bitmap loadImgFromMemory(String key) { if (null == mMemoryCache) { throw new IllegalArgumentException("the memoryCache could not be null"); } if (mMemoryCache.size() < 1) { return null; } Bitmap bitmap = mMemoryCache.get(key); return bitmap; }
/** * 将图片存入SD卡 * * @param key 存取的key * @param inputStream 文件输入流 */ private void addImg2Disk(String key, InputStream inputStream) { String MD5key = MD5Util.getMD5Str(key); try { DiskLruCache.Editor editor = mDiskLruCache.edit(MD5key); OutputStream outputStream = editor.newOutputStream(0); if (writeImgToDisk(outputStream, inputStream)) { editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } /** * 将图片写入SD卡 * * @param outputStream 文件的输出流 * @param inputStream 数据的输入流 * @return 写入操作是否成功 */ private boolean writeImgToDisk(OutputStream outputStream, InputStream inputStream) { BufferedInputStream bis = new BufferedInputStream(inputStream); BufferedOutputStream bos = new BufferedOutputStream(outputStream); int b; try { while ((b = bis.read()) != -1) { bos.write(b); } bis.close(); bos.flush(); bos.close(); return true; } catch (IOException e) { e.printStackTrace(); } return false; } /** * 从SD卡内加载所需图片 * @param key 存取key * @param reqWidth * @param reqHeight * @return */ private Bitmap loadImgFromDisk(String key, int reqWidth, int reqHeight) { Bitmap bitmap = null; String MD5key = MD5Util.getMD5Str(key); try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(MD5key); if (null != snapshot) { //该处传入参数0的意义不做赘述,技术细节延后讨论 FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0); FileDescriptor fd = fileInputStream.getFD(); bitmap = ImgResizer.decodeFromFileDescriptor(fd, reqWidth, reqHeight); if (null != bitmap) { addImg2Memory(key, bitmap); } } } catch (IOException e) { e.printStackTrace(); } return bitmap; }如果在SD卡缓存内同样没有找到图片,则需要从网络进行加载(在这里我们不做网络加载的优化的说明和探讨,只给出最简单的网络请求加载方式。网络请求的优化包括使用线程池调度请求和断点续传等留待下次专门开一篇博客进行探讨)
/** * 从网络获取图片 * @param url 获取图片的链接 * @return 网络请求得到的输入流 */ private InputStream reqImgFromHttp(String url) { try { URL httpUrl = new URL(url); InputStream inputStream = httpUrl.openConnection().getInputStream(); return inputStream; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; }至此,我们的三级缓存是编写完了。我们来完善我们最开始定义的同步加载和异步加载的方法(这里只对异步加载的方法进行说明,同步加载就不做赘述了):
/** * 异步的方式将图片绑定到控件上 * * @param source 图片来源 * @param img 需要绑定的控件 */ public void bind(final String source, final ImageView img, final int reqWidth, final int reqHeight) { if (TextUtils.isEmpty(source) || img == null) { return; } Bitmap bitmap = loadImgFromMemory(source); if (null != bitmap) { img.setImageBitmap(bitmap); } else { bitmap = loadImgFromDisk(source, reqWidth, reqHeight); if (null != bitmap) { img.setImageBitmap(bitmap); } else { new Thread(new Runnable() { @Override public void run() { InputStream inputStream = reqImgFromHttp(source); addImg2Disk(source, inputStream); final Bitmap bitmap = ImgResizer.decodeFromStream(inputStream, reqWidth, reqHeight); if (bitmap == null) { return; } addImg2Memory(source, bitmap); Looper looper = Looper.getMainLooper(); Handler handler = new Handler(looper); handler.post(new Runnable() { @Override public void run() { img.setImageBitmap(bitmap); } }); } }).start(); } } }
package com.york.devbase.imgload; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.io.FileDescriptor; import java.io.InputStream; /** * Created by york_zhang on 2016/3/24. */ public class ImgResizer { /** * 计算bitmap的缩放比例 * * @param options bitmap的原始相关信息 * @param reqWidth 所需的图片的宽 * @param reqHeight 所需的图片的高 * @return bitmap的缩放比例 */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { if (reqWidth == 0 || reqHeight == 0) { return 1; } final int width = options.outWidth; final int height = options.outHeight; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { int halfWidth = width / 2; int halfHeight = height / 2; /** * 确保缩放比例能同时适用控件的宽高 * */ while ((halfWidth / inSampleSize) > reqWidth) { inSampleSize++; } while ((halfHeight / inSampleSize) > reqHeight) { inSampleSize++; } } return inSampleSize; } /** * 从文件内读取bitmap * ps:该处不调用BitmapFactory.decodeFile()原因在于,FileInputStream是一种有序的文件流, * 两次调用decode方法会影响文件流的位置属性,导致第二次调用的时候得到的bitmap为null * * @param fd 文件 * @param reqWidth 需要加载图片的宽度 * @param reqHeight 需要加载图片的高度 * @return 缩放后的bitmap */ public static Bitmap decodeFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); /** * 设置inJustDecodeBounds为true,只读取bitmap的相关参数,不会真正解析bitmap */ options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fd, null, options); int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inSampleSize = inSampleSize; /** * 设置inJustDecodeBounds为false,真正解析bitmap */ options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options); return bitmap; } /** * 从流内解析并按需求压缩bitmap * * @param inputStream 待解析的流 * @param reqWidth 需要加载图片的宽度 * @param reqHeight 需要加载图片的高度 * @return 缩放后的bitmap */ public static Bitmap decodeFromStream(InputStream inputStream, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); /** * 设置inJustDecodeBounds为true,只读取bitmap的相关参数,不会真正解析bitmap */ options.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, options); int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inSampleSize = inSampleSize; /** * 设置inJustDecodeBounds为false,真正解析bitmap */ options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); return bitmap; } }下面我们看一下这个框架的使用(使用起来特别方便):
<span style="white-space:pre"> </span>ImgLaodManager imgLaodManager = new ImgLaodManager(); imgLaodManager.init(new ImgLoaderConfig(),this); imgLaodManager.bind("http://upload.news.cecb2b.com/2014/1209/1418100017289.jpg", mImageView,ScreenUtils.dip2px(100),ScreenUtils.dip2px(100<span style="white-space:pre"> </span>));