學習內容:java
1.CacheDispatcher緩存請求調度...數組
2.Cache緩存數據的保存...緩存
3.DiskBasedCache基於磁盤的緩存類實現方式...服務器
前面說到使用Volley發送網絡請求的時候,每個請求都會保存在請求隊列RequestQueue中,RequestQueue會開啓一個緩存請求調度線程,若干網絡請求調度線程來對這些請求進行相關的處理...緩存調度線程經過調用緩存數據來完成此次請求的提交...而網絡請求調度線程經過發送網絡請求來完成請求的提交...最後經過把數據發送給客戶端就完成了此次請求和響應的整個過程...網絡
1.CacheDispathcer.java(緩存請求線程)...異步
緩存請求調度線程,用於處理緩存過的請求..當此類開啓的時候會不斷的從緩存隊列當中取出請求,若是緩存存在,那麼從緩存獲取的數據經過deliverResponse方法去分發響應,若是沒有進行緩存,那麼經過發送網絡請求從服務器端獲取數據,所以在實例化對象時還須要傳遞網絡請求隊列...ide
1.1 變量的定義..函數
一些相關變量的定義,很是的簡單...post
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,造成了良好的擴展性...