Glide v4 緩存解析

先看下Glide官方文檔對圖片加載性能優(yōu)化的兩個(gè)方面:

  • 圖片解碼速度
  • 解碼圖片帶來的資源壓力

主要采用的步驟如下:

  1. 自動汽馋、智能地下采樣(downsampling)和緩存(caching)突倍,以最小化存儲開銷和解碼次數(shù)甲捏;
  2. 積極的資源重用鞋怀,例如字節(jié)數(shù)組和Bitmap静汤,以最小化昂貴的垃圾回收和堆碎片影響蒂誉;
  3. 深度的生命周期集成虱肄,以確保僅優(yōu)先處理活躍的Fragment和Activity的請求,并有利于應(yīng)用在必要時(shí)釋放資源以避免在后臺時(shí)被殺掉抖仅。

Glide緩存機(jī)制說明

官方文檔已經(jīng)有了詳細(xì)的使用說明
https://muyangmin.github.io/glide-docs-cn/doc/caching.html

多級緩存邏輯

多級緩存

  1. 活動資源 (Active Resources) - 現(xiàn)在是否有另一個(gè) View 正在展示這張圖片坊夫?
  2. 內(nèi)存緩存 (Memory cache) - 該圖片是否最近被加載過并仍存在于內(nèi)存中?
  3. 資源類型(Resource) - 該圖片是否之前曾被解碼撤卢、轉(zhuǎn)換并寫入過磁盤緩存环凿?
  4. 數(shù)據(jù)來源 (Data) - 構(gòu)建這個(gè)圖片的資源是否之前曾被寫入過文件緩存?

前兩步檢查圖片是否在內(nèi)存中放吩,如果是則直接返回圖片智听。后兩步則檢查圖片是否在磁盤上,以便快速但異步地返回圖片渡紫。如果四個(gè)步驟都未能找到圖片到推,則Glide會返回到原始資源以取回?cái)?shù)據(jù)(原始文件,Uri, Url等)惕澎。

緩存策略

內(nèi)存緩存策略

內(nèi)存中會緩存上述的活動資源 和 內(nèi)存緩存資源莉测;
活動資源采用采取了HashMap進(jìn)行弱引用進(jìn)行存儲;
內(nèi)存緩存資源采用LRU緩存進(jìn)行存儲集灌;

硬盤緩存策略類型(見DiskCacheStrategy):
  1. DiskCacheStrategy.DATA:磁盤寫入數(shù)據(jù)為未被加載過程修改過原始數(shù)據(jù)悔雹;
  2. DiskCacheStrategy.RESOURCE: 將解碼复哆,變換后的資源寫入磁盤;
  3. DiskCacheStrategy.ALL:遠(yuǎn)程的資源(URL資源)會寫入原始資源和變換后的資源腌零;本地文件資源只會寫入解碼變換后的資源梯找;
  4. DiskCacheStrategy.NONE: 不寫入任何數(shù)據(jù)到硬盤;
  5. DiskCacheStrategy.AUTOMATIC:它會嘗試對本地和遠(yuǎn)程圖片使用最佳的策略益涧。當(dāng)你加載遠(yuǎn)程數(shù)據(jù)(比如锈锤,從URL下載)時(shí),AUTOMATIC 策略僅會存儲未被你的加載過程修改過(比如闲询,變換)的原始數(shù)據(jù)久免,因?yàn)橄螺d遠(yuǎn)程數(shù)據(jù)相比調(diào)整磁盤上已經(jīng)存在的數(shù)據(jù)要昂貴得多。對于本地?cái)?shù)據(jù)扭弧,AUTOMATIC 策略則會僅存儲變換過的縮略圖阎姥,因?yàn)榧词鼓阈枰俅紊闪硪粋€(gè)尺寸或類型的圖片,取回原始數(shù)據(jù)也很容易鸽捻。

緩存鍵值

不同于以往的緩存鍵值僅以URL作為唯一標(biāo)識呼巴,Glide 針對不同緩存場景構(gòu)建了不同的key,活動資源和內(nèi)存緩存使用的鍵還和磁盤資源緩存略有不同御蒲,以適應(yīng)內(nèi)存 衣赶,選項(xiàng),比如影響 Bitmap 配置的選項(xiàng)或其他解碼時(shí)才會用到的參數(shù)厚满。
以內(nèi)存鍵值為例府瞄,加入請求的資源的圖片大小發(fā)生了變化,則無法命中緩存中的key碘箍,需要去磁盤中或者網(wǎng)絡(luò)中重新獲取后進(jìn)行變化遵馆。
我們看下相關(guān)Key的構(gòu)造方法;

EngineKey(
      Object model,
      Key signature,
      int width,
      int height,
      Map<Class<?>, Transformation<?>> transformations,
      Class<?> resourceClass,
      Class<?> transcodeClass,
      Options options)

  ResourceCacheKey(
      ArrayPool arrayPool,
      Key sourceKey,
      Key signature,
      int width,
      int height,
      Transformation<?> appliedTransformation,
      Class<?> decodedResourceClass,
      Options options)

DataCacheKey(Key sourceKey, Key signature)

在 Glide v4 里敲街,所有緩存鍵都包含至少兩個(gè)元素:

  1. 請求加載的 model(File, Url, Url)团搞。如果你使用自定義的 model, 它需要正確地實(shí)現(xiàn) hashCode()equals()
  2. 一個(gè)可選的 簽名(Signature)
    另外,步驟1-3(活動資源多艇,內(nèi)存緩存,資源磁盤緩存)的緩存鍵還包含一些其他數(shù)據(jù)像吻,包括:
  3. 寬度和高度
  4. 可選的變換(Transformation)
  5. 額外添加的任何選項(xiàng)(Options)
  6. 請求的數(shù)據(jù)類型 (Bitmap, GIF, 或其他)

修改默認(rèn)緩存配置

當(dāng)然Glide也提供了修改默認(rèn)的緩存配置項(xiàng)的方式;

//更改內(nèi)存緩存配置大小(當(dāng)然邮屁,可以定制自己的緩存)
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
    builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
  }
}

以及使用加載某個(gè)圖片的時(shí)候配置緩存策略:

Glide.with(fragment)
  .load(url)
  .diskCacheStrategy(DiskCacheStrategy.ALL)
  .into(imageView);

Glide緩存源碼分析

正常加載一張圖片的常規(guī)操作如下:

加載圖片.png

RequestBuilder

我們進(jìn)入RequestBuilder看下into(imageView)方法
第一步做了一個(gè)requestOptions的重新賦值琐鲁,大概的邏輯是requestOptions會隨著ImageView設(shè)置的scaleType重新賦值。

然后進(jìn)入:

into(
        glideContext.buildImageViewTarget(view, transcodeClass),
        /*targetListener=*/ null,
        requestOptions,
        Executors.mainThreadExecutor())

然后通過buildRequest創(chuàng)建一個(gè)請求惭每,并進(jìn)行

private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> options,
      Executor callbackExecutor) {
    Preconditions.checkNotNull(target);
    if (!isModelSet) {
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }
    //1. 創(chuàng)建一個(gè)請求
    Request request = buildRequest(target, targetListener, options, callbackExecutor);

    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
     //如果兩次請求為相同請求骨饿,上一個(gè)請求如果失敗了亏栈,則重新去請求
    // 如果上一次已經(jīng)正在運(yùn)行了,將繼續(xù)運(yùn)行上一次請求宏赘,不去打斷他绒北。
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        //這里是判斷上一個(gè)相同的請求如果沒有運(yùn)行,就直接運(yùn)行開始上一次的請求任務(wù)察署。
       // 這樣可以優(yōu)化一些像設(shè)置站維護(hù)闷游,記錄,獲取圖片信息等一次請求中需要的操作
        previous.begin();
      }
      return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
   // 開始執(zhí)行請求
    requestManager.track(target, request);

    return target;
  }

SingleRequest

最終通過requestManager.track(target, request)調(diào)用RequestTracker.runRequest贴汪,通過SingleRequest.begin脐往,SingleRequest.onSizeReady方法進(jìn)入Engine.load方法;

我們重點(diǎn)研究一下Engine.load方法

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) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

   //第一步扳埂,生成原始的緩存key
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
     //第二步业簿,從內(nèi)存中讀取資源
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
      //第三步,開啟一個(gè)新的任務(wù):從磁盤中讀取或者從網(wǎng)絡(luò)中讀取需要的資源阳懂。
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }

第一步:生成緩存的key辖源,這里會根據(jù)上面所說的,模型希太,簽名克饶,長寬,變換以及原始資源誊辉,變換后的資源矾湃,以及選項(xiàng)創(chuàng)建一個(gè)key。這個(gè)是原始的key堕澄,后續(xù)如果執(zhí)行到磁盤緩存會根據(jù)原生key生成新的key邀跃。
第二步:從內(nèi)存中嘗試獲取資源文件,內(nèi)存中分為兩部分:活動資源以及內(nèi)存緩存資源蛙紫。

  @Nullable
  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {
      return null;
    }
    //活動資源
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }
    //內(nèi)存緩存資源
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      return cached;
    }

    return null;
  }

活動資源
活動資源最終實(shí)現(xiàn)是通過ActiveResources拍屑。內(nèi)部維護(hù)了一個(gè)Map<Key, ResourceWeakReference> ,也就是一個(gè)弱引用的hashMap坑傅,將活動資源存儲在hashmap的弱引用中僵驰。

@VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

  synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

內(nèi)存緩存

  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;
  }

其中關(guān)鍵實(shí)現(xiàn)為MemoryCache 定義的cache,MemoryCache 的具體實(shí)現(xiàn)類為LruResourceCache唁毒。LruResourceCache 集成了LruCache蒜茴,就是實(shí)現(xiàn)了LRU算法來維護(hù)內(nèi)存緩存數(shù)據(jù)。

第三步浆西,開啟一個(gè)新的任務(wù):從磁盤中讀取或者從網(wǎng)絡(luò)中讀取需要的資源粉私。
通過waitForExistingOrStartNewJob 進(jìn)入 engineJob.start(decodeJob);
decodeJob 實(shí)現(xiàn)了runnable接口,engineJob實(shí)現(xiàn)內(nèi)部通過線程池去執(zhí)行decodeJob
看下DecodeJob 的run -> runWrapped -> runGenerators

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;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }
  }

其中currentGenerator.startNext() 方法為真正執(zhí)行加載近零,我們來看下currentGenerator 有哪些具體實(shí)現(xiàn)類诺核。

DataFetcherGenerator實(shí)現(xiàn)類.png

下面為如果根據(jù)緩存策略返回當(dāng)前的階段抄肖,后面會根據(jù)當(dāng)前階段生成對應(yīng)的生成器。

  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);
    }
  }

三種生產(chǎn)器會根據(jù)當(dāng)前的階段進(jìn)行創(chuàng)建

  • (變換后的)資源緩存文件對應(yīng)ResourceCacheGenerator
  • 沒有被變換后的元數(shù)據(jù)緩存文件對應(yīng)DataCacheGenerator
  • 源數(shù)據(jù)數(shù)據(jù)(網(wǎng)絡(luò)或者本地文件)對應(yīng)的SourceGenerator
    大概邏輯就是窖杀,先從本地資源緩存文件查找有沒有需要的資源漓摩,如果沒有找到,則去查找元數(shù)據(jù)緩存陈瘦,如果還是沒有就去源數(shù)據(jù)去加載幌甘,并將當(dāng)前的數(shù)據(jù)寫入磁盤緩存。
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);
    }
  }
ResourceCacheGenerator
 public boolean startNext() {
       ···省略代碼
      currentKey =
          new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
    // 根據(jù)硬盤緩存判斷是否存在痊项,具體實(shí)現(xiàn)為DiskLruCache
      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;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }

這里會先從DiskCache嘗試獲取锅风,默認(rèn)的實(shí)現(xiàn)是DiskLruCache,在這里進(jìn)行讀取鞍泉,然后在資源編碼完成之后皱埠,存入磁盤。

DataCacheGenerator

DataCacheGenerator.startNext的邏輯和ResourceCacheGenerator基本相同咖驮。
第一步边器,創(chuàng)建DataCacheKey,這里和ResourceCacheGenerator生成Key方式不太一致托修。
第二步忘巧,嘗試通過DiskCache獲取元數(shù)據(jù)緩存
第三方,根據(jù)modelLoader加載數(shù)據(jù)

SourceGenerator

開始嘗試去加載源數(shù)據(jù)睦刃,主要通過DataFetcher實(shí)現(xiàn)砚嘴,DataFetcher會根據(jù)數(shù)據(jù)源類型進(jìn)行選擇合適的進(jìn)行調(diào)用。

    private void startNextLoad(final LoadData<?> toStart) {
    loadData.fetcher.loadData(
        helper.getPriority(),
        new DataCallback<Object>() {
          @Override
          public void onDataReady(@Nullable Object data) {
            if (isCurrentRequest(toStart)) {
              onDataReadyInternal(toStart, data);
            }
          }

          @Override
          public void onLoadFailed(@NonNull Exception e) {
            if (isCurrentRequest(toStart)) {
              onLoadFailedInternal(toStart, e);
            }
          }
        });
  }

DataFetcher 實(shí)現(xiàn)類如下圖所示涩拙。

DataFetcher 實(shí)現(xiàn)類

我們進(jìn)入HttpUrlFetcher 看下具體實(shí)現(xiàn)际长,這里我們看到loadData方法內(nèi)部的loadDataWithRedirects方法進(jìn)入真正的文件網(wǎng)絡(luò)數(shù)據(jù)讀取。

private InputStream loadDataWithRedirects(
      URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
   
    urlConnection = connectionFactory.build(url);
    for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
      urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
    }
    urlConnection.setConnectTimeout(timeout);
    urlConnection.setReadTimeout(timeout);
    urlConnection.setUseCaches(false);
    urlConnection.setDoInput(true);
    final int statusCode = urlConnection.getResponseCode();
    if (isHttpOk(statusCode)) {
      return getStreamForSuccessfulRequest(urlConnection);
    } else if (isHttpRedirect(statusCode)) {
      String redirectUrlString = urlConnection.getHeaderField("Location");
      if (TextUtils.isEmpty(redirectUrlString)) {
        throw new HttpException("Received empty or null redirect url");
      }
      URL redirectUrl = new URL(url, redirectUrlString);
      // Closing the stream specifically is required to avoid leaking ResponseBodys in addition
      // to disconnecting the url connection below. See #2352.
      cleanup();
      return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
    } else if (statusCode == INVALID_STATUS_CODE) {
      throw new HttpException(statusCode);
    } else {
      throw new HttpException(urlConnection.getResponseMessage(), statusCode);
    }
  }

最終通過onDataFetcherReady 回調(diào)DecodeJob里面兴泥,然后通過LoadPath.load方法進(jìn)行解碼工育,最終通過ResourceTranscoder 轉(zhuǎn)碼成我們需要的BitmapResource、BitmapDrawableResource搓彻、FileResource等如绸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市好唯,隨后出現(xiàn)的幾起案子竭沫,更是在濱河造成了極大的恐慌,老刑警劉巖骑篙,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異森书,居然都是意外死亡靶端,警方通過查閱死者的電腦和手機(jī)谎势,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杨名,“玉大人脏榆,你說我怎么就攤上這事√ǖ” “怎么了须喂?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長趁蕊。 經(jīng)常有香客問我坞生,道長,這世上最難降的妖魔是什么掷伙? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任是己,我火速辦了婚禮,結(jié)果婚禮上任柜,老公的妹妹穿的比我還像新娘卒废。我一直安慰自己,他們只是感情好宙地,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布摔认。 她就那樣靜靜地躺著,像睡著了一般宅粥。 火紅的嫁衣襯著肌膚如雪参袱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天粹胯,我揣著相機(jī)與錄音蓖柔,去河邊找鬼。 笑死风纠,一個(gè)胖子當(dāng)著我的面吹牛况鸣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竹观,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼镐捧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了臭增?” 一聲冷哼從身側(cè)響起懂酱,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎誊抛,沒想到半個(gè)月后列牺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拗窃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年瞎领,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泌辫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡九默,死狀恐怖震放,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驼修,我是刑警寧澤殿遂,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站乙各,受9級特大地震影響墨礁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜觅丰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一饵溅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妇萄,春花似錦蜕企、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懦底,卻和暖如春唇牧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聚唐。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工丐重, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杆查。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓扮惦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亲桦。 傳聞我的和親對象是個(gè)殘疾皇子崖蜜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容