[转]教你写Android ImageLoader框架之初始配置与请求调度(二)

本文转自Mr.Simple的博客,如侵删


前言

教你写Android ImageLoader框架之基本架构中我们对SimpleImageLoader框架进行了基本的介绍,今天我们就从源码的角度来剖析ImageLoader的设计与实现。 在我们使用ImageLoader前都会通过一个配置类来设置一些基本的东西,比如加载中的图片、加载失败的图片、缓存策略等等,SimpleImageLoader的设计也是如此。配置类这个比较简单,我们直接看源码吧。


ImageLoaderConfig配置

/** 
 * ImageLoader配置类, 
 *  
 * @author mrsimple 
 */  
public class ImageLoaderConfig {  
  
  
    /** 
     * 图片缓存配置对象 
     */  
    public BitmapCache bitmapCache = new MemoryCache();  
  
  
    /** 
     * 加载图片时的loading和加载失败的图片配置对象 
     */  
    public DisplayConfig displayConfig = new DisplayConfig();  
    /** 
     * 加载策略 
     */  
    public LoadPolicy loadPolicy = new SerialPolicy();  
  
  
    /** 
     *  
     */  
    public int threadCount = Runtime.getRuntime().availableProcessors() + 1;  
  
  
    /** 
     * @param count 
     * @return 
     */  
    public ImageLoaderConfig setThreadCount(int count) {  
        threadCount = Math.max(1, count);  
        return this;  
    }  
  
  
    public ImageLoaderConfig setCache(BitmapCache cache) {  
        bitmapCache = cache;  
        return this;  
    }  
  
  
    public ImageLoaderConfig setLoadingPlaceholder(int resId) {  
        displayConfig.loadingResId = resId;  
        return this;  
    }  
  
  
    public ImageLoaderConfig setNotFoundPlaceholder(int resId) {  
        displayConfig.failedResId = resId;  
        return this;  
    }  
  
  
    public ImageLoaderConfig setLoadPolicy(LoadPolicy policy) {  
        if (policy != null) {  
            loadPolicy = policy;  
        }  
        return this;  
    }  
}  

都是很简单的setter函数,但是不太一样的是这些setter都是返回一个ImageLoaderConfig对象的,在这里也就是返回了自身。这个设计是类似Builder模式的,便于用户的链式调用,例如:

private void initImageLoader() {  
    ImageLoaderConfig config = new ImageLoaderConfig()  
               .setLoadingPlaceholder(R.drawable.loading)  
               .setNotFoundPlaceholder(R.drawable.not_found)  
               .setCache(new DoubleCache(this))  
               .setThreadCount(4)  
               .setLoadPolicy(new ReversePolicy());  
       // 初始化  
       SimpleImageLoader.getInstance().init(config);  
}  

对于Builder模式不太了解的同学可以参考 Android源码分析之Builder模式。构建好配置对象之后我们就可以通过这个配置对象来初始化SimpleImageLoader了。SimpleImageLoader会根据配置对象来初始化一些内部策略,例如缓存策略、线程数量等。调用init方法后整个ImageLoader就正式启动了。


SimpleImageLoader的实现

SimpleImageLoader这个类的职责只是作为用户入口,它的工作其实并没有那么多,只是一个门童罢了。我们看看它的源码吧。

/** 
 * 图片加载类,支持url和本地图片的uri形式加载.根据图片路径格式来判断是网络图片还是本地图片,如果是网络图片则交给SimpleNet框架来加载, 
 * 如果是本地图片那么则交给mExecutorService从sd卡中加载 
 * .加载之后直接更新UI,无需用户干预.如果用户设置了缓存策略,那么会将加载到的图片缓存起来.用户也可以设置加载策略,例如顺序加载{@see 
 * SerialPolicy}和逆向加载{@see ReversePolicy}. 
 *  
 * @author mrsimple 
 */  
public final class SimpleImageLoader {  
    /** SimpleImageLoader实例 */  
    private static SimpleImageLoader sInstance;  
  
  
    /** 网络请求队列  */  
    private RequestQueue mImageQueue;  
    /** 缓存 */  
    private volatile BitmapCache mCache = new MemoryCache();  
  
  
    /** 图片加载配置对象 */  
    private ImageLoaderConfig mConfig;  
  
  
    private SimpleImageLoader() {  
    }  
  
  
    /** 
     * 获取ImageLoader单例 
     *  
     * @return 
     */  
    public static SimpleImageLoader getInstance() {  
        if (sInstance == null) {  
            synchronized (SimpleImageLoader.class) {  
                if (sInstance == null) {  
                    sInstance = new SimpleImageLoader();  
                }  
            }  
        }  
        return sInstance;  
    }  
  
  
    /** 
     * 初始化ImageLoader,启动请求队列 
     * @param config 配置对象 
     */  
    public void init(ImageLoaderConfig config) {  
        mConfig = config;  
        mCache = mConfig.bitmapCache;  
        checkConfig();  
        mImageQueue = new RequestQueue(mConfig.threadCount);  
        mImageQueue.start();  
    }  
  
  
    private void checkConfig() {  
        if (mConfig == null) {  
            throw new RuntimeException(  
                    "The config of SimpleImageLoader is Null, please call the init(ImageLoaderConfig config) method to initialize");  
        }  
  
  
        if (mConfig.loadPolicy == null) {  
            mConfig.loadPolicy = new SerialPolicy();  
        }  
        if (mCache == null) {  
            mCache = new NoCache();  
        }  
  
  
    }  
  
  
    public void displayImage(ImageView imageView, String uri) {  
        displayImage(imageView, uri, null, null);  
    }  
  
  
    public void displayImage(final ImageView imageView, final String uri,  
            final DisplayConfig config, final ImageListener listener) {  
        BitmapRequest request = new BitmapRequest(imageView, uri, config, listener);  
        // 加载的配置对象,如果没有设置则使用ImageLoader的配置  
        request.displayConfig = request.displayConfig != null ? request.displayConfig  
                : mConfig.displayConfig;  
        // 添加对队列中  
        mImageQueue.addRequest(request);  
    }  
  
  
      // 代码省略...  
  
  
    /** 
     * 图片加载Listener 
     *  
     * @author mrsimple 
     */  
    public static interface ImageListener {  
        public void onComplete(ImageView imageView, Bitmap bitmap, String uri);  
    }  
}  

从上述代码中我们可以看到SimpleImageLoader的工作比较少,也比较简单。它就是根据用户传递进来的配置来初始化ImageLoader,并且作为图片加载入口,用户调用displayImage之后它会将这个调用封装成一个BitmapRequest请求,然后将该请求添加到请求队列中。


BitmapRequest图片加载请求

BitmapRequest只是一个存储了ImageView、图片uri、DisplayConfig以及ImageListener的一个对象,封装这个对象的目的在加载图片时减少参数的个数,在BitmapRequest的构造函数中我们会将图片uri设置为ImageView的tag,这样做的目的是防止图片错位显示。BitmapRequest类实现了Compare接口,请求队列会根据它的序列号进行排序,排序策略用户也可以通过配置类来设置,具体细节在加载策略的章节我们再聊吧。

public BitmapRequest(ImageView imageView, String uri, DisplayConfig config,  
           ImageListener listener) {  
       mImageViewRef = new WeakReference<ImageView>(imageView);  
       displayConfig = config;  
       imageListener = listener;  
       imageUri = uri;  
       // 设置ImageView的tag为图片的uri  
       imageView.setTag(uri);  
       imageUriMd5 = Md5Helper.toMD5(imageUri);  
   }  

RequestQueue图片请求队列

请求队列我们采用了SImpleNet中一样的模式,通过封装一个优先级队列来维持图片加载队列,mSerialNumGenerator会给每一个请求分配一个序列号,PriorityBlockingQueue会根据BitmapRequest的compare策略来决定BitmapRequest的顺序。RequestQueue内部会启动用户指定数量的线程来从请求队列中读取请求,分发线程不断地从队列中读取请求,然后进行图片加载处理,这样ImageLoader就happy起来了。

/** 
 * 请求队列, 使用优先队列,使得请求可以按照优先级进行处理. [ Thread Safe ] 
 *  
 * @author mrsimple 
 */  
public final class RequestQueue {  
    /** 
     * 请求队列 [ Thread-safe ] 
     */  
    private BlockingQueue<BitmapRequest> mRequestQueue = new PriorityBlockingQueue<BitmapRequest>();  
    /** 
     * 请求的序列化生成器 
     */  
    private AtomicInteger mSerialNumGenerator = new AtomicInteger(0);  
  
  
    /** 
     * 默认的核心数 
     */  
    public static int DEFAULT_CORE_NUMS = Runtime.getRuntime().availableProcessors() + 1;  
    /** 
     * CPU核心数 + 1个分发线程数 
     */  
    private int mDispatcherNums = DEFAULT_CORE_NUMS;  
    /** 
     * NetworkExecutor,执行网络请求的线程 
     */  
    private RequestDispatcher[] mDispatchers = null;  
  
  
    /** 
     *  
     */  
    protected RequestQueue() {  
        this(DEFAULT_CORE_NUMS);  
    }  
  
  
    /** 
     * @param coreNums 线程核心数 
     * @param httpStack http执行器 
     */  
    protected RequestQueue(int coreNums) {  
        mDispatcherNums = coreNums;  
    }  
  
  
    /** 
     * 启动RequestDispatcher 
     */  
    private final void startDispatchers() {  
        mDispatchers = new RequestDispatcher[mDispatcherNums];  
        for (int i = 0; i < mDispatcherNums; i++) {  
            mDispatchers[i] = new RequestDispatcher(mRequestQueue);  
            mDispatchers[i].start();  
        }  
    }  
  
  
    public void start() {  
        stop();  
        startDispatchers();  
    }  
  
  
    /** 
     * 停止RequestDispatcher 
     */  
    public void stop() {  
        if (mDispatchers != null && mDispatchers.length > 0) {  
            for (int i = 0; i < mDispatchers.length; i++) {  
                mDispatchers[i].interrupt();  
            }  
        }  
    }  
  
  
    /** 
     * 不能重复添加请求 
     * @param request 
     */  
    public void addRequest(BitmapRequest request) {  
        if (!mRequestQueue.contains(request)) {  
            request.serialNum = this.generateSerialNumber();  
            mRequestQueue.add(request);  
        } else {  
            Log.d("", "### 请求队列中已经含有");  
        }  
    }  
  
  
    private int generateSerialNumber() {  
        return mSerialNumGenerator.incrementAndGet();  
    }  
}  

RequestDispatcher请求分发

请求Dispatcher,继承自Thread,从网络请求队列中循环读取请求并且执行,也比较简单,直接上源码吧。

final class RequestDispatcher extends Thread {  
  
  
    /** 
     * 网络请求队列 
     */  
    private BlockingQueue<BitmapRequest> mRequestQueue;  
  
  
    /** 
     * @param queue 图片加载请求队列 
     */  
    public RequestDispatcher(BlockingQueue<BitmapRequest> queue) {  
        mRequestQueue = queue;  
    }  
  
  
    @Override  
    public void run() {  
        try {  
            while (!this.isInterrupted()) {  
                final BitmapRequest request = mRequestQueue.take();  
                if (request.isCancel) {  
                    continue;  
                }  
                // 解析图片schema  
                final String schema = parseSchema(request.imageUri);  
                // 根据schema获取对应的Loader  
                Loader imageLoader = LoaderManager.getInstance().getLoader(schema);  
                // 加载图片  
                imageLoader.loadImage(request);  
            }  
        } catch (InterruptedException e) {  
            Log.i("", "### 请求分发器退出");  
        }  
    }  
  
  
    /** 
     * 这里是解析图片uri的格式,uri格式为: schema:// + 图片路径。 
     */  
    private String parseSchema(String uri) {  
        if (uri.contains("://")) {  
            return uri.split("://")[0];  
        } else {  
            Log.e(getName(), "### wrong scheme, image uri is : " + uri);  
        }  
  
  
        return "";  
    }  
  
}  

第一个重点就是run函数了,不断地从队列中获取请求,然后解析到图片uri的schema,从schema的格式就可以知道它是存储在哪里的图片。例如网络图片对象的schema是http或者https,sd卡存储的图片对应的schema为file。根据schema我们从LoaderManager中获取对应的Loader来加载图片,这个设计保证了SimpleImageLoader可加载图片类型的可扩展性,这就是为什么会增加loader这个包的原因。用户只需要根据uri的格式来构造图片uri,并且实现自己的Loader类,然后将Loader对象注入到LoaderManager即可,后续我们会再详细说明。

这里的另一个重点是parseSchema函数,它的职责是解析图片uri的格式,uri格式为: schema:// + 图片路径,例如网络图片的格式为 http://xxx.image.jpg, 而本地图片的uri为file:///sdcard/xxx/image.jpg。如果你要实现自己的Loader来加载特定的格式,那么它的uri格式必须以schema://开头,否则解析会错误,例如可以为drawable://image,然后你注册一个schema为"drawable"的Loader到LoaderManager中,SimpleImageLoader在加载图片时就会使用你注册的Loader来加载图片,这样就可以应对用户的多种多样的需求。如果不能拥抱变化那就不能称之为框架了,应该叫功能模块。


本章总结

最后我们来整理一下这个过程吧,SimpleImageLoader根据用户的配置来配置、启动请求队列,请求队列又会根据用户配置的线程数量来启动几个分发线程。这几个线程不断地从请求队列(线程安全)中读取图片加载请求,并且执行图片加载请求。这些请求是用户通过调用SimpleImageLoader的displayImage函数而产生的,内部会把这个调用封装成一个BitmapRequest对象,并且将该对象添加到请求队列中。这样就有了生产者(用户)和消费者(分发线程),整个SimpleImageLoader就随着CPU跳动而热血沸腾起来了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容