深入理解Android中的緩存機制(一)緩存簡介

概述

說起緩存,大家可能很容易想到Http的緩存機制,LruCache,其實緩存最初是針對於網絡而言的,也是狹義上的緩存,廣義的緩存是指對數據的複用,我這裏提到的也是廣義的緩存,比較常見的是內存緩存以及磁盤緩存,不過要想進一步理解緩存體系,其實還需要複習一點計算機知識。

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父類

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的時候,他們也會閃爍,下面展示一下閃爍的效果

其實上面兩種情況都會出現閃爍,共同原因就是因爲內存緩存的問題,Picasso的issue裏面有人提過,作者JakeWharton是這麼回答的

是的200ms,如果Bitmap沒有讀取成功,那麼就會出現閃爍,這樣正好解釋了上面的兩種情況,由於我們設置了佔位圖,第一種閃爍是因爲我們把內存緩存的讀取放到了一個線程裏面,線程的創建,切換這些都是需要時間的,那麼就導致了總時間會超過200ms;同理,第二種情況如果沒有設置內存緩存,那麼只能從網絡或磁盤中讀取這個時間肯定會超過200ms,同樣會閃爍,所以這也是爲什麼圖片加載框架優先從內存中讀取,當不設置內存緩存的時候也會閃爍的原因。

同時磁盤緩存需要藉助於Http緩存機制來保證緩存的時效性,後面會具體分析。

總結

其實緩存的改變比較好理解,就是在使用內存緩存的時候需要注意防止內存泄露,使用磁盤緩存的時候需要注意結合Http的緩存機制來來確保緩存的時效性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章