• Android 学习笔记之Volley开源框架解析(三)


     

    学习内容:

    1.CacheDispatcher缓存请求调度...

    2.Cache缓存数据的保存...

    3.DiskBasedCache基于磁盘的缓存类实现方式...

      前面说到使用Volley发送网络请求的时候,每一个请求都会保存在请求队列RequestQueue中,RequestQueue会开启一个缓存请求调度线程,若干网络请求调度线程来对这些请求进行相关的处理...缓存调度线程通过调用缓存数据来完成这次请求的提交...而网络请求调度线程通过发送网络请求来完成请求的提交...最后通过把数据发送给客户端就完成了这次请求和响应的整个过程...

    1.CacheDispathcer.java(缓存请求线程)...

      缓存请求调度线程,用于处理缓存过的请求..当此类开启的时候会不断的从缓存队列当中取出请求,如果缓存存在,那么从缓存获取的数据通过deliverResponse方法去分发响应,如果没有进行缓存,那么通过发送网络请求从服务器端获取数据,因此在实例化对象时还需要传递网络请求队列...

      1.1 变量的定义..

      一些相关变量的定义,非常的简单...

     private static final boolean DEBUG = VolleyLog.DEBUG;
    
        /** The queue of requests coming in for triage. */
        private final BlockingQueue<Request> mCacheQueue; //缓存请求队列..
    
        /** The queue of requests going out to the network. */
        private final BlockingQueue<Request> mNetworkQueue; //网络请求队列...
    
        /** The cache to read from. */
        private final Cache mCache; //缓存,用于保存缓存数据...
    
        /** For posting responses. */
        private final ResponseDelivery mDelivery; //用于分发请求...
    
        /** Used for telling us to die. */
        private volatile boolean mQuit = false; //布尔值,用于判断线程是否结束...

      1.2 public CacheDispatcher(){}

      CacheDispatcher的构造函数,用于实例化缓存请求线程调度的对象...

    public CacheDispatcher(
                BlockingQueue<Request> cacheQueue, BlockingQueue<Request> networkQueue,
                Cache cache, ResponseDelivery delivery) {
            mCacheQueue = cacheQueue;
            mNetworkQueue = networkQueue;
            mCache = cache;
            mDelivery = delivery;
        }

      1.3 public void quit(){}

      退出函数...当主线程注销的同时...这些线程也就被注销掉了...非常的简单...

     public void quit() {
            mQuit = true;
            interrupt();
        }

      1.4 public void run(){}

      前面已经说到,CacheDispatcher是一个线程,那么自然需要继承Thread,其中run()是线程内部中最重要的方法...Volley为什么能够异步实现数据信息的加载,就是因为缓存请求调度和网络网络请求调度是通过继承Thread来实现的,这两个类是异步线程的实现类...也正是因为是异步线程,才能够完成异步操作..从而使得请求能够更加的高效,有质量,同时也减少了服务器上的负担...

    public void run() {
            if (DEBUG) VolleyLog.v("start new dispatcher");
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);//设置线程的优先级...
    
            mCache.initialize(); //缓存的初始化...
    
            while (true) {
                try {
                    final Request request = mCacheQueue.take(); //从缓存队列中取出请求...
                    request.addMarker("cache-queue-take"); //添加标识符...方便以后调试..
    
                    if (request.isCanceled()) { //如果中途撤消了请求...
                       /*
                        * 那么结束这次请求,将请求从当前请求队列中移除..并且如果相同请求队列中还有这一类的请求,那么将所有请求移出相同请求队列..将请求交给缓存队列处理..
                        */
                        request.finish("cache-discard-canceled"); 
                        continue;
                    }
    
                    // Attempt to retrieve this item from cache.
                    Cache.Entry entry = mCache.get(request.getCacheKey()); 
                    if (entry == null) { //判断缓存中是否保存了这次请求..
                        request.addMarker("cache-miss"); //缓存丢失...
                        // Cache miss; send off to the network dispatcher.
                        mNetworkQueue.put(request); //交给网络请求队列执行网络请求...
                        continue;
                    }
    
                    // If it is completely expired, just send it to the network.
                    if (entry.isExpired()) { //判断缓存的新鲜度...
                        request.addMarker("cache-hit-expired"); //已经不新鲜,说白了就是失效...
                        request.setCacheEntry(entry); //保存entry 
                        mNetworkQueue.put(request); //提交网络请求...
                        continue;
                    }
    
                    request.addMarker("cache-hit");  //添加标识缓存命中...
                    Response<?> response = request.parseNetworkResponse(
                            new NetworkResponse(entry.data, entry.responseHeaders)); //建立一个response对象解析响应返回的数据...
                    request.addMarker("cache-hit-parsed"); //添加标识缓存命中,并且已被解析..
    
                    if (!entry.refreshNeeded()) { //判断缓存是需要刷新..
                 
                        mDelivery.postResponse(request, response);//不需要刷新就直接发送...
                    } else {
                   
                        request.addMarker("cache-hit-refresh-needed");//添加标识标识缓存命中后需要刷新...
                        request.setCacheEntry(entry); //保存entry...
    
                        response.intermediate = true;
    
                        //需要刷新,那么就再次提交网络请求...获取服务器的响应...
                        mDelivery.postResponse(request, response, new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    mNetworkQueue.put(request);
                                } catch (InterruptedException e) {
                                    // Not much we can do about this.
                                }
                            }
                        });
                    }
    
                } catch (InterruptedException e) {
                    if (mQuit) {
                        return;
                    }
                    continue;
                }
            }
        }                

      这里我解释一下缓存刷新的概念...缓存刷新的概念表示,客户端虽然提交过这次请求,并且请求获取的数据报中的数据也已经被保存在了缓存当中,但是服务器端发生了改变..也就是说服务器数据发生了变化,那么就会导致这次请求对应的数据已经有所变化了,但是客户端缓存保存的仍然是没有改变的数据,因此即使缓存命中也无法获取到正确的数据信息,因此需要重新提交新的网络请求...因此这就是缓存刷新的概念...

      同时我们还可以看到,一直都有entry的出现...上面只是说保存了entry对象,但是却没有说它到底是做什么的...下面说一下Entry对象...说Entry就不得不说Cache了...

    2.Cache.java

      保存缓存数据的类...内部定义了一些抽象方法和一个Entry类...

      2.1 抽象方法的定义...

      几种抽象方法的操作都是针对于Map的操作...也正是Map<String,CacheHeader>以键值对的形式保存在一个集合当中...并且CacheHeader中封装了Entry对象...

        public Entry get(String key); //通过键获取Entry对象...
      
        public void put(String key, Entry entry); //在缓存中放入键值...
    
        public void initialize(); //缓存的初始化...
    
        public void invalidate(String key, boolean fullExpire); //使Entry对象在缓存中无效...
    
        public void remove(String key); //移除函数...
    
        public void clear();  //清空函数...

       2.2 Entry类...

      Entry是定义在Cache类中的一个静态类...用于保存缓存的一些属性,比如说过期时间,失效时间...何时需要刷新等等...以及缓存数据...

      public static class Entry {
        
            public byte[] data;  //保存Body实体中的数据...
    
            public String etag;  //用于缓存的新鲜度验证...
    
            public long serverDate; //整个请求-响应的过程花费的时间...
    
            public long ttl; //缓存过期的时间...
    
            public long softTtl; //缓存新鲜时间..
         
            public Map<String, String> responseHeaders = Collections.emptyMap(); //Map集合..用于保存请求的url和数据...
    
            public boolean isExpired() {
                return this.ttl < System.currentTimeMillis();//判断是否新鲜(失效)...
            }
    
            public boolean refreshNeeded() { //判断缓存是否需要刷新...
                return this.softTtl < System.currentTimeMillis();
            }
        }

      Cache类中主要的东西还是封装的抽象方法,有了这些方法,缓存才能够真正的起到作用,只有类而没有实现方法,那么显然缓存是没意义的,因此定义了这些具体如何实现缓存的方法,之所以定义为抽象,目的是非常清晰的,为了形成良好的扩展,我们可以使用Volley的缓存机制,那么同时我们自己也可以自己重写缓存机制...至于如何重写那就取决于需求了...我们来说一下系统提供了缓存机制的实现...

    3.DiskBasedCache.java

      缓存类的具体实现类,是基于磁盘的一种缓存机制...

      首先是变量的定义和构造函数...

      3.1 变量的定义和构造函数...

    private final Map<String, CacheHeader> mEntries =
                new LinkedHashMap<String, CacheHeader>(16, .75f, true); //map集合,以键值对的形式保存缓存...
    
        private long mTotalSize = 0; //额外增加的大小...用于缓存大小发生变化时需要记录增加的数值...
    
        /** The root directory to use for the cache. */
        private final File mRootDirectory; //缓存文件的根目录..
    
        /** The maximum size of the cache in bytes. */
        private final int mMaxCacheSizeInBytes; //缓存分配的最大内存...
    
        /** 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; //浮点数..用于缓存优化..
    
        /** Magic number for current version of cache file format. */
        private static final int CACHE_MAGIC = 0x20120504; //缓存的内存分区..
    
        //构造函数...这个是通过人为指定缓存的最大大小来实例化一个缓存对象...
        public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
            mRootDirectory = rootDirectory;
            mMaxCacheSizeInBytes = maxCacheSizeInBytes;
        }
    
        //使用了系统默认分配的缓存大小来实例化一个缓存对象...
        public DiskBasedCache(File rootDirectory) {
            this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
        }

      3.2 public synchronized void initialize(){}

      缓存初始化函数,用于初始化缓存,初始化的过程是对缓存文件的扫描,在源码中我们可以看到文件的遍历过程,把所有的缓存数据进行保存,然后写入到内存当中...其中调用了其他函数ReadHeader()..这个函数通过对缓存文件数据的读取,将数据保存在Entry当中...最后通过键值对的形式把封装后的Entry保存起来...

     @Override
        public synchronized void initialize() {
            if (!mRootDirectory.exists()) {   //如果缓存文件不存在,那么需要报错...
                if (!mRootDirectory.mkdirs()) {
                    VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
                }
                return;
            }
    
            File[] files = mRootDirectory.listFiles();//获取所有的缓存文件..保存在数组中...
            if (files == null) {
                return;
            }
            for (File file : files) { //通过遍历所有文件,将数据进行保存...
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(file); //获取文件的I/O流...
                    CacheHeader entry = CacheHeader.readHeader(fis); //将读取的数据保存在Entry当中...
                    entry.size = file.length();
                    putEntry(entry.key, entry); //将封装好的数据保存在Map当中...
                } catch (IOException e) {
                    if (file != null) {
                       file.delete();
                    }
                } finally {
                    try {
                        if (fis != null) {
                            fis.close();
                        }
                    } catch (IOException ignored) { }
                }
            }
        }

      3.3 private void putEntry(String key,CacheHeader entry){}

      这个函数是对缓存数据放入Map中的一个调用过程,首先需要判断缓存是否已经存在,如果内部不存在,那么直接设置缓存的数据长度大小,如果内部存在,那么说明当前的缓存数据大小已经发生了变化,也就是同一个键值对应的数据已经发生了变化,那么我们需要重新设置缓存数据增加的大小...

     private void putEntry(String key, CacheHeader entry) { //首先对缓存命中进行判断...
            if (!mEntries.containsKey(key)) {
                mTotalSize += entry.size;  //如果缓存中没有保存过当前数据...那么定义缓存数据的长度...
            } else {
                CacheHeader oldEntry = mEntries.get(key);//如果缓存命中,那么说明缓存的数据大小已经发生了改变..
                mTotalSize += (entry.size - oldEntry.size);//赋上新的数据长度值...
            }
            mEntries.put(key, entry); //调用放入函数...
        }

      3.4 public synchronized void put(String key, Entry entry) {}

      通过键值对的形式将Entry数据保存在HashMap当中,保存的值是以CacheHeader的形式进行保存的,CacheHeader算是对Entry的数据的一个封装,将Entry中保存的数据封装,最后以Map的形式将数据保存起来...

     public synchronized void put(String key, Entry entry) {
            pruneIfNeeded(entry.data.length); //判断缓存是否需要经过优化...
            File file = getFileForKey(key); //获取缓存文件的key值..
            try {
                FileOutputStream fos = new FileOutputStream(file); //获取文件的I/O流..
                CacheHeader e = new CacheHeader(key, entry);//创建一个新的CacheHeader对象...
                e.writeHeader(fos); //按照指定方式写头部信息,包括缓存过期时间,新鲜度等等...
                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());
            }
        }

      我们可以看到这里调用了一个pruneIfNeeded()函数...这个函数是在数据被放入之前需要进行调用的...下面就来说一下...

      3.5  private void pruneIfNeeded(int neededSpace) {}

      这个函数的调用目的是对当前缓存数据大小的一个判断过程,如果缓存的数据大小小于指定的规格大小,那么直接就return就可以了,说明指定的大小可以满足缓存的存储大小,但是如果缓存的数据大小超出了我们预先指定的规格,那么我们需要对缓存数据进行优化,优化成可以满足预先指定的规格...具体优化的大小为0.9倍的指定的大小...

    private void pruneIfNeeded(int neededSpace) { 
            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
                return; //如果缓存数据的大小小于预先指定的大小..直接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(); //对Map保存的数据进行遍历...
            while (iterator.hasNext()) {  //迭代过程...
                Map.Entry<String, CacheHeader> entry = iterator.next();
                CacheHeader e = entry.getValue(); //获取entry对象...
                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);
            }
        }

      这里涉及了优化过程,其实优化的也仅仅是文件名字的长度,为了满足预先设置的大小需求,同时还要作为键值保存在Map当中,因此文件名字还需要具有唯一性,因此这个优化是需要考虑到一些事情的...优化将调用 getFilenameForKey()函数...简答说一下这个函数...

      3.6  private String getFilenameForKey(String key) {}

      这个函数其实就是优化缓存大小的过程,被优化的东西不是数据,而是键的优化...由于原本保存的键值对其中的键值是保存的文件的名称,由于文件的名称唯一,所以保存的时候也就不会出现由于相同键值二次插入而导致的错误发生...但是由于其长度不一定能顾满足预先设置的缓存大小,因此我们需要对键值进行优化...优化的过程是折半截取文件名并获取Hash码的过程从而能够获取唯一的键值...

    private String getFilenameForKey(String key) {
            int firstHalfLength = key.length() / 2; //获取名字长度的一般...
            String localFilename = String.valueOf(key.substring(0,  firstHalfLength).hashCode()); //对文件名字符串进行截取...
            localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); //获取其Hash码..
            return localFilename;
        }

      3.7 public synchronized Entry get(String key) {}

      通过给定键值来获取缓存数据中的内容...非常的简单,涉及的东西到不是很多,思想就是通过键值来获取缓存文件,通过获取缓存文件的I/O流,然后将数据写出来..最后进行返回就可以了...

     public synchronized Entry get(String key) {
            CacheHeader entry = mEntries.get(key); //通过key获取Entry对象..
            // if the entry does not exist, return.
            if (entry == null) {
                return null;  //如果不存在,直接return掉...
            }
    
            File file = getFileForKey(key); //返回键值对应的缓存文件...
            CountingInputStream cis = null; 
            try {
                cis = new CountingInputStream(new FileInputStream(file)); //封装成流...
                CacheHeader.readHeader(cis); // eat header
                byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); //读取数据...
                return entry.toCacheEntry(data); //返回entry中保存的数据...
            } catch (IOException 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;
                    }
                }
            }
        }

      这里将文件封装成流使用了CountInputStream()...写入数据的方法则是采用了streamToBytes()方法...

     3.8 CountInputStream()...

     这个类继承与FilterInputStream(),FilterInputStream()继承与InputStream()...类与类之间采用了装饰者模式...

        private static class CountingInputStream extends FilterInputStream {
            private int bytesRead = 0;
    
            private CountingInputStream(InputStream in) {
                super(in);
            }
    
            @Override
            public int read() throws IOException {
                int result = super.read();
                if (result != -1) {
                    bytesRead++;
                }
                return result;
            }
    
            @Override
            public int read(byte[] buffer, int offset, int count) throws IOException {
                int result = super.read(buffer, offset, count);
                if (result != -1) {
                    bytesRead += result;
                }
                return result;
            }
        }

      3.9 private static byte[] streamToBytes(InputStream in, int length) throws IOException {}

     通过指定的输入流,对数据进行写入的一个过程...非常的简单,没什么可以进行解释的...

     private static byte[] streamToBytes(InputStream in, int length) throws IOException {
            byte[] bytes = new byte[length];
            int count;
            int pos = 0;
            while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
                pos += count;
            }
            if (pos != length) {
                throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
            }
            return bytes;
        }

     3.10 public synchronized void clear() {}

     清空所有的文件缓存,其实就是delete,释放内存...

     public synchronized void clear() {
            File[] files = mRootDirectory.listFiles();
            if (files != null) {
                for (File file : files) {
                    file.delete();
                }
            }
            mEntries.clear();
            mTotalSize = 0;
            VolleyLog.d("Cache cleared.");
        }

     3.11 public void removeEntry(String key){}

       public synchronized void remove(String key) {}

     移除函数,如果缓存内部已经存在了当前键值的缓存数据,那么将这个键值进行删除...比较简单...函数调用由函数1调用函数2...

    private void removeEntry(String key) {
            CacheHeader entry = mEntries.get(key);
            if (entry != null) {
                mTotalSize -= entry.size;
                mEntries.remove(key);
            }
        }
    public synchronized void remove(String key) {
            boolean deleted = getFileForKey(key).delete();
            removeEntry(key);
            if (!deleted) {
                VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                        key, getFilenameForKey(key));
            }
        }

      这样与以键值对形式保存的缓存数据集合的操作就全部完成了..剩下的源码过程涉及的就是缓存数据的封装...CacheHeader类,由于CacheHeader还是涉及了一些其他的东西,因此就单独抽出来...

    4.CacheHeader.java

      CacheHeader,对缓存数据的一个打包过程...

      4.1 变量的定义...

     public long size;
    
            /** The key that identifies the cache entry. */
            public String key; //缓存的键值
    
            /** ETag for cache coherence. */
            public String etag; //新鲜度验证...
    
            /** Date of this response as reported by the server. */
            public long serverDate;  //响应过程中花费的时间...
    
            /** TTL for this record. */
            public long ttl; //缓存过期时间...
    
            /** Soft TTL for this record. */
            public long softTtl; //缓存的新鲜时间...
    
            /** Headers from the response resulting in this cache entry. */
            public Map<String, String> responseHeaders;  //保存响应头部信息的map

      4.2 CacheHeader的构造函数...

      构造函数也是很简单的,将Entry中所有保存的数据进行了一下封装...包含着缓存的一些基本属性,以及数据报的头部信息...

     public CacheHeader(String key, Entry entry) {
                this.key = key;
                this.size = entry.data.length;
                this.etag = entry.etag;
                this.serverDate = entry.serverDate;
                this.ttl = entry.ttl;
                this.softTtl = entry.softTtl;
                this.responseHeaders = entry.responseHeaders;
            }

      4.3  public static CacheHeader readHeader(InputStream is) throws IOException {}

      用于获取响应数据报的头部属性,也就是Header,其中包括了一些Http版本的信息等一些基本条件...这个函数由3.7上面的get()函数进行调用,将缓存数据保存的头部数据进行了相关的获取...这里调用了非常多的读取函数,都是按照指定的格式对数据获取的方式...

      public static CacheHeader readHeader(InputStream is) throws IOException {
                CacheHeader entry = new CacheHeader();
                int magic = readInt(is); 
                if (magic != CACHE_MAGIC) {
                    // don't bother deleting, it'll get pruned eventually
                    throw new IOException();
                }
                entry.key = readString(is);
                entry.etag = readString(is);
                if (entry.etag.equals("")) {
                    entry.etag = null;
                }
                entry.serverDate = readLong(is);
                entry.ttl = readLong(is);
                entry.softTtl = readLong(is);
                entry.responseHeaders = readStringStringMap(is);
                return entry;
            }

      4.4 public Entry toCacheEntry(byte[] data) {}

     将缓存中的数据封装给CacheHeader...

    public Entry toCacheEntry(byte[] data) {
                Entry e = new Entry();
                e.data = data;
                e.etag = etag;
                e.serverDate = serverDate;
                e.ttl = ttl;
                e.softTtl = softTtl;
                e.responseHeaders = responseHeaders;
                return e;
            }

      4.5 public boolean writeHeader(OutputStream os){}

     在向缓存中放入数据的时候,也就是调用put()函数的时候,需要将缓存将缓存的内容写入到文件当中,需要写入缓存包含的一些头部信息Header以及Body实体的数据部分...那么写入头部信息就需要调用上述函数...这个函数只是一个调用的模块...调用了许多的write()函数...其实都是一些基本函数的调用,writeInt()一类的函数是通过使用位运算的方式来完成数据的读取,而writeString()方法,则是采用字节流的形式来完成数据的读取...涉及到了很多的从内存读取的read()函数,以及写入内存的write()函数...在这里就不进行一一粘贴了...也没什么好解释的...

     public boolean writeHeader(OutputStream os) {
                try {
                    writeInt(os, CACHE_MAGIC);
                    writeString(os, key);
                    writeString(os, etag == null ? "" : etag);
                    writeLong(os, serverDate);
                    writeLong(os, ttl);
                    writeLong(os, softTtl);
                    writeStringStringMap(responseHeaders, os);
                    os.flush();
                    return true;
                } catch (IOException e) {
                    VolleyLog.d("%s", e.toString());
                    return false;
                }
            }
    static void writeInt(OutputStream os, int n) throws IOException {
            os.write((n >> 0) & 0xff);
            os.write((n >> 8) & 0xff);
            os.write((n >> 16) & 0xff);
            os.write((n >> 24) & 0xff);
        }

      也就这么多了,缓存请求的线程调度类,缓存类,以及缓存的实现也都进行了详细的介绍,Volley采用接口的方式形成了一种良好的扩展,在这里也是很容易体现出来的,就拿Cache来说吧,对外提供接口,方便去自己实现缓存,也可以去使用系统给设置的缓存类,DiskBasedCahce,形成了良好的扩展性...


              

     

     

            

  • 相关阅读:
    yk20192320
    JS常用方法 限制用户输入的方法
    话说最强悍的团队,博客园团队!
    输入框打开禁用自动填充功能
    老生常谈:Asp.net MVC 3+ Jquery UI Autocomplete实现百度效果
    节日logo
    第一篇:Asp.net MVP模式介绍
    VBS 类
    《JavaScript编程精解》简明读书心得上
    Three.js源码阅读笔记1
  • 原文地址:https://www.cnblogs.com/RGogoing/p/4890374.html
Copyright © 2020-2023  润新知