volley系列之流程简析(二)+绝妙的缓存

上一篇说了Volley的请求流程,但是没有说请求到response后怎么处理,这篇文章就来详细的说一说。让我们回忆一下,不管是在CacheDispatcher还是NetworkDispatcher中只要获得response,就通过这种方式传递出去

mDelivery.postResponse(request, response);

让我们探进去看看都发生了什么?

@Override
    public void postResponse(Request<?> request, Response<?> response) {
        postResponse(request, response, null);
    }

    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }

前一个函数是直接调用第二个函数,方法中前两句都是做标记,重点看最后一行代码,首先mResponsePoster是个啥东西呀,

mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };

Executor仅仅是一个接口,它只有一个execute方法,大家也看见了,需要一个参数Runnable。也就是说他其实就是一个包装,然后放进handler执行,那么这个handler是哪个handler?

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

他其实是在RequestQueue的构造函数里面进行初始化的。想想也是,获得数据以后一般都要进行UI操作,所以必须得放在主线程中操作。好了,相比大家的好奇心都没了吧,让我们来看主线。刚才说到他需要一个Runnable,里面传的是ResponseDeliveryRunnable,让我们跟进去在看,

public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
            mRequest = request;
            mResponse = response;
            mRunnable = runnable;
        }
        public void run() {
            // If this request has canceled, finish it and don't deliver.
            if (mRequest.isCanceled()) {
                mRequest.finish("canceled-at-delivery");
                return;
            }

            // Deliver a normal response or error, depending.
            if (mResponse.isSuccess()) {
                mRequest.deliverResponse(mResponse.result);
            } else {
                mRequest.deliverError(mResponse.error);
            }

            // If this is an intermediate response, add a marker, otherwise we're done
            // and the request can be finished.
            if (mResponse.intermediate) {
                mRequest.addMarker("intermediate-response");
            } else {
                mRequest.finish("done");
            }

            // If we have been provided a post-delivery runnable, run it.
            if (mRunnable != null) {
                mRunnable.run();
            }
       }
    }

直接看run方法,前面都是对request进行判断,如果取消的话就直接结束,然后不管成功或者失败,都通过request来发送这个response,探进去看看request这个方法:

@Override
    protected void deliverResponse(String response) {
        if (mListener != null) {
            mListener.onResponse(response);
        }
    }
public StringRequest(int method, String url, Listener<String> listener,
            ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
    }

两个结合可以看出直接把response传递给Listener<String> listener,它就是Respnose中定义的接口,是不是有点熟悉,其实就是我们初始化request定义的两个监听器中的其中一个,另一个同理,就不贴出来了。原来最后将response的成功或者失败都交给我们处理,联系上边的知道我们的处理方法都被放在了主线程的handler,所以可以放心进行UI操作。这下流程大体都清楚了吧。细心的同学会发现ResponseDeliveryRunnable中还可以传递一个runnable,这个是怎么用呢,用在哪呢,其实这儿Volley只有一个地方用到了,就是在CacheDispatcher中,假如有些response的soft-TTL(response存活时间)到了,就会发送一个runnable,让他重新进行网络请求获取response,假如返回的是304(就是不需要更新),就仅仅更新一下他的存活时间,什么也不做。假如返回的是一个新的response,就会在NetworkDispatcher中重新发送给request进行再一次操作。把代码贴出来让你们再回顾一下。

if (!entry.refreshNeeded()) {
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                } else {
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);
            .............
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response,
                            new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }

中间有一些省略,看重点就可以了。
上篇文章说这一篇讲一下缓存的精彩之处,但是想想还是要把Volley的流程全部要搞明白,所以就。。。下面说说缓存是怎么精彩的,先说一部分,也是最精彩的部分,至少是我认为的。
大家一说到缓存,就能想到二级缓存,三级缓存(其实也就是二级),lru算法等。那么Volley中有没有呢?网上有人说没有lru,这儿我是不赞同的。来看看我为什么不赞同,同时希望你们有自己的判断。
直接看缓存的类,他有一个接口Cache,让我们看他的子类,

public class DiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

    /** Total amount of space currently used by the cache in bytes. */
    private long mTotalSize = 0;

    /** The root directory to use for the cache. */
    private final File mRootDirectory;

    /** Default maximum disk usage in bytes. */
    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

    /** High water mark percentage for the cache */
    private static final float HYSTERESIS_FACTOR = 0.9f;

    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory using
     * the default maximum cache size of 5MB.
     * @param rootDirectory The root directory of the cache.
     */
    public DiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

    /**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }

        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
            CacheHeader.readHeader(cis); // eat header
            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        }  catch (NegativeArraySizeException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }

    /**
     * Puts the entry with the specified key into the cache.
     */
    @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

这儿我只放了一些重点的代码,看缓存当然是要看他的get和put方法。我先说一个java的集合--LinkedHashMap,它保证了插入的顺序和读取的顺序是一致的,还内置了LRU算法,这是关键。好了,来看代码:他首先有一个LinkedHashMap的成员变量mEntries,以request的url为key,CacheHeader为value存放在该变量中。而CacheHeader是一个轻量级的类,里面的成员变量和方法并不多。看名字就知道,该类仅仅是存放response的head,里面只是response的一些说明信息,并没有真正的数据。还有一个mRootDirectory,这里面才是存放真正的数据,默认大小为5M。

先看get方法,先从mEntries获取一个CacheHeader,如果为空就直接返回,不为空就从文件中取出相应的数据,最后转化成CacheEntry返回。完了,再来看put方法,首先判断空间是否装下传过来的Entry,先假设能装的下,然后就直接写入磁盘,也就是file中。同时也写入map中,就是这个方法putEntry(key, e);然后再说它是怎么判断的,直接看代码吧

private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }
        if (VolleyLog.DEBUG) {
            VolleyLog.v("Pruning old cache entries.");
        }

        long before = mTotalSize;
        int prunedFiles = 0;
        long startTime = SystemClock.elapsedRealtime();

        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey(e.key).delete();
            if (deleted) {
                mTotalSize -= e.size;
            } else {
               VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                       e.key, getFilenameForKey(e.key));
            }
            iterator.remove();
            prunedFiles++;

            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        if (VolleyLog.DEBUG) {
            VolleyLog.v("pruned %d files, %d bytes, %d ms",
                    prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
        }
    }

首先看当前的大小和需要的容量的和是否比最大容量小,小的话就直接返回,假如不够的话,从mEntries中获取他的迭代器,然后不断获取CacheHeader ,然后再从CacheHeader 取得key,再从file中删除对应的缓存,然后也从mEntries删除。然后再看容量是否满足所需要的。不满足再不断的循环,直到满足为止。这儿有一个关键,首先它利用LinkedHashMap的内置LRU算法,然后仅仅是将缓存头部信息添加到内存,也就是Map中,然后将数据放在磁盘里。当添加或者删除的时候,都会先从Map中查询,这样大大减少磁盘操作,同时磁盘是有容量的,当添加时候容量不够了,会先从Map中删除,同时将磁盘中也删除,这样它两就是联动啊,同时拥有了LRU算法和容量,真特么精彩。
好了,这篇文章也就完了。具体Volley有没有实现lru,大家自行判断。Volley的流程也说完了,接下来的文章会探讨它的一些代码技巧、框架结构、打log 的方式等。要是文章有什么错误或者不稳妥的地方,还望大家指出来,一起讨论提高。欢迎阅读!

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

推荐阅读更多精彩内容