最近整理了公司有关图片加载代码,这部分代码也不知道当时怎么想的,自己写了一套图片懒加载控件,我是觉得这应该用一些稳定的图片加载开源库,比如 Glide 之类的,毕竟这些开源库有那么多人的多年维护,用起来不会有很多暗病,最近整理这些图片加载的代码真是弄的心力交瘁。
一直改不是办法,想着应该也不难,就自己动手写了一个,下面看看吧!
实现思路
这里整理了一下图片懒加载的一个过程,实际就是下载到显示,当然我这写的思路仅供参考:
- 初始化
- 设置 (默认图、取内存缓存)
- 加载 (取本地缓存、下载、存本地缓存)
- 处理 (创建Bitmap、压缩)
- 缓存 (内存缓存)
- 更新 (主线程更新)
简单说下,初始化就是设置一些数据,设置就是设置图片链接,我把默认图和内存缓存图写一起了,放在里面,加载就是取文件,处理是从文件到 bitmap 并加上其他处理,缓存是到内存缓存,更新是主线程更新,这里还有一层本地缓存,但是我不想全部写在这里,耦合性太高,后面使用一个专门的文件处理类来实现。
具体实现
通过上面思路,实际只要将各个部分解耦开来,一步步实现就好,这里代码也不多,我就直接全部贴出来了,看下面:
import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.util.Consumer;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.util.Log;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author: silence
* @date: 2021-05-27
* @description: 简单图片懒加载
*/
public class LazyImageView extends AppCompatImageView {
//图片链接
private String mUrl = "";
//默认图片
private static Bitmap sDefaultBitmap;
//失败图片
private static Bitmap sErrorBitmap;
//文件处理工具
private static IFileHelper sFileHelper;
//图片处理工具
private static IBitmapHelper sBitmapHelper;
//内存缓存 - 10条,自动删除最老数据,Bitmap会自动回收
private final static Map<String, Bitmap> sBitmapCache = new LinkedHashMap<String, Bitmap>() {
protected boolean removeEldestEntry(Map.Entry<String, Bitmap> eldest) {
return size() >= 10;
}
};
public LazyImageView(Context context) {
super(context);
}
public LazyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LazyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//初始化
public static void init(Bitmap defaultBitmap, Bitmap errorBitmap, IFileHelper fileHelper, IBitmapHelper bitmapHelper) {
//BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
LazyImageView.sDefaultBitmap = defaultBitmap;
LazyImageView.sErrorBitmap = errorBitmap;
LazyImageView.sFileHelper = fileHelper;
LazyImageView.sBitmapHelper = bitmapHelper;
}
//设置
public void show(String url) {
Log.d("TAG", "show: " + url);
if (!mUrl.equals(url)) {
mUrl = url;
//取内存缓存,无内存缓存设为默认图
display(null != sBitmapCache.get(url) ? sBitmapCache.get(url) : sDefaultBitmap);
//加载链接
load(file -> {
if (null != file) {
Bitmap bitmap = handle(file);
cache(bitmap);
display(bitmap);
} else {
display(sErrorBitmap);
}
});
}
}
//加载
private void load(Consumer<File> resultHandler) {
Log.d("TAG", "load: ");
sFileHelper.download(mUrl, resultHandler);
}
//处理
private Bitmap handle(File file) {
Log.d("TAG", "handle: ");
return sBitmapHelper.handle(file);
}
//缓存
private void cache(Bitmap bitmap) {
Log.d("TAG", "cache: ");
sBitmapCache.put(mUrl, bitmap);
}
//显示
private void display(Bitmap bitmap) {
Log.d("TAG", "display: ");
this.post(()-> setImageBitmap(bitmap));
}
//文件处理,解耦,如有需要重写 download 即可
public interface IFileHelper {
void download(String url, Consumer<File> resultHandle);
}
//图片处理,解耦,如有需要重写handle函数
public interface IBitmapHelper {
Bitmap handle(File file);
}
}
简单说明
简单说明一下,实际这里的代码和上面思路完全一致,处理构造函数,剩下的就是六个步骤对应的函数,这里写了两个接口,用来处理获取文件和处理 bitmap,具体实现可以根据需要另做处理。
//默认图片
private static Bitmap sDefaultBitmap;
//失败图片
private static Bitmap sErrorBitmap;
上面是默认显示图片和加载失败图片的 bitmap,写成了类变量,节省内存占用。
//内存缓存 - 10条,自动删除最老数据,Bitmap会自动回收
private final static Map<String, Bitmap> sBitmapCache = new LinkedHashMap<String, Bitmap>() {
protected boolean removeEldestEntry(Map.Entry<String, Bitmap> eldest) {
return size() >= 10;
}
};
内存缓存使用了 LinkedHashMap,主要使用它的移除老元素功能,内存缓存我不希望过多,但是页面经常刷新的时候,快速复用已有 bitmap 还是很有必要的。
//初始化
public static void init(Bitmap defaultBitmap, Bitmap errorBitmap, IFileHelper fileHelper, IBitmapHelper bitmapHelper) {
//BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
LazyImageView.sDefaultBitmap = defaultBitmap;
LazyImageView.sErrorBitmap = errorBitmap;
LazyImageView.sFileHelper = fileHelper;
LazyImageView.sBitmapHelper = bitmapHelper;
}
在 init 函数总对类变量就行赋值,写成了静态函数,全局只需要设置一次即可。
BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
对于从图片资源 id 到 bitmap 可以通过上面方法实现,需要在 context 环境中执行,我不想耦合进来,所以在 init 函数前自行转换吧。
//设置
public void show(String url) {
Log.d("TAG", "show: " + url);
if (!mUrl.equals(url)) {
mUrl = url;
//取内存缓存,无内存缓存设为默认图
display(null != sBitmapCache.get(url) ? sBitmapCache.get(url) : sDefaultBitmap);
//加载链接
load(file -> {
if (null != file) {
Bitmap bitmap = handle(file);
cache(bitmap);
display(bitmap);
} else {
display(sErrorBitmap);
}
});
}
}
可以看到,实际上主要逻辑都是在 show 函数中实现的,这里将其他函数组合起来,因为 load 函数中需要在异步线程中处理,所以传递了一个 Consumer 进去,这里 Consumer 要用旧版本的 Consumer 不然需要安卓 v24 以上才能用。通过 Lambda 表达式,我们对拿到 load 函数完成后的结果,成功则缓存、显示,失败则显示失败的 bitmap。至于缓存和显示里面的代码,应该不用解释了,很简单。
实现获取文件
上面代码中我们只设置了一个接口来获取文件,下面我们来实现该接口:
/**
* @author: silence
* @date: 2021-05-27
* @description: 简单文件处理工具
*/
public class FileHelper implements LazyImageView.IFileHelper {
//缓存路径,应用默认储存路径
private static final String CACHE_DIR =
"/data" + Environment.getDataDirectory().getAbsolutePath() + "/" +
getApplication().getPackageName() + "/cache/";
//缓存大小
private static final int BUFFER_SIZE = 1024;
//线程池
final ExecutorService threadPool = Executors.newFixedThreadPool(8);
//相同链接的锁, 这里用LinkedHashMap限制一下储存的数量
private final Map<String, Semaphore> mUrlLockMap =
new LinkedHashMap<String, Semaphore>() {
protected boolean removeEldestEntry(Map.Entry<String, Semaphore> eldest) {
return size() >= 64 * 0.75;
}
};
//下载文件
public void download(String url, Consumer<File> resultHandler) {
threadPool.execute(()-> {
File downloadFile = null;
//空路径
if (TextUtils.isEmpty(url)) {
//注意使用旧版本Consumer
resultHandler.accept(null);
return;
}
//检查本地缓存
downloadFile = new File(getLocalCacheFileName(url));
if (downloadFile.exists()) {
resultHandler.accept(downloadFile);
return;
}
//同时下载文件会对同一个文件做修改,需要使用锁机制,使用信号量简单点
Semaphore semaphore;
synchronized (mUrlLockMap) {
semaphore = mUrlLockMap.get(url);
if (null == semaphore) {
semaphore = new Semaphore(1);
mUrlLockMap.put(url, semaphore);
}
}
//保证锁一定解锁
try {
semaphore.acquire();
//再次检查是否有本地缓存,解开锁之后可能下载完成
downloadFile = new File(getLocalCacheFileName(url));
if (downloadFile.exists()) {
resultHandler.accept(downloadFile);
return;
}
//网络下载部分
HttpURLConnection conn = null;
BufferedInputStream inputStream = null;
FileOutputStream outputStream = null;
RandomAccessFile randomAccessFile;
File cacheFile = new File(getLocalCacheFileName(url));
//要下载文件大小
long remoteFileSize = 0, sum = 0;
byte[] buffer = new byte[BUFFER_SIZE];
try {
URL conUrl = new URL(url);
conn = (HttpURLConnection) conUrl.openConnection();
remoteFileSize = Long.parseLong(conn.getHeaderField("Content-Length"));
existsCase:
if (cacheFile.exists()) {
long cacheFileSize = cacheFile.length();
//异常情况
if (cacheFileSize == remoteFileSize) {
break existsCase;
} else if (cacheFileSize > remoteFileSize) {
//如果出现文件错误,要删除
//noinspection ResultOfMethodCallIgnored
cacheFile.delete();
cacheFile = new File(getLocalCacheFileName(url));
cacheFileSize = 0;
}
conn.disconnect(); // must reconnect
conn = (HttpURLConnection) conUrl.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", "VcareCity");
conn.setRequestProperty("RANGE", "buffer=" + cacheFileSize + "-");
conn.setRequestProperty("Accept",
"image/gif,image/x-xbitmap,application/msword,*/*");
//随机访问
randomAccessFile = new RandomAccessFile(cacheFile, "rw");
randomAccessFile.seek(cacheFileSize);
inputStream = new BufferedInputStream(conn.getInputStream());
//继续写入文件
int size;
sum = cacheFileSize;
while ((size = inputStream.read(buffer)) > 0) {
randomAccessFile.write(buffer, 0, size);
sum += size;
}
randomAccessFile.close();
} else {
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
if (!cacheFile.exists()) {
//noinspection ResultOfMethodCallIgnored
cacheFile.createNewFile();
}
inputStream = new BufferedInputStream(conn.getInputStream());
outputStream = new FileOutputStream(cacheFile);
int size;
while ((size = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, size);
sum += size;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != conn) conn.disconnect();
if (null != inputStream) inputStream.close();
if (null != outputStream) outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//下载结束
long dwonloadFileSize = cacheFile.length();
if (dwonloadFileSize == remoteFileSize && dwonloadFileSize > 0) {
//成功
resultHandler.accept(new File(getLocalCacheFileName(url)));
}else {
resultHandler.accept(null);
}
} catch (Exception e) {
e.printStackTrace();
//异常的话传递空值
resultHandler.accept(null);
} finally {
//释放信号量
semaphore.release();
}
});
}
//获取缓存文件名
private String getLocalCacheFileName(String url) {
return CACHE_DIR + url.substring(url.lastIndexOf("/"));
}
}
这里使用了线程池来下载文件,缓存路径中需要使用到 context,解耦不太充分,读者可以直接设置路径。
//相同链接的锁, 这里用LinkedHashMap限制一下储存的数量
private final Map<String, Semaphore> mUrlLockMap =
new LinkedHashMap<String, Semaphore>() {
protected boolean removeEldestEntry(Map.Entry<String, Semaphore> eldest) {
return size() >= 64 * 0.75;
}
};
为了避免相同的链接触发对同一个文件的修改,这里还是用到了锁机制,具体做法是对每个链接设置一个允许数量为1的信号量,相同的链接被允许下载的时候才能下载,不过要记得释放信号量。
实现图片处理
下面实现图片处理逻辑,实际上这里可以对 bitmap 进行压缩,读者可以自行设计。
/**
* @author: silence
* @date: 2021-05-27
* @description: 简单图片处理工具
*/
public class BitmapHelper implements LazyImageView.IBitmapHelper {
@Override
public Bitmap handle(File file) {
return decodePhotoFile(file);
}
//根据文件生成bitmap
private Bitmap decodePhotoFile(File file) {
Bitmap bitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
try(FileInputStream instream = new FileInputStream(file);) {
bitmap = BitmapFactory.decodeStream(instream, null, options);
}catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
}
我这里只是最简单的将文件转换成了 bitmap。
实际使用
这里演示一下代码使用,而在 XML 中使用类似且更简单。
LazyImageView.init(
BitmapFactory.decodeResource(getResources(), R.mipmap.img_def),
BitmapFactory.decodeResource(getResources(), R.mipmap.img_load_fail),
new FileHelper(), new BitmapHelper());
LinearLayout linearLayout = findViewById(R.id.title);
LazyImageView lazyImageView = new LazyImageView(this);
linearLayout.addView(lazyImageView);
lazyImageView.show("https://api.dujin.org/bing/1920.php");
这里我用必应每日一图做了实验,可以下载并显示,第一次下载完成后取缓存速度会快很多。
其他设置宽高大小什么的和 ImageView 一样,毕竟是从 ImageView 继承过来的,圆角什么的就自己搞了吧!
结语
这里虽然自己写代码实现图片的懒加载,可是我还是觉得应该用一些稳定的开源库去加载图片,一是文档,而是交接给别人也好理解,当然这样一个功能写在来,还是挺有意思的,特别是解耦、下载和锁机制,希望能帮到有需要的读者!
网友评论