一、图片的压缩
1、本地图片的压缩
无论是本地亦或者是网络来源图片,加载都需要进行合适的压缩,然后通过控件显示出来。
第一步:获得ImageView控件想要显示的大小
/**
* Created by ZaneLove on 2015/3/15.
*/
public class ImageSizeUtil {
/**
* a.根据ImageView获得适应的压缩的宽和高
* @param imageView
* @return
*/
public static ImageSize getImageViewSize(ImageView imageView){
ImageSize imageSize = new ImageSize();
DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
int width = imageView.getWidth();
if(width <= 0) {
width = lp.width;
}
/**
* 可以看到这里或者最大宽度,我们用的反射,而不是getMaxWidth();为啥呢,因为getMaxWidth竟然要API 16,我也是醉了;为了兼容性,我们采用反射的方案。
*/
if(width <= 0 ) {
width = getImageViewFieldValue(imageView,"mMaxWidth");
}
if(width <=0) {
width = displayMetrics.widthPixels;
}
int height = imageView.getHeight();
if(height <= 0) {
height = lp.height;
}
if(height <= 0) {
height = getImageViewFieldValue(imageView,"mMaxHeight");
}
if(height <= 0) {
height = displayMetrics.heightPixels;
}
imageSize.width = width;
imageSize.height = height;
return imageSize;
}
/**
* 通过反射获取ImageView控件的某个属性值
* @param object
* @param fieldName
* @return
*/
private static int getImageViewFieldValue(Object object, String fieldName) {
int value = 0;
try{
Field field = ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true);
int fieldValue = field.getInt(object);
if(fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
value = fieldValue;
}
}catch (Exception e) {
e.printStackTrace();
}
return value;
}
public static class ImageSize {
public int width;
public int height;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
第二步:设置合适的inSampleSize
上面,我们获得了图片想要显示的宽和高,那么,接下来就是拿着宽和高与图片真正的宽和高做比较,得到一个合适的inSampleSize,再去对图片进行压缩。
①、拿到图片真正的宽和高,并存在options里面
//获的图片真正的宽和高,并不把图片加载到内存中
BitmapFactory.Options options = new BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(getResources(),R.drawable.v,options)
//BitmapFactory.decodeFile(path, options)
②、获得inSampleSize
public class ImageSizeUtil {
/**
* b.根据需求的宽和高以及图片实际的宽和高计算SampleSize,为了对图片进行压缩
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
public static int caculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight) {
/**
* options里面存了实际的宽和高;reqWidth和reqHeight就是我们之前得到的想要显示的大小;经过比较,得到一个合适的inSampleSize
*/
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if(width > reqWidth || height > reqHeight) {
int widthRadio = Math.round(width * 1.0f / reqWidth);
int heightRadio = Math.round(height * 1.0f / reqHeight);
inSampleSize = Math.max(widthRadio,heightRadio);
}
return inSampleSize;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
options里面存了实际的宽和高;reqWidth和reqHeight就是我们之前得到的想要显示的大小;经过比较,得到一个合适的inSampleSize。
③、对图片进行压缩
//根据ImageView获得适应的压缩的宽和高
ImageSizeUtil.ImageSize imageSize = ImageSizeUtil.getImageViewSize(iv)
//根据需求的宽和高以及图片实际的宽和高计算SampleSize,为了对图片进行压缩
options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options,imageSize.width,imageSize.height)
//使用获得到的inSampleSize,再次解析图片并压缩图片
options.inJustDecodeBounds = false
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.v, options)
//BitmapFactory.decodeFile(path, options)
iv.setImageBitmap(bitmap)
2、网络图片的压缩
方案:
a、直接下载存到sd卡,然后采用本地的压缩方案。这种方式当前是在硬盘缓存开启的情况下,如果没有开启呢?
b、使用BitmapFactory.decodeStream(is, null, opts);
/**
* Created by ZaneLove on 2015/3/15.
*/
public class DownloadImgUtil {
/**
* 根据url下载图片在指定的文件
* @param urlStr
* @param imageView
* @return
*/
public static Bitmap downloadImageByUrl(String urlStr,ImageView imageView) {
InputStream is = null;
try {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
is = new BufferedInputStream(conn.getInputStream());
/**
* BufferedInputStream类调用mark(int readlimit)方法后读取多少字节标记才失效,是取readlimit和BufferedInputStream类的缓冲区大小两者中的最大值,而并非完全由readlimit确定。这个在JAVA文档中是没有提到的。
*/
is.mark(is.available());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);
ImageSizeUtil.ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView);
options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options,imageSize.width,imageSize.height);
options.inJustDecodeBounds = false;
is.reset();
bitmap = BitmapFactory.decodeStream(is,null,options);
conn.disconnect();
return bitmap;
}catch (Exception e) {
e.printStackTrace();
}finally {
try{
if(is != null) {
is.close();
}
}catch (Exception e) {
}
}
return null;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
/**
* 下载网络图片,并将图片显示在控件上
*/
Bitmap bitmap1 = DownloadImgUtil.downloadImageByUrl("https://img-my.csdn.net/uploads/201407/26/1406383299_1976.jpg", iv);
iv.setImageBitmap(bitmap1);
在此,图片压缩已结束,那么,就应该放入LruCache,然后再设置到我们的ImageView上 。
二、图片的缓存
基于此,我将带领大家一起打造一个从压缩到缓存的图片加载框架的架构。
打造一个从压缩到缓存的图片加载框架的架构
基本思路
1、单例模式,包含一个LruCache用来管理缓存图片。
2、任务队列,每来一次加载图片的请求,就封装成Task存入到TaskQueue中。
3、包含一个后台线程,这个线程在第一次初始化实例的时候启动,然后会一直在后台运行,它用来干什么呢?大伙还记得我们有个任务队列么,有队列任务,得有人干活呀,所以,当每来一次加载图片请求的时候,我们同时发一个消息到后台线程,后台线程去使用线程池去TaskQueue去取一个任务来执行。
4、调度策略,在3中说过,后台线程去TaskQueue去取一个任务,这个任务不是随便取的,有策略可以选择,一个是FIFO(先进先出),一个是LIFO(后进先出),我更倾向于后者。
让我们一步两步分析代码如何实现吧!!
1、构造方法 – 使用单例模式
/**
* 单例模式
*/
private static ImageLoader instance = null;
private ImageLoader(int threadCount,Type type){
init(threadCount,type);
}
public static ImageLoader getInstance(int threadCount,Type type) {
if(instance == null) {
synchronized (ImageLoader.class) {
if(instance == null) {
instance = new ImageLoader(threadCount, type);
return instance;
}
}
}
return instance;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
接下来, 让我们一起来看构造函数当中init()方法:(附带全局变量)
1、初始化后台轮询线程
2、获得应用最大可用内存的1/8,分配给LruCache
3、创建线程池
private int threadCount;
private Type mType = Type.LIFO;
private LruCache<String,Bitmap> mLruCache;
private ExecutorService mThreadPool;
private static final int DEAFULT_THREAD_COUNT = 1;
private LinkedList<Runnable> mTaskQueue;
private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
private Semaphore mSemaphoreThreadPool;
private Thread mPoolThread;
private Handler mPoolThreadHandler;
private Runnable task;
private Handler mUIHandler;
private boolean isDiskCacheEnable = true;
public enum Type {
FIFO,
LIFO
}
/**
* 初始化
* @param threadCount 线程数量
* @param type 队列调度方式
*/
private void init(int threadCount, Type type) {
initBackThread();
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheMemory = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
mThreadPool = Executors.newFixedThreadPool(threadCount);
mTaskQueue = new LinkedList<Runnable>();
mType = type;
mSemaphoreThreadPool = new Semaphore(threadCount);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
private void initBackThread() {
mPoolThread = new Thread(){
@Override
public void run() {
Looper.prepare();
mPoolThreadHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
mThreadPool.execute(getTask());
try{
mSemaphoreThreadPool.acquire();
}catch (Exception e) {
}
}
};
mSemaphorePoolThreadHandler.release();
Looper.loop();
}
};
mPoolThread.start();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
/**
* 从任务队列取出一个任务
* @return
*/
public Runnable getTask() {
if(mType == Type.FIFO) {
return mTaskQueue.removeFirst();
}else if(mType == Type.LIFO) {
return mTaskQueue.removeLast();
}
return null;
}
让我们在好好的分析分析一下以上代码,它到底做了什么?
-
在构造方法中我们调用init()方法
-
在init()方法中:
- 开启后台加载图片线程 – initBackThread(),实际上是让Looper轮询器不断的loop,随时往消息队列中拿消息进行处理
- 初始化mPoolThreadHandler用于发送消息到此线程
- 初始化LruCache缓存机制,并分配了缓存空间
- 创建线程池、初始化任务队列(TaskQueue),使用线程池去TaskQueue中去取一个任务执行
目前为止,我们轮询器开启了、消息队列有了、处理消息的Handler处理器也有了、LruCache缓存机制又随时待命中、最后连线程池都有了,简直就是万事俱备,只欠东风。
2、加载图片(本地 + 网络)
/**
* 根据path为ImageView设置图片
*/
public void loadImage(final String path,final ImageView imageView,final boolean isFromNet) {
imageView.setTag(path);
if(mUIHandler == null) {
mUIHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
Bitmap bm = holder.bitmap;
ImageView imageView = holder.imageView;
String path = holder.path;
if(imageView.getTag().toString().equals(path)) {
imageView.setImageBitmap(bm);
}
}
};
}
Bitmap bm = getBitmapFromLruCache(path);
if(bm != null) {
refreashBitmap(path,imageView,bm);
}else {
addTask(buildTask(path,imageView,isFromNet));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
/**
* 根据path从缓存当中获取bitmap
* @param key
* @return
*/
private Bitmap getBitmapFromLruCache(String key) {
return mLruCache.get(key);
}
/**
* 去设置图片显示在控件上
* @param path
* @param imageView
* @param bm
*/
private void refreashBitmap(String path, ImageView imageView, Bitmap bm) {
Message message = Message.obtain();
ImgBeanHolder holder = new ImgBeanHolder();
holder.bitmap = bm;
holder.path = path;
holder.imageView = imageView;
message.obj = holder;
mUIHandler.sendMessage(message);
}
/**
* 新建一个任务
* @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 bm = null;
if (isFromNet) {
File file = getDiskCacheDir(imageView.getContext(), md5(path));
if (file.exists()) {
bm = loadImageFromLocal(file.getAbsolutePath(), imageView);
} else {
if (isDiskCacheEnable) {
boolean downloadState = DownloadImgUtil.downloadImgByUrl(path, file);
if (downloadState) {
bm = loadImageFromLocal(file.getAbsolutePath(), imageView);
}
} else {
bm = DownloadImgUtil.downloadImageByUrl(path,imageView);
}
}
} else {
bm = loadImageFromLocal(path, imageView);
}
addBitmapToLruCache(path, bm);
refreashBitmap(path, imageView, bm);
mSemaphoreThreadPool.release();
}
};
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
/**
* 添加任务到队列
* @param runnable
*/
private synchronized void addTask(Runnable runnable) {
mTaskQueue.add(runnable);
try{
if(mPoolThreadHandler == null) {
mSemaphorePoolThreadHandler.acquire();
}
}catch (Exception e) {
}
mPoolThreadHandler.sendEmptyMessage(0x110);
}
/**
* 获得缓存图片的地址
* @param context
* @param mdStr
* @return
*/
private File getDiskCacheDir(Context context, String mdStr) {
String cachePath;
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
cachePath = context.getExternalCacheDir().getPath();
isDiskCacheEnable = true;
}else {
cachePath = context.getCacheDir().getPath();
isDiskCacheEnable = false;
}
return new File(cachePath + File.separator + mdStr);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
/**
* 从本地缓存文件中加载图片
* @param path
* @param imageView
* @return
*/
private Bitmap loadImageFromLocal(String path, ImageView imageView) {
Bitmap bm;
ImageSizeUtil.ImageSize imageSize = ImageSizeUtil.getImageViewSize(imageView);
bm = decodeSampledBitmapFromPath(path,imageSize.width,imageSize.height);
return bm;
}
/**
* 将图片加入LruCache
* @param path
* @param bm
*/
private void addBitmapToLruCache(String path, Bitmap bm) {
if(getBitmapFromLruCache(path) == null) {
if(bm != null) {
mLruCache.put(path,bm);
}
}
}
分析以上代码:
- 在外界调用loadImage()方法时,传入路径、展示控件(ImageView)和是否是来自于网络
- 在loadImage()方法中:
- 为ImageView.setTag
- 初始化一个mUIHandler,用于用户更新ImageView控件,因此这个方法肯定是主线程调用的
- 首先根据path从缓存当中获取bitmap
- 如果缓存当中找到了该图片,调用refreshBitmap()–设置图片显示在控件上
- 如果缓存当中没有找到该图片,通过buildTask()去新建一个任务,再addTask()到任务队列当中
- buildTask()方法中:
- 判断是否来自网络
- 如果来自网络
- 获得缓存图片的地址
- 如果在缓存文件中发现,那就说明还在本地,就调用loadImageFromLocal()方法,从本地缓存文件当中加载图片
- 如果没有发现
- 检测是否开启硬盘缓存
- 如果开启了硬盘缓存,从网络下载图片,并存储到硬盘当中,这样的话又是本地缓存了,然后调用localImageFromLocal()从缓存文件中加载图片
- 否则,直接从网络加载
- 如果来自本地,那么就直接调用loadImageFromLocal()从本地缓存文件中加载图片即可
- 将新加载过来的图片加入到缓存机制当中
- 设置图片显示在控件上
PS:就是在代码中,你会看到一些信号量的身影:
第一个:mSemaphorePoolThreadHandler = new Semaphore(0); 用于控制我们的mPoolThreadHandler的初始化完成,我们在使用mPoolThreadHandler会进行判空,如果为null,会通过mSemaphorePoolThreadHandler.acquire()进行阻塞;当mPoolThreadHandler初始化结束,我们会调用.release();解除阻塞。
第二个:mSemaphoreThreadPool = new Semaphore(threadCount);这个信号量的数量和我们加载图片的线程个数一致;每取一个任务去执行,我们会让信号量减一;每完成一个任务,会让信号量+1,再去取任务;目的是什么呢?为什么当我们的任务到来时,如果此时在没有空闲线程,任务则一直添加到TaskQueue中,当线程完成任务,可以根据策略去TaskQueue中去取任务,只有这样,我们的LIFO才有意义。
到此为止,布局我就buCopy了,大伙需要的话就去下载源码吧!嘎嘎,好坏好坏滴那种!
示例代码戳Here
转自:http://blog.csdn.net/zanelove/article/details/44278783