应用图片加载服务与第三方实现库的解耦

阿凡达2018-07-09 13:39

统一图片加载库接口设计

本文背景

云课堂android端,目前使用的图片加载库是UniversalImageLoader(简称UIL)。在5.4.0迭代版本中,因首页又增加了几个页面,发现启动app后,内存暴增,在排查问题后发现,是图片加载库的使用方式存在问题,以及该加载库对内存并不友好。因此在对比Glide后,发下启动app后,内存有很大改善,决定使用Glide。因此需要将原有的图片加载库替换成新的。这套新的方案,需要在未来的几年中使用,并且能够灵活替换图片加载库。

问题分析

在工程中发现原来使用UIL尽管被封装在EduImageLoaderUtil类中,包括UIL的初始配置以及默认选项。但还是有些UIL包内的类如ImageLoadingListenerDisplayImageOptions暴露在公用方法中,在外部调用的时候传入。这个严重影响了封装性,不方便后期替换图片加载库。

需求分析

  • 接口与实现隔离开来,对于业务上层有一个统一的管理类以及统一的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);
        }
    }


}
本文来自网易实践者社区,经作者陈柏宁 授权发布。