前言
首先,最近是在忙okhttp没错。不过或许有人问为什么忙着okhttp怎么又扯到了图片加载上了。其实,最近想实现下断点续传以及多文件下载,但并不知道怎么搞。群里有小伙伴提出了控制线程池来实现。然后我就想到了图片加载需要控制线程池,所以在此巩固下。
概述
好了,进入正题了。优秀的图片加载框架不要太多,什么UIL,Picasso,Glide等等。但我们需要了解其中的原理。所以今天我来介绍下如何自己写一个图片加载框架。有人可能会说,自己写会不会很渣,运行效率,内存溢出神马的。至于这个,等你看完这篇博客自己试试就知道了。
缓存
加载图片肯定是需要用到缓存的,这样可以提高我们的加载效率,而且还可以省流量。所有的图片从缓存读取,保证图片的内存不会超过预期的空间。
压缩
除了缓存,更重要的就是压缩了。不然别人用起来。直接oom,肯定要破口大骂,尼玛炸了!!
加载速度
好了。谈完压缩,缓存,基本没问题了把?不不不,你加载速度不快谁用?加载速度分两种,先进先出和后进先出。如果一个列表有几千甚至上万张图片,我一次性滑倒底,你要是在从第一张开始加载。我估计我一把lol打完你还没加载完把。所以我们选择后进先出,当前呈现给用户的,最新加载;当前未呈现的,选择加载。
小结
关于加载网络图片,其实原理差不多,就多了个是否启用硬盘缓存的选项,如果启用了,加载时,先从内存中查找,然后从硬盘上找,最后去网络下载。下载完成后,别忘了写入硬盘,加入内存缓存。如果没有启用,那么就直接从网络压缩获取,加入内存即可。
效果图
终于扯完了,我们先来看下效果图:
加载本地
很流畅对吧,我们在看看网络图片.
加载网络
效果还是不错的,之前花了好久找的100个妹纸图。想录长一点的,结果要求必须小于2M,也是醉了。整体效果应该可以看出来。很流畅。perfect~~~
整体实现
整体实现我采用了建造者模式,对建造者模式不了解的。去看android建造者模式
图片压缩
不管是从网络还是本地的图片,加载都需要进行压缩,然后显示。
首先压缩。我们是需要得到ImageView的宽和高,也就是大小。没大小怎么压缩?
//压缩
protected Bitmap SampledBitmap(String path, Edit edit) {
// 获得图片的宽和高,并把图片加载到内存中
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = calculateInSampleSize(options, edit.imageView.getWidth(), edit.imageView.getHeight());
// 使用获得到的InSampleSize再次解析图片
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
if (edit.orientation != 0) {
int drawablewidth = bitmap.getWidth();
int drawableheight = bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setRotate(edit.orientation);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, drawablewidth, drawableheight,
matrix, true);
}
return bitmap;
}
我们需要设置恰当的insamplesize。根据需求的宽和高以及图片实际的宽和高计算SampleSize 。
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//源图片的高度和宽度
final int height = options.outHeight;//得到要加载的图片高度
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
//计算出实际宽高和目标宽高的比例
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
上面是进行本地图片的压缩。网络图片就方便了。只要把网络图片下载到sd卡,然后在进行本地压缩。
public static boolean downloadImgByUrl(String urlStr, File file) {
boolean isok = false;
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[1024 * 4];
int len = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
isok = 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) {
}
}
return isok;
}
到这边图片压缩是结束了,接下来是缓存。
缓存
// 图片缓存的核心对象
private LruCache<String, Bitmap> lruCache;
private Context context;
//任务队列
private LinkedList<Runnable> taskQueue = new LinkedList<>();
//线程池
private ExecutorService threadPool;
private Semaphore semaphoreThreadPool;
private Semaphore semaphorePoolThreadHandler = new Semaphore(0);
//UI线程
private Handler uiHandler;
//后台线程
private Thread backthread;
private Handler backthreadhandler;
//线程数量
private static final int THREAD_POOL_COUNT = 3;
private void BackGroundThread() {
// 后台轮询线程
backthread = new Thread() {
public void run() {
Looper.prepare();
backthreadhandler = new Handler() {
public void handleMessage(Message msg) {
// 线程池去取出一个任务进行执行
threadPool.execute(taskQueue.removeLast());
try {
semaphoreThreadPool.acquire();
} catch (InterruptedException e) {
}
}
};
semaphorePoolThreadHandler.release();
Looper.loop();
}
};
backthread.start();
}
private void setLruCache() {
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
lruCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
// 创建线程池
threadPool = Executors.newFixedThreadPool(THREAD_POOL_COUNT);
semaphoreThreadPool = new Semaphore(THREAD_POOL_COUNT);
}
对图片进行完压缩以及缓存后,下面就是加载图片了。
加载图片
我们需要调用一个方法来执行加载图片的操作:
public void loadImage(Edit edit) {
edit.imageView.setTag(edit.loadUrl);
edit.imageView.setImageResource(edit.load_pic);
if (uiHandler == null) {
uiHandler = new Handler() {
public void handleMessage(Message msg) {
myImageBean myImageBean = (myImageBean) msg.obj;
Bitmap bm = myImageBean.bitmap;
ImageView imageview = myImageBean.imageView;
String path = myImageBean.path;
if (imageview.getTag().toString().equals(path)) {
imageview.setImageBitmap(bm);
}
}
};
}
// 根据path在缓存中获取bitmap
Bitmap bitmap = getBitmapFromLruCache(edit.loadUrl);
if (bitmap != null) {
refreashBitmap(edit, bitmap);
} else {
addTask(buildTask(edit));
}
}
我们需要得到他的缓存,所以:
private Bitmap getBitmapFromLruCache(String key) {
return lruCache.get(key);
}
如果缓存存在的话,那么我们需要刷新bitmap。代表这个图片已经完成加载了。
private void refreashBitmap(Edit edit, Bitmap bitmap) {
Message message = Message.obtain();
myImageBean holder = new myImageBean();
holder.bitmap = bitmap;
holder.path = edit.loadUrl;
holder.imageView = edit.imageView;
message.obj = holder;
uiHandler.sendMessage(message);
}
否则,我们需要去把他添加到lurcache中,以便之后快捷的加载。
private void addTask(Runnable runnable) {
taskQueue.add(runnable);
try {
if (backthreadhandler == null)
semaphorePoolThreadHandler.acquire();
} catch (InterruptedException e) {
}
backthreadhandler.sendEmptyMessage(0x250);
}
private Runnable buildTask(final Edit edit) {
return new Runnable() {
public void run() {
Bitmap bitmap = null;
if (edit.isNet) {
File file = getDiskCacheDir(context, MD5Utils.hashKeyFromUrl(edit.loadUrl));
if (file.exists()) {
bitmap = loadImageFromLocal(file.getAbsolutePath(), edit);
} else {
boolean success = downloadImgByUrl(edit.loadUrl, file);
if (success) {
bitmap = loadImageFromLocal(file.getAbsolutePath(), edit);
}
}
} else {
bitmap = loadImageFromLocal(edit.loadUrl, edit);
}
if (lruCache.get(edit.loadUrl) == null) {
if (bitmap != null)
lruCache.put(edit.loadUrl, bitmap);
}
addBitmapToLruCache(edit.loadUrl, bitmap);
refreashBitmap(edit, bitmap);
semaphoreThreadPool.release();
}
};
}
下载到本地之后,其实也是执行一个压缩操作,代码很简单:
private Bitmap loadImageFromLocal(String path, Edit edit) {
Bitmap bm;
bm = SampledBitmap(path, edit);
return bm;
}
到此,我们的代码应该分析完了,不过好像还缺少了什么,对了。Edit这个。到底做了什么处理呢。
public class Edit {
private boolean isNet = true;
private int orientation = 0;
private String loadUrl;
private ImageView imageView;
private int load_pic=R.drawable.ic_loading;
public Edit ic_loading(int load_pic) {
this.load_pic = load_pic;
return this;
}
public Edit into(ImageView imageView) {
this.imageView = imageView;
return this;
}
public Edit setUrl(String loadUrl) {
this.loadUrl = loadUrl;
return this;
}
public Edit isNet(boolean net) {
isNet = net;
return this;
}
public void load() {
loadImage(this);
}
public Edit setOrientation(int orientation) {
this.orientation = orientation;
return this;
}
}
一个个介绍。isnet代表是否从网络加载,默认是从网络加载,至于orientation是什么呢,这个是图片的旋转角度,在座的小伙伴如果有三星手机的,你打开boss直聘选择头像,然后读取本地图片会发现,一些图片是经过旋转的,其实这是三星手机的一个bug。当然我们也知道了。他的图片加载其实也是自己写的,并没有用三方的jar。其他属性应该不用介绍了把。
参考
洪洋:http://blog.csdn.net/lmj623565791/article/details/41874561
总结
图片加载主要考虑的就是压缩,缓存,加载速度以及线程池(一次加载几张)。只要掌握好这些,你也可以写一个图片加载框架。
对于开发最常用的就是,网络加载,图片加载以及布局的刷新加载。现在就剩对okhttp的深入了解了。后续我会更上。我们需要知道其中的原理来解决这些问题,虽然网上都有现成的。但我们如果不分析直接拿来用的话,对自己水平提高的实在是太少了。今天就这样吧~~~