Android 主流開源框架(七)Glide 的緩存機(jī)制

前言

最近有個(gè)想法——就是把 Android 主流開源框架進(jìn)行深入分析,然后寫成一系列文章更振,包括該框架的詳細(xì)使用與源碼解析宙帝。目的是通過鑒賞大神的源碼來(lái)了解框架底層的原理,也就是做到不僅要知其然捌锭,還要知其所以然俘陷。

這里我說下自己閱讀源碼的經(jīng)驗(yàn),我一般都是按照平時(shí)使用某個(gè)框架或者某個(gè)系統(tǒng)源碼的使用流程入手的观谦,首先要知道怎么使用拉盾,然后再去深究每一步底層做了什么,用了哪些好的設(shè)計(jì)模式豁状,為什么要這么設(shè)計(jì)捉偏。

系列文章:

更多干貨請(qǐng)關(guān)注 AndroidNotes

上一篇主要講了 Glide 的執(zhí)行流程,當(dāng)時(shí)是禁用了內(nèi)存與磁盤緩存的泻红,所以涉及到緩存相關(guān)的流程都省略了夭禽,還沒看上篇的建議先去看一遍,因?yàn)檫@篇講的緩存機(jī)制很多都是要與上篇聯(lián)系起來(lái)的谊路。

一讹躯、Glide 中的緩存

默認(rèn)情況下,Glide 在加載圖片之前會(huì)依次檢查是否有以下緩存:

  1. 活動(dòng)資源 (Active Resources):正在使用的圖片
  2. 內(nèi)存緩存 (Memory cache):內(nèi)存緩存中的圖片
  3. 資源類型(Resource):磁盤緩存中轉(zhuǎn)換過后的圖片
  4. 數(shù)據(jù)來(lái)源 (Data):磁盤緩存中的原始圖片

也就是說 Glide 中實(shí)際有四級(jí)緩存缠劝,前兩個(gè)屬于內(nèi)存緩存潮梯,后兩個(gè)屬于磁盤緩存。以上每步是按順序檢查的惨恭,檢查到哪一步有緩存就直接返回圖片秉馏,否則繼續(xù)檢查下一步。如果都沒有緩存脱羡,則 Glide 會(huì)從原始資源(File萝究、Uri、遠(yuǎn)程圖片 url 等)中加載圖片轻黑。

二糊肤、緩存 Key

緩存功能必然要有一個(gè)唯一的緩存 Key 用來(lái)存儲(chǔ)和查找對(duì)應(yīng)的緩存數(shù)據(jù)。那么下面我們就看下 Glide 的緩存 Key 是怎么生成的氓鄙。
其實(shí)上一篇文章中已經(jīng)瞄過一眼了馆揉,是在 Engine 類的 load() 方法中生成的:

  /*Engine*/
  public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {

    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

     ...

  }

繼續(xù)跟進(jìn):

  /*EngineKeyFactory*/
  EngineKey buildKey(
      Object model,
      Key signature,
      int width,
      int height,
      Map<Class<?>, Transformation<?>> transformations,
      Class<?> resourceClass,
      Class<?> transcodeClass,
      Options options) {
    return new EngineKey(
        model, signature, width, height, transformations, resourceClass, transcodeClass, options);
  }
class EngineKey implements Key {

  ...


  @Override
  public boolean equals(Object o) {
    if (o instanceof EngineKey) {
      EngineKey other = (EngineKey) o;
      return model.equals(other.model)
          && signature.equals(other.signature)
          && height == other.height
          && width == other.width
          && transformations.equals(other.transformations)
          && resourceClass.equals(other.resourceClass)
          && transcodeClass.equals(other.transcodeClass)
          && options.equals(other.options);
    }
    return false;
  }

  @Override
  public int hashCode() {
    if (hashCode == 0) {
      hashCode = model.hashCode();
      hashCode = 31 * hashCode + signature.hashCode();
      hashCode = 31 * hashCode + width;
      hashCode = 31 * hashCode + height;
      hashCode = 31 * hashCode + transformations.hashCode();
      hashCode = 31 * hashCode + resourceClass.hashCode();
      hashCode = 31 * hashCode + transcodeClass.hashCode();
      hashCode = 31 * hashCode + options.hashCode();
    }
    return hashCode;
  }

  ...

}

可以看到,這里傳入了 model(File抖拦、Uri升酣、遠(yuǎn)程圖片 url 等)舷暮、簽名、寬高(這里的寬高是指顯示圖片的 View 的寬高噩茄,不是圖片的寬高)等參數(shù)下面,然后通過 EngineKeyFactory 構(gòu)建了一個(gè) EngineKey 對(duì)象(即緩存 Key),然后 EngineKey 通過重寫 equals() 與 hashCode() 方法來(lái)保證緩存 Key 的唯一性绩聘。

雖然決定緩存 Key 的參數(shù)很多沥割,但是加載圖片的代碼寫好后這些參數(shù)都是不會(huì)變的。很多人遇到的 “服務(wù)器返回的圖片變了凿菩,但是前端顯示的還是以前的圖片” 的問題就是這個(gè)原因机杜,因?yàn)殡m然服務(wù)器返回的圖片變了,但是圖片 url 還是以前那個(gè)衅谷,其他決定緩存 Key 的參數(shù)也不會(huì)變铸本,Glide 就認(rèn)為有該緩存腹泌,就會(huì)直接從緩存中獲取,而不是重新下載,所以顯示的還是以前的圖片掰烟。

對(duì)于這個(gè)問題臀规,有幾種方法可以解決姥卢,分別如下:
(1)圖片 url 不要固定
也就是說如果某個(gè)圖片改變了摔蓝,那么該圖片的 url 也要跟著改變。

(2)使用 signature() 更改緩存 Key
我們剛剛知道了決定緩存 Key 的參數(shù)包括 signature预茄,剛好 Glide 提供了 signature() 方法來(lái)更改該參數(shù)兴溜。具體如下:

Glide.with(this).load(url).signature(new ObjectKey(timeModified)).into(imageView);

其中 timeModified 可以是任意數(shù)據(jù),這里用圖片的更改時(shí)間耻陕。例如圖片改變了拙徽,那么服務(wù)器應(yīng)該改變?cè)撟侄蔚闹担缓箅S圖片 url 一起返回給前端诗宣,這樣前端加載的時(shí)候就知道圖片改變了膘怕,需要重新下載。

(3)禁用緩存
前端加載圖片的時(shí)候設(shè)置禁用內(nèi)存與磁盤緩存召庞,這樣每次加載都會(huì)重新下載最新的岛心。

Glide.with(this)
        .load(url)
        .skipMemoryCache(true) // 禁用內(nèi)存緩存
        .diskCacheStrategy(DiskCacheStrategy.NONE) // 禁用磁盤緩存
        .into(imageView);

以上 3 種方法都可以解決問題,但是推薦使用第一種篮灼,這樣設(shè)計(jì)是比較規(guī)范的忘古,后臺(tái)人員就應(yīng)該這么設(shè)計(jì)。第二種方法也可以诅诱,但是這樣無(wú)疑是給后端髓堪、前端人員都增加了麻煩。第三種是最不推薦的,相當(dāng)于舍棄了緩存功能干旁,每次都要從服務(wù)器重新下載圖片驶沼,不僅浪費(fèi)用戶流量,而且每次加載需要等待也影響用戶體驗(yàn)争群。

三回怜、緩存策略

在講 Glide 中的內(nèi)存緩存與磁盤緩存之前,我們先了解下緩存策略换薄。例如加載一張圖片顯示到設(shè)備上的緩存策略應(yīng)該這樣設(shè)計(jì):

當(dāng)程序第一次從網(wǎng)絡(luò)上加載圖片后玉雾,就將它緩存到設(shè)備磁盤中,下次使用這張圖片的時(shí)候就不用再?gòu)木W(wǎng)絡(luò)上加載了专控。為了提升用戶體驗(yàn)抹凳,往往還會(huì)在內(nèi)存中緩存一份,因?yàn)閺膬?nèi)存中加載圖片比從磁盤中加載要快伦腐。程序下次加載這張圖片的時(shí)候首先從內(nèi)存中查找,如果沒有就去磁盤中查找失都,都沒有才從網(wǎng)絡(luò)上加載柏蘑。

這里的緩存策略涉及到緩存的添加、獲取和刪除操作粹庞,什么時(shí)候進(jìn)行這些操作等邏輯就構(gòu)成了一種緩存算法咳焚。目前常用的一種緩存算法是 LRU(Least Recently Used),即最近最少使用算法庞溜。它的核心思想是當(dāng)緩存滿時(shí)革半,會(huì)優(yōu)先淘汰那些最近最少使用的緩存對(duì)象。采用 LRU 算法的緩存有兩種:LruCache 和 DiskLruCache流码,LruCache 用于實(shí)現(xiàn)內(nèi)存緩存又官,DiskLruCache 則用于實(shí)現(xiàn)磁盤緩存,兩者結(jié)合使用就可以實(shí)現(xiàn)上面的緩存策略漫试。

LruCache 和 DiskLruCache 的內(nèi)部算法原理是采用一個(gè) LinkedHashMap 以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象六敬,其提供了 get() 和 put() 方法來(lái)完成緩存的獲取和添加的操作。當(dāng)緩存滿時(shí)驾荣,會(huì)移除較早使用的緩存對(duì)象外构,然后再添加新的緩存對(duì)象〔ブ溃可以用如下流程圖表示:

下面要講的 Glide 中的內(nèi)存緩存與磁盤緩存也是用的 LruCache 和 DiskLruCache审编,只不過 LruCache 用的不是 SDK 中的,而是自己寫的歧匈,但是看了原理其實(shí)是一樣的垒酬。而 DiskLruCache 用的是 JakeWharton 封裝的 DiskLruCache

四、內(nèi)存緩存

Glide 默認(rèn)是配置了內(nèi)存緩存的伤溉,當(dāng)然 Glide 也提供了 API 給我們開啟和禁用般码,如下:

// 開啟內(nèi)存緩存
Glide.with(this).load(url).skipMemoryCache(false).into(imageView);
// 禁用內(nèi)存緩存
Glide.with(this).load(url).skipMemoryCache(true).into(imageView);

文章開頭說了,Glide 在加載圖片之前會(huì)依次檢查四級(jí)緩存÷夜耍現(xiàn)在緩存 Key 也拿到了板祝,那么我們先看看前兩級(jí)中的內(nèi)存緩存是怎么獲取的(下面分析的時(shí)候需要用默認(rèn)加載語(yǔ)句或者手動(dòng)開啟內(nèi)存緩存)。從上一篇文章知道走净,獲取內(nèi)存緩存的代碼也是在 Engine 類的 load() 方法中券时,我們進(jìn)去看看:

  /*Engine*/
  public <R> LoadStatus load(...) {

    // 構(gòu)建緩存 Key
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      // 從內(nèi)存中加載緩存數(shù)據(jù)
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      ...

    }

    // 加載完成回調(diào)
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }

繼續(xù)點(diǎn)擊 loadFromMemory() 方法進(jìn)去看看:

  /*Engine*/
  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {
      return null;
    }

    //(1)
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }

    //(2)
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }

這里我標(biāo)記了兩個(gè)關(guān)注點(diǎn),分別如下:

  • (1):表示從 ActiveResources 中加載緩存數(shù)據(jù)伏伯。
  • (2):表示從內(nèi)存緩存中加載緩存數(shù)據(jù)橘洞。

是的,這就是 Glide 四級(jí)緩存中的前兩級(jí)说搅。ActiveResources 里面主要包含了一個(gè) HashMap 的相關(guān)操作炸枣,然后 HashMap 中保存的值又是弱引用來(lái)引用的,也就是說這里是采用一個(gè)弱引用的 HashMap 來(lái)緩存活動(dòng)資源弄唧。下面我們分析下這兩個(gè)關(guān)注點(diǎn):

  • Engine#loadFromMemory() 中的關(guān)注點(diǎn)(1)
    我們點(diǎn)擊關(guān)注點(diǎn)(1)看看:
  /*Engine*/
  private EngineResource<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }

繼續(xù)看 get() 方法:

  /*ActiveResources*/
  synchronized EngineResource<?> get(Key key) {
    // 從 HashMap 中獲取 ResourceWeakReference
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    // 從弱引用中獲取活動(dòng)資源
    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }

可以看到适肠,這里首先從 HashMap 中獲取 ResourceWeakReference(繼承了弱引用),然后從弱引用中獲取了活動(dòng)資源(獲取活動(dòng)資源)候引,即正在使用的圖片侯养。也就是說正在使用的圖片實(shí)際是通過弱引用維護(hù),然后保存在 HashMap 中的澄干。

繼續(xù)看 acquire() 方法:

  /*EngineResource*/
  synchronized void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    ++acquired;
  }

發(fā)現(xiàn)這里是將 acquired 變量 +1逛揩,這個(gè)變量用來(lái)記錄圖片被引用的次數(shù)。該變量除了 acquire() 方法中做了 +1 操作麸俘,還在 release() 方法中做了 -1 的操作辩稽,如下:

  /*EngineResource*/
  void release() {
    boolean release = false;
    synchronized (this) {
      if (acquired <= 0) {
        throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
      }
      if (--acquired == 0) {
        release = true;
      }
    }
    if (release) {
      listener.onResourceReleased(key, this);
    }
  }

  /*Engine*/
  @Override
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
    }
  }

  /*ActiveResources*/
  synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
  }

可以看到,當(dāng) acquired 減到 0 的時(shí)候疾掰,又回調(diào)了 Engine#onResourceReleased()搂誉。在 onResourceReleased() 方法中首先將活動(dòng)資源從弱引用的 HashMap 中移除(清理活動(dòng)資源),然后將它緩存到內(nèi)存緩存中(存儲(chǔ)內(nèi)存緩存)静檬。

也就是說炭懊,release() 方法主要是釋放資源。當(dāng)我們從一屏滑動(dòng)到下一屏的時(shí)候拂檩,上一屏的圖片就會(huì)看不到侮腹,這個(gè)時(shí)候就會(huì)調(diào)用該方法。還有我們關(guān)閉當(dāng)前顯示圖片的頁(yè)面時(shí)會(huì)調(diào)用 onDestroy() 方法稻励,最終也會(huì)調(diào)用該方法父阻。這兩種情況很明顯是不需要用到該圖片了愈涩,那么理所當(dāng)然的會(huì)調(diào)用 release() 方法來(lái)釋放弱引用的 HashMap 中緩存的活動(dòng)資源。

這樣也就實(shí)現(xiàn)了正在使用中的圖片使用弱引用來(lái)進(jìn)行緩存加矛,不在使用中的圖片使用 LruCache 來(lái)進(jìn)行緩存的功能履婉。

  • Engine#loadFromMemory() 中的關(guān)注點(diǎn)(2)
    我們點(diǎn)擊關(guān)注點(diǎn)(2)看看:
  /*Engine*/
  private EngineResource<?> loadFromCache(Key key) {
    //(2.1)
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      //(2.2)
      cached.acquire();
      //(2.3)
      activeResources.activate(key, cached);
    }
    return cached;
  }

這里我標(biāo)注了 3 個(gè)關(guān)注點(diǎn),分別如下:

  • (2.1):這里是獲取內(nèi)存緩存斟览。點(diǎn)進(jìn)去看看:
  /*Engine*/
  private EngineResource<?> getEngineResourceFromCache(Key key) {
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }

可以看到毁腿,這里的 cache 就是 LruResourceCache,remove() 操作就是移除緩存的同時(shí)獲取該緩存(獲取內(nèi)存緩存)苛茂。LruResourceCache 繼承了 LruCache已烤,雖然不是 SDK 中的 LruCache,但是看了原理其實(shí)是一樣的妓羊,也就是說內(nèi)存緩存使用的是 LRU 算法實(shí)現(xiàn)的胯究。

  • (2.2):與關(guān)注點(diǎn)(1)中的獲取活動(dòng)資源一樣,也是將 acquired 變量 +1躁绸,然后用來(lái)記錄圖片被引用的次數(shù)裕循。
  • (2.3):將內(nèi)存中獲取的緩存數(shù)據(jù)緩存到弱引用的 HashMap 中。

再回去看我標(biāo)記了高亮的文字涨颜,發(fā)現(xiàn)這 2 個(gè)關(guān)注點(diǎn)主要做了獲取活動(dòng)資源费韭、清理活動(dòng)資源、獲取內(nèi)存緩存庭瑰、存儲(chǔ)內(nèi)存緩存。其中清理內(nèi)存緩存的操作 LRU 算法已經(jīng)自動(dòng)幫我們實(shí)現(xiàn)了抢埋,那是不是發(fā)現(xiàn)少了存儲(chǔ)活動(dòng)資源的步驟弹灭?

活動(dòng)資源是哪里來(lái)的呢?其實(shí)就是我們從網(wǎng)絡(luò)請(qǐng)求中返回的數(shù)據(jù)揪垄。從上一篇文章(可以回去搜索 onEngineJobComplete() 回憶下上下文)可以知道網(wǎng)絡(luò)請(qǐng)求回來(lái)后先進(jìn)行解碼穷吮,然后在 Engine#onEngineJobComplete() 方法中進(jìn)行了活動(dòng)資源的存儲(chǔ),我再貼下代碼:

  /*Engine*/
  @Override
  public synchronized void onEngineJobComplete(
      EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }

    jobs.removeIfCurrent(key, engineJob);
  }

  /*ActiveResources*/
  synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);

    // 存儲(chǔ)活動(dòng)資源
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

以上就是 Glide 內(nèi)存緩存的原理饥努,但是我們發(fā)現(xiàn)除了利用 LruCache 實(shí)現(xiàn)的內(nèi)存緩存捡鱼,還有一個(gè)是利用弱引用的 HashMap 實(shí)現(xiàn)的。一般如果讓我們?cè)O(shè)計(jì)酷愧,可能就只會(huì)想到用 LruCache 實(shí)現(xiàn)內(nèi)存緩存驾诈。那這里設(shè)計(jì)多一個(gè)弱引用的 HashMap 的好處是什么呢?

郭霖的深入探究Glide的緩存機(jī)制中是這樣描述的使用activeResources來(lái)緩存正在使用中的圖片溶浴,可以保護(hù)這些圖片不會(huì)被LruCache算法回收掉乍迄。我覺得這樣解釋不太合理,我看完源碼并沒有覺得這個(gè)弱引用的 HashMap 起到了 “保護(hù)圖片不被 LRU 算法回收” 的作用士败。我覺得有如下作用(如有不對(duì)請(qǐng)指出):
(1)提高訪問效率
因?yàn)?ActiveResources 用的是 HashMap闯两,而 LruCache 用的是 LinkedHashMap,并且在實(shí)例化 LinkedHashMap 時(shí)是設(shè)置了訪問順序的(如下設(shè)置),所以 HashMap 的訪問速度是要比 LinkedHashMap 快的漾狼。

// accessOrder 設(shè)置為 true重慢,表示是訪問順序模式
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);

(2)防止內(nèi)存泄漏
ActiveResources 中的 HashMap 是弱引用維護(hù)的,而 LruCache 中的 LinkedHashMap 用的是強(qiáng)引用逊躁。因?yàn)槿跻脤?duì)象會(huì)隨時(shí)被 gc 回收似踱,所以可以防止內(nèi)存泄漏。這里列舉下各種引用的區(qū)別:

  • 強(qiáng)引用:直接的對(duì)象引用志衣。
  • 軟引用:當(dāng)一個(gè)對(duì)象只有軟引用存在時(shí)屯援,系統(tǒng)內(nèi)存不足時(shí)此對(duì)象會(huì)被 gc 回收。
  • 弱引用:當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí), 此對(duì)象會(huì)隨時(shí)被 gc 回收念脯。

五狞洋、磁盤緩存

5.1 磁盤緩存策略

前面說了禁用緩存只需要如下設(shè)置即可:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);

上面的 DiskCacheStrategy 封裝的是磁盤緩存策略,一共有如下幾種策略:

  1. ALL:既緩存原始圖片绿店,也緩存轉(zhuǎn)換過后的圖片吉懊。
  2. NONE:不緩存任何內(nèi)容。
  3. DATA:只緩存原始圖片假勿。
  4. RESOURCE:只緩存轉(zhuǎn)換過后的圖片借嗽。
  5. AUTOMATIC:默認(rèn)策略,它會(huì)嘗試對(duì)本地和遠(yuǎn)程圖片使用最佳的策略转培。如果是遠(yuǎn)程圖片恶导,則只緩存原始圖片;如果是本地圖片浸须,那么只緩存轉(zhuǎn)換過后的圖片惨寿。

其實(shí) 5 種策略總結(jié)起來(lái)對(duì)應(yīng)的就是文章開頭說的后兩級(jí)緩存,即資源類型(Resource)與 數(shù)據(jù)來(lái)源(Data)删窒,下面通過源碼來(lái)分析下它們是在哪里獲取裂垦、存儲(chǔ)和清理緩存的。

5.2 資源類型(Resource)

該級(jí)緩存只緩存轉(zhuǎn)換過后的圖片肌索,那么我們需要先配置如下策略:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(imageView);

通過上一篇文章可知蕉拢,當(dāng)我們從主線程切換到子線程去執(zhí)行請(qǐng)求的時(shí)候用到了磁盤緩存策略,那么我們這里直接從 DecodeJob 任務(wù)的 run() 方法開始分析:

  /*DecodeJob*/
  @Override
  public void run() {

    ...

    try {
      // 執(zhí)行
      runWrapped();
    } catch (CallbackException e) {
      throw e;
    }

    ...

  }

繼續(xù) runWrapped() 方法:

  /*DecodeJob*/
  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        // 1. 獲取資源狀態(tài)
        stage = getNextStage(Stage.INITIALIZE);
        // 2. 根據(jù)資源狀態(tài)獲取資源執(zhí)行器
        currentGenerator = getNextGenerator();
        // 3. 執(zhí)行
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

  /*DecodeJob*/
  private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE
            : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE
            : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

  /*DecodeJob*/
  private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }

這里會(huì)根據(jù)緩存策略獲取到資源狀態(tài)诚亚,然后再根據(jù)資源狀態(tài)獲取資源執(zhí)行器晕换,最后調(diào)用 runGenerators() 方法:

  /*DecodeJob*/
  private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled
        && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
  }

可以看到,該方法中會(huì)調(diào)用當(dāng)前執(zhí)行器的 startNext() 方法亡电,因?yàn)槲覀兣渲玫木彺娌呗允?RESOURCE届巩,所以這里直接看 ResourceCacheGenerator 的 startNext() 方法:

  /*ResourceCacheGenerator*/
  @Override
  public boolean startNext() {

    ...

    while (modelLoaders == null || !hasNextModelLoader()) {

      ...
        
      //(1)
      currentKey =
          new ResourceCacheKey(
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      //(2)
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(
              cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        //(3)
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }

可以看到,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先構(gòu)建緩存 Key份乒,然后根據(jù)緩存 Key 去獲取緩存文件(獲取轉(zhuǎn)換后的圖片)恕汇,最后將緩存文件加載成需要的數(shù)據(jù)腕唧。其中 helper.getDiskCache() 為 DiskLruCacheWrapper,內(nèi)部是通過 DiskLruCache 操作的瘾英,也就是說這一級(jí)的磁盤緩存使用的是 LRU 算法實(shí)現(xiàn)的枣接。

因?yàn)楂@取的是緩存文件,所以這里的 loadData.fetcher 實(shí)際為 ByteBufferFileLoader缺谴,繼續(xù)看 ByteBufferFileLoader#(loadData):

    /*ByteBufferFileLoader*/
    @Override
    public void loadData(
        @NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
      ByteBuffer result;
      try {
        result = ByteBufferUtil.fromFile(file);
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
        }
        callback.onLoadFailed(e);
        return;
      }

      callback.onDataReady(result);
    }

這里主要是將緩存文件轉(zhuǎn)換成 ByteBuffer但惶,然后通過 onDataReady() 方法回調(diào)出去,最終回調(diào)到 DecodeJob 的 onDataFetcherReady() 方法中湿蛔,后面的流程就跟上一篇文章差不多了膀曾。

上面是獲取緩存的流程,那么是哪里存儲(chǔ)緩存的呢阳啥?我們可以用反推的方法添谊,剛剛獲取緩存 Key 的時(shí)候用的是 ResourceCacheKey,那么存儲(chǔ)緩存與獲取緩存肯定都是用的 ResourceCacheKey察迟,經(jīng)過查找發(fā)現(xiàn)除了 ResourceCacheGenerator斩狱,只有在 DecodeJob 的 onResourceDecoded() 方法中使用到:

  /*DecodeJob*/
  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    
    ...

    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      if (encoder == null) {
        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
      }
      final Key key;
      //(1)
      switch (encodeStrategy) {
        case SOURCE:
          key = new DataCacheKey(currentSourceKey, signature);
          break;
        case TRANSFORMED:
          key =
              new ResourceCacheKey(
                  decodeHelper.getArrayPool(),
                  currentSourceKey,
                  signature,
                  width,
                  height,
                  appliedTransformation,
                  resourceSubClass,
                  options);
          break;
        default:
          throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
      }

      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
      //(2)
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }

內(nèi)部又調(diào)用了 init() 方法:

  private static class DeferredEncodeManager<Z> {
    private Key key;
    private ResourceEncoder<Z> encoder;
    private LockedResource<Z> toEncode;

    <X> void init(Key key, ResourceEncoder<X> encoder, LockedResource<X> toEncode) {
      this.key = key;
      this.encoder = (ResourceEncoder<Z>) encoder;
      this.toEncode = (LockedResource<Z>) toEncode;
    }

    void encode(DiskCacheProvider diskCacheProvider, Options options) {
      GlideTrace.beginSection("DecodeJob.encode");
      try {
        //(3)
        diskCacheProvider
            .getDiskCache()
            .put(key, new DataCacheWriter<>(encoder, toEncode, options));
      } finally {
        toEncode.unlock();
        GlideTrace.endSection();
      }
    }
}

可以看到,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先根據(jù)緩存策略構(gòu)建不同的緩存 Key扎瓶,然后調(diào)用 DeferredEncodeManager 的 init() 方法給變量 key 賦值所踊,然后 key 又在 encode() 方法中使用了,該方法中就做了存儲(chǔ)緩存的操作(存儲(chǔ)轉(zhuǎn)換后的圖片)概荷。

那么我們現(xiàn)在看看 encode() 方法在哪里被調(diào)用了唄秕岛,點(diǎn)擊發(fā)現(xiàn)只在 DecodeJob 的 notifyEncodeAndRelease() 方法中被調(diào)用了:

  /*DecodeJob */
  private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    if (resource instanceof Initializable) {
      ((Initializable) resource).initialize();
    }

    Resource<R> result = resource;
    LockedResource<R> lockedResource = null;
    if (deferredEncodeManager.hasResourceToEncode()) {
      lockedResource = LockedResource.obtain(resource);
      result = lockedResource;
    }

    notifyComplete(result, dataSource);

    stage = Stage.ENCODE;
    try {
      if (deferredEncodeManager.hasResourceToEncode()) {
        // 將資源緩存到磁盤
        deferredEncodeManager.encode(diskCacheProvider, options);
      }
    } finally {
      if (lockedResource != null) {
        lockedResource.unlock();
      }
    }
    // Call onEncodeComplete outside the finally block so that it's not called if the encode process
    // throws.
    onEncodeComplete();
  }

notifyEncodeAndRelease() 方法是我們上一篇文章中講的解碼完成了通知下去的步驟,也就是說第一次加載的時(shí)候在 SourceGenerator#startNext() 中請(qǐng)求到數(shù)據(jù)误证,然后解碼完成瓣蛀,最后再存儲(chǔ)緩存。

上面已經(jīng)實(shí)現(xiàn)了轉(zhuǎn)換后的圖片的獲取雷厂、存儲(chǔ),剩下的清理操作 LRU 算法已經(jīng)自動(dòng)幫我們實(shí)現(xiàn)了叠殷。接下來(lái)繼續(xù)看下原始圖片是怎么獲取改鲫、存儲(chǔ)與清理的。

5.3 數(shù)據(jù)來(lái)源(Data)

該級(jí)緩存只緩存原始圖片林束,那么我們需要先配置如下策略:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.DATA).into(imageView);

與資源類型一樣像棘,只不過這里緩存策略換成了 DATA,所以前面就不講了壶冒,我們直接看 DataCacheGenerator 的 startNext() 方法:

  /*DataCacheGenerator*/
  @Override
  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {

      ...

      //(1)
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      //(2)
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(
              cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        //(3)
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

可以看到缕题,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先構(gòu)建緩存 Key,然后根據(jù)緩存 Key 去獲取緩存文件(獲取原始圖片)胖腾,最后將緩存文件加載成需要的數(shù)據(jù)烟零。與資源類型一樣瘪松,這里的 helper.getDiskCache() 也為 DiskLruCacheWrapper,所以這一級(jí)的磁盤緩存使用的也是 LRU 算法實(shí)現(xiàn)的锨阿。

這里獲取的同樣是緩存文件宵睦,所以這里的 loadData.fetcher 也為 ByteBufferFileLoader,最終也是回調(diào)到 DecodeJob 的 onDataFetcherReady() 方法中墅诡。

那么是哪里存儲(chǔ)緩存的呢壳嚎?同樣用反推的方法,但是發(fā)現(xiàn)除了 DataCacheGenerator 還有兩個(gè)地方用到了末早。
第一個(gè)與資源類型一樣是在 DecodeJob#onResourceDecoded():

  /*DecodeJob*/
  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    
    ...

    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
    //(1)
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      if (encoder == null) {
        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
      }
      final Key key;
      switch (encodeStrategy) {
        case SOURCE:
          key = new DataCacheKey(currentSourceKey, signature);
          break;
        case TRANSFORMED:
          key =
              new ResourceCacheKey(
                  decodeHelper.getArrayPool(),
                  currentSourceKey,
                  signature,
                  width,
                  height,
                  appliedTransformation,
                  resourceSubClass,
                  options);
          break;
        default:
          throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
      }

      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }

這里的關(guān)注點(diǎn)(1)做了一個(gè)緩存策略的判斷烟馅,因?yàn)榍懊媾渲玫木彺娌呗允?DATA,所以這里調(diào)用的是 DATA 中的 isResourceCacheable() 方法:

  /*DiskCacheStrategy*/
  public static final DiskCacheStrategy DATA =
      new DiskCacheStrategy() {
        @Override
        public boolean isDataCacheable(DataSource dataSource) {
          return dataSource != DataSource.DATA_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE;
        }

        // 調(diào)用的是該方法
        @Override
        public boolean isResourceCacheable(
            boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
          return false;
        }

        @Override
        public boolean decodeCachedResource() {
          return false;
        }

        @Override
        public boolean decodeCachedData() {
          return true;
        }
      };

可以看到然磷,isResourceCacheable() 方法始終返回 false郑趁,所以上面關(guān)注點(diǎn)(1)是進(jìn)不去的,可以排除样屠。

那我們繼續(xù)看下另一個(gè)地方用到的:

  /*SourceGenerator*/
  private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      //(1)
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      //(2)
      helper.getDiskCache().put(originalKey, writer);

      ...

    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }

這里首先構(gòu)建緩存 Key穿撮,然后存儲(chǔ)緩存(存儲(chǔ)原始圖片)。而該方法是在 SourceGenerator#startNext() 中調(diào)用的:

  /*SourceGenerator*/
  @Override
  public boolean startNext() {
    //(1)
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
              || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        startNextLoad(loadData);
      }
    }
    return started;
  }

可以看到痪欲,關(guān)注點(diǎn)(1)中首先判斷緩存不為空才進(jìn)行緩存數(shù)據(jù)的操作悦穿,那我們看下 dataToCache 是哪里被賦值了唄,查找發(fā)現(xiàn)只有在 SourceGenerator#onDataReadyInternal() 中賦值過:

  /*SourceGenerator*/
  void onDataReadyInternal(LoadData<?> loadData, Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      // 賦值
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      // 回調(diào)
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(
          loadData.sourceKey,
          data,
          loadData.fetcher,
          loadData.fetcher.getDataSource(),
          originalKey);
    }
  }

可以看到业踢,onDataReadyInternal() 方法又是我們熟悉的栗柒,也就是上一篇文章中加載完數(shù)據(jù)后調(diào)用的。上一篇文章是因?yàn)榻昧司彺嬷伲宰叩氖?else瞬沦。這里配置的緩存策略是 DATA,所以自然走的是 if雇锡。

那么賦值完成逛钻,下一步肯定要用到,我們繼續(xù)跟這里的回調(diào)方法锰提,發(fā)現(xiàn)調(diào)用的是 EngineJob 的 reschedule() 方法:

  /*EngineJob*/
  @Override
  public void reschedule(DecodeJob<?> job) {
    getActiveSourceExecutor().execute(job);
  }

這里又用線程池執(zhí)行了 DecodeJob曙痘,所以最后又回到了 SourceGenerator 的 startNext() 方法,這時(shí)候 dataToCache 就不是空了立肘,所以就將數(shù)據(jù)緩存起來(lái)了边坤。其實(shí) cacheData() 方法中存儲(chǔ)緩存的時(shí)候還構(gòu)建了一個(gè) DataCacheGenerator,然后存儲(chǔ)完成又執(zhí)行了 DataCacheGenerator#startNext()谅年,這里再?gòu)拇疟P獲取緩存后才將圖片顯示到控件上茧痒,也就是說網(wǎng)絡(luò)請(qǐng)求拿到數(shù)據(jù)后是先緩存數(shù)據(jù),然后再?gòu)拇疟P獲取緩存才顯示到控件上融蹂。

同理旺订,原始圖片的清理操作也是 LRU 算法自動(dòng)幫我們實(shí)現(xiàn)了弄企。

可以看到,這里資源類型(Resource)與數(shù)據(jù)來(lái)源(Data)中存儲(chǔ)緩存的步驟我都是利用緩存 Key 去反推得出數(shù)據(jù)是哪里緩存的耸峭。有時(shí)候適當(dāng)?shù)睦?strong>反推是挺方便的桩蓉,如果你不習(xí)慣利用反推,可以跟著程序走劳闹,也就是網(wǎng)絡(luò)請(qǐng)求到數(shù)據(jù)后再一步步跟著去看是哪里緩存的院究。

六、總結(jié)

通過分析 Glide 的緩存機(jī)制本涕,發(fā)現(xiàn)設(shè)計(jì)的確實(shí)精妙业汰。利用四級(jí)緩存大大提高了圖片的加載效率,磁盤緩存策略也提升了框架的靈活性菩颖,如果讓我們?cè)O(shè)計(jì)一個(gè)圖片加載框架样漆,完全可以將 Glide 中的這些優(yōu)點(diǎn)用上。

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晦闰,一起剝皮案震驚了整個(gè)濱河市放祟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呻右,老刑警劉巖跪妥,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異声滥,居然都是意外死亡眉撵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門落塑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)纽疟,“玉大人,你說我怎么就攤上這事憾赁∥坌啵” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵龙考,是天一觀的道長(zhǎng)膘壶。 經(jīng)常有香客問我,道長(zhǎng)洲愤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任顷锰,我火速辦了婚禮柬赐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘官紫。我一直安慰自己肛宋,他們只是感情好州藕,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酝陈,像睡著了一般床玻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沉帮,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天锈死,我揣著相機(jī)與錄音,去河邊找鬼穆壕。 笑死待牵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喇勋。 我是一名探鬼主播缨该,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼川背!你這毒婦竟也來(lái)了贰拿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤熄云,失蹤者是張志新(化名)和其女友劉穎膨更,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皱碘,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡询一,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了癌椿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片健蕊。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖踢俄,靈堂內(nèi)的尸體忽然破棺而出缩功,到底是詐尸還是另有隱情,我是刑警寧澤都办,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布嫡锌,位于F島的核電站,受9級(jí)特大地震影響琳钉,放射性物質(zhì)發(fā)生泄漏势木。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一歌懒、第九天 我趴在偏房一處隱蔽的房頂上張望啦桌。 院中可真熱鬧,春花似錦、人聲如沸甫男。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)板驳。三九已至又跛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間若治,已是汗流浹背慨蓝。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留直砂,地道東北人菌仁。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像静暂,于是被迫代替她去往敵國(guó)和親济丘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345