一、简介
一个优秀的ImageLoader应该具有如下功能:
图片的同步加载;
图片的异步加载;
图片压缩;
内存缓存;
磁盘缓存;
网络拉取;
图片的同步加载是指能够以同步的方式向调用者提供所加载的图片,这个图片可能是从内存缓存中读取的,也可能是从磁盘缓存中读取的,还可能是从网络拉取的。
图片的异步加载是在ImageLoader内部需要自己在线程中加载图片并将图片设置给所需要的ImageView。
图片压缩这是降低OOM概率的有效手段,ImageLoader必须合适地处理图片的压缩问题。
内存缓存和磁盘缓存是ImageLoader的核心,通过这两级缓存都不可用时才需要从网络拉取。
ImageLoader还需要处理一些特殊情况,比如在ListView或者GridView中,Vie、w复用是它们的优点也是它们的缺点 。大家都知道的它的优点,就是减少流量消耗和减少CPU处理次数。那缺点是什么呢?就是常见的错位问题。ImageLoader需要正确地处理这些特殊情况。
二 、代码实现
1.图片压缩功能的实现
具体的详情请看:Android Bitmap 的高效加载https://www.jianshu.com/p/2be8a8679c05
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
//1、创建 BitmapFactory.Options对像并inJustDecodeBounds设为true
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//设置 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//4、将inJustDecodeBounds设为false
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
//2、根据Optuins获取原始图片宽高outWidth\outHeight
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
//3、根据原始宽高计算出采样率 inSampleSize
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//计算最大的采样值,即2的幂,并同时保持
//高度和宽度大于要求的高度和宽度
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize = 2;
}
}
return inSampleSize;
}
}
2.内存缓存和磁盘缓存的实现
ImageLoader在初始化时,会创建LruCache和DiskLruCache;
需要注意的是Android SDK没有提供DiskLruCache的实现,需要自己导入相关实现包: implementation 'com.jakewharton:disklrucache:2.0.2'
private Context mContext;
private ImageResizer mImageResizer=new ImageResizer();
private LruCache mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context){
mContext=context.getApplicationContext();
//maxMemory是拿到的程序最大可以使用的内存以k为单位
int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
//ImageLoader的内存缓存的容量为当前进程可用内存的1/8.磁盘缓存为50MB
int cacheSize=maxMemory/8;
mMemoryCache=new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes()*bitmap.getHeight()/1024;
}
};
File diskCacheDir =getDiskCacheDir(mContext,"bitmap");
if (!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir)>DISK_CACHCE_SIZE){
try {
mDiskLruCache=DiskLruCache.open(diskCacheDir,1,1,DISK_CACHCE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
mIsDiskLruCacheCreated=true;
}
}
内存缓存和磁盘缓存创建完毕后,还需要提供方法来完成缓存的添加和读取。
(1)内存缓存添加与读取
//添加缓存数据
private void addBitmapToMemoryCache(String key,Bitmap bitmap){
if (getBitmapFromMemCache(key)==null){
mMemoryCache.put(key,bitmap);
}
}
//添加读取数据
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
(2)磁盘缓存添加与读取
-- 磁盘缓存添加
//磁盘缓存添加
private String hashKeyFormUrl(String uri) {
String cacheKey = null;
try {
//图片的url中很可能有特殊字符,这将影响url在android中的使用,所以一般采用url的md5值作为key
final MessageDigest mDigest=MessageDigest.getInstance("MD5");
mDigest.update(uri.getBytes());
cacheKey=bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb=new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
//此方法返回的字符串表示的无符号整数参数所表示的值以十六进制(基数为16)
String hex=Integer.toHexString(0xFF & bytes[i]);
if (hex.length()==1){
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
获取文件输出流:
String key=hashKeyFormUrl(uri);
//通过 Editor 可以得到一个文件输出流
DiskLruCache.Editor editor =mDiskLruCache.edit(key);
if (editor !=null){
//由于DiskLruCache的 open 方法在前面设置了一个节点只能有一个数据,所以 DISK_CACHCE_INFEX 设为0就可
OutputStream outputStream=editor.newOutputStream(DISK_CACHCE_INFEX);
if (downloadUrlToStream(uri,outputStream)){
editor.commit();
}else {
editor.abort();
}
mDiskLruCache.flush();
}
通过文件输出流写入到文件系统上
private boolean downloadUrlToStream(String uriString, OutputStream outputStream) {
HttpURLConnection urlConnection=null;
BufferedOutputStream out=null;
BufferedInputStream in=null;
try {
final URL url =new URL(uriString);
urlConnection= (HttpURLConnection) url.openConnection();
in=new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
out=new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
int b;
while ((b=in.read())!=-1){
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downLoadBitmap failed ,"+e );
}finally {
if (urlConnection!=null){
urlConnection.disconnect();
}
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
-- 磁盘缓存读取
Bitmap bitmap=null;
String key=hashKeyFormUrl(uri);
DiskLruCache.Snapshot snapshot=mDiskLruCache.get(key);
if (snapshot!=null){
FileInputStream fileInputStream=(FileInputStream)snapshot.getInputStream(DISK_CACHCE_INFEX);
FileDescriptor fileDescriptor=fileInputStream.getFD();
bitmap=mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if (bitmap!=null){
addBitmapToMemoryCache(key,bitmap);
}
}
3.同步加载和异步加载
同步加载需要外部在线程中调用,这是因为同步加载很可能比较耗时。
//同步加载
private Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap=loadBitmapFromMemCache(uri);
if (bitmap!=null){
Log.d(TAG, "loadBitmapFromMemCache: uri = "+uri);
return bitmap;
}
try {
bitmap=loadBitmapFromMemDiskCache(uri,reqWidth,reqHeight);
if (bitmap!=null){
Log.d(TAG, "loadBitmapFromMemDiskCache: uri = "+uri);
return bitmap;
}
bitmap=loadBitmapFromHttp(uri,reqWidth,reqHeight);
if (bitmap!=null){
Log.d(TAG, "loadBitmapFromHttp: uri = "+uri);
return bitmap;
}
}catch (Exception e){
e.printStackTrace();
}
if (bitmap==null&&!mIsDiskLruCacheCreated){
Log.w(TAG, "encounter error ,DiskLruCache is not created .");
bitmap=downloadBitmapFromUrl(uri);
}
return bitmap;
}
异步加载 这里用到线程池 ,所以首先要创建一个线程池。
//创建线程工厂
private static final ThreadFactory sThreadFactory =new ThreadFactory() {
private final AtomicInteger mCount=new AtomicInteger(1);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r,"ImageLoader:"+mCount.getAndIncrement());
}
};
//线程池创建
public static final Executor THERAD_POOL_EXECUTOR=new ThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,new LinkedBlockingQueue(),sThreadFactory);
//异步加载
public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth,final int reqHeight){
imageView.setTag(TAG_KEY_URI,uri);
final Bitmap bitmap=loadBitmapFromMemCache(uri);
if (bitmap!=null){
imageView.setImageBitmap(bitmap);
return;
}
final Runnable loadBitmapTask=new Runnable() {
@Override
public void run() {
Bitmap bitmapp=loadBitmap(uri,reqWidth,reqHeight);
if (bitmap!=null){
LoaderResult result=new LoaderResult(imageView,uri,bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
}
}
};
THERAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
图片错位问题用handler 来解决实现:
//通过handler 在设置图片之前都会检查它的url有没有改变,如果发生改变就不再给它设置图片。这样可以解决图片错位问题
private Handler mMainHandler=new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
LoaderResult result= (LoaderResult) msg.obj;
ImageView imageView=result.imageView;
imageView.setImageBitmap(result.bitmap);
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!");
}
}
};
完整类实现点击:https://www.jianshu.com/p/046ff196c73e