從零開始擼一個Fresco之硬盤緩存

轉載請註明出處 Fresco源代碼文檔翻譯項目請看這裏:Fresco源代碼文檔翻譯項目 硬盤緩存是android圖片框架中比較重要的一個模塊,Fresco中本身重寫了一個硬盤緩存框架,代替了android自己的DiskLruCache,因此今天咱們就來介紹Fresco中的硬盤緩存,而且將其提取出來成爲咱們本身的框架。我已經成功提取出了 Fresco 中的硬盤緩存框架,這是項目地址Frsco硬盤緩存框架項目地址,建議你們在看文章的時候結合項目代碼,項目中的每一個class文件中都有註釋,看起來仍是比較容易的。android

1、目錄介紹

  • 1.binaryResource包:這裏面有一個接口和一個類,Fresco遵循面向接口編程這一原則,因此不少地方都會用接口來加強可擴展性。總的來講BinaryResource這個接口表明一個字節序列,它抽象了底層的資源,好比一個file文件。FileBinaryResource內部就有一個File成員,以方便對File進行操做。
  • 2.cacheEventAndListenner包:在硬盤緩存的過程當中,會有許多的事件發生,好比查找緩存時候命中了、要插入一個緩存、讀取緩存失敗了等等。此時客戶端會須要一個監聽器來監聽各類硬盤緩存事件的發生。此時CacheEventListener接口就能夠當客戶端對硬盤緩存的監聽器,CacheEventListener中的每一個方法中都傳入了一個CacheEvent。CacheEvent是一個接口,其實現類中包裹了許多與硬盤緩存相關的東西,SettableCacheEvent就是CacheEvent的惟一實現類,因爲硬盤緩存事件產生的很頻繁,因此SettableCacheEvent使用了享元模式(在內存中只維持兩個對象,不斷的回收重用)。CacheErrorLogger接口是硬盤緩存的讀或寫產生異常的時候使用到的類。CacheEventListener和CacheErrorLogger都只有一個空實現用來填充代碼,具體的實現須要使用者實現。
  • 3.cacheKey包:但咱們插入或者取出一個文件緩存的時候就會用到CacheKey,SimpleCacheKey的內部其實就是一個String,MultiCacheKey的內部只是一系列CacheKey的集合。咱們通常使用的是SimpleCacheKey,由於咱們去網絡加載圖片的時候Uri就是一個最好的key。
  • 4.comparator包:這裏的EntryEvictionComparator實現了Comparator<DiskStorage.Entry>,在後面咱們會知道DiskStorage.Entry表示一條文件緩存。因此這個接口是爲了比較每一條文件緩存。EntryEvictionComparatorSupplier使用提供者模式,在get()中返回了一個EntryEvictionComparator,因此只須要實現EntryEvictionComparatorSupplier,在get()中返回EntryEvictionComparator的具體實現,咱們就能夠定義一個DiskStorage.Entry的比較器。目前Fresco中自定義了兩個比較器DefaultEntryEvictionComparatorSupplier和ScoreBasedEvictionComparatorSupplier,他們分別是基於LRU和權重的比較器。
  • 5.fileTree包:硬盤緩存會使用到文件系統,因此此時對一個目錄全部文件的遍歷是必不可少的,這裏FileTree負責遍歷一個目錄下的全部文件和提供一個安全的刪除文件夾方式,其遍歷的時候將每一個文件交與FileTreeVisitor進行處理。這是一個很好的訪問文件系統的方式。FileTreeVisitor在DiskStorageCache中有兩個具體實現。
  • 6.trimmable包:在硬盤緩存中有一個類會直接負責和硬盤上的文件打交道,因此其會使用了大量硬盤空間,一旦硬盤空間不足的話,可能形成硬盤緩存失敗的狀況。因此此時咱們能夠將該class實現DiskTrimmable接口,而後在DiskTrimmableRegistry的實現類中註冊,一旦硬盤空間不夠了,使用DiskTrimmableRegistry實現類通知全部實現了DiskTrimmable而且註冊過的對象,讓該對象刪除緩存文件以釋放硬盤空間,這裏的釋放規則是LRU。Fresco中只有一個NoOpDiskTrimmableRegistry空 實現,這裏須要讓使用者本身去實現更符合本身app的通知方式。
  • 7.util包:這個包中放了一些工具類,你們有興趣能夠去看看
  • 8.core包:前面7個包都是輔助的包,Fresco真正的硬盤緩存核心類在這個包中,這裏DiskStorage接口的實現類負責和android的文件系統直接打交道,提供緩存的文件的增刪改api。FileCache接口的實現類則是負責緩存的邏輯,好比緩存滿了的清除邏輯。FileCache接口的實現類直接持有DiskStorage接口的引用以操做文件系統。咱們接下來要看的就是這個包中的類

2、硬盤緩存核心類分析

先上一張圖,讓你們簡單瞭解各個接口提供的api。 git

核心類關係圖

1.DefaultDiskStorage

這個class的代碼就不貼了,強烈建議讀者把我前面的項目下載下來,結合博客一塊兒觀看。github

這個類是DiskStorage接口的實現類,前面說了這個類是直接與android的文件系統打交道的類。這個類有如下幾個功能特色:算法

  • 1.該類構造函數中在傳入的緩存根目錄(下面稱該文件夾爲cache)下建立一個當前緩存版本的文件夾,接下來該對象經手的緩存文件都儲存在這裏文件夾中,咱們在後面稱這個文件夾爲 version1.0。
  • 2.三星的老手機有一個問題,就是一個文件夾下面不能放置過多的文件,這個問題被稱爲RFS。所以在每次儲存緩存文件的時候會將 緩存文件key的hash值對100取模,這個值就是文件夾的名字,若是這個文件夾沒建立就建立一個,而後將緩存文件放入其中。在取緩存文件的時候也要經歷這個流程。
  • 3.緩存文件在插入的時候,有兩個步驟1.將該文件寫成.tmp後綴的臨時文件,此時該文件對使用者不可見。2.將.tmp後綴的文件更名爲.cnt後綴的文件,此時該文件對使用者可見。
  • 4.該對象在兩種狀況下須要使用到FileTree:
    • 1.清理不須要的文件(如tmp文件或不是本版本的文件),此時會調用purgeUnexpectedResources()使用FileTree.walkFileTree()遍歷cache文件夾,而後使用FileTreeVisitor的實現類PurgingVisitor對每一個文件進行判斷,看看是否須要清除該文件
    • 2.獲取Entry:由上面的圖中咱們能夠看見Entry是DiskStorage的一個內部接口,DefaultDiskStorage中用EntryImpl實現了它,而每個Entry就表明着一個文件緩存。因此要獲取全部的緩存信息就應該遍歷 version1.0。而這裏使用的FileTree.walkFileTree()和EntriesCollector。
  • 5.硬盤緩存的清理算法通常使用的都是LRU,因此在每次調用getResource()獲取文件緩存的時候,都會將該文件的LastModified設置成目前的時間以便在後面進行緩存的清理。
  • 6.咱們從前面的圖中看見DiskStorage接口insert()方法返回了一個Inserter,這個Inserter在本class中被實現了。使用這種方式是爲了進行併發地寫入多條緩存條目。在Insert.commit()調用以前,這個緩存條目對客戶端是不可見的。
  • 7.除上面的功能,本class還提供了經過Key和Entry刪除緩存、清理全部緩存、經過Key查詢文件緩存這些功能。

2.DiskStorageCache

一樣不貼代碼,再次建議你們下載項目代碼觀看博客。編程

這個類是FileCache的實現類,其經過DefaultDiskStorage與android文件系統打交道,而且處理文件緩存的各類邏輯。來講說它的功能特色:設計模式

  • 1.這個對象是CacheEventListener監聽的對象,用戶能夠傳入一個本身的Listenner來監聽硬盤緩存的各類活動,好比插入緩存、刪除緩存、緩存失敗等等事件。
  • 2.在該類中有一個CacheStats內部類(用於儲存該對象已經使用的硬盤空間,以及緩存條目數量)和一個HashSet(mResourceIndex用於儲存全部緩存條目的Id),因爲在Fresco中硬盤緩存使用是很頻繁的,因此若是實時刷新這兩個東西是不值得的,所以該類中設置了一個時間(FILECACHE_SIZE_UPDATE_PERIOD_MS),每隔這麼多時間上面兩個對象的狀態就會被刷新。
  • 3.該對象能夠在getResource(CacheKey)方法中經過DefaultDiskStorage#getResource()來獲取傳入key所對應的緩存文件,每次調用這個方法都會刷新該緩存文件的時間戳,爲以後的LRU刪除緩存作準備
  • 4.該對象能夠在probe(CacheKey)方法中經過DefaultDiskStorage#touch()查詢傳入Key的緩存文件是否存在,若是存在,一樣會改變該緩存文件的時間戳。
  • 5.使用者能夠調用該對象的insert(CacheKey,WriterCallback)方法插入一個緩存文件,這裏須要在使用者覆蓋WriterCallback的write(OutputStream)方法,經過OutputStream將須要儲存的數據寫入緩存文件。instert(CacheKey,WriterCallback)方法中會建立一個緩存文件的OutputStream,而後將其傳入WriterCallback#get()中以供使用者使用。
  • 6.在remove(CacheKey)會經過DefaultDiskStorage#remove()方法,刪除傳入key對應的緩存文件,同時會刪除mResourceIndex中該緩存文件的id。
  • 7.clearOldEntries(long cacheExpirationMs)會經過DefaultDiskStorage#getEntries()和DefaultDiskStorage#remove()這兩個方法刪除比傳入時間cacheExpirationMs老的緩存文件。
  • 8.maybeEvictFilesInCacheDir()會在插入一個緩存文件以前,判斷本對象使用的硬盤空間是否已經超過初始化時設置的限制,若是超過限制就會調用evictAboveSize()(先在getSortedEntries()方法中經過DefaultEntryEvictionComparatorSupplier將全部Entry排序,而後按時間順序一個個刪除緩存文件,直至達到緩存空間要求)。
  • 9.hasKeySync(CacheKey)在mResourceIndex中判斷該文件緩存是否存在,因爲該對象不是實時刷新,因此會出現滯後性即有些文件緩存已經存在,可是mResourceIndex中並無其id。
  • 10.hasKey(CacheKey)該方法是hasKeySync()的升級版,其不只會經過hasKeySync()在mResourceIndex查找,若是沒找到還會經過DefaultDiskStorage#contains()方法查找,這樣也提升了查找效率。
  • 11.因爲該class實現了DiskTrimmable接口,因此其在硬盤空間吃緊的時候也會調用evictAboveSize()進行緩存文件的清理,不過Fresco中DiskTrimmableRegistry的默認實現是NoOpDiskTrimmableRegistry,這個實現中不會作任何事情。因此具體的監聽須要使用者來作。

3、Fresco硬盤緩存框架的使用

Fresco在使用硬盤緩存框架的時候,與其餘模塊通訊的時候使用了兩個類DiskCacheConfig和DiskStorageCache。DiskCacheConfig很好理解,負責用Builder模式建立一個DiskStorageCache。因此這裏歸更到底就是使用DiskStorageCache暴露出來的增刪改查等api,而硬盤緩存框架中的其餘類與Fresco的其餘模塊是解耦的,這也是軟件工程中的一個重要的思想。因此接下來咱們就來使用一下DiskStorageCache,也算是對這篇博客的總結。api

這裏代碼很少因此貼下代碼:緩存

public class MainActivity extends AppCompatActivity {
FileCache mFileCache;
Button buttonInsert;
Button buttonHasKey;
Button buttonRemove;
Button buttonClearAll;
Button buttonGetCache;
ImageView imageView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initDiskCache();
    initView();

    final int[] times = {0};
    buttonInsert.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            SimpleCacheKey simpleCacheKey=new SimpleCacheKey(String.valueOf(times[0]));
            times[0]++;
            insert(simpleCacheKey);
        }
    });

    buttonHasKey.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "key 5 是否存在?" + mFileCache.hasKey(new SimpleCacheKey("5")), Toast.LENGTH_SHORT).show();
        }
    });

    buttonRemove.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            times[0]--;
            SimpleCacheKey simpleCacheKey=new SimpleCacheKey(String.valueOf(times[0]));
            mFileCache.remove(simpleCacheKey);
        }
    });

    buttonClearAll.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mFileCache.clearAll();
            times[0]=0;
        }
    });

    buttonGetCache.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            getCache();
        }
    });
}

private void initDiskCache(){

    DiskCacheConfig diskCacheConfig=DiskCacheConfig.newBuilder(this).build();
    Toast.makeText(this, "緩存文件夾:"+diskCacheConfig.getBaseDirectoryPathSupplier().get().getPath(), Toast.LENGTH_SHORT).show();
    DefaultDiskStorage defaultDiskStorage=new DefaultDiskStorage(
            diskCacheConfig.getBaseDirectoryPathSupplier().get(),
            diskCacheConfig.getVersion(),
            diskCacheConfig.getCacheErrorLogger());

    DiskStorageCache.Params params = new DiskStorageCache.Params(
            diskCacheConfig.getMinimumSizeLimit(),
            diskCacheConfig.getLowDiskSpaceSizeLimit(),
            diskCacheConfig.getDefaultSizeLimit());


    CacheEventListener cacheEventListener=new CacheEventListener() {
        @Override
        public void onHit(CacheEvent cacheEvent) throws IOException {
            Log.d("MainActivity Cache hit", cacheEvent.getCacheKey().getUriString());
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }

        @Override
        public void onMiss(CacheEvent cacheEvent) throws IOException {
            Log.d("MainActivity Cache miss", cacheEvent.getCacheKey().getUriString());
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }

        @Override
        public void onWriteAttempt(CacheEvent cacheEvent) throws IOException {
            Log.d("MainActivity Cache write start", cacheEvent.getCacheKey().getUriString());
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }

        @Override
        public void onWriteSuccess(CacheEvent cacheEvent) throws IOException {
            Log.d("MainActivity Cache write success", cacheEvent.getCacheKey().getUriString());
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }

        @Override
        public void onReadException(CacheEvent cacheEvent) {
            Log.d("MainActivity Cache ReadException", cacheEvent.getCacheKey().getUriString());
        }

        @Override
        public void onWriteException(CacheEvent cacheEvent) {
            Log.d("MainActivity Cache WriteException", cacheEvent.getCacheKey().getUriString());
        }

        @Override
        public void onEviction(CacheEvent cacheEvent) throws IOException {
            Log.d("MainActivity Cache Eviction", cacheEvent.getCacheKey().getUriString());
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }

        @Override
        public void onCleared() throws IOException {
            Log.d("MainActivity", "Cleared");
            Log.d("MainActivity", "mFileCache.getDumpInfo():" + mFileCache.getDumpInfo());
        }
    };

    mFileCache=new DiskStorageCache(
            defaultDiskStorage,
            diskCacheConfig.getEntryEvictionComparatorSupplier(),
            params,
            cacheEventListener,
            diskCacheConfig.getCacheErrorLogger(),
            diskCacheConfig.getDiskTrimmableRegistry(),
            diskCacheConfig.getContext(),
            Executors.newSingleThreadExecutor(),
            diskCacheConfig.getIndexPopulateAtStartupEnabled());

}

private void initView(){
    buttonInsert=(Button)findViewById(R.id.insert);
    buttonHasKey=(Button)findViewById(R.id.hasKey);
    buttonRemove=(Button)findViewById(R.id.remove);
    buttonClearAll=(Button)findViewById(R.id.clearAll);
    buttonGetCache=(Button)findViewById(R.id.getCache);
    imageView=(ImageView)findViewById(R.id.image);
}

private void insert(SimpleCacheKey simpleCacheKey){
    try {
        mFileCache.insert(simpleCacheKey, new WriterCallback() {
            @Override
            public void write(OutputStream os) throws IOException {
                FileUtils.bitmapToFile(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher),os);
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private void getCache(){
    BinaryResource diskCacheResource = mFileCache.getResource(new SimpleCacheKey("2"));
    if (diskCacheResource==null) Toast.makeText(this, "miss 2", Toast.LENGTH_SHORT).show();
    else {
        Toast.makeText(this, "hit 2", Toast.LENGTH_SHORT).show();
        try {
            imageView.setImageBitmap(BitmapFactory.decodeStream(diskCacheResource.openStream()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
}
複製代碼
  • 1.首先是使用DiskCacheConfig初始化DiskStorageCache:
    • 1.因爲DiskCacheConfig採用的是Bulider模式,因此Builder中內置了許多默認屬性。若是咱們沒有特殊要求能夠直接獲取一個DiskCacheConfig對象。
    • 2.經過DiskCacheConfig中的默認屬性建立DefaultDiskStorage和DiskStorageCache.Params以供後面使用。
    • 3.咱們能夠選擇建立一個CacheEventListener以在客戶端監聽硬盤緩存的行爲。固然也能夠選擇不監聽。
    • 4.最後使用上面的屬性建立一個FileCache,由於解耦設計,咱們只須要獲取一個FileCache而不是DiskStorageCache。這裏要注意一下,這個硬盤緩存框架沒有內置線程池,而通常來講對於硬盤的讀寫都是在其餘線程中的,Fresco中就是建立了一個讀線程池和一個寫線程池將DiskStorageCache的讀寫操做放在其中操做。我這裏爲了方便就直接在UI線程進行操做了。你們在使用的時候須要根據本身的需求建立線程池。此外咱們注意到在FileCache的建立過程當中傳入了一個Executors.newSingleThreadExecutor(),注意這不是用於讀寫操做的,你們進入DiskStorageCache的構造器中能夠看見,這個線程池根本沒有被保存爲成員變量,只是用於作了一些初始化的操做。
  • 2.初始化了5個Button和一個ImageView
  • 3.第一個按鈕是插入硬盤緩存的按鈕:能夠看見我使用了SimpleCacheKey以從0遞增的String做爲緩存的id。而後在insert()方法中調用了FileCache#insert(),其中經過WriterCallback#get()提供的OutputStream將R.mipmap.ic_launcher寫入了硬盤緩存中。
  • 4.第二個按鈕是判斷是否有給定的緩存:我調用FileCache#hasKey()判斷id爲5的緩存是否存在。
  • 5.第三個按鈕是經過id刪除硬盤緩存:調用的是FileCache.remove()方法
  • 6.第四個按鈕是清空緩存:經過FileCache.clearAll()實現;
  • 7.第五個按鈕顯示id爲2的緩存圖片:在getCache()中經過FileCache.getResource()獲取了一個BinaryResource,這個類其實是FileBinaryResource,能夠直接獲取其InputStream來顯示圖片。

以上就是Fresco硬盤緩存框架的使用。安全

4、總結

Fresco的硬盤緩存框架,仍是挺有趣的,其中用到了許多軟件工程的思想與Java設計模式。Fresco中還有許多模塊很是有趣,作個預告下一篇博客將會分析Fresco的內存緩存框架有興趣的同窗必定別錯過了。網絡

相關文章
相關標籤/搜索