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的创建下次再继续。