深入理解Android中的快取機制(一)快取簡介

wustor發表於2018-02-03

概述

說起快取,大家可能很容易想到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的快取機制來來確保快取的時效性

相關文章