挥舞着指尖,谱写指尖的艺术
这次的手写Glide图片缓存框架,并不是引入Glide三方,对其进行自定义配置;而是自己实现一个类似Glide图片加载框架。
附带源码下载地址,文末有地址。
大概就这么多,而我们这次手写Glide主要实现的有:
1 .这里实现的是有缺点的,调用顺序不能改变,必须按照三步走方式;
2 .有兴趣爱好的可以自己完善修复;
Android开发之手写Glide图片加载/缓存 框架
实现的几个关键类:
- Glide.java //链式调用者
- bitmapRequest.java //链式封装
- RequestManager.java //对每个请求进行封装管理,队列模式管理请求
- PicDispatcherThread.java //图片下载/内存缓存/数据库缓存 的线程类
- DatabaseSQLite.java //二级缓存-数据库缓存的创建者
- DatabaseManager.java //二级缓存-数据库缓存的put,get,remove方法实现类
- CompressPic.java //图片像素压缩处理
- LruCacheUtils.java //内存缓存机制
//上下文、下载地址、显示的控件
Glide.with(this).load(URL).into(view);
先上部分代码#Glide
public class Glide {
public static bitmapRequest with(Context context){
return new bitmapRequest(context);
}
}
public class bitmapRequest {
//上下文
private Context context;
//图片URL
private String URL;
//显示图片的控件
private SoftReference<ImageView> imageView;
//回调接口
private RequestListener listener;
//请求访问标志
private String urlMD5;
//占位图
private int ResId;
public static ArrayList<String> md5 = new ArrayList<>();
public bitmapRequest(Context context) {
this.context = context;
//创建缓存图片的数据库
DatabaseManager.getInstance(context);
}
public bitmapRequest load(String URL){
this.URL = URL;
this.urlMD5 = MD5Tool.getMD5(URL);
md5.add(urlMD5);
return this;
}
public void removeToSqlCache(){
DatabaseManager.remove();
}
public bitmapRequest loadError(int redId){
this.ResId = redId;
return this;
}
public bitmapRequest listener(RequestListener listener){
this.listener = listener;
return this;
}
/**
* 当执行到最后一步时,拿到RequestManager
* 把请求加入请求队列
* @param imageView
*/
public void into(ImageView imageView){
imageView.setTag(this.urlMD5);
this.imageView = new SoftReference<>(imageView);
RequestManager.getInstance().addRequest(this);
}
}
回调接口没用上,可用于监听图片下载进度等… 有兴趣的可以根据回调接口拦截图片下载失败/下载成功后的下一步 步骤。
在调用构造函数时,会创建一个单例模式的数据库二级缓存。
这里把urlMD5作为图片控件的唯一tag标志,根据这个标志去判断图片的唯一key,也防止图片错位。再把每个key都保存下来,方便一键清除缓存。
into方法中调用了单例模式的请求管理,把每个请求都封装好,给每个请求安排多线程模式进行处理。
RequestManager.getInstance().addRequest(this);
先上代码
/*
* 管理消息隊列
* 單例模式
* */
public class RequestManager {
private static RequestManager manager = new RequestManager();
//创建请求队列,放入各个请求
LinkedBlockingQueue<bitmapRequest> queue;
//每个请求对应的线程
private PicDispatcherThread[] dispatcher;
//创建线程的最大数
private int threadCount;
public RequestManager() {
queue = new LinkedBlockingQueue<>();
//执行线程时,先关闭线程池里所有运行时的线程
stopThread();
//初始化线程
initThread();
}
public static RequestManager getInstance() {
return manager;
}
/**
* 1.拿到APP最大能创建的线程数
*/
private void initThread() {
threadCount = Runtime.getRuntime().availableProcessors();
dispatcher = new PicDispatcherThread[threadCount];
//创建线程,把线程存入线程数组中
for (int i = 0; i < threadCount; i++) {
PicDispatcherThread thread = new PicDispatcherThread(queue);
thread.start();
dispatcher[i] = thread;
}
}
/**
* 将请求的对象加入到请求队列之中
*
* @param request
*/
public void addRequest(bitmapRequest request) {
if (!queue.contains(request)) {
queue.add(request);
}
}
public void stopThread() {
if (dispatcher != null && dispatcher.length > 0) {
for (PicDispatcherThread thread : dispatcher) {
if (!thread.isInterrupted()) {//当线程没被回收,停止时
thread.interrupt();//kill thread
}
}
}
}
}
可自行看注释,注释详细标明了。
接着上部分代码,一次性上太多了有点眼花。
先看主体内容,线程运行中的几个方法。
@Override
public void run() {
super.run();
//无限循环请求对象
while (true) {
try {
bitmapRequest request = queue.take();
//占位图片
showDefaultPic(request);
//下载图片
Bitmap bitmap = loadBitmap(request);
//加载图片
showPic(request, bitmap);
} catch (Exception e) {
Log.e("thread:", "下载异常");
e.printStackTrace();
}
}
}
这里无限循环去读取队列中的消息,暂未实现线程同步,有需求的可以去加上synchronize进行加锁机制,因为实现的请求队列内部已经是有锁状态,我这里没太多需求,不需要去另行加锁,首先queue.take()取出其中一个请求队列,先给个图片占位图(图片未下载成功时,显示的默认图片),再去下载图片,下载返回一个bitmap类型再执行showPic回到主线程加载图片。
主要看loadBitmap方法。先上代码
/**
* 请求图片,使用三方框架
* OKhttp
*
* @param request
* @return
*/
private Bitmap loadBitmap(bitmapRequest request) throws Exception {
Bitmap bitmap = null;
lruCacheUtils = LruCacheUtils.getInstance();
//从缓存拿
bitmap = (Bitmap) lruCacheUtils.get(request.getUrlMD5());
if (bitmap != null) {
Log.e("缓存", "从缓存中拿到");
return bitmap;
}
//从数据库拿
bitmap = DatabaseManager.get(request.getUrlMD5());
if (bitmap != null) {
Log.e("数据库", "从数据库中拿到");
lruCacheUtils.put(request.getUrlMD5(), bitmap);
return bitmap;
}
//下载图片
bitmap = downLoadUrlPic(request.getURL(),request);
if (bitmap != null) {
//加入缓存中
Cache_put(request, bitmap);
}
return bitmap;
}
该类的全部代码奉上
/**
* 创建请求队列中的每个线程
*/
public class PicDispatcherThread extends Thread {
//内存缓存
LruCacheUtils lruCacheUtils;
//请求队列
private LinkedBlockingQueue<bitmapRequest> queue;
//拿到主线程
private Handler handler = new Handler(Looper.getMainLooper());
public PicDispatcherThread(LinkedBlockingQueue<bitmapRequest> queue) {
this.queue = queue;
}
@Override
public void run() {
super.run();
//无限循环请求对象
while (true) {
try {
bitmapRequest request = queue.take();
//占位图片
showDefaultPic(request);
//下载图片
Bitmap bitmap = loadBitmap(request);
//加载图片
showPic(request, bitmap);
} catch (Exception e) {
Log.e("thread:", "下载异常");
e.printStackTrace();
}
}
}
/**
* 加载图片到控件中
*
* @param request
* @param bitmap
*/
private void showPic(bitmapRequest request, final Bitmap bitmap) {
final ImageView imageView = request.getImageView();
//md5解决图片错位问题,给每个图片设置tag
if (bitmap != null && request.getImageView() != null && request.getImageView().getTag().equals(request.getUrlMD5())) {
handler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}
/**
* 请求图片,使用三方框架
* OKhttp
*
* @param request
* @return
*/
private Bitmap loadBitmap(bitmapRequest request) throws Exception {
Bitmap bitmap = null;
lruCacheUtils = LruCacheUtils.getInstance();
//从缓存拿
bitmap = (Bitmap) lruCacheUtils.get(request.getUrlMD5());
if (bitmap != null) {
Log.e("缓存", "从缓存中拿到");
return bitmap;
}
//从数据库拿
bitmap = DatabaseManager.get(request.getUrlMD5());
if (bitmap != null) {
Log.e("数据库", "从数据库中拿到");
lruCacheUtils.put(request.getUrlMD5(), bitmap);
return bitmap;
}
//下载图片
bitmap = downLoadUrlPic(request.getURL(),request);
if (bitmap != null) {
//加入缓存中
Cache_put(request, bitmap);
}
return bitmap;
}
private void Cache_put(bitmapRequest request, Bitmap bitmap) {
//内存缓存
lruCacheUtils.put(request.getUrlMD5(), bitmap);
//数据库缓存-将bitmap转换为byte
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
byte[] bit = outputStream.toByteArray();
DatabaseManager.put(request.getUrlMD5(), bit);
}
private Bitmap downLoadUrlPic(String URL,bitmapRequest requestBitmap) {
Bitmap bitmap = null;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(URL).build();
try {
Response response = client.newCall(request).execute();
Log.e("下载", "图片 "+requestBitmap.getURL()+"下载成功");
//bitmap = CompressTwo.simaplePic(BitmapFactory.decodeStream(response.body().byteStream()));
bitmap = CompressPic.decodeBitmap(response.body().bytes(), 100, 100);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
private void showDefaultPic(bitmapRequest request) {
//切回主线程
if (request.getResId() > 0 && request.getImageView() != null) {
final int res = request.getResId();
final ImageView imageView = request.getImageView();
handler.post(new Runnable() {
@Override
public void run() {
imageView.setImageResource(res);
}
});
}
}
}
该类就是实现了一个单例的LruCache缓存
public class LruCacheUtils extends LruCache {
public static LruCacheUtils lruCacheUtils;
public LruCacheUtils(int maxSize) {
super(maxSize);
}
public static LruCacheUtils getInstance(){
if (lruCacheUtils == null){
lruCacheUtils = new LruCacheUtils(1000);
}
return lruCacheUtils;
}
@Override
protected int sizeOf(Object key, Object value) {
return super.sizeOf(key, value);
}
}
/**
* 数据库创建
* 充当二级缓存
*/
public class DatabaseSQLite extends SQLiteOpenHelper {
public static final String CREATE_TABLE = "create table cache_table (" +
"id integer primary key autoincrement," +
"ke text unique," +
"content blob" +
")";
private Context context;
public DatabaseSQLite(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_TABLE);
Toast.makeText(context,"表创建成功",Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
}
}
这里就简单的创建了数据库和表
上代码
/**
* 数据库CRUD
*/
public class DatabaseManager {
private static DatabaseManager manager;
private static SQLiteDatabase db;
private static String DatabaseName = "cache.db";
/**
* 单例模式
* @param context
* @return
*/
public static DatabaseManager getInstance(Context context) {
if (manager == null) {
manager = new DatabaseManager(context);
}
return manager;
}
/**
* getWritableDatabase():它以读写的方式去打开数据库,当数据库的磁盘空间满了时,就只能读不能写。
* getReadableDatabase():它内部也调用了getWritableDatabase(),以读写的方式打开,如果数据库磁盘空间满了,则打开失败。
* 打开失败后,如果继续尝试对数据库的读,则会读取数据,不能写
*
* @param context
*/
private DatabaseManager(Context context) {
DatabaseSQLite sqlIte = new DatabaseSQLite(context, DatabaseName, null, 1);
db = sqlIte.getWritableDatabase();
}
/**
* 插入数据进数据库
* @param md5Key 图片的key
* @param value 图片value
*/
public static void put(String md5Key, byte[] value) {
ContentValues values = new ContentValues();
values.put("ke", md5Key);//md5
values.put("content", value);//图片资源
db.insert("cache_table", null, values);
}
/**
* 读取数据库数据
* @param key 图片key
* @return 返回Bitmap
*/
public static Bitmap get(String key) {
String md5Key;//存放图片key
byte[] PicContent = null;//存放图片二进制
Bitmap resultPic = null;//返回图片
Cursor query = db.query(true, "cache_table", null,
null, null, null, null, null, null);
if (query.moveToFirst()) {
do {
//查询到当前图片的key时
md5Key = query.getString(query.getColumnIndex("ke"));
//一个key,对应一个value,当查询到了就break退出
if (key.equals(md5Key)){
//拿到图片的value
PicContent = query.getBlob(query.getColumnIndex("content"));
break;
}
} while (query.moveToNext());
}
//如果数据为null时则不进行转换
if (PicContent != null) {
resultPic = BitmapFactory.decodeByteArray(PicContent, 0, PicContent.length);
}
query.close();
return resultPic;
}
/**
* 删除数据库缓存
*/
public static void remove() {
try {
db.delete("cache_table", null,null);
Log.e("delete", "删除成功");
} catch (Exception e) {
Log.e("delete", "数据库暂无数据");
}
}
}
这里主要就是一些 put,get,remove操作。自行看注释,很详细了。
get读取图片时,传入的key,就是urlMD5,和数据库中的urlMD5进行对比,拿到则返回图片资源
public class CompressPic {
/**
* 图片缩小工具类
* @param stream 图片的字节流
* @param reqWidth 要缩小的宽
* @param reqHeight 要缩小的高
* @return
*/
public static Bitmap decodeBitmap(byte[] stream, int reqWidth, int reqHeight){
int inPictureHeight;
int inPictureWidth;
int inSample = 1;
BitmapFactory.Options options = new BitmapFactory.Options();
// 设置为true时,bitmap = null,不加载进内存,但可以得到图片的宽高
//只获取图片的大小信息,而不是将整张图片载入在内存中,避免内存溢出
options.inJustDecodeBounds = true;
//byte, byte.length, options
BitmapFactory.decodeByteArray(stream, 0,stream.length, options);
//获取图片资源的宽高
inPictureHeight = options.outHeight;
inPictureWidth = options.outWidth;
//图片缩小算法
BitmapFactory.Options resultOption = calculationWidthAndHeight(options,inPictureHeight,inPictureWidth,reqWidth,reqHeight,inSample);
return BitmapFactory.decodeByteArray(stream,0,stream.length,resultOption);
}
private static BitmapFactory.Options calculationWidthAndHeight(BitmapFactory.Options options, int inPictureHeight, int inPictureWidth,
int reqWidth, int reqHeight, int inSample) {
//更改原始像素为reqWidth,reqHeight比例,如果原始像素比自定义像素要大,则跳过此步骤
if (inPictureWidth > reqWidth || inPictureHeight >reqHeight){
while (inPictureWidth / inSample > reqWidth || inPictureHeight / inSample >reqHeight){
//要求取值为n的2次冥,不是二次幂则四舍五入到最近的二次幂
inSample = inSample *2;
}
}
//得到的最终值
options.inSampleSize = inSample;
//设置编码为RGB_565,默认为ARGB_8888
options.inPreferredConfig = Bitmap.Config.RGB_565;
//设置为false,得到实际的bitmap,不然只会得到宽高等信息
options.inJustDecodeBounds = false;
//设置图片可以复用
options.inMutable = true;
return options;
}
}
注释很详细了喔 !!!
我这里用的是RecyclerView进行图片的加载。
具体调用如下:
with是上下文唷。loadError是占位图。
图片的重试会在后序补充。
最后 感谢 腾讯课堂给予的知识补充 !!!