所谓三级缓存指的是内存、磁盘缓存、网络加载 。bitmap获取存取逻辑大致如下:
1.获取 url ,请求网络加载。
2.把加载成功的流存入到磁盘(DiskLruCache)
3.流对象转成bitmap对象,并压缩存入到内存中(LruCache)
4.设置压缩后的bitmap对象给ImageView
此时图片已成功显示并实现存储,再次加载时可依次从内存、磁盘中取出bitmap,当都为空去请求网络并存储,以此重复。
其中涉及到LruCache和DiskLruCache(LRU算法),线程池使用,图片压缩,Handler解决异步问题,通过setTag来标识ImageView。
关于:
LruCache
LRU:(Least Recently Used)是近期最少使用算法,内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,核心思想是当缓存满时优先淘汰近期最少使用的缓存对象(JakeWharton/DiskLruCache* 下载*)。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getWidth() / 1024;
}
};
只需要提供缓存的总容量大小并重写sizeOf方法,sizeOf方法的作用是计算缓存对象的大小。特殊情况还需重写entryRemoved方法,移除旧缓存时会调用此方法,因此可以其中做些资源回收工作(如需要)。
ThreadPoolExecutor
线程池:包括 corePoolSize 核心线程数、maximumPoolSize 最大线程数、keepAliveTime 非核心线闲置时的超时时长 、unit 时长单位、workQueue 任务队列、threadFactory 线程工厂,其中线程工厂为线程池提供创建新线程的功能,只有 Thread new Thread(Runnabler)方法。
执行任务时大致遵循如下规则:
1)如果线程中的线程数量 未达到 核心线程的数量 ,那么会直接启动一个核心线程来执行任务。
2)如果 线程中的线程数量 已经达到或者超过核心线程的数量 ,那么任务会被插入到任务队列中排除等待执行。
3)如果 在步骤2中无法将任务插入到任务队列,这往往是任务队列 已满,这个时候 如果线程数量未达到线程池规定的最大值 ,那么会立刻启动一个非核心线程来执行任务。
4)如果步骤3中线程数量已经达到 线程池规定的最大值 ,那么就拒绝执行此任务,
ThreadPoolExecutor 会调用 RejectedExecutionHandler 的 rejectedExecution 方法来通知调用者。
线程池的优点:
1)重用线程池中的线程,避免因为线程的创建和销毁所带来的性能 开销。
2)能有效控制 线程池的最大并发数据,避免大量的线程之前 因互相抢占系统资源而导致阻塞的现象。
3)能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。
其实Android封装了四类线程池,其中FixedThreadPoo只有核心线程并且核心线程不会被回收,没有超时机制,另外队列也没有大小限制 ,所以能更加快速地响应外界的请求;CachedThreadPool适合执行大量的耗时较少的任务;ScheduledThreadPool适合处理定时和具有固定重复任务;SingleThreadExcuor内部只有一个核心线程,它确保 所有的任务都 在同一个线程中按顺序执行,所以这些任务之间不需要处理线程同步的问题。可以根据需求选择合适的线程池,原理其实就在创建线程池时设置不同的参数,此处不做详解。
图片压缩
想要压缩,我们第一步应该是获得imageview想要显示的大小,没大小肯定没办法压缩?
那么如何获得imageview想要显示的大小呢?(该模块转自HongYang)
/**
* http://blog.csdn.net/lmj623565791/article/details/41874561
*
* @author zhy
*/
public class ImageSizeUtil {
/**
* 根据需求的宽和高以及图片实际的宽和高计算SampleSize
*
* @param options
* @return
*/
public static int caculateInSampleSize(Options options, int reqWidth,
int reqHeight) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
while (width > reqWidth || height > reqHeight) {
width /= 2;
height /= 2;
inSampleSize++;
}
return inSampleSize;
}
/**
* 根据ImageView获适当的压缩的宽和高
*
* @param imageView
* @return
*/
public static ImageSize getImageViewSize(ImageView imageView) {
ImageSize imageSize = new ImageSize();
DisplayMetrics displayMetrics = imageView.getContext().getResources()
.getDisplayMetrics();
LayoutParams lp = imageView.getLayoutParams();
int width = imageView.getWidth();// 获取imageview的实际宽度
if (width <= 0) {
width = lp.width;// 获取imageview在layout中声明的宽度
}
if (width <= 0) {
//width = imageView.getMaxWidth();// 检查最大值
width = getImageViewFieldValue(imageView, "mMaxWidth");
}
if (width <= 0) {
width = displayMetrics.widthPixels;
}
int height = imageView.getHeight();// 获取imageview的实际高度
if (height <= 0) {
height = lp.height;// 获取imageview在layout中声明的宽度
}
if (height <= 0) {
height = getImageViewFieldValue(imageView, "mMaxHeight");// 检查最大值
}
if (height <= 0) {
height = displayMetrics.heightPixels;
}
imageSize.width = width;
imageSize.height = height;
return imageSize;
}
public static class ImageSize {
int width;
int height;
}
/**
* 通过反射获取imageview的某个属性值
*
* @param object
* @param fieldName
* @return
*/
public 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) {
}
return value;
}
}
可以看到,我们拿到imageview以后:
首先企图通过getWidth获取显示的宽;有些时候,这个getWidth返回的是0;
那么我们再去看看它有没有在布局文件中书写宽;
如果布局文件中也没有精确值,那么我们再去看看它有没有设置最大值;
如果最大值也没设置,那么我们只有拿出我们的终极方案,使用我们的屏幕宽度;
总之,不能让它任性,我们一定要拿到一个合适的显示值。
可以看到这里或者最大宽度,我们用的反射,而不是getMaxWidth();因为getMaxWidth竟然要API 16;为了兼容性,我们采用反射的方案。
得到尺寸大小后可通过caculateInSampleSize()方法设置合适的inSampleSize。
好吧,代码给了很详尽的注释,具体实现可根据代码看更加清晰。
完整代码如下 :
/**
* Created by zhanFeng on 2017/3/22.
*/
public class ImageLoader {
private static ImageLoader sImageLoader;
public static ImageLoader getInstance(Context context) {
if (sImageLoader == null) {
synchronized (ImageLoader.class) {
if (sImageLoader == null) {
sImageLoader = new ImageLoader(context);
}
}
}
return sImageLoader;
}
private static final String TAG = "ImageLoader";
private Context mContext;
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
//磁盘缓存空间 20M
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 20;
private boolean mIsDiskLruCacheCreated = false;
private final static int TAG_KEY_URI = R.id.imageLoader_uri;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXMUN_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactor = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
//创建线程池
private static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXMUN_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactor);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult loaderResult = (LoaderResult) msg.obj;
ImageView imageView = loaderResult.mImageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(loaderResult.uri)) {
System.out.println("Handler:" + uri);
imageView.setImageBitmap(loaderResult.bitmap);
} else {
Log.e(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private ImageLoader(Context context) {
//单例中防止上下文引起的内存泄漏
mContext = context.getApplicationContext();
//获取应用内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//内存缓存空间为应用内存的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getWidth() / 1024;
}
};
//创建保存bitmap的文件
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
//获取手机存储空间大小,当空间允许时创建本地缓存对象
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
//创建成功时设置标记
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取手机存储空间大小
*
* @param path
* @return
*/
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs statFs = new StatFs(path.getPath());
return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
}
/**
* 创建保存bitmap的文件
* true 有SD卡且未移除时创建在SD卡创建 false 创建在内置存储卡上
*
* @param context
* @param bitmapName
* @return
*/
private File getDiskCacheDir(Context context, String bitmapName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + bitmapName);
}
/**
* 从内存中获取bitmap
*
* @param url
* @return
*/
private Bitmap loadBitmapFromMemCache(String url) {
String key = hashKeyFormUrl(url);
return mMemoryCache.get(key);
}
/**
* 添加bitmap到内存中
*
* @param url
* @param bitmap
*/
private void addBitmapToMemoryCache(String url, Bitmap bitmap) {
if (loadBitmapFromMemCache(url) == null) {
if (bitmap != null) {
mMemoryCache.put(url, bitmap);
}
}
}
public void displayImage(@Nullable final String url, final ImageView imageView) {
//setTag 与 Handler中getTag可对imageView进行识别,避免异步造成图片显示错位
imageView.setTag(TAG_KEY_URI, url);
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//创建任务
Runnable runnableTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, imageView);
if (bitmap != null) {
LoaderResult loaderResult = new LoaderResult(imageView, url, bitmap);
//Handler发送消息
mMainHandler.obtainMessage(0, loaderResult).sendToTarget();
}
}
};
//将任务放入线程池执行
THREAD_POOL_EXECUTOR.execute(runnableTask);
}
/**
* @param url
* @param imageView
* @return
*/
private Bitmap loadBitmap(String url, ImageView imageView) {
Bitmap bitmap = null;
bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, imageView);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(url, imageView);
} catch (IOException e) {
e.printStackTrace();
}
//存储空间不够大时未创建磁盘缓存,直接从网络加载
if (bitmap == null && !mIsDiskLruCacheCreated) {
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
/**
* 从磁盘中加载bitmap
*
* @param url
* @param imageView
* @return bitmap
* @throws IOException
*/
private Bitmap loadBitmapFromDiskCache(String url, ImageView imageView) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it's not recommend!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0);
FileDescriptor fileDescriptor = fileInputStream.getFD();
ImageSizeUtil.ImageSize imageViewSize = ImageSizeUtil.getImageViewSize(imageView);
//将文件描述符压缩并转换成bitmap
bitmap = decodeSampledBitmapFromPath(fileDescriptor, imageViewSize.width, imageViewSize.height);
if (null != bitmap) {
//保存到内存中
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
private static class LoaderResult {
private ImageView mImageView;
private String uri;
private Bitmap bitmap;
public LoaderResult(ImageView imageView, String url, Bitmap bitmap) {
this.mImageView = imageView;
this.uri = url;
this.bitmap = bitmap;
}
}
public static final int IO_BUFFER_SIZE = 1024 * 8;
private Bitmap downloadBitmapFromUrl(String uri) {
Bitmap bitmap = null;
HttpURLConnection httpURLConnection = null;
BufferedInputStream is = null;
try {
URL url = new URL(uri);
try {
httpURLConnection = (HttpURLConnection) url.openConnection();
is = new BufferedInputStream(httpURLConnection.getInputStream(), IO_BUFFER_SIZE);
//此处可把bitmap对象进行压缩,可自行实现,很简单的
bitmap = BitmapFactory.decodeStream(is);
} catch (IOException e) {
Log.e(TAG, "Error in downloadBitmap:" + e);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != httpURLConnection) {
try {
httpURLConnection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return bitmap;
}
/**
* 从网络下载图片
*
* @param url
* @param imageView
* @return
*/
private Bitmap loadBitmapFromHttp(String url, ImageView imageView) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
try {
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
if (null != edit) {
OutputStream outputStream = edit.newOutputStream(0);
if (downloadUrlToStream(url, outputStream)) {
edit.commit();
} else {
edit.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, imageView);
} catch (IOException e) {
return null;
}
}
/**
* 从网络加载图片并保存到磁盘中
*
* @param urlString
* @param outputStream
* @return
*/
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* md5加密,避免url存在特殊字符
*
* @param url
* @return
*/
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] digest) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF & digest[i]);
if (hex.length() == 1) {
stringBuilder.append('0');
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
/**
* 根据图片需要显示的宽和高对图片进行压缩
*
* @param fileDescriptor
* @param width
* @param height
* @return
*/
protected Bitmap decodeSampledBitmapFromPath(FileDescriptor fileDescriptor, int width, int height) {
// 获得图片的宽和高,并不把图片加载到内存中
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
//根据需求的宽和高以及图片实际的宽和高计算SampleSize
options.inSampleSize = ImageSizeUtil.caculateInSampleSize(options,
width, height);
// 使用获得到的InSampleSize再次解析图片
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
return bitmap;
}
}
好吧,终于自己实现了图片加载,设置属性可通过builder模式来进行封装。当然,其中遇到一个坑就是MD5加密时,相应位置替换成mDigest.digest().length和mDigest.digest().[i]会出现不同的url转化的key相同的问题,此处还得对mDigest.digest()转化成一个数组对象再进行调用!
参考:
《Abdroid开发艺术探索》
Android 框架练成 教你打造高效的图片加载框架
Android照片墙完整版,完美结合LruCache和DiskLruCache