Android 三重缓存

Android 三重缓存文章目录内存缓存Bitmap内存复用在Android应用中不可避免地要显示很多图片,如果不做处理,不管图片是否显示过,每次启动时都需要从网络拉取,这就极大影响了图片加载速度和浪费用户流量,并且整个应用中的图片内存无法控制在一个总的范围内。因此,图片缓存在一个图片加载模块中很重要并且不可缺少。目前比较流行的图片框架,如Glide、Fresco等,都使用了“内存-本地-网络”三级缓存策略。首…

大家好,又见面了,我是你们的朋友全栈君。

在 Android 应用中不可避免地要显示很多图片,如果不做处理,不管图片是否显示过,每次启动时都需要从网络拉取,这就极大影响了图片加载速度和浪费用户流量,并且整个应用中的图片内存无法控制在一个总的范围内。因此,图片缓存在一个图片加载模块中很重要并且不可缺少。

目前比较流行的图片框架,如 Glide、Fresco等,都使用了“内存-本地-网络”三级缓存策略。首先应用程序访问网络拉取图片,分别将加载的图片保存在本地存储和内存中,当程序再一次需要加载图片时,先判断内存中是否有缓存,有则直接从内存中拉取,否则查看本地缓存目录中是否有缓存,本地缓存目录中如果存在缓存,则从本地缓存卡中拉取,否则从网络加载图片。三级缓存的实现逻辑如图所示。
在这里插入图片描述

内存缓存

使用软引用或弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但现在不建议开发者使用这种方案。从 API 9(Android 2.3)开始,Android 系统垃圾回收器更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠,并且从Android 3.0(API Level 11)开始,图片的数据无法用一种可预见的方式将其释放,这就存在潜在的内存溢出风险。

使用 LruCache 来实现图片内存管理是一种可靠的方式,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除

接下来使用 LruCache 实现一个图片的内存缓存


public class MemoryCache {

    private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
    private LruCache<String, Bitmap> mMemoryCache;
    private final String TAG = "MemoryCache";

    public MemoryCache(float sizePer) {
        init(sizePer);
    }

    /**
     * 初始化 LruCache,
     *
     * @param sizePer 参数 sizePer 是设置图片内存池的 size,因为不同的 ROM,会根据硬
     *                件配置给应用下发不同的最大堆大小,所以使用一个百分比作为参数,一般的应用分配 1/8
     *                的堆大小就能满足,图片较大的应用可以适当调整大小。
     */
    private void init(float sizePer) {
        int cacheSize = DEFAULT_MEM_CACHE_SIZE;
        if (sizePer > 0) {
            cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
        }
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            /**
             * sizeOf(String key,Bitmap value),这个方法是必须重写的
             * @return 返回加入的缓存对象大小,这里是返回一个新的 Bitmap 对象内存大小。
             */
            @Override
            protected int sizeOf(String key, Bitmap value) {
                final int size = getBitmapSize(value) / 1024;
                return size == 0 ? 1 : size;
            }

            /**
             * 重写 entryRemoved(boolean evicted,String key,Bitmap oldValue,Bitmap newValue)
             * 这个方法在把最近最少使用的对象在缓存值达到预设定值之前从内存中移除时会回调,其中参数 oldValue 是即将要从
             * LruCache 中移除的对象,这里重写是因为后面会用到。
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                super.entryRemoved(evicted, key, oldValue, newValue);
            }
        };
    }

    /**
     * 获取bitmap大小
     */
    public int getBitmapSize(Bitmap bitmap) {
        return bitmap.getAllocationByteCount();
    }

    /**
     * 通过 url 获取内存中的 Bitmap 对象,如果没有,则返回 null,需要进行下一步的加载图
     * 片逻辑。
     */
    public Bitmap getBitmap(String url) {
        Bitmap bitmap = null;
        if (mMemoryCache != null) {
            bitmap = mMemoryCache.get(url);
        }
        return bitmap;
    }

    /**
     * 增加一个新的 Bitmap 到内存池中,url 为 KEY。
     */
    public void addBitmapToCache(String url, Bitmap bitmap) {
        if (url == null || bitmap == null) {
            return;
        }
        mMemoryCache.put(url, bitmap);
    }

    /**
     * 清空缓存
     */
    public void clearCache() {
        if (mMemoryCache != null) {
            mMemoryCache.evictAll();
        }
    }
}


这样就实现了一个简单的图片缓存管理类,在对图片解码后,把图片放到内存中,代码如下:

if (bitmap !=null){
            memoryCache.addBitmapToCache(url , bitmap);
}

在加载图片前,先尝试从 MemoryCache 中获取,实现如下代码:

 Bitmap bitmap = memoryCache.getBitmap(url);
        if (bitmap !=null){
            // 设置展示到ImageView
        }else{
            // 加载Bitmap
        }

这样,当解码出一组新图片时,都会缓存到 LruCache 中,在下次获取图片时,首先从LruCache 中拉取,但 LruCache 的大小是有一定限制的,从前面的内容可以看到,LruCache的大小是系统分配给应用的最大堆内存的百分比,具体如下所示:

cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024)

这个百分比多少合适呢,可以从以下几个点来考虑。

  • 应用中内存的占用情况,除了图片以外,是否还有大内存的数据需要缓存到内存。
  • 在应用中大部分情况要同时显示多少张图片,优先保证最大图片的显示数量的缓存支持。
  • Bitmap 的规格,计算出一张图片占用的内存大小。
  • 图片访问的频率。

缓存的大小需要设计好,一个过小的缓存不但没有任何好处,还会引起额外的开销,一个过大的缓存可能使 java.lang.OutOfMemory 异常的概率增加,并且应用剩余部分只留下很小的内存,导致其他数据缓存空间变小。

在应用中,如果有一些图片的访问频率要比其他的大一些,或者必须一直显示出来,就需要一直保持在内存中,这种情况可以使用多个 LruCache 对象来管理多组Bitmap,对 Bitmap 进行分级,不同级别的 Bitmap 放到不同的 LruCahe 中。

Bitmap内存复用

Bitmap 内存复用,从 Android 3.0 版本开始支持 Bitmap 内存复用,也就是 BitmapFactory.Options.inBitmap 属性,如果这个属性被设置有效的目标复用对象,decode 方法就在加载内容时重用已经存在的 Bitmap。这意味着 Bitmap 的内存被重新利用,这可以减少内存的分配与回收,提高图片的性能。在 MemoryCache 的基础上增加内存复用的功能。使用 mReusableBitmaps 存放可重用 Bitmap 的软引用的集合,代码如下:

private Set<SoftReference<Bitmap>> mReuseableBitmaps;

  private void init(float sizePer) {
        int cacheSize = DEFAULT_MEM_CACHE_SIZE;
        if (sizePer > 0) {
            cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
        }

        mReuseableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());


        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            /**
             * sizeOf(String key,Bitmap value),这个方法是必须重写的
             * @return 返回加入的缓存对象大小,这里是返回一个新的 Bitmap 对象内存大小。
             */
            @Override
            protected int sizeOf(String key, Bitmap value) {
                final int size = getBitmapSize(value) / 1024;
                return size == 0 ? 1 : size;
            }

            /**
             * 重写 entryRemoved(boolean evicted,String key,Bitmap oldValue,Bitmap newValue)
             * 这个方法在把最近最少使用的对象在缓存值达到预设定值之前从内存中移除时会回调,其中参数 oldValue 是即将要从
             * LruCache 中移除的对象,这里重写是因为后面会用到。
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
            	// 添加
                mReuseableBitmaps.add(new SoftReference<Bitmap>(oldValue));
            }
        };
    }

在需要分配一个新的 Bitmap 对象时,首先检查是否有可内存复用的 Bitmap 对象。代码如下:

public static Bitmap decodeSampledBitmapFromStream(InputStream in, BitmapFactory.Options options, ImageCache imageCache) {
        addInBitmapOptions(options, imageCache);
        return BitmapFactory.decodeStream(in, null, options);
    }

    public static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
        options.inMutable = true;
        if (cache != null) {
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
            if (inBitmap != null) {
                options.inBitmap = inBitmap;
            }
        }
    }

从代码中可以看到addInBitmapOptions 方 法 在 可 重 用 的 集 合 中 , 通 过cache.getBitmap-FromReusableSet(options)方法查找一个合适的 bitmap 赋值给 inBitmap。getBitmapFrom-ReusableSet 方法如下

public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {

            Bitmap bitmap = null;
            if (mReuseableBitmaps != null && !mReuseableBitmaps.isEmpty()) {
                final Iterator<SoftReference<Bitmap>> iterator = mReuseableBitmaps.iterator();
                Bitmap item;
                while (iterator.hasNext()) {
                    item = iterator.next().get();
                    if (null != item && item.isMutable()) {
                        if (canUseForInBitmap(item, options)) {
                            bitmap = item;
                            iterator.remove();
                            break;
                        }
                    } else {
                        iterator.remove();
                    }
                }
            }
            return bitmap;
        }


getBitmapFromReusableSet 方法从软引用集合 mReusableBitmaps 中查找规格可利用的Bitmap 作为内存复用对象,因为使用 inBitmap 有一些限制,在 Android 4.4 之前,只支持同等大小的位图。因此通过 canUseForInBitmap(item,options)方法来判断该 Bitmap 是否可以复用,代码如下:只要满足条件,就实现内存复用。

private boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options options) {
            int width = options.outWidth / options.inSampleSize;
            int height = options.outHeight / options.inSampleSize;
            int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
            return byteCount <= candidate.getAllocationByteCount();
}

磁盘缓存

内存缓存在访问最近浏览过的 Bitmap 是很有效的,能明显提高显示的速度,然而并不是所有的图片在这个缓存中都是有效的。最典型的场景就是 GridView 等一类带有大数据的组件,可以很快填满内存缓存,一旦用户继续浏览,应用就不得不再次处理每一张图片。

如果增加一级磁盘缓存,在磁盘缓存中保存加载过的图片,在图片在内存缓存中失效的地方从本地读取图片数据,再解码,就可以减少图片从网络加载所需的时间。当然,从磁盘上获取这些图片要比从内存中加载慢,并且由于磁盘读取时间是不可预知的,所以图片的解码和文件读取都应该在后台进程中完成。

DiskLruCache 是 Android 提供的一个管理磁盘缓存的类。该类可用于在程序中把从网络加载的数据保存到磁盘上作为缓存数据,DiskLruCache 并不是 Google 官方提供的,是获得官方认可的一个开源工具类

引入DiskLRUCache库如下

implementation 'com.jakewharton:disklrucache:2.0.2'
  • 初始化DiskLruCache
    DiskLruCache 初始化是调用它的 open()静态方法,如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

directory 是缓存图片数据的文件夹,一般建议缓存到 SD 卡上,SD 卡有足够的空间。appVersion 是应用/引擎版本号,根据应用的业务需求来维护,如果 appVersion 发生变化,会自动删除前一个版本的数据。valueCount 是指 Key 与 Value 的对应关系,一般情况下是 1 对 1 关系,这里就写 1。maxSize 是缓存图片的最大缓存数据大小,根据业务自身需求设置。

初始化 DiskLruCache 的代码如下:

private int appVersion = 1;
    private DiskLruCache mDiskLruCache;
    public void init(final long cacheSize, final File cacheFile) {
        new Thread() {
            @Override
            public void run() {
                synchronized (mDiskCacheLock) {
                    if (!cacheFile.exists()) {
                        cacheFile.mkdir();
                    }
                    try {
                        mDiskLruCache = DiskLruCache.open(cacheFile, appVersion, 1, cacheSize);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

因为 open 方法中有 IO 操作,所以建议在异步线程中初始化,但如果在初始化前就要操作写或者读操作会导致失败,所以在初始化过程中加锁,避免同步问题。

  • 写入 DiskLruCache
    DiskLruCache.Editor 类实现写入的操作。通过调用 DiskLruCache 的 edit()方法来获取实例,写入操作流程如下:
  1. 获取 Editor 实例
    Editor 实例需要传入一个 Key 来获取参数,Key 必须与图片 URL 有唯一对应关系,但由于 URL 中的字符可能会带文件名不支持的字符类型,所以取 URL 的 MD5 值作为文件名,实现 Key 与图片的对应关系,通过 URL 获取 MD5 值的代码如下:
private String hashKeyForDisk(String key) {
        String cacheKey = null;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = byteToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return cacheKey;
    }

    private String byteToHexString(byte[] bytes) {
        StringBuilder builder = new StringBuilder();
        for (byte aByte : bytes) {
            String hex = Integer.toHexString(0xFF & aByte);
            if (hex.length() == 1) {
                builder.append('0');
            }
            builder.append(hex);
        }
        return builder.toString();
    }
  1. 写入需要保存的数据
    有了 DiskLruCache.Editor 的实例之后,可以调用它的 newOutputStream()方法来创建一个输出流,把需要保存的数据写入 OutputStream。在写入操作执行完之后,再调用 commit()方法提交才能使写入生效,调用 abort()方法的话则表示放弃此次写入。
public void saveToDisk(String imageUrl, InputStream inputStream) {
        synchronized (mDiskCacheLock) {
            String key = hashKeyForDisk(imageUrl);
            try {
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (inputStream != null && editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    if (downloadUrlToStream(inputStream, outputStream)) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }
                }
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public boolean downloadUrlToStream(InputStream inputStream, OutputStream outputStream) {
        // 下载
        return false;
    }
  1. 读取图片缓存
 public Bitmap getBitmapFromDiskCache(String url ,BitmapConfig bitmapConfig){

        synchronized (mDiskCacheLock) {

            if (mDiskLruCache != null){

                String key = hashKeyForDisk(url);
                try {
                    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
                     if (snapshot == null){
                         return null;
                     }
                     InputStream inputStream = snapshot.getInputStream(0);
                     if ( inputStream != null){

                         // 根据 Option 读取 Bitmap 返回
                        return bitmap;
                     }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }

        }
        return null;
    }

但需要注意的是,读取并解码成Bitmap数据和保存图片数据都是有一定耗时的IO操作。所以一定要记住不能放在主线程中调用这些方法。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/143371.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(2)
blank

相关推荐

  • 2020美赛A题解题方法

    2020美赛A题解题方法题目:问题A:向北移动全球海洋温度影响某些海洋生物的栖息地质量。当温度变化太大,它们无法继续繁荣时,这些物种就会迁移到其他更适合它们现在和未来生活和繁殖成功的栖息地。其中一个例子就是美国缅因州的龙虾种群,它们正缓慢地向北迁移到加拿大,那里的海洋温度较低,为它们提供了更合适的栖息地。这种地理种群的转移可能会严重影响依赖海洋生物稳定性的公司的生计。您的团队已被苏格兰北大西洋渔业管理协会聘请为顾问…

  • 国内期货软件开发_正规期货外盘平台

    国内期货软件开发_正规期货外盘平台期货软件搭建-期货软件开发-期货平台搭建,要想做期货软件开发定制,网站的安全性和稳定性非常重要,要搭建一款安全有保障的期货软件,找专业的金融软件搭建公司是必不可少的,那么期货软件开发,期货平台搭建的流程特点有哪些呢?  1.要搭建好用的软件,软件的部署成本和软件的易用性要好一款成熟稳重的期货软件,要经得起时间的推敲,很多公司通过购买软件给到用户,其实他们满足不了二次开发的需求,导致软件更新和后期维护上面根本维护不了。2.要搭建期货平台,价格一定要合理,不要贪图小便宜好的期货平台搭建很多人贪图小便宜,

  • 基于python的电影推荐系统_python为什么叫python

    基于python的电影推荐系统_python为什么叫python好莱坞知名媒体THR《好莱坞报道者》,邀请了2800多名好莱坞影视从业人员,包括779名演员,365名制片人,268名导演等等,由他们选出自己最爱的剧集,最终汇总成为这个百大经典美(英)剧清单。看看你追的剧上榜了吗?看到第一名时,瞬间热泪盈眶!果然是他,最经典,没有之一!100、绝望主妇DesperateHousewives(2004-2012)ABC99、弗尔蒂旅馆FawltyTowe…

  • flash 外国小游戏教程网站[通俗易懂]

    flash 外国小游戏教程网站[通俗易懂]http://www.tutorialized.com/tutorial/game-tutorial-part-1-character-movement/44240  相关的小游戏制作教程:有兴趣可以看看 http://www.emanueleferonato.com/2007/05/15/create-a-flash-racing-game-tutorial/

    2022年10月29日
  • KaOS Linux放出最新版ISO镜像喜迎五周岁

    KaOS Linux放出最新版ISO镜像喜迎五周岁

  • 计算机网络网络适配器的作用是什么原因,网络适配器是什么东西?网络适配器主要功能…

    计算机网络网络适配器的作用是什么原因,网络适配器是什么东西?网络适配器主要功能…网络适配器就是俗称的网卡,网卡是工作在链路层的网络组件,是局域网中连接计算机和传输介质的接口,简单来说就是,网卡有问题网络就有问题。网卡是工作在链路层的网络组件,是连接计算机和传输介质的接口。但是很多朋友还是不知道网络适配器是什么。下面window小编就来具体说说网络适配器是什么适配器是一个接口转换器,适配器可以是一个独立的硬件接口设备也可以是信息接口。网络适配器就是一种信息接口,用来接受或发送网…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号