当前位置:网站首页>LruCache与DiskLruCache结合简单实现ImageLoader
LruCache与DiskLruCache结合简单实现ImageLoader
2022-08-10 05:35:00 【丶咸鱼】
这主要是记录一下Android中的图片缓存的基本策略,实现一个粗糙的图片加载框架
Android的图片加载框架都是万变不离其宗嘛,Bitmap的压缩裁剪,LruCache和DiskLruCache缓存策略的使用,ThreadPoolExecutor线程池的使用,其它各种贴心优化等等。
LruCache这个不多说了可以看我历史悠久的文章LruCach解析和使用 ,或者百度谷歌一下郭神的博客。主要说一下DiskLruCache的使用和结合实现图片加载。
DiskLruCache
DiskLruCache顾名思意磁盘缓存,与LruCache是一对好兄弟,在Android中图片框架中几乎形影不离。不同的是DiskLruCache不是google官方所写,但是得到了官方推荐,DiskLruCache没有编写到SDK中去,如需使用可直接copy这个类到项目中去。如何使用DiskLruCache呢,需要自己手动导入项目中才能使用。
DiskLruCache下载地址:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
https://github.com/JakeWharton/DiskLruCache
现在这个时候使用AS的推荐使用JakeWharton大神的Github链接在gradle中直接引入compile 'com.jakewharton:disklrucache:2.0.2'
DiskLruCache使用
DiskLruCache并不能通过构造方法来创建,它提供了opean方法用于创建自身,如下所示:public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open()方法接收四个参数:
第一个参数:指定的是数据的缓存地址
第二个参数:指定当前应用程序的版本号(如果版本发生改变会清空之前的缓存文件,在实际开发中一般我们并不需要缓存文件随着版本号的改变而清空数据,所以一般设为1即可)
第三个参数:指定同一个key可以对应多少个缓存文件,基本都是传1
第四个参数:指定最多可以缓存多少字节的数据(超过这个设定值后会自动清除一些缓存数据来保证总大小不大于这个设定值)
数据缓存通常都会存放在 /sdcard/Android/data/<package_name>/cache 这个路径下,其中package_name表示当前应用的包名,当应用被卸载后,此目录会一并被删除。这个缓存路径可以根据需求灵活设置路径,一般如果希望应用卸载后就希望删除缓存文件的,那么就选择sdcard上的缓存目录,如果希望保留缓存数据的不随着应用卸载而没有的那就应该选择sdcard上的其他特定目录。现在的手机有些可能没有SD卡,或者没内存了。一般代码都会做判断。如下:
/** * 获取磁盘缓存地址 * * @param ctx * @param uniqueName 唯一标示名 * @return */
private File getDiskCacheDir(Context ctx, String uniqueName) {
boolean externalAvailable = (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
|| !Environment.isExternalStorageRemovable());
String cachePath;
if (externalAvailable) {
cachePath = ctx.getExternalCacheDir().getPath();///sdcard/Android/data/package_name/cache 这个路径
} else {
cachePath = ctx.getCacheDir().getPath(); ///data/data/package_name/cache 这个路径。
}
return new File(cachePath + File.separator + uniqueName);
}
DiskLruCache的一些常用方法
方法 | 备注 |
---|---|
DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) | 打开一个缓存目录,如果没有则首先创建它,directory:指定数据缓存地址 appVersion:APP版本号,当版本号改变时,缓存数据会被清除 valueCount:同一个key可以对应多少文件 maxSize:最大可以缓存的数据量 |
Editor edit(String key) | 通过key可以获得一个DiskLruCache.Editor,通过Editor可以得到一个输出流,进而缓存到本地存储上 |
void flush() | 强制缓冲文件保存到文件系统 |
Snapshot get(String key) | 通过key值来获得一个Snapshot,如果Snapshot存在,则移动到LRU队列的头部来,通过Snapshot可以得到一个输入流InputStream |
boolean remove(String key) | 根据key值来删除对应的数据,如果该数据正在被编辑,则不能删除 |
void delete() | 关闭缓存并且删除目录下所有的缓存数据,即使有的数据不是由DiskLruCache 缓存到本目录的 |
void close() | 关闭DiskLruCache,缓存数据会保留在外存中 |
boolean isClosed() | 判断DiskLruCache是否关闭,返回true表示已关闭 |
long size() | 缓存数据的大小,单位是byte |
File getDirectory() | 缓存数据的目录 |
一些关于DiskLruCache的API使用呢,由于篇幅限制不单独写代码解释,如有不懂的可以看Android DiskLruCache完全解析,硬盘缓存的最佳方案。这篇主要是结合来实现图片加载缓存框架,关于简单的图片缓存加载步骤,理论上应该分为以下几点:
- 先根据图片地址从内存(LruCache)中查找是否有该缓存,有则直接读取显示
- 内存中没有在从磁盘中(DiskLruCache)中查找是否有该缓存,有则直接显示,并存入内存中
- 上面两者中都没得话,从网上下载并缓存到磁盘和内存中
- 如果没有磁盘SD卡的话,或者磁盘没有多余的容量,则只能从网上直接拉取显示
ImageLoader实现
- gradle中引入disklrucache
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.2.1'
compile 'com.android.support:design:23.2.1'
compile 'com.jakewharton:disklrucache:2.0.2'
}
- AndroidManifest.xml配置网络权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
- 编写一个简单的图片压缩工具类
/** * 图片压缩 */
public class ImageCompress {
public ImageCompress() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//计算inSampleSize 采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight){
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);
}
/** * 获取图片的采样率,缩放比例 * * @param options * @param reqWidth * @param reqHeight * @return */
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int inSampleSize = 1;
if (reqWidth == 0 || reqHeight == 0)
return inSampleSize;
int height = options.outHeight;
int width = options.outWidth;
if (height > reqHeight || width > reqWidth) {
int halHeight = height / 2;
int halWidth = width / 2;
while ((halHeight / inSampleSize) >= reqHeight &&
(halWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
- 核心类ImageLoader的代码实现,代码里面有注释
public class ImageLoader {
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
private static final int DISK_CACHE_INDEX = 0;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int TAG_KEY_URI = R.id.imageloader_uri; //防止图片错位的tag
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private boolean mIsDiskLruCacheCreated = false; //是否有使用了磁盘
private ImageCompress imageCompress = new ImageCompress();
private Context mContext;
private static ImageLoader mImageLoader;
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
}
}
};
public static ImageLoader getInstance(Context ctx) {
if (mImageLoader == null) {
mImageLoader = new ImageLoader(ctx);
}
return mImageLoader;
}
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
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.getByteCount() / 1024;
}
};
//获取图片缓存路径
File diskCacheDir = getDiskCacheDir(mContext, "thumbBtimap");
if (!diskCacheDir.exists())
diskCacheDir.mkdirs();
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
// mDiskLruCache = DiskLruCache.open(diskCacheDir, getAppVersion(mContext), 1, DISK_CACHE_SIZE);
// 创建DiskLruCache实例,初始化缓存数据
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
//第二个参数是应用版本号,一般设为1即可。当版本号发生改变时会清空之前所有的缓存文件,
// 而这个特性在实际开发中作用不大。实际开发中一般版本更新了数据缓存仍然要有效
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void loadBitmap(String url, ImageView imgView) {
loadBitmap(url, imgView, 0, 0);
}
public void loadBitmap(final String url, final ImageView imgView, final int reqWidth, final int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(url);
imgView.setTag(TAG_KEY_URI, url);//防止错位
if (bitmap != null) {
imgView.setImageBitmap(bitmap);
return;
}
Runnable runnable = new Runnable() {
@Override
public void run() {
Bitmap runBitmap = loadBitmap(url, reqWidth, reqHeight);
if (runBitmap != null) {
LoaderResult result = new LoaderResult(imgView, url, runBitmap);
mMainHandler.obtainMessage(1, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(runnable);
}
/** * 加载bitmap * 1.先从内存中查找是否有该bitmap * 2.内存中没有在从磁盘中查找是否有该bitmap * 3.上面两者都没有就从网上下载,并缓存到磁盘和内存中 * 4.如果内存中没有,磁盘并不存在或者缓存内存不足已使用的时候,直接从网络拉取 * * @param url * @param reqWidth * @param reqHeight * @return */
private Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(url);//内存读取
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);//磁盘读取
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);//网络下载缓存
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
bitmap = downloadBitmapFromUrl(url);//直接网络拉取图片
}
return bitmap;
}
private Bitmap loadBitmapFromMemCache(String url) {
String key = hashKeyFormUrl(url);
return getBitmapFromMemoryCache(key);
}
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
//获取到文件输出流后下载文件写入到缓存
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToSteam(url, outputStream)) {
editor.commit();
} else {
editor.abort();//如果没有下载bitmap并写入outputsteam成功则回退整个操作
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
/** * 从磁盘中获取缓存,并存入内存中 * * @param url * @param reqWidth * @param reqHeight * @return * @throws IOException */
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
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(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = imageCompress.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
/** * DiskLruCache的缓存查找是通过DiskLruCache的get()方法得到一个Snapshot对象, * 然后再通过Snapshot对象得到缓存的文件输入流,这样就能获取到Bitmap */
return bitmap;
}
/** * 一般采用URL的MD5作为key,避免非法字符 * * @param url */
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(url.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/** * 添加进内存中 * * @param key * @param bitmap */
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/** * 从内存中获取bitmap * * @param key * @return */
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
/** * 获取版本号 * * @param ctx * @return */
private int getAppVersion(Context ctx) {
PackageInfo info = null;
try {
info = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/** * 获取空闲的可用空间大小 * * @param path 存放路径 * @return */
private long getUsableSpace(File path) {
return path.getUsableSpace();
}
/** * 获取磁盘缓存地址 * * @param ctx * @param uniqueName 唯一标示名 * @return */
private File getDiskCacheDir(Context ctx, String uniqueName) {
boolean externalAvailable = (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
|| !Environment.isExternalStorageRemovable());
String cachePath;
if (externalAvailable) {
cachePath = ctx.getExternalCacheDir().getPath();///sdcard/Android/data/package_name/cache 这个路径
} else {
cachePath = ctx.getCacheDir().getPath(); ///data/data/package_name/cache 这个路径。
}
return new File(cachePath + File.separator + uniqueName);
}
/** * 写入缓存 * * @param urlPath * @param outputStream * @return */
private boolean downloadUrlToSteam(String urlPath, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(urlPath);
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) {
e.printStackTrace();
} finally {
if (urlConnection != null)
urlConnection.disconnect();
try {
if (out != null)
out.close();
if (in != null)
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/** * 通过url地址直接网络下载 * * @param urlPath * @return */
private Bitmap downloadBitmapFromUrl(String urlPath) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlPath);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (final IOException e) {
} finally {
if (urlConnection != null)
urlConnection.disconnect();
try {
if (in != null)
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
//---------------------配置线程池----------------------
private static final int CPU_COUNT = Runtime.getRuntime()
.availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1; //核心数
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //最大核心数
private static final long KEEP_ALIVE = 10L; //线程闲置超时时长
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
我相信这代码大多数人都能看懂,这ImageLoader的实现参考了郭神的博客和主席(任玉刚)的《Android开发艺术探索》。如果一些知识点上面没表达清楚的,可以去两位大神的博客看看。r结合LruCache和DiskLruCache和线程池简单的实现了一个ImageLoader,效果图如下:
Demo源码下载地址
边栏推荐
猜你喜欢
随机推荐
LeetCode 1351.统计有序矩阵中的负数(简单)
树结构——二叉查找树原理与实现
Decentralized and p2p networks and traditional communications with centralization at the core
【List练习】遍历集合并且按照价格从低到高排序,
pytorch-09. Multi-classification problem
LeetCode 100.相同的树(简单)
[Difference between el and template]
WeChat applet wx.writeBLECharacteristicValue Chinese character to buffer problem
机器学习——聚类——商场客户聚类
LeetCode 292. Nim Game (Simple)
Common class BigDecimal
一个基于.Net Core 开源的物联网基础平台
LeetCode 1894. Find the student number that needs to be supplemented with chalk
LeetCode 938.二叉搜索树的范围和(简单)
LeetCode Interview Question 17.14 Minimum k Number (Moderate)
STM32单片机OLED俄罗斯方块单片机小游戏
常用类 BigDecimal
探索性数据分析EDA
详解 Hough 变换(下)圆形检测
PyTorch之训练技巧