图片库的封装实践

背景调研看这里->图片缓存第三方库


引文

去年年初的时候,有做过一轮调研。当时是选了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 wuyisheng@seeyoutime.com 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: 现在公司代码管控很严格,所以今后只能讲理论,不会直接放代码了。 : (

相关文章
相关标签/搜索