Android自定义图片加载框架
大神原网址: http://blog.csdn.net/lmj623565791/article/details/41874561
思路:
1. 压缩图片
压缩本地图片: 获得imageview想要显示的大小 -> 设置合适的inSampleSize
压缩网络图片:
a. 硬盘缓存开启 -> 直接下载存到sd卡,然后采用本地的压缩方案
b. 硬盘缓存关闭 -> 使用BitmapFactory.decodeStream(is, null, opts);
2. 图片加载架构
图片压缩加载完 -> 放入LruCache -> 设置到ImageView上
1、单例,包含一个LruCache用于管理我们的图片;
2、任务队列,我们每来一次加载图片的请求,我们会封装成Task存入我们的TaskQueue;
3、包含一个后台线程,这个线程在第一次初始化实例的时候启动,然后会一直在后台运行;
任务呢?还记得我们有个任务队列么,有队列存任务,得有人干活呀;
所以,当每来一次加载图片请求的时候,我们同时发一个消息到后台线程,后台线程去使用线程池去TaskQueue去取一个任务执行;
4、调度策略;3中说了,后台线程去TaskQueue去取一个任务,这个任务不是随便取的,有策略可以选择,一个是FIFO(先进先出),一个是LIFO(后进先出),我倾向于后者。
代码简析:
三个主要文件:
ImageSizeUtil.java
package com.carloz.diy.imageloader; import android.graphics.BitmapFactory; import android.util.DisplayMetrics; import android.view.ViewGroup; import android.widget.ImageView; /** * Created by root on 15-11-13. */ public class ImageSizeUtil { public static class ImageSize { int width; int height; } /** * 获得imageview想要显示的大小 * * 可以看到,我们拿到imageview以后: * 首先企图通过getWidth获取显示的宽;有些时候,这个getWidth返回的是0; * 那么我们再去看看它有没有在布局文件中书写宽; * 如果布局文件中也没有精确值,那么我们再去看看它有没有设置最大值; * 如果最大值也没设置,那么我们只有拿出我们的终极方案,使用我们的屏幕宽度; * 总之,不能让它任性,我们一定要拿到一个合适的显示值。 * 可以看到这里或者最大宽度,我们用的反射,而不是getMaxWidth(); * 维萨呢,因为getMaxWidth竟然要API 16,我也是醉了;为了兼容性,我们采用反射的方案。反射的代码就不贴了。 * @param imageView * @return imageView的大小 */ public static ImageSize getImageViewSize(ImageView imageView) { ImageSize imageSize = new ImageSize(); DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics(); ViewGroup.LayoutParams layoutParams = imageView.getLayoutParams(); int width = imageView.getWidth(); if(width <= 0) width = layoutParams.width; // if(width <= 0) width = imageView.getMaxWidth(); if(width <= 0) width = displayMetrics.widthPixels; int height = imageView.getHeight(); if(height <= 0) height = layoutParams.height; // if(height <= 0) height = imageView.getMaxHeight(); if(height <= 0) height = displayMetrics.heightPixels; imageSize.width = width; imageSize.height = height; return imageSize; } /** * 计算BitmapFactory.Options options 中的 inSampleSize, 加载图片的大小的重要参数 * 根据需求的宽和高以及图片实际的宽和高计算inSampleSize * 1. 如果 inSampleSize > 1,返回一个较小的图像保存在内存中. * 例如,insamplesize = = 4返回一个图像是1 / 4的宽度/高度的图像 * 2. 如果 inSampleSize <= 1, 返回原始图像 * @param options 保存着实际图片的大小 * @param reqWidth 压缩后图片的 width * @param reqHeight 压缩后图片的 height * @return options里面存了实际的宽和高;reqWidth和reqHeight就是我们之前得到的想要显示的大小; * 经过比较,得到一个合适的inSampleSize; */ public static int caculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { int inSampleSize = 1; int width = options.outWidth, height=options.outHeight; if(width > reqWidth || height > reqHeight) { int widthRadio = Math.round(width/reqWidth); int heightRadio = Math.round(height/reqHeight); inSampleSize = Math.max(widthRadio, heightRadio); } return inSampleSize; } }
DownloadImgUtils.java
package com.carloz.diy.imageloader; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.widget.ImageView; import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; /** * Created by root on 15-11-16. */ public class DownloadImgUtils { public static Bitmap downloadImageByUrl(String imgUrl, ImageView imageView) { if (null == imgUrl) return null; try { URL url = new URL(imgUrl); HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); InputStream is = new BufferedInputStream(httpConn.getInputStream()); is.mark(is.available()); // 在InputStream中设置一个标记位置. // 参数readlimit 表示多少字节可以读取. // 调用reset()将重新流回到标记的位置 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); // 获取imageview想要显示的宽和高 ImageSizeUtil.ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView); options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options, imageSize.width, imageSize.height); options.inJustDecodeBounds = false; is.reset(); // Resets this stream to the last marked location. Bitmap bitmap = BitmapFactory.decodeStream(is, null, options); httpConn.disconnect(); is.close(); return bitmap; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 根据url下载图片在指定的文件 * @param urlStr * @param file * @return */ public static boolean downloadImageByUrl(String urlStr, File file) { FileOutputStream fos = null; InputStream is = null; try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); is = conn.getInputStream(); fos = new FileOutputStream(file); byte[] buf = new byte[512]; int len = 0; while ((len = is.read(buf)) != -1) { fos.write(buf, 0, len); } fos.flush(); return true; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } try { if (fos != null) fos.close(); } catch (IOException e) { e.printStackTrace(); } } return false; } }
MyImageLoader.java
package com.carloz.diy.imageloader; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.util.LruCache; import android.widget.ImageView; import java.io.File; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.LinkedList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * Created by root on 15-11-13. */ public class MyImageLoader { public static final String TAG = "MyImageLoader"; private static MyImageLoader mInstance; private LruCache<String, Bitmap> mLruCache; // 图片缓存的核心对象 private ExecutorService mThreadPool; // 线程池 private static final int DEFAULT_THREAD_COUNT = 1; private LinkedList<Runnable> mTaskQueue; // 任务队列 private Thread mBackstageThread; // 后台轮询线程 private Handler mBackstageThreadHandler; // Semaphore, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。 // 也是操作系统中用于控制进程同步互斥的量。 // Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。 // 停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用 private Semaphore mBackstageThreadSemaphore; private Semaphore mBackstageThreadHandlerSemaphore = new Semaphore(0); private static final Object syncObject = new Object(); // 单例模式 && synchronized public enum QueueType {FIFO, LIFO} private QueueType mType = QueueType.LIFO; private boolean isDiskCacheEnable = true; // 硬盘缓存可用 // UI Thread private Handler mUIHandler; /** * 单例模式 * * @param threadCount * @return */ public static MyImageLoader getInstance(int threadCount, QueueType type) { if (mInstance == null) { synchronized (syncObject) { if (mInstance == null) { mInstance = new MyImageLoader(threadCount, type); } } } return mInstance; } public static MyImageLoader getInstance() { if (mInstance == null) { synchronized (syncObject) { if (mInstance == null) { mInstance = new MyImageLoader(DEFAULT_THREAD_COUNT, QueueType.LIFO); } } } return mInstance; } private MyImageLoader(int threadCount, QueueType type) { init(threadCount, type); } private void init(int threadCount, QueueType type) { initBackThread(); // get the max available memory int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheMemory = maxMemory / 8; // 继承LruCache时,必须要复写sizeof方法,用于计算每个条目的大小 // the size of bitmap can not over cacheMemory mLruCache = new LruCache<String, Bitmap>(cacheMemory) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; // create thread pool that // reuses a fixed number of threads operating off a shared unbounded queue. mThreadPool = Executors.newFixedThreadPool(threadCount); mTaskQueue = new LinkedList<>(); mType = type; mBackstageThreadSemaphore = new Semaphore(threadCount); } /** * 初始化后台轮询线程 */ private void initBackThread() { mBackstageThread = new Thread() { @Override public void run() { Looper.prepare(); mBackstageThreadHandler = new Handler() { @Override public void handleMessage(Message msg) { mThreadPool.execute(getTask()); // 线程池去取出一个任务进行执行 try { mBackstageThreadSemaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } } }; mBackstageThreadHandlerSemaphore.release(); // 释放一个信号量 Looper.loop(); } }; mBackstageThread.start(); } public void loadImage(String path, final ImageView imageView, boolean isFromNet) { imageView.setTag(path); if (mUIHandler == null) { mUIHandler = new Handler() { @Override public void handleMessage(Message msg) { ImageBeanHolder holder = (ImageBeanHolder) msg.obj; Bitmap bm = holder.bitmap; ImageView iv = holder.imageView; String path2 = holder.path; if (iv.getTag().toString().equals(path2)) { iv.setImageBitmap(bm); } } }; } Bitmap bitmap = getBitmapFromLruCache(path); // 根据path在缓存中获取bitmap if (bitmap != null) { refreshBitmap(path, imageView, bitmap); } else { addTask(buildTask(path, imageView, isFromNet)); } } private void refreshBitmap(String path, final ImageView imageView, Bitmap bitmap) { Message msg = Message.obtain(); ImageBeanHolder holder = new ImageBeanHolder(); holder.bitmap = bitmap; holder.path = path; holder.imageView = imageView; msg.obj = holder; mUIHandler.sendMessage(msg); } /** * 就是runnable加入TaskQueue,与此同时使用mBackstageThreadHandler(这个handler还记得么, * 用于和我们后台线程交互。)去发送一个消息给后台线程,叫它去取出一个任务执行 * * @param runnable */ private synchronized void addTask(Runnable runnable) { mTaskQueue.add(runnable); try { if (mBackstageThreadHandler == null) mBackstageThreadHandlerSemaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } mBackstageThreadHandler.sendEmptyMessage(0x110); // } /** * 就是根据Type从任务队列头或者尾进行取任务 * * @return */ private Runnable getTask() { if (mType == QueueType.FIFO) { return mTaskQueue.removeFirst(); } else if (mType == QueueType.LIFO) { return mTaskQueue.removeLast(); } return null; } /** * 我们新建任务,说明在内存中没有找到缓存的bitmap;我们的任务就是去根据path加载压缩后的bitmap返回即可,然后加入LruCache,设置回调显示。 * 首先我们判断是否是网络任务? * 如果是,首先去硬盘缓存中找一下,(硬盘中文件名为:根据path生成的md5为名称)。 * 如果硬盘缓存中没有,那么去判断是否开启了硬盘缓存: * 开启了的话:下载图片,使用loadImageFromLocal本地加载图片的方式进行加载(压缩的代码前面已经详细说过); * 如果没有开启:则直接从网络获取(压缩获取的代码,前面详细说过); * 如果不是网络图片:直接loadImageFromLocal本地加载图片的方式进行加载 * 经过上面,就获得了bitmap;然后加入addBitmapToLruCache,refreashBitmap回调显示图片 * * @param path * @param imageView * @param isFromNet * @return */ private Runnable buildTask(final String path, final ImageView imageView, final boolean isFromNet) { return new Runnable() { @Override public void run() { Bitmap bitmap = null; if (isFromNet) { File file = getDiskCacheDir(imageView.getContext(), md5(path)); if (file.exists()) { // 如果本地已经缓存了该文件 bitmap = loadImageFromLocal(file.getAbsolutePath(), imageView); if (bitmap == null) Log.d(TAG, "load image failed from local: " + path); } else { // 需要从网络下载 if (isDiskCacheEnable) { // 检测是否开启硬盘缓存 boolean downloadState = DownloadImgUtils.downloadImageByUrl(path, file); if (downloadState) { bitmap = loadImageFromLocal(file.getAbsolutePath(), imageView); } if (bitmap == null) Log.d(TAG, "download image failed to diskcache(" + path + ")"); } else { // 直接从网络加载到imageView bitmap = DownloadImgUtils.downloadImageByUrl(path, imageView); if (bitmap == null) Log.d(TAG, "download image failed to memory(" + path + ")"); } } } else { bitmap = loadImageFromLocal(path, imageView); } addBitmapToLruCache(path, bitmap); refreshBitmap(path, imageView, bitmap); mBackstageThreadSemaphore.release(); } }; } /** * 使用loadImageFromLocal本地加载图片的方式进行加载 * * @param path * @param imageView * @return */ private Bitmap loadImageFromLocal(final String path, final ImageView imageView) { Bitmap bitmap = null; // 1、获得图片需要显示的大小 ImageSizeUtil.ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView); // 2、压缩图片 bitmap = decodeSampledBitmapFromPath(path, imageSize.width, imageSize.height); return bitmap; } /** * 将图片加入LruCache * * @param path * @param bitmap */ protected void addBitmapToLruCache(String path, Bitmap bitmap) { if (getBitmapFromLruCache(path) == null) { if (bitmap != null) mLruCache.put(path, bitmap); } } /** * 根据图片需要显示的宽和高对图片进行压缩 * * @param path * @param width * @param height * @return */ protected Bitmap decodeSampledBitmapFromPath(String path, int width, int height) { // 获得图片的宽和高,并不把图片加载到内存中 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options, width, height); // 使用获得到的InSampleSize再次解析图片 options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(path, options); if (null == bitmap) Log.d(TAG, "options.inSampleSize = " + options.inSampleSize + ", " + path); return bitmap; } /** * 获得缓存图片的地址 * * @param context * @param uniqueName * @return */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (false && Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState())) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } /** * 根据path在缓存中获取bitmap * * @param key * @return */ private Bitmap getBitmapFromLruCache(String key) { return mLruCache.get(key); } /** * 利用签名辅助类,将字符串字节数组 * * @param str * @return */ public String md5(String str) { byte[] digest = null; try { MessageDigest md = MessageDigest.getInstance("md5"); digest = md.digest(str.getBytes()); return bytes2hex02(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } /** * 方式二 * * @param bytes * @return */ public String bytes2hex02(byte[] bytes) { StringBuilder sb = new StringBuilder(); String tmp = null; for (byte b : bytes) { // 将每个字节与0xFF进行与运算,然后转化为10进制,然后借助于Integer再转化为16进制 tmp = Integer.toHexString(0xFF & b); if (tmp.length() == 1)// 每个字节8为,转为16进制标志,2个16进制位 { tmp = "0" + tmp; } sb.append(tmp); } return sb.toString(); } private class ImageBeanHolder { Bitmap bitmap; ImageView imageView; String path; } }
以上就是整个框架
使用方法: mImageLoader.loadImage(path, iv_popup, true);
在 4.0 的屏幕上不能录制屏幕... 好伤心, 以后再补