Glide緩存原理

glide緩存分為內(nèi)存緩存和磁盤緩存。內(nèi)存緩存分為活動緩存和cache捉捅。磁盤緩存又分為resource和data闯捎。本文將圍繞加載圖片流程介紹glide的緩存。

 Glide.with(this)
          .load(url)
           .skipMemoryCache(true)
            // .transform(CircleCrop())
           // .error(R.drawable.ic_launcher_background)
          .diskCacheStrategy(DiskCacheStrategy.NONE)
          .listener(getListener(index))
          .into(mIv)

1叙谨、load

public <R> LoadStatus load(
      GlideContext glideContext,
      Object model) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height);

    EngineResource<?> memoryResource;
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height);
      }
    }
  cb.onResourceReady(
        memoryResource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
    return null;
  }

1.1温鸽、loadFromMemory 先從內(nèi)從中加載。

//Engine
private EngineResource<?> loadFromMemory(
            EngineKey key, boolean isMemoryCacheable, long startTime) {

        if (!isMemoryCacheable) {
           return null;
         }

        EngineResource<?> active = loadFromActiveResources(key);
        if (active != null) {
            return active;
        }
        EngineResource<?> cached = loadFromCache(key);
        if (cached != null) {
            return cached;
        }
        return null;
    }

可以看到從內(nèi)存加載又分為活動緩存和 cache
1.11、活動緩存
舉例: 一個LinearLayout中有10個imageview涤垫,通過glide加載姑尺。那么這10個圖片就緩存在activityResource中。 當點擊的條目蝠猬,刪除一個item. 這個ActivityResource緩存并沒有被移除切蟋。當gc運行時,將這個item回收掉之后了榆芦,就會將這個緩存加入到resourceReferenceQueue柄粹,然后通過遍歷這個引用隊列,從活動緩存中移除匆绣。
舉例: 當在recyclerview的每個item加載一個imagview驻右。 那么這個活動緩存的最大數(shù)目為= 可見數(shù)據(jù)+ 預加載數(shù)目 + 復用池1 ,因為從recyclerview移除之后崎淳,不一定就被立即回收了堪夭,當從復用池中復用item時,會主動釋放上一次item對應的活動緩存凯力。此時活動緩存就減少了茵瘾。
活動緩存是何時存入的了?
1)咐鹤、從緩存中獲取到資源時拗秘,加入到活動緩存。

 private EngineResource<?> loadFromCache(Key key) {
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }

2)祈惶、從網(wǎng)絡和磁盤加載資源完成時雕旨,加入到活動緩存。
活動緩存何時刪除的了捧请?
1)凡涩、onDestroy時,request調(diào)用clear() 會釋放當前對應key的活動緩存疹蛉。
2)活箕、不能設(shè)置資源給target,釋放當前key的活動緩存可款。
3)育韩、獲取活動緩存,發(fā)現(xiàn)該活動緩存EngineResource不存在闺鲸,就刪除key的活動緩存筋讨。也就是setIsActiveResourceRetentionAllowed(false)的情況。
4)摸恍、創(chuàng)建ActiveResources對象時悉罕,就開啟了一個線程,一直輪詢這個queue,如果存在的EngineResource壁袄,就刪除對應的活動緩存类早。
1.12、內(nèi)存緩存
獲取內(nèi)存時就從內(nèi)存緩存中移除然想。

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

何時加入內(nèi)存
從活動緩存移除就加入了內(nèi)存緩存莺奔。內(nèi)存緩存超過容量后被釋放的bitmap等會被加入到bitmapPool中欣范。從活動緩存中移除的要么加入內(nèi)存緩存变泄,要么加入到bitmapPool中。

//Engine
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
     //加入內(nèi)存緩存
      cache.put(cacheKey, resource);
    } else {
   //資源釋放恼琼,bitmap支持復用則加入到bitmapPool
      resourceRecycler.recycle(resource, /* forceNextFrame= */ false);
    }
  }


//BitmapResource
@Override
  public void recycle() {
    bitmapPool.put(bitmap);
  }

緩存資源的key是通過寬高,加載圖片的model妨蛹、options等生成的。所以寬高變化晴竞,可能導致內(nèi)存中找不到這張圖蛙卤。
1.2、內(nèi)存不存在噩死,開啟一個job從網(wǎng)絡加載加載颤难。

  private <R> Engine.LoadStatus waitForExistingOrStartNewJob(
            GlideContext glideContext,
            Object model,
            Key signature,
            int width,
            int height) {

        EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
        if (current != null) {
            current.addCallback(cb, callbackExecutor);
            return new Engine.LoadStatus(cb, current);
        }

        EngineJob<R> engineJob =
                engineJobFactory.build(
                        key,
                        isMemoryCacheable,
                        useUnlimitedSourceExecutorPool,
                        useAnimationPool,
                        onlyRetrieveFromCache);

        DecodeJob<R> decodeJob =
                decodeJobFactory.build(
                        glideContext,
                        model,
                        key,
                        signature,
                        width,
                        height,
                        engineJob);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb, callbackExecutor);
        engineJob.start(decodeJob);
        return new Engine.LoadStatus(cb, engineJob);
    }

可以看到先判斷,是否是從從緩存中取獲取緩存job已维。
如果緩存的job沒有找到行嗤。則創(chuàng)建一個job。并且執(zhí)行這個RunableJob垛耳。

  public synchronized void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor =
        decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }

所以接下來栅屏,我們看下這個runnable(DecodeJob)的run方法。

 public void run() {
        DataFetcher<?> localFetcher = currentFetcher;
        try {
            runWrapped();
        }
    }

private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
    }
  }

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

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

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

可以看到堂鲜,根據(jù)當前的狀態(tài)和diskCacheStrategy磁盤策略的配置栈雳,來決定是加載RESOURCE_CACHE、DATA_CACHE缔莲、SOURCE哥纫。
RESOURCE_CACHE : 是轉(zhuǎn)換過或者改變過采樣率的文件。解碼之后的痴奏。
DATA_CACHE:原始的未修改的數(shù)據(jù)蛀骇。解碼之前的。
SOURCE :網(wǎng)絡文件抛虫,源文件松靡。
1.3、INITIALIZE
如果配置全部允許建椰,會首先去加載Resource_cache雕欺,再加載DATA_CACHE,如果都加載不了,再從網(wǎng)絡加載SOURCE屠列。
ResourceCacheGenerator和DataCacheGenerator會從磁盤緩存中加載數(shù)據(jù)啦逆,這里不看了,看下從Remote加載笛洛,SourceGenerator從網(wǎng)絡加載數(shù)據(jù)夏志。

// SourceGenerator
public boolean startNext() {
        if (dataToCache != null) {
            Object data = dataToCache;
            boolean isDataInCache = cacheData(data);
            dataToCache = null;
        }
        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;
    }

    private void startNextLoad(final ModelLoader.LoadData<?> toStart) {
        loadData.fetcher.loadData(
                helper.getPriority(),
                new DataFetcher.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);
                        }
                    }
                });
    }

  void onDataReadyInternal(LoadData<?> loadData, Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(
          loadData.sourceKey,
          data,
          loadData.fetcher,
          loadData.fetcher.getDataSource(),
          originalKey);
    }
  }

首先從網(wǎng)絡loadData.fetcher.loadData中加載,然后判斷isDataCacheable()是否可以緩存data苛让,這個配置是緩存策略中配置的沟蔑。如果可以緩存,調(diào)用cb.reschedule()狱杰。
1.4瘦材、SWITCH_TO_SOURCE_SERVICE

  private void reschedule(RunReason runReason) {
    this.runReason = runReason;
    callback.reschedule(this);
  }

  @Override
  public void reschedule() {
    reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE);
  }

這個方法會再次調(diào)用run()方法,狀態(tài)變?yōu)镾WITCH_TO_SOURCE_SERVICE從網(wǎng)絡獲取數(shù)據(jù)切換到從磁盤獲取仿畸, 還會再次執(zhí)行要這個類食棕。 此時startNext方法中的dataToCache不為空,并且會調(diào)用將數(shù)據(jù)緩存到磁盤中错沽,然后調(diào)用DataCacheGenerator.startNext()簿晓,調(diào)用磁盤緩存加載,返回磁盤加載的結(jié)果千埃。

// SourceGenerator
 private boolean cacheData(Object dataToCache) throws IOException {
        try {
            DataRewinder<Object> rewinder = helper.getRewinder(dataToCache);
            Object data = rewinder.rewindAndGet();
            Encoder<Object> encoder = helper.getSourceEncoder(data);
            DataCacheWriter<Object> writer = new DataCacheWriter<>(encoder, data, helper.getOptions());
            DataCacheKey newOriginalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
            DiskCache diskCache = helper.getDiskCache();
            diskCache.put(newOriginalKey, writer);

            if (diskCache.get(newOriginalKey) != null) {
                originalKey = newOriginalKey;
                sourceCacheGenerator =
                        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
                return true;
            } else {
                cb.onDataFetcherReady(
                        loadData.sourceKey,
                        rewinder.rewindAndGet(),
                        loadData.fetcher,
                        loadData.fetcher.getDataSource(),
                        loadData.sourceKey);
            }
            return false;
        }
    }

上面就是在SourceGenerator中緩存數(shù)據(jù)Stream到磁盤過程憔儿。
1.5、DECODE_DATA
從getNextStage分析完INITIALIZE镰禾、SWITCH_TO_SOURCE_SERVICE皿曲,接下看下DECODE_DATA狀態(tài),從網(wǎng)絡請求對應的HttpGlideUrlLoader吴侦,返回的輸入流數(shù)據(jù)是ContentLengthInputStream屋休。
從磁盤緩存對應的FileLoader,返回的輸入流是文件輸入流备韧。
對于這些流需要轉(zhuǎn)化成Bitmap劫樟、Drawable等。
decode的過程 就是將InputStream 轉(zhuǎn)化成bitmap的過程织堂。

//DecodeJob
 private void decodeFromRetrievedData() {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource, isLoadingFromAlternateCacheKey);
    } else {
      runGenerators();
    }
  }
 //DecodePath 
 public Resource<Transcode> decode(
      DataRewinder<DataType> rewinder,
      int width,
      int height,
      @NonNull Options options,
      DecodeCallback<ResourceType> callback)
      throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded);
    return transcoder.transcode(transformed, options);
  }

//DecodeJob
<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    @SuppressWarnings("unchecked")
    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
    Transformation<Z> appliedTransformation = null;
    Resource<Z> transformed = decoded;
    if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
      appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
      transformed = appliedTransformation.transform(glideContext, decoded, width, height);
    }

    final EncodeStrategy encodeStrategy;
    final ResourceEncoder<Z> encoder;
    if (decodeHelper.isResourceEncoderAvailable(transformed)) {
      encoder = decodeHelper.getResultEncoder(transformed);
      encodeStrategy = encoder.getEncodeStrategy(options);
    } else {
      encoder = null;
      encodeStrategy = EncodeStrategy.NONE;
    }

    Resource<Z> result = transformed;
    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      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;
  }

上面第一步是decode的流程叠艳,將stream轉(zhuǎn)化成bitmap,設(shè)置采樣率等易阳,transformed附较,可以看到在解析完成調(diào)用了diskCacheStrategy.isResourceCacheable()這個方法主要是為了初始化Resouce_cache相關(guān)信息。

// DecodeJob
private void notifyEncodeAndRelease(
      Resource<R> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    GlideTrace.beginSection("DecodeJob.notifyEncodeAndRelease");
    try {
      Resource<R> result = resource;
      LockedResource<R> lockedResource = null;
      if (deferredEncodeManager.hasResourceToEncode()) {
        lockedResource = LockedResource.obtain(resource);
        result = lockedResource;
      }
      notifyComplete(result, dataSource, isLoadedFromAlternateCacheKey)
      stage = Stage.ENCODE;
      try {
        if (deferredEncodeManager.hasResourceToEncode()) {
          deferredEncodeManager.encode(diskCacheProvider, options);
        }
      } finally {
      onEncodeComplete();
    } finally {
      GlideTrace.endSection();
    }
  }

private void notifyComplete(
      Resource<R> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    setNotifiedOrThrow();
    callback.onResourceReady(resource, dataSource, isLoadedFromAlternateCacheKey);
  }

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

notifyComplete就是調(diào)用callback.onResourceReady這個方法最終就回調(diào)到我們給request設(shè)置的RequestListener中潦俺。
上面我們知道緩存Resource相關(guān)key已經(jīng)初始化了拒课,接著deferredEncodeManager.encode將resource緩存在本地徐勃,整個加載過程就結(jié)束。
上面我們一直提到磁盤緩存策略早像,那么我們看一下DiskCacheStrategy.ALL的代碼.

 public static final DiskCacheStrategy ALL =
      new DiskCacheStrategy() {
        @Override
        public boolean isDataCacheable(DataSource dataSource) {
          return dataSource == DataSource.REMOTE;
        }

        @Override
        public boolean isResourceCacheable(
            boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
          return dataSource != DataSource.RESOURCE_DISK_CACHE
              && dataSource != DataSource.MEMORY_CACHE;
        }

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

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

isDataCacheable 表示可以將原始數(shù)據(jù)data緩存僻肖。
isResourceCacheable 表示可以將Resouce數(shù)據(jù)緩存。
decodeCachedResource 表示可以從Resource 中加載數(shù)據(jù)卢鹦。
decodeCachedData 表示可以從Data中加載數(shù)據(jù)臀脏。

image.png

上面這幅圖展示了glide緩存加載流程,沒有BitmapPool冀自,BitmapPool 只是從磁盤緩存揉稚、網(wǎng)絡加載圖片時,decode圖片需要復用的之前圖片的內(nèi)存凡纳,從bitmappool中獲取窃植。 活動內(nèi)存釋放帝蒿、cache的大小超過了容量會加入到bitmapPool荐糜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葛超,隨后出現(xiàn)的幾起案子暴氏,更是在濱河造成了極大的恐慌,老刑警劉巖绣张,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件答渔,死亡現(xiàn)場離奇詭異,居然都是意外死亡侥涵,警方通過查閱死者的電腦和手機沼撕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芜飘,“玉大人务豺,你說我怎么就攤上這事∴旅鳎” “怎么了笼沥?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長娶牌。 經(jīng)常有香客問我奔浅,道長,這世上最難降的妖魔是什么诗良? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任汹桦,我火速辦了婚禮,結(jié)果婚禮上鉴裹,老公的妹妹穿的比我還像新娘舞骆。我一直安慰自己灵嫌,他們只是感情好,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布葛作。 她就那樣靜靜地躺著寿羞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赂蠢。 梳的紋絲不亂的頭發(fā)上绪穆,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音虱岂,去河邊找鬼玖院。 笑死,一個胖子當著我的面吹牛第岖,可吹牛的內(nèi)容都是我干的难菌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蔑滓,長吁一口氣:“原來是場噩夢啊……” “哼郊酒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起键袱,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤燎窘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蹄咖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褐健,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年澜汤,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚜迅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡俊抵,死狀恐怖谁不,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情务蝠,我是刑警寧澤拍谐,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站馏段,受9級特大地震影響轩拨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜院喜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一亡蓉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧喷舀,春花似錦砍濒、人聲如沸淋肾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽樊卓。三九已至,卻和暖如春杠河,著一層夾襖步出監(jiān)牢的瞬間碌尔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工券敌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唾戚,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓待诅,卻偏偏與公主長得像叹坦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卑雁,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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