深入理解Android中的缓存机制(一)缓存简介

3,344 阅读7分钟

概述

说起缓存,大家可能很容易想到Http的缓存机制,LruCache,其实缓存最初是针对于网络而言的,也是狭义上的缓存,广义的缓存是指对数据的复用,我这里提到的也是广义的缓存,比较常见的是内存缓存以及磁盘缓存,不过要想进一步理解缓存体系,其实还需要复习一点计算机知识。

computer

CPU

CPU分为运算器跟控制器,是计算机的主要设备之一,功能主要是解释计算机指令以及处理计算机软件中的数据。计算机的可编程性主要是指对中央处理器的编程。中央处理器、内部存储器和输入/输出设备是现代电脑的三大核心部件。

存储器

存储器的种类很多,按用途可以分为主存储器和辅助存储器,下面依次介绍一下。

主存储器

又称内存是CPU能直接寻址的存储空间,它的特点是存取速率快。内存一般采用半导体存储单元,包括随机存储器(Random Access Memory)、只读存储器(Read Only Memory)和高级缓存(Cache)。

  • RAM:随机存储器可以随机读写数据,但是电源关闭时存储的数据就会丢失;
  • ROM:只能读取,不能更改,即使机器断电,数据也不会丢失
  • Cache:它是介于CPU与内存之间,常用有一级缓存(L1)、二级缓存(L2)、三级缓存(L3)(一般存在于Intel系列)。它的读写速度比内存还快,当CPU在内存中读取或写入数据时,数据会被保存在高级缓冲存储器中,当下次访问该数据时,CPU直接读取高级缓冲存储器,而不是更慢的内存。

辅助存储器

辅助存储器又称外存储器,简称外存,对于电脑而言,通常说的是硬盘或者光盘等,对于手机一般指的是SD卡,不过现在很多厂商都已经整合在一起了

缓存类型

  • 内存缓存:这里的内存主要指的存储器缓存
  • 磁盘缓存:这里主要指的是外部存储器,电脑指的是硬盘,手机的话指的就是SD卡

缓存容量

就是缓存的大小,到达这个限度之后,那么就需要进行缓存清理了

缓存策略

不管是内存缓存还是磁盘缓存,缓存的容量都是有限制的,所以跟线程池满了之后的线程处理策略类似,缓存满了的时候,我们也需要有相应的处理策略,常见的策略有:

  • FIFO(first in first out):先进先出策略,类似队列。

  • LFU(less frequently used):最少使用策略,RecyclerView的缓存采用了此策略。

  • LRU(least recently used):最近最少使用策略,Picasso在进行内存缓存的时候采用了此策略。

当缓存容量达到设定的容量的时候,会根据制定的策略进行删除相应的元素。

内存泄露

这个主要发生在内存缓存中,当生命周期段的对象持有了生命周期长的对象的引用就会发生内存泄露,解决这种问题通常有两种方式

  • 引用置空:将缓存中引用的对象置空,然后GC就能够回收这些对象
  • 采用弱引用:采用弱引用关联对象,这样就能够不干涉对象的生命周期,以便GC能够正常回收

实际上在防止内存泄露的过程中这两种方式都使用地比较平凡,不过我们大多数时候使用的还是弱引用。

其实Java有四种引用,强引用,软引用,弱引用,虚引用,这些并没什么好说的,我们平时使用最多的还是弱引用,也就是WeakReference。

弱引用VS软引用

只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

下面简单描述一下这两种防止内存泄露的方法的区别

引用置空

RecyclerView的内部类LayoutManager持有了RecyclerView的使用,没有采用弱引用,但是提供了置空的方法

 public static abstract class LayoutManager {
        ChildHelper mChildHelper;
        RecyclerView mRecyclerView;
        @Nullable
        SmoothScroller mSmoothScroller;
        private boolean mRequestedSimpleAnimations = false;
        boolean mIsAttachedToWindow = false;
        private boolean mAutoMeasure = false;
        private boolean mMeasurementCacheEnabled = true;
        private int mWidthMode, mHeightMode;
        private int mWidth, mHeight;

    void setRecyclerView(RecyclerView recyclerView) {
            if (recyclerView == null) {
              //回收
                mRecyclerView = null;
                mChildHelper = null;
                mWidth = 0;
                mHeight = 0;
            } else {
              //初始化
                mRecyclerView = recyclerView;
                mChildHelper = recyclerView.mChildHelper;
                mWidth = recyclerView.getWidth();
                mHeight = recyclerView.getHeight();
            }
            mWidthMode = MeasureSpec.EXACTLY;
            mHeightMode = MeasureSpec.EXACTLY;
        }

采用弱引用

用Picasso中的Action为例,父类采用了WeakReference

Action

Action父类

abstract class Action<T> {
  final WeakReference<T> target;
  Action(Picasso picasso, T target, Request request, int memoryPolicy, int networkPolicy,
      int errorResId, Drawable errorDrawable, String key, Object tag, boolean noFade) {
    this.picasso = picasso;
    this.request = request;
    this.target =target ;
    this.memoryPolicy = memoryPolicy;
    this.networkPolicy = networkPolicy;
    this.noFade = noFade;
    this.errorResId = errorResId;
    this.errorDrawable = errorDrawable;
    this.key = key;
    this.tag = (tag != null ? tag : this);
  }

ImageAction子类

class ImageViewAction extends Action<ImageView> {
  Callback callback;
  ImageViewAction(Picasso picasso, ImageView imageView, Request data, int memoryPolicy,
      int networkPolicy, int errorResId, Drawable errorDrawable, String key, Object tag,
      Callback callback, boolean noFade) {
    super(picasso, imageView, data, memoryPolicy, networkPolicy, errorResId, errorDrawable, key,tag, noFade);
    this.callback = callback;
  }

  @Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
    if (result == null) {
      throw new AssertionError(
          String.format("Attempted to complete action with no result!\n%s", this));
    }

    ImageView target = this.target.get();
    if (target == null) {
      return;
    }
    Context context = picasso.context;
    boolean indicatorsEnabled = picasso.indicatorsEnabled;
    PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
  }

由于ImageView持有Context的引用,所以导致Activity回收之后,如果ImageView是强引用,那么GC就不会去回收,而采用了弱引用之后,一旦Activity被回收,那么ImageViewAction的引用不会干扰到Activity的回收。

缓存时间

根据业务需要可以自行设定,但是注意,缓存的其实判断时间都应该以服务器时间为准,可以从服务器的返回数据的Response的header中的时间戳作为判断依据。

读取顺序

内存缓存读取速度远远高于磁盘缓存,我们都知道Picasso是采用了内存缓存跟磁盘缓存这两种缓存的,但是他获取的时候首先是从内存中进行读取,然后把磁盘缓存加到网络缓存中去,其实一开始,我不是这样子做的,我是把内存缓存,磁盘缓存以及网络缓存读取都实例化了一个Runnable,然后在加载下一页的时候,总是会出现图片闪烁,但是我用Picasso,UIL跟Glide就不会闪烁,但是当我设置Picasso他们的内存缓存策略为MemoryPolicy.NO_CACHE的时候,他们也会闪烁,下面展示一下闪烁的效果

flicker

其实上面两种情况都会出现闪烁,共同原因就是因为内存缓存的问题,Picasso的issue里面有人提过,作者JakeWharton是这么回答的

flick

是的200ms,如果Bitmap没有读取成功,那么就会出现闪烁,这样正好解释了上面的两种情况,由于我们设置了占位图,第一种闪烁是因为我们把内存缓存的读取放到了一个线程里面,线程的创建,切换这些都是需要时间的,那么就导致了总时间会超过200ms;同理,第二种情况如果没有设置内存缓存,那么只能从网络或磁盘中读取这个时间肯定会超过200ms,同样会闪烁,所以这也是为什么图片加载框架优先从内存中读取,当不设置内存缓存的时候也会闪烁的原因。

同时磁盘缓存需要借助于Http缓存机制来保证缓存的时效性,后面会具体分析。

总结

其实缓存的改变比较好理解,就是在使用内存缓存的时候需要注意防止内存泄露,使用磁盘缓存的时候需要注意结合Http的缓存机制来来确保缓存的时效性