应用图片加载服务与第三方实现库的解耦
统一图片加载库接口设计
本文背景
云课堂android端,目前使用的图片加载库是UniversalImageLoader(简称UIL)。在5.4.0迭代版本中,因首页又增加了几个页面,发现启动app后,内存暴增,在排查问题后发现,是图片加载库的使用方式存在问题,以及该加载库对内存并不友好。因此在对比Glide后,发下启动app后,内存有很大改善,决定使用Glide。因此需要将原有的图片加载库替换成新的。这套新的方案,需要在未来的几年中使用,并且能够灵活替换图片加载库。
问题分析
在工程中发现原来使用UIL尽管被封装在EduImageLoaderUtil
类中,包括UIL的初始配置以及默认选项。但还是有些UIL包内的类如ImageLoadingListener
、DisplayImageOptions
暴露在公用方法中,在外部调用的时候传入。这个严重影响了封装性,不方便后期替换图片加载库。
需求分析
- 接口与实现隔离开来,对于业务上层有一个统一的管理类以及统一的Api,不关心具体的第三方实现。当有更好的图片加载库出现时,可以灵活切换。业务上层只关心图片加载的服务接口,不关心真正的实现者。
- 增加全局配置类,如想实现在不同网络环境显示不同清晰度的图片,以及常见的内存缓存配置,磁盘缓存配置等等。
- 增加每次加载图片的配置类,因为每次加载图片可能需求不一样,比如轮播图的地方它的优先级要高一些,延迟性要低一些,质量要高一些,要做到可配置。
- 增加对图片转换的处理配置,因为有些地方的图可能想要圆角,有些地方又需要裁剪。
解决方案
- 定义一个ImageLoader接口,里面有上层业务需要使用的api。具体的实现分别放在glide包、uil包等。定义一个ImageLoaderManager类,负责管理ImageLoader。
- 一个全局的配置项,具体的实现类,通过ImageLoaderManager类注入进来。如在wifi下高清大图的功能开关,默认的初始图片质量,(图片Url可以拼接quality)
- 每次图片加载的配置项,有一个默认的全局配置,在类ImageLoaderManager中。在ImageLoader接口中应当提供通过配置类,决定展示图片的方案。
- 通过一个枚举值,标识每次图片展示的转换配置。然后各个图片加载库参照枚举描述,做出对应的转换。
设计方案
下面是类图:
下面是代码实现:
类ImageLoaderManager
由于图片库在一个应用中只会选择一种实现方案,所以这里的ImageLoader管理类,简单处理,配有一个默认的实现,一个默认的全局配置,一个默认的图片加载配置。提供了接口去修改默认的。
package com.netease.framework.imagemodule;
import com.netease.framework.annotation.NonNull;
import com.netease.framework.imagemodule.glide.GlideImageLoader;
/**
* ImageLoader管理类,默认的ImageLoader实现是GlideImageLoader。
* 提供一些注入接口,来修改默认实现以及默认配置
* Created by hzchenboning on 2017/10/8.
*/
public class ImageLoaderManager {
private static ImageLoader sImageLoader = new GlideImageLoader(); //默认的ImageLoader实现,Glide
private static DisplayImageConfig sDefaultDisPlayImageConfig = new DisplayImageConfig.Builder().build();
private static GlobalImageConfig sGlobalImageConfig = new GlobalImageConfig.Builder().build();
public static ImageLoader getImageLoader() {
return sImageLoader;
}
public static @NonNull GlobalImageConfig getGlobalImageConfig() {
return sGlobalImageConfig;
}
public static @NonNull DisplayImageConfig getDefaultDisPlayImageConfig() {
return sDefaultDisPlayImageConfig;
}
/**
* 修改默认的ImageLoader实现类
* @param imageLoader
*/
public static void setImageLoader(@NonNull ImageLoader imageLoader) {
sImageLoader = imageLoader;
}
/**
* 修改默认的每次图片加载配置项
* @param sDefaultDisPlayImageConfig
*/
public static void setDefaultDisPlayImageConfig(@NonNull DisplayImageConfig sDefaultDisPlayImageConfig) {
ImageLoaderManager.sDefaultDisPlayImageConfig = sDefaultDisPlayImageConfig;
}
/**
* 修改默认的全局配置项
* @param sGlobalImageConfig
*/
public static void setGlobalImageConfig(@NonNull GlobalImageConfig sGlobalImageConfig) {
ImageLoaderManager.sGlobalImageConfig = sGlobalImageConfig;
}
}
类ImageLoader
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
/**
* 图片加载器对外提供的服务接口
* Created by hzchenboning on 17/9/28.
*/
public interface ImageLoader {
/**
* 展示图片
*/
void displayImage(Context context, String imageUrl, ImageView imageView);
/**
* 展示指定尺寸
*/
void displayImage(Context context, String imageUrl, ImageView imageView, int width, int height);
/**
* 根据配置展示图片
*/
void displayImage(Context context, String imageUrl, ImageView imageView, DisplayImageConfig config);
/**
* 根据配置展示指定大小图片
*/
void displayImage(Context context, String imageUrl, ImageView imageView, DisplayImageConfig config, int width, int height);
/**
* 展示图片,并且监听图片加载回调
*/
<R> void displayImage(Context context, String imageUrl, ImageView imageView, ResourceListener<R> listener);
/**
* 根据配置展示图片,并且监听图片加载回调
*/
<R> void displayImage(Context context, String imageUrl, ImageView imageView, DisplayImageConfig config, ResourceListener<R> listener);
/**
* 展示高斯模糊图片
* @param radius 高斯模糊半径(像素),不包含中心点的像素,取值范围[1, 50]
* @param sigma 高斯模糊标准差
*/
void displayBlurImage(Context context, String imageUrl, ImageView imageView, int radius, int sigma);
/**
* 展示圆形图片
* 圆形的半径为图片的Math.min(width, height)/2
*/
void displayCircleImage(Context context, String imageUrl, ImageView imageView);
/**
* 下载图片
*/
<R> void loadImage(Context context, String imageUrl, ResourceListener<R> resourceListener);
/**
* 根据配置下载图片
*/
<R> void loadImage(Context context, String imageUrl, DisplayImageConfig config, ResourceListener<R> resourceListener);
/**
* 从缓存中(内存、磁盘)获取图片
*/
Bitmap getBitmapFromCache(String url);
interface ResourceListener<R> {
void onResourceReady(R resouce);
}
}
类DisplayImageConfig
import com.netease.edu.framework.R;
/**
* 每次图片加载的配置项
* Created by hzchenboning on 17/10/9.
*/
public class DisplayImageConfig {
int imageResOnLoading;
int imageResOnFail;
Priority priority;
boolean cacheOnDisk;
boolean cacheOnMemory;
boolean needThumbnail;
float thumbnail;
BitmapTransformation transformation;
private DisplayImageConfig(Builder builder) {
this.imageResOnLoading = builder.imageResOnLoading;
this.imageResOnFail = builder.imageResOnFail;
this.priority = builder.priority;
this.cacheOnDisk = builder.cacheOnDisk;
this.cacheOnMemory = builder.cacheOnMemory;
this.needThumbnail = builder.needThumbnail;
this.thumbnail = builder.thumbnail;
this.transformation = builder.transformation;
}
public int getImageResOnLoading() {
return imageResOnLoading;
}
public int getImageResOnFail() {
return imageResOnFail;
}
public Priority getPriority() {
return priority;
}
public boolean isCacheOnDisk() {
return cacheOnDisk;
}
public boolean isCacheOnMemory() {
return cacheOnMemory;
}
public boolean isNeedThumbnail() {
return needThumbnail;
}
public float getThumbnail() {
return thumbnail;
}
public static class Builder {
int imageResOnLoading = R.drawable.default_img;//加载中显示的图片
int imageResOnFail = R.drawable.default_img;//加载失败后显示的图片
Priority priority = Priority.NORMAL;//加载优先级
boolean cacheOnDisk = true;
boolean cacheOnMemory = true;
boolean needThumbnail = true;//是否先显示缩略图
float thumbnail = 0.1f;//缩略图为原图的十分之一
BitmapTransformation transformation = BitmapTransformation.none;
public Builder setImageResOnLoading(int imageResOnLoading) {
this.imageResOnLoading = imageResOnLoading;
return this;
}
public Builder setImageResOnFail(int imageResOnFail) {
this.imageResOnFail = imageResOnFail;
return this;
}
public Builder setPriority(Priority priority) {
this.priority = priority;
return this;
}
public Builder setCacheOnDisk(boolean cacheOnDisk) {
this.cacheOnDisk = cacheOnDisk;
return this;
}
public Builder setCacheOnMemory(boolean cacheOnMemory) {
this.cacheOnMemory = cacheOnMemory;
return this;
}
public Builder setNeedThumbnail(boolean needThumbnail) {
this.needThumbnail = needThumbnail;
return this;
}
public Builder setThumbnail(float thumbnail) {
this.thumbnail = thumbnail;
return this;
}
public Builder setTransformation(BitmapTransformation transformation) {
this.transformation = transformation;
return this;
}
public DisplayImageConfig build() {
return new DisplayImageConfig(this);
}
}
public enum Priority {
IMMEDIATE, //0ms
LOW, //300ms
NORMAL, //100ms
HIGH //50ms
}
/**
* 每个新增的转换,需要增加对应的描述
* 新增的命名就按照circleCrop、roundCrop
*/
public enum BitmapTransformation {
none, //(无变化)
}
}
类GlobalImageConfig
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 全局的图片加载配置
* Created by hzchenboning on 17/10/9.
*/
public class GlobalImageConfig {
//--------- 以下是接口及常量 -------------
@Retention(RetentionPolicy.SOURCE)
@IntDef({HIGH_IMAGE_QUALITY, NORMAL_IMAGE_QUALITY, LOW_IMAGE_QUALITY})
private @interface ImageQualityMode {}
public static final int HIGH_IMAGE_QUALITY = 100;
public static final int NORMAL_IMAGE_QUALITY = 80;
public static final int LOW_IMAGE_QUALITY = 50;
//磁盘缓存文件 250MB
private static final String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
private static final int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
//--------- 以上是接口及常量 -------------
public static boolean NEED_ADJUST_IMAGE_QUALITY = false;
private static int sImageQuality = HIGH_IMAGE_QUALITY;
private final boolean useExternalDiskCacheDir;
private final String cacheFolderName;
private final int diskCacheSize;
private final int memoryCacheSize;
public GlobalImageConfig(boolean useExternalDiskCacheDir, String cacheFolderName, int diskCacheSize, int memoryCacheSize) {
this.useExternalDiskCacheDir = useExternalDiskCacheDir;
this.cacheFolderName = cacheFolderName;
this.diskCacheSize = diskCacheSize;
this.memoryCacheSize = memoryCacheSize;
}
public static int getImageQuality() {
return sImageQuality;
}
public static void setImageQuality(@ImageQualityMode int quality) {
sImageQuality = quality;
}
public boolean isUseExternalDiskCacheDir() {
return useExternalDiskCacheDir;
}
public String getCacheFolderName() {
return cacheFolderName;
}
public int getDiskCacheSize() {
return diskCacheSize;
}
public int getMemoryCacheSize() {
return memoryCacheSize;
}
public static class Builder {
boolean useExternalDiskCacheDir = true; // 默认使用外部存储卡,false的话使用内部
String cacheFolderName = DEFAULT_DISK_CACHE_DIR;
int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
int memoryCacheSize = 0;//如果为0,交给第三方去计算最合适的大小
public Builder setUseExternalDiskCacheDir(boolean useExternalDiskCacheDir) {
this.useExternalDiskCacheDir = useExternalDiskCacheDir;
return this;
}
public Builder setCacheFolderName(String cacheFolderName) {
this.cacheFolderName = cacheFolderName;
return this;
}
public Builder setDiskCacheSize(int diskCacheSize) {
this.diskCacheSize = diskCacheSize;
return this;
}
public Builder setMemoryCacheSize(int memoryCacheSize) {
this.memoryCacheSize = memoryCacheSize;
return this;
}
public GlobalImageConfig build() {
return new GlobalImageConfig(useExternalDiskCacheDir, cacheFolderName, diskCacheSize, memoryCacheSize);
}
}
}
本文来自网易云社区,经作者陈柏宁授权发布。
原文地址:应用图片加载服务与第三方实现库的解耦
更多网易研发、产品、运营经验分享请访问网易云社区。