Glide框架設計<二>-------Glide復用池、磁盤緩存、圖片資源加載手寫實現


繼續接着上一次https://www.cnblogs.com/webor2006/p/12313876.html的緩存進行編寫。

Bitmap復用池:

概念:

關於啥是Bitmap復用,這里還是借用博主的這篇文章https://www.jianshu.com/p/97fd67720b34的說明過一下,先來看兩張對比圖:

復用前:

 

可以看到每一張Bitmp在顯示時都申請了一塊內存,而如果采用復用Bitmap的情況,則會長成這樣:

也就是如果采用了復用之后,如果存在能被復用的圖片會重復使用該圖片的內存。那復用之后除了減少內存的開銷之外,還有另一層意義:

具體實現:

在上一次的LruMemoryCache中還有一塊待完善的,就是移出的方法還木有寫,如下:

先把它完善一下:

另外關於這里面的移除還分為主動和被動移除,如果是調了上面這個remove2很顯示是主動移除,此時就不應該回調這個回調了:

為啥?因為如果回調了此方法,是需要將這個資源往復用池中添加的,如果是主動要移除的資源就沒必要往復用池中放了;而如果是被動移除的像LruCache中已經達到了最大上限了再添加則就會將最少使用的給移除掉,此時移除掉的就需要放到復用池當中,所以這里需要加個標識來判斷一下,如下:

好,接下來則來開始定義咱們的復用池了,先定義相關的接口:

然后定義具體類:

package com.android.glidearcstudy.glide.recycle;

import android.graphics.Bitmap;

import androidx.collection.LruCache;

public class LruBitmapPool extends LruCache<Integer, Bitmap> implements BitmapPool {

    private final static int MAX_OVER_SIZE_MULTIPLE = 2;

    public LruBitmapPool(int maxSize) {
        super(maxSize);
    }

    /**
     * 將Bitmap放入復用池
     */
    @Override
    public void put(Bitmap bitmap) {
        //TODO
    }

    /**
     * 獲得一個可復用的Bitmap
     */
    @Override
    public Bitmap get(int width, int height, Bitmap.Config config) {
        return null;
    }


    @Override
    protected int sizeOf(Integer key, Bitmap value) {
        return value.getAllocationByteCount();
    }

    @Override
    protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
        //TODO
    }
}

它也是一個LruCache,接下來具體來實現一下,先實現sizeOf(),跟上次的內存緩存一樣:

然后再來實現一下存放方法:

接下來實現get方法,這里從復用池中的所有可復用的bitmap中要找到一個近視差不多大小的圖才行,如果要顯示的圖的大小在復用池中找不到,那此時就不能從復用池來拿了,實現如下:

圖中打問號的則是我們接下來要處理的,如何從復用池中來獲取跟我們要申請圖片大小近視大的呢?當然我們可以用遍歷的方法來實現它,但是這里可以借助於不怎么常用的一個Map比較方便的實現,如下:

其中NavigableMap是個啥東東,看一下官方的解釋:

而我們調用的是這個方法:

好,接下來繼續完善這個get()方法:

好,接下來咱們來應用一下復用池:

public class CacheTest implements Resource.OnResourceListener, MemoryCache.ResourceRemoveListener {

    LruMemoryCache lruMemoryCache;
    ActiveResources activeResource;
    BitmapPool bitmapPool; public Resource test(Key key) {
        bitmapPool = new LruBitmapPool(10);
        //內存緩存
        lruMemoryCache = new LruMemoryCache(10);
        lruMemoryCache.setResourceRemoveListener(this);
        //活動資源緩存
        activeResource = new ActiveResources(this);

        /**
         * 第一步 從活動資源中查找是否有正在使用的圖片
         */
        Resource resource = activeResource.get(key);
        if (null != resource) {
            //當不使用的時候 release
            resource.acquire();
            return resource;
        }
        /**
         * 第二步 從內存緩存中查找
         */
        resource = lruMemoryCache.get(key);
        if (null != resource) {
            //1.為什么從內存緩存移除?
            // 因為lru可能移除此圖片 我們也可能recycle掉此圖片
            // 如果不移除,則下次使用此圖片從活動資源中能找到,但是這個圖片可能被recycle掉了
            lruMemoryCache.remove2(key);//從內存緩存中移除
            resource.acquire();
            activeResource.activate(key, resource);//再加入到活動資源緩存中
            return resource;
        }
        return null;
    }

    /**
     * 這個資源沒有正在使用了
     * 將其從活動資源移除
     * 重新加入到內存緩存中
     */
    @Override
    public void onResourceReleased(Resource resource) {
        //TODO
    }

    /**
     * 從內存緩存被動移除
     * 此時得放入復用池
     */
    @Override
    public void onResourceRemoved(Resource resource) {
        bitmapPool.put(resource.getBitmap());
    }
}

最后onResourceReleased()代表該資源沒有在使用了,此時需要將它加入到內存緩存中,這里的回調還是增加一個key參數,所以修改一下回調方法:

至此,關於內存緩存和活動資源緩存的代碼就寫完了。

磁盤緩存:

當內存緩存木有找到我們想要的圖片之后,接下來則需要從磁盤緩存開找了,對於glide官方而言,磁盤緩存也是使用三方開源的,看一下:

這里就不詳細看了,代碼量也有點大,先將其拷進工程來直接當工具類來使用既可:

大致瞅一下,其實它也是一個LRU算法,只是說是自己實現了一個,而不是繼承至LruCache來實現的:

也有像LruCache的trimToSize()的實現:

 

接下來咱們利用這個三方開源的磁盤緩存類來使用一下,先新建一個接口:

接下來具體實現一下,也是大致的過一下,跟緩存的實現差不多:

package com.android.glidearcstudy.glide.cache;

import android.content.Context;

import androidx.annotation.Nullable;

import com.android.glidearcstudy.glide.Utils;
import com.android.glidearcstudy.glide.disklrucache.DiskLruCache;

import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;

public class DiskLruCacheWrapper implements DiskCache {


    final static int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
    final static String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";

    private MessageDigest MD;
    private DiskLruCache diskLruCache;

    public DiskLruCacheWrapper(Context context) {
        this(new File(context.getCacheDir(), DEFAULT_DISK_CACHE_DIR), DEFAULT_DISK_CACHE_SIZE);
    }

    protected DiskLruCacheWrapper(File directory, long maxSize) {
        try {
            MD = MessageDigest.getInstance("SHA-256");
            //打開一個緩存目錄,如果沒有則首先創建它,
            // directory:指定數據緩存地址
            // appVersion:APP版本號,當版本號改變時,緩存數據會被清除
            // valueCount:同一個key可以對應多少文件
            // maxSize:最大可以緩存的數據量
            diskLruCache = DiskLruCache.open(directory, 1, 1, maxSize);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String getKey(Key key) {
        key.updateDiskCacheKey(MD);
        return new String(Utils.sha256BytesToHex(MD.digest()));
    }

    @Nullable
    @Override
    public File get(Key key) {
        String k = getKey(key);
        File result = null;
        try {
            DiskLruCache.Value value = diskLruCache.get(k);
            if (value != null) {
                result = value.getFile(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public void put(Key key, Writer writer) {
        String k = getKey(key);
        try {
            DiskLruCache.Value current = diskLruCache.get(k);
            if (current != null) {
                return;
            }
            DiskLruCache.Editor editor = diskLruCache.edit(k);
            try {
                File file = editor.getFile(0);
                if (writer.write(file)) {
                    editor.commit();
                }
            } finally {
                editor.abortUnlessCommitted();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void delete(Key key) {
        String k = getKey(key);
        try {
            diskLruCache.remove(k);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void clear() {
        try {
            diskLruCache.delete();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            diskLruCache = null;
        }
    }
}

其中用到了一個加密:

其中我們的Key接口需要定義一下:

然后它具體的實現類有兩個:

實現也比較簡單:

package com.android.glidearcstudy.glide.load;

import com.android.glidearcstudy.glide.cache.Key;

import java.security.MessageDigest;

public class EngineKey implements Key {

    private final Object model;
    private final int width;
    private final int height;

    public EngineKey(Object model, int width, int height) {
        this.model = model;
        this.width = width;
        this.height = height;
    }

    @Override
    public void updateDiskCacheKey(MessageDigest messageDigest) {
        messageDigest.update(getKeyBytes());
    }

    @Override
    public byte[] getKeyBytes() {
        return toString().getBytes();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EngineKey engineKey = (EngineKey) o;

        if (width != engineKey.width) return false;
        if (height != engineKey.height) return false;
        return model != null ? model.equals(engineKey.model) : engineKey.model == null;
    }

    @Override
    public int hashCode() {
        int result = model != null ? model.hashCode() : 0;
        result = 31 * result + width;
        result = 31 * result + height;
        return result;
    }

    @Override
    public String toString() {
        return "EngineKey{" +
                "model=" + model +
                ", width=" + width +
                ", height=" + height +
                '}';
    }
}
package com.android.glidearcstudy.glide.load;

import com.android.glidearcstudy.glide.cache.Key;

import java.security.MessageDigest;

public class ObjectKey implements Key {

    private final Object object;

    public ObjectKey(Object object) {
        this.object = object;
    }

    /**
     * 當磁盤緩存的時候 key只能是字符串
     * ObjectKey變成一個字符串
     * 序列化:json
     * <p>
     * 將ObjectKey轉變成一個字符串的手段
     *
     * @param md md5/sha1
     */
    @Override
    public void updateDiskCacheKey(MessageDigest md) {
        md.update(getKeyBytes());
    }

    @Override
    public byte[] getKeyBytes() {
        return object.toString().getBytes();
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ObjectKey objectKey = (ObjectKey) o;

        return object != null ? object.equals(objectKey.object) : objectKey.object == null;
    }

    @Override
    public int hashCode() {
        return object != null ? object.hashCode() : 0;
    }
}

關於磁盤緩存先大致過到這。

圖片資源加載:

接下來則是圖片加載的處理了,圖片加載有N種方式:

所以咱們需要將其抽像一下,如下:

package com.android.glidearcstudy.glide.load.model;


import com.android.glidearcstudy.glide.cache.Key;
import com.android.glidearcstudy.glide.load.model.data.DataFetcher;

/**
 * @param <Model> 此泛型表示的是數據的來源
 * @param <Data>  此泛型加載成功后的數據類型,比如InputStream、byte[]
 */
public interface ModelLoader<Model, Data> {

    class LoadData<Data> {
        //緩存的key
        public final Key key;
        //加載數據
        public final DataFetcher<Data> fetcher;

        public LoadData(Key key, DataFetcher<Data> fetcher) {
            this.key = key;
            this.fetcher = fetcher;
        }
    }

    /**
     * 此Loader是否能夠處理對應Model的數據
     */
    boolean handles(Model model);

    /**
     * 創建加載數據
     */
    LoadData<Data> buildData(Model model);
}

其中DataFetcher也是一個接口,不同的形式加載是不一樣的:

package com.android.glidearcstudy.glide.load.model.data;

/**
 * 負責數據獲取
 */
public interface DataFetcher<Data> {

    interface DataFetcherCallback<Data> {
        /**
         * 數據加載完成
         */
        void onFetcherReady(Data data);

        /**
         * 加載失敗
         */
        void onLoadFaled(Exception e);
    }

    void loadData(DataFetcherCallback<? super Data> callback);

    void cancel();

    Class<?> getDataClass();

}

好,下面來實現一下具體的數據獲取的方式,首先是從網絡上獲取,最終返回是一個InputStream,所以此時實現如下:

package com.android.glidearcstudy.glide.load.model.data;

import android.net.Uri;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 網絡數據加載
 */
public class HttpUriFetcher implements DataFetcher<InputStream> {

    private final Uri uri;
    private boolean isCanceled;

    public HttpUriFetcher(Uri uri) {
        this.uri = uri;
    }

    @Override
    public void loadData(DataFetcherCallback<? super InputStream> callback) {
        HttpURLConnection conn = null;
        InputStream is = null;
        try {
            URL url = new URL(uri.toString());
            conn = (HttpURLConnection) url.openConnection();
            conn.connect();
            is = conn.getInputStream();
            int responseCode = conn.getResponseCode();
            if (isCanceled) {
                return;
            }
            if (responseCode == HttpURLConnection.HTTP_OK) {
                callback.onFetcherReady(is);
            } else {
                callback.onLoadFaled(new RuntimeException(conn.getResponseMessage()));
            }
        } catch (Exception e) {
            callback.onLoadFaled(e);
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != conn) {
                conn.disconnect();
            }
        }
    }

    @Override
    public void cancel() {
        isCanceled = true;
    }

    @Override
    public Class<InputStream> getDataClass() {
        return InputStream.class;
    }
}

而文件數據加載則是:

package com.android.glidearcstudy.glide.load.model.data;

import android.content.ContentResolver;
import android.net.Uri;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * 文件數據加載
 */
public class FileUriFetcher implements DataFetcher<InputStream> {

    private final Uri uri;
    private final ContentResolver cr;

    public FileUriFetcher(Uri uri, ContentResolver cr) {
        this.uri = uri;
        this.cr = cr;
    }

    @Override
    public void loadData(DataFetcherCallback<? super InputStream> callback) {
        InputStream is = null;
        try {
            is = cr.openInputStream(uri);
            callback.onFetcherReady(is);
        } catch (FileNotFoundException e) {
            callback.onLoadFaled(e);
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void cancel() {

    }

    @Override
    public Class<InputStream> getDataClass() {
        return InputStream.class;
    }
}

好,對於之前定義的ModelLoader定義了接口還木有定義實現類,下面針對HTTP的來實現一下:

package com.android.glidearcstudy.glide.load.model;

import android.net.Uri;

import com.android.glidearcstudy.glide.load.ObjectKey;
import com.android.glidearcstudy.glide.load.model.data.HttpUriFetcher;

import java.io.InputStream;

public class HttpUriLoader implements ModelLoader<Uri, InputStream> {

    /**
     * http類型的uri此loader才支持,只支持http和https
     */
    @Override
    public boolean handles(Uri uri) {
        String scheme = uri.getScheme();
        return scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https");
    }

    @Override
    public LoadData<InputStream> buildData(Uri uri) {
        return new LoadData<InputStream>(new ObjectKey(uri), new HttpUriFetcher(uri));
    }

}

可以看到這個ModelLoader就已經將要加載的所有東東都組裝好,那怎么用呢?下面用測試類來試一下:

可見使用非常之靈活,目前只實現了一個Http的加載器,還有其它的加載類型只要對ModelLoader進行拓展既可,基實可以看一下glide官方其實拓展了N多個Loader,可以大致瞅一下:

重點是要學會這種基礎靈活的框架搭建的思路,目前先只創建了一個http的加載器,關於其它的Loader的創建下次再繼續。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM