背景调研看这里->图片缓存第三方库
引文
去年年初的时候,有做过一轮调研。当时是选了Glide,也针对它做了一些封印。把它解耦出来,方便换库。时间向前,当时那个项目已经停了快一年了,代码的封装还是蛮细心的,这里放上来给自己做个笔记吧。
正文
Glide的API很友好,使用起来挺简单的。二级缓存主要是LRU算法。
LRU 即 Least Recently Used。每发生一次读内存操作,首先查找待读取的数据是否存在于缓存中,若是,则缓存命中,返回数据;若否,则缓存未命中,从内存中读取数据,并把该数据添加到缓存中。向缓存添加数据时,如果缓存已满,则需要删除访问时间最早的那条数据,这种更新缓存的方法就叫做LRU。实现的话,简单做就是双向链表+hashmap。
这个算法很经典。kernel的页缓存管理也是用这个的。
当时困扰我的问题
- Glide在RecyclerView中使用过程中会闪一下
- Glide bitmap弱引用的问题
- Glide和CircleImageView有冲突
当然这几个问题都修正了,细节看代码 :)
需要额外确定的问题有
- Glide消耗的磁盘大小,存储的路径
- Glide缓存失效的机制
使用
//from string url
//can use in recyclerView,listView
final String url = "https://www.baidu.com/img/bd_logo1.png";
IMAGs.INSTANCE.bind(context, view, url);
//from string url , with default res_id
IMAGs.INSTANCE.bind(context, view, url, R.drawable.gift_default_icon);
//call in async-callback
IMAGs.INSTANCE.bind(context, new ImageMgr.Option(url), new ImageMgr.Callback() {
@Override
public void onFail(@NonNull Exception e, Drawable errorDrawable) {
//do sth in error
}
@Override
public void onSuccess(@NonNull Bitmap bitmap) {
//do something in success
}
});
代码
base on ‘com.github.bumptech.glide:glide:3.7.0’
if 4.x -> migrating
调用入口
public class IMGs {
public static final ImageLoader INSTANCE = new ImageLoaderGlideImpl();
}
接口
interface ImageLoader {
@UiThread
fun printCurrentDiskCacheSizeToLog()
fun cleanCache(): Boolean
fun cleanCacheOnDisk(): Boolean
fun getCacheFile(): File
@UiThread
fun bind(context: Context, view: ImageView, uri: Uri)
/**
* used ImageLoader#bind(Context,ImageView, String, int) instead
*/
@Deprecated("")
@UiThread
fun bind(context: Context, view: ImageView, url: String?)
@UiThread
fun bind(context: Context, view: ImageView, url: String?, errorResId: Int)
@UiThread
fun bind(context: Context, view: ImageView, resId: Int)
/**
* In context'Lifecycle , bind option to ImageView
*/
@UiThread
fun bind(context: Context, view: ImageView, options: Options)
/**
* In context'Lifecycle , bind option to Callback
*/
@UiThread
fun bind(context: Context, options: Options, bitmapCall: BitmapCall)
@UiThread
fun bindCircle(context: Context, view: ImageView, errorResId: Int)
@UiThread
fun bindCircle(context: Context, view: ImageView, url: String?, errorResId: Int)
@UiThread
fun bindCircle(context: Context, view: ImageView, url: String?, errorResId: Int, cornerPx: Int)
}
public interface BitmapCall {
@UiThread
void onFail(@NonNull Exception e, @Nullable Drawable errorDrawable);
@UiThread
void onSuccess(@NonNull Bitmap bitmap);
}
Glide 实现类
/*********************************************************************
* This file is part of seeyoutime project
* Created by [email protected] on 2017/2/21.
* Copyright (c) 2017 XingDian Co.,Ltd. - All Rights Reserved
*********************************************************************/
final class ImageLoaderGlideImpl implements ImageLoader {
private final RequestListener<Object, GlideDrawable> drawableRequestListener;
ImageLoaderGlideImpl() {
drawableRequestListener = new RequestListener<Object, GlideDrawable>() {
@Override
public boolean onException(Exception e, Object model,
Target<GlideDrawable> target, boolean isFirstResource) {
if (e != null) Lg.e(e);
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource,
Object model, Target<GlideDrawable> target,
boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
};
}
@SuppressLint("StaticFieldLeak")
@UiThread
@Override
public void printCurrentDiskCacheSizeToLog() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
File files = getCacheFile();
long size = getSize(files);
Lg.d(size / (1024 * 1024) + "M/" + size);
return null;
}
}.execute();
}
@Override
public boolean cleanCache() {
Glide.get(SeeYouApp.getInstance()).clearMemory();
return true;
}
@Override
public boolean cleanCacheOnDisk() {
Glide.get(SeeYouApp.getInstance()).clearDiskCache();
return true;
}
@NonNull
@Override
public File getCacheFile() {
return Glide.getPhotoCacheDir(SeeYouApp.getInstance());
}
@Override
public void bind(@NonNull Context context, @NonNull ImageView view, @NonNull final Uri uri) {
bind(context, view, new Options(uri));
}
@Deprecated
@Override
public void bind(@NonNull Context context, @NonNull ImageView view, @Nullable final String url) {
bind(context, view, new Options(url));
}
@Override
public void bind(@NonNull Context context, @NonNull ImageView view, final int resId) {
bind(context, view, new Options(resId));
}
@UiThread
public void bind(@NonNull Context context, @NonNull ImageView view, @Nullable final String url,
final int errorResId) {
bind(context, view, new Options(url, errorResId));
}
@UiThread
public void bindCircle(@NonNull Context context, @NonNull ImageView view, final int errorResId) {
Options ops = new Options(errorResId);
ops._roundCorner = true;
ops._roundCornerPx = view.getWidth();
bind(context, view, ops);
}
@UiThread
public void bindCircle(@NonNull Context context, @NonNull ImageView view, @Nullable final String url,
final int errorResId) {
Options ops = new Options(url, errorResId);
ops._roundCorner = true;
ops._roundCornerPx = view.getWidth();
bind(context, view, ops);
}
@UiThread
public void bindCircle(@NonNull Context context, @NonNull ImageView view, @Nullable final String url,
final int errorResId, final int cornerPx) {
Options ops = new Options(url, errorResId);
ops._roundCorner = true;
ops._roundCornerPx = cornerPx;
bind(context, view, ops);
}
@UiThread
public void bind(@NonNull final Context context, @NonNull ImageView view,
@NonNull final Options options) {
if (!isValidContext(context)) {
return;
}
if (!options.isValid()) {
if (options._placeDrawable != null) {
view.setImageDrawable(options._placeDrawable);
return;
} else if (options._errorDrawable != null) {
view.setImageDrawable(options._errorDrawable);
return;
}
//Is frustrated that 'url' from remoteService(http) can be empty/null
//only inner-test can throw exception
//throw new IllegalArgumentException("ImageMgr option is not valid,'_resId、_url、_uri、_placeResId、_placeDrawable、_errorDrawable' can't be empty");
return;
}
if (view instanceof CircleImageView) {
options._dontAnimate = true;
}
RequestManager requestManager = Glide.with(context);
options.build(requestManager).listener(drawableRequestListener).into(view);
}
@Override
public void bind(@NonNull final Context context, @NonNull final Options options,
@NonNull final BitmapCall bitmapCall) {
if (!isValidContext(context)) {
Lg.e("not Valid Context");
return;
}
if (!options.isValid()) {
//Is frustrated that 'url' from remoteService(http) can be empty/null
//only inner-test can throw exception
//throw new IllegalArgumentException("ImageMgr option is not valid,'_resId、_url、_uri' can't be empty");
Lg.repr(Author.YISHENG, new Lg.ErrReporter() {{
_http_report = true;
_message = context.getClass().getName() + options.toString();
_tag = ImageLoaderGlideImpl.class.getSimpleName();
}});
bitmapCall.onFail(new IllegalArgumentException("option is not valid"), null);
return;
}
RequestManager requestManager = Glide.with(context);
SimpleTarget<GlideDrawable> target = new SimpleTarget<GlideDrawable>() {
@Override
public void onLoadFailed(@Nullable Exception e, @Nullable Drawable errorDrawable) {
e = e == null ? new Exception("un-know error,onLoadFailed") : e;
Lg.e(e);
bitmapCall.onFail(e, errorDrawable);
}
@Override
public void onResourceReady(GlideDrawable bitmap, GlideAnimation glideAnimation) {
if (bitmap != null && bitmap instanceof GlideBitmapDrawable) {
Bitmap tmp = ((GlideBitmapDrawable) bitmap).getBitmap();
if(tmp != null){
bitmapCall.onSuccess(tmp);
return;
}
}
bitmapCall.onFail(new Exception("un-know error,bitmap is null"), null);
}
};
options.build(requestManager).listener(drawableRequestListener).into(target);
}
private boolean isValidContext(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
Lg.e("not support : Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2");
return false;
}
if (context instanceof Activity) {
Activity ac = ((Activity) context);
if (ac.isFinishing() || ac.isDestroyed()) {
Lg.d("Activity isFinishing or Destroyed");
return false;
}
}
return true;
}
private long getSize(File file) {
int size = 0;
if (file != null && file.exists()) {
if (file.isDirectory()) {
File[] fs = file.listFiles();
if (fs != null) {
for (File f : fs) {
size += getSize(f);
}
}
} else if (file.isFile()) {
size += file.length();
}
}
return size;
}
}
/***
* 图片加载自定义option
*/
public class Options {
public static final int DEFAULT_RES_ID = 0;
//目标资源Id
private final int _resId;
//目标资源网络地址
private final String _url;
//目标资源URI
private final Uri _uri;
//优先级别:如果是头像那些,优先级可以提高一点
public Priority _priority = Priority.NORMAL;
//加载占位图资源Id*/
public int _placeResId = DEFAULT_RES_ID;
public Drawable _placeDrawable = null;
public int _errorResId = DEFAULT_RES_ID;
public Drawable _errorDrawable = null;
//定义图片大小/占位 px
public int _sizeWidth = DEFAULT_RES_ID;
public int _sizeHeight = DEFAULT_RES_ID;
//缩略图显示
public boolean _thumbnail = false;
//缩略图缩小尺寸 default:eg 1000×1000px to 100*100px
public float _thumbnailSize = 0.1f;
//矩形图片 直角 显示成 圆角
public boolean _roundCorner = false;
//定义圆角弧度
public int _roundCornerPx = 0;
//circleImageView 会干扰 Glide 的渐变动画
//(合理的方法是替换掉全部的circleImageView,但是修改略多)
public boolean _dontAnimate = false;
public Options(@Nullable String url) {
this._url = url;
this._resId = DEFAULT_RES_ID;
this._uri = null;
}
public Options(@NonNull Uri uri) {
this._uri = uri;
this._url = null;
this._resId = DEFAULT_RES_ID;
}
public Options(int resId) {
this._resId = resId;
this._url = null;
this._uri = null;
}
public Options(@Nullable String url, int errorResId) {
this._errorResId = errorResId;
if (TextUtils.isEmpty((url))) {
this._url = null;
this._resId = errorResId;
} else {
this._url = url;
this._resId = DEFAULT_RES_ID;
}
this._uri = null;
}
@Override
public String toString() {
//noinspection StringBufferReplaceableByString
return new StringBuilder("_resId:").append(_resId)
.append(",_url:").append(_url)
.append(",_uri:").append(_uri)
.append(",_placeResId:").append(_placeResId)
.append(",_errorResId:").append(_errorResId)
.append(",_sizeWidth:").append(_sizeWidth)
.append(",_sizeHeight:").append(_sizeHeight)
.append(",_thumbnail:").append(_thumbnail)
.append(",_thumbnailSize:").append(_thumbnailSize)
.append(",_roundCorner:").append(_roundCorner)
.append(",_roundCornerPx:").append(_roundCornerPx)
.toString();
}
/*package*/ boolean isValid() {
return (_resId != DEFAULT_RES_ID && _resId != -1) || !TextUtils.isEmpty(_url) || _uri != null;
}
/*package*/ DrawableTypeRequest<?> build(RequestManager requestManager) {
if (!isValid()) {
throw new IllegalArgumentException("ImageMgr option is not valid");
}
DrawableTypeRequest<?> builder;
if (_resId != DEFAULT_RES_ID) {
builder = requestManager.load(_resId);
} else if (_uri != null) {
builder = requestManager.load(_uri);
} else if (!TextUtils.isEmpty(_url)) {
builder = requestManager.load(_url);
} else {
builder = requestManager.load(_errorResId);
}
if (builder != null) {
builder.diskCacheStrategy(DiskCacheStrategy.ALL);
builder.signature(new StringSignature(Facts.getInstance().appVersion));
if (_errorResId != DEFAULT_RES_ID) {
builder.error(_errorResId);
}
if (_placeResId != DEFAULT_RES_ID) {
builder.placeholder(_placeResId);
}
if (_placeDrawable != null) {
builder.placeholder(_placeDrawable);
}
if (_errorDrawable != null) {
builder.error(_errorDrawable);
}
if (_sizeWidth > 0 && _sizeHeight > 0) {
builder.override(_sizeWidth, _sizeHeight);
}
if (_thumbnail && _thumbnailSize > 0) {
builder.thumbnail(_thumbnailSize);
}
if (_roundCorner && _roundCornerPx > 0) {
builder.transform(new RoundCornerTransformation(_roundCornerPx));
}
if (_dontAnimate) {
builder.dontAnimate();
}
builder.priority(_priority);
}
return builder;
}
private static class RoundCornerTransformation extends BitmapTransformation {
private int BITMAP_ID = 0;
private int _roundPx = 0;
RoundCornerTransformation(int roundPx) {
super(SeeYouApp.getInstance());
this._roundPx = roundPx;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
if (_roundPx > 0) {
BITMAP_ID = toTransform.hashCode();
Bitmap output = pool.get(toTransform.getWidth(),
toTransform.getHeight(), Bitmap.Config.ARGB_4444);
if (output == null) {
output = Bitmap.createBitmap(toTransform.getWidth(),
toTransform.getHeight(), Bitmap.Config.ARGB_4444);
}
Canvas canvas = new Canvas(output);
final int color = 0xff424242;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, toTransform.getWidth(), toTransform.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawRoundRect(rectF, _roundPx, _roundPx, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(toTransform, rect, rect, paint);
return output;
} else {
return toTransform;
}
}
@Override
public String getId() {
return String.valueOf(BITMAP_ID);
}
}
}
public class XDGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) { /* no costumization */ }
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(String.class, InputStream.class, new HeaderLoader.Factory());
}
private static class HeaderLoader extends BaseGlideUrlLoader<String> {
HeaderLoader(Context context) {
super(context);
}
@Override
protected String getUrl(String model, int width, int height) {
return model;
}
@Override
protected Headers getHeaders(String model, int width, int height) {
LazyHeaders.Builder builder = new LazyHeaders.Builder();
for (Map.Entry<String, Object> entry :
NetMgr.getInstance().headerTemplate().entrySet()) {
builder.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
}
return builder.build();
}
static class Factory implements ModelLoaderFactory<String, InputStream> {
@Override
public StreamModelLoader<String> build(Context context, GenericLoaderFactory factories) {
return new HeaderLoader(context);
}
@Override
public void teardown() { /* nothing to free */ }
}
}
}
PS: 现在公司代码管控很严格,所以今后只能讲理论,不会直接放代码了。 : (