Android Glide 源码分析和学习

1.引言

最近在开发产品过程中,项目经理提了一个问题:"为什么图片加载这么缓慢?",我看了看的确非常缓慢,图片加载用的经典的Glide框架,慢难道仅仅只是网络的原因?带着这份不解,我下了一个glide源码,开启了学习之路。

2.正题

通过学习想彻底弄懂这几个问题:

  1. Glide加载流程
  2. Glide切换Activity,是否有中断和恢复操作
  3. Glide的内存管理
  4. Glide的弱网管理机制

2.1 Glide总体加载流程

未命名文件.jpg

Glide框架就像是一条流水线,每个环节做什么,每个环节为下一步输出什么。都非常清楚明了。把握其中的主要流程和环节,那么搞懂全貌只是时间问题。

接下来我们以下面的代码为基础开始正式分析我们的glide源码加载流程:

Glide.with(Context context)
         .load(Strint url)
         .into(ImageView imageView);

2.1.1 Request产生阶段

Glide类:

Glide类和OkhttpClient一样,首先都是一个单例,目的是为整个框架做初始化以及为使用者提供唯一的使用窗口:

Glide.with 接受如下几个参数:

  • @see #with(android.app.Activity)
  • @see #with(android.app.Fragment)
  • @see #with(androidx.fragment.app.Fragment)
  • @see #with(androidx.fragment.app.FragmentActivity)

不同的参数,所代表的lifecycle也就不同。这样在生命周期处理这块就有所不同。with()方法最终会调用RequestManagerRetriever的get方法得到一个RequestManager

RequestManagerRetriever类:

RequestManagerRetriever类中是直接生产RequestManager的,生产主要是靠RequestManagerFactory。其次调用checkAndInitializeGlide()来初始化Glide

GlideBuilder类:

checkAndInitializeGlide()方法最终会调用到GlideBuilder.build. 对整个Glide框架中的GlideExecutor,加载引擎Engine,DiskCache 等做初始化。一个非常典型的建造者模式使用场景

RequestManager类:

RequestManager以Manager,肯定是包含了:

  • 请求的创建,setRequestOptions
  • 请求的管理队列 移除、等待
  • 请求开始、请求暂停、取消、重新开始,设置RequestListener

请求的暂停、取消等是通过:RequestTracker类去跟踪处理
请求的创建以及各种配置是通过:RequestBuilder类完成

RequestBuilder类:
为Request设置完毕RequestOptions之后就会开始,进行build得到真正的Request对象。这一步骤是通过调用into(Target t)方法。

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

    Request request = buildRequest(target, targetListener, options, callbackExecutor);//

    Request previous = target.getRequest();//判断此请求是否被加载过
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
      // If the request is completed, beginning again will ensure the result is re-delivered,
      // triggering RequestListeners and Targets. If the request is failed, beginning again will
      // restart the request, giving it another chance to complete. If the request is already
      // running, we can let it continue running without interruption.
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        // Use the previous request rather than the new one to allow for optimizations like skipping
        // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions
        // that are done in the individual Request.
        previous.begin();//假如Request存在的话 就直接走begin开始走Engin流程
      }
      return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request);//开启网络请求

    return target;
  }

这一步骤应该有俩个疑问:

  • 传入的ImageView如何转换成Target的

  • buildRequest方法中是如何产生一个Request的

解答1:

load(ImageView )方法最终会调用GlideContext.buildImageViewTarget 方法转换得到Target。源码如下:

public class ImageViewTargetFactory {
  @NonNull
  @SuppressWarnings("unchecked")
  public <Z> ViewTarget<ImageView, Z> buildTarget(
      @NonNull ImageView view, @NonNull Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }
}

解答2:
Request是通过GlideContext.buildRequestRecursive 产生的。Request是一个抽象类:具体的实现有这几个:

image.png

根据配置不同生成的Request就不同。一般网络请求产生的是SingleRequest; 到这里 前期工作做好了,Request也产生了。于是开始交给Engine引擎去深层次加工。

小结:Glide前期代码执行流程:

image.png

2.1.2 加载Request

上面得到Request之后,会调用RequestManager.track()开启 加载。RequestManager中对请求的管理。最终都是通过RequestTracker实现。

RequestTracker类:
作用:开始、暂停、取消请求

RunRequest方法 代码如下:

/** Starts tracking the given request. */
  public void runRequest(@NonNull Request request) {
    requests.add(request);//添加Request到set中
    if (!isPaused) {
      request.begin();//开启真正的请求
    } else {
      request.clear();
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Paused, delaying request");
      }
      pendingRequests.add(request);
    }
  }

SingleRequest类:
由上面我们知道了,产生的request是SingleRequest.我们来看下SingleRequest是干什么的。

image.png

每个SingleRequest都有这6中状态。SingleRequest类有一个Status变量,用来标记当前SingleRequst的状态。

SingleRequest#begin方法如下:

@Override
  public void begin() {
    synchronized (requestLock) {
     
      //加载完成从内存中去取
      if (status == Status.COMPLETE) {
        onResourceReady(
            resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);        return;
      }

      // Restarts for requests that are neither complete nor running can be treated as new requests
      // and can run again from the beginning.

      status = Status.WAITING_FOR_SIZE;
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
      } else {
        target.getSize(this);
      }

      if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
          && canNotifyStatusChanged()) {
        target.onLoadStarted(getPlaceholderDrawable());
      }
      if (IS_VERBOSE_LOGGABLE) {
        logV("finished run method in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

onSizeReady() 调用了Engine.load方法正式开始加载。是加载的入口

      loadStatus =
          engine.load(
              glideContext,
              model,
              requestOptions.getSignature(),
              this.width,
              this.height,
              requestOptions.getResourceClass(),
              transcodeClass,
              priority,
              requestOptions.getDiskCacheStrategy(),
              requestOptions.getTransformations(),
              requestOptions.isTransformationRequired(),
              requestOptions.isScaleOnlyOrNoTransform(),
              requestOptions.getOptions(),
              requestOptions.isMemoryCacheable(),
              requestOptions.getUseUnlimitedSourceGeneratorsPool(),
              requestOptions.getUseAnimationPool(),
              requestOptions.getOnlyRetrieveFromCache(),
              this,
              callbackExecutor);


Engine类:
engine是整个Glide的灵魂所在,著名的缓存机制都是在Engine中进行的。和我们之前学习一样。Engine肯定又是一个“大家长”。其主要功能是负责开启load Request 且管理资源的缓存。这些功能都是由其内部一个又一个类构成。往往在这个类中就会声明实现各个模块的“小部件”


public class Engine
    implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
  private static final String TAG = "Engine";
  private static final int JOB_POOL_SIZE = 150;
  private static final boolean VERBOSE_IS_LOGGABLE = Log.isLoggable(TAG, Log.VERBOSE);
  private final Jobs jobs;//通过HashMap记录产生的EngineJob
  private final EngineKeyFactory keyFactory;
  private final MemoryCache cache;//内存缓存机制第二层
  private final EngineJobFactory engineJobFactory;//生产EngineJob
  private final ResourceRecycler resourceRecycler;
  private final LazyDiskCacheProvider diskCacheProvider;
  private final DecodeJobFactory decodeJobFactory;
  private final ActiveResources activeResources;//内存缓存机制第一层

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;

    //标记EngineResource的独一无二的key
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime); //缓存机制1:从内存加载
      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }

loadFromMemory方法:就是Glide框架中的第一层缓存机制---内存缓存。

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

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

    return null;
  }

我们首先看看第一层内存缓存 loadFromActiveResources:

  @Nullable
  private EngineResource<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }

ActiveResources类:

final class ActiveResources {
  private final boolean isActiveResourceRetentionAllowed;
  private final Executor monitorClearedResourcesExecutor;
  @VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();//缓存EngineResource。
  private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>();

  private ResourceListener listener;

ResourceWeakReference 对象可以理解成是对EngineResource包装。本身是弱引用。当前程序中被使用的EngineResource都会放在这个map中。这样当在其他地方再次使用,可以直接从内存中查找出来,根据EngineKey。

active.acquire();采用了引用计数机制(类似jvm内存管理中的),被引用一次就+1,当为0的时候,就会退居二线内存容器中,也就是会被添加到MemoryCache中。

我们首先看看第二层内存缓存loadFromCache:

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

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指的就是MemoryCache.其实现类是:LruResourceCache 类继承自 LruCache。内部主要是维护了一个LinkedHashMap。

关于Lru算法可以参考这个文章:Lru算法
LinkHashMap 参考这个:LinkHashMap介绍

GetEngineResourceFromCache 就是从第二层LruCache中查找EngineResource。上面是调用remove方法。意味着当命中EngineKey。LruCache会删除这个EngineKey。同时activeResources调用put方法。放进一级缓存中。同时计数+1

流程图:


image.png

内存缓存的机制就讲到这里。因为第一次加载网络图片,内存中肯定是找不到EngineResource的。 找不到怎么办呢? 会调用waitForExistingOrStartNewJob 方法进而产生一个EngineJob和DecodeJob

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

    DecodeJob<R> decodeJob =
        decodeJobFactory.build(
            glideContext,
            model,
            key,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            onlyRetrieveFromCache,
            options,
            engineJob);

    jobs.put(key, engineJob);

    engineJob.addCallback(cb, callbackExecutor);
    engineJob.start(decodeJob);

EngineJob类:

EngineJob是Engine处理每个请求的最小单元,里面有load success/fail 等回调;也有开启load,取消load等操作

image.png

DecodeJob类:

DecodeJob 负责从diskCache中或者服务器上解码得到图片的数据。实现了Runnable接口。是真正进行请求的核心类;也是文件缓存的入口。

EngineJob调用Start方法治好,会执行DecodeJob run方法。run代码如下:

@SuppressWarnings("PMD.AvoidRethrowingException")
  @Override
  public void run() {
    GlideTrace.beginSectionFormat("DecodeJob#run(model=%s)", model);
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();//关键
    } catch (CallbackException e) {
      throw e;
    } catch (Throwable t) {
  }

  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;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

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

DataFetcherGenerator是一个接口,实现类如下:

image.png
万众瞩目的文件缓存来!!!

runWrapped() 调用顺序

  • 调用ResourceCacheGenerator.startNext方法 从文件中加载数据
  • 找不到数据,调用DataCacheGenerator的startNext 从另外的文件中查询数据;
  • 依然查询不到就调用SourceGenerator.startNext从网络上加载数据。

ResourceCacheGenerator.startNext方法如下:

 currentKey =
          new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      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.startNext(helper.getPriority(), this);
      }
    }

cacheFile = helper.getDiskCache().get(currentKey);
从文件去寻找图片资源

找到之后,调用去读取数据

loadData.fetcher.startNext(helper.getPriority(), this);

DataCacheGenerator的流程和ResourceCacheGenerator流程几乎一样。都是从文件中加载图片区别在于:

  • ResourceCacheGenerator文件中保存的是经过转换的图片,例如转换成圆角。那张圆角就是保存在ResourceCacheGenerator 中

  • DataCacheGenerator 是保存最原始的,也就是从服务器拉下来的那张图片。

当然这也与Glide 缓存策略有关:

ALL:既缓存原始图片,也缓存转换过后的图片;对于远程图片,缓存 DATA 和 RESOURCE;对于本地图片,只缓存 RESOURCE。

AUTOMATIC (默认策略):尝试对本地和远程图片使用最佳的策略。当你加载远程数据(比如,从 URL 下载)时,AUTOMATIC 策略仅会存储未被你的加载过程修改过 (比如,变换、裁剪等) 的原始数据(DATA),因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,AUTOMATIC 策略则会仅存储变换过的缩略图(RESOURCE),因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。

DATA:只缓存未被处理的文件。我的理解就是我们获得的 stream。它是不会被展示出来的,需要经过装载 decode,对图片进行压缩和转换,等等操作,得到最终的图片才能被展示。

NONE:表示不缓存任何内容。

RESOURCE:表示只缓存转换过后的图片(也就是经过decode,转化裁剪的图片)。

完整的内存缓存图:

image.png

DataFetcherGenerator 加载数据最终都是靠DataFetcher来实现的。DataFetcher接口。常用的DataFetch实现由如下三种:

image.png

HttpUrlFetcher:默认的从服务器拉取数据的实现。SourceGenerator 持有的DataFetcher

FileFetcher:从文件中读取数据。ResourceCacheGenerator持有的。

接下来看看 HttpUrlFetcher#loadData

@Override
  public void loadData(
      @NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
    long startTime = LogTime.getLogTime();
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

loadDataWithRedirects方法就不展开讲了,内部就是通过网络请求最终返回一个InputStream.

到此整个请求的数据就拿到了。Glide的缓存机制也分析了一遍。接下来就是各种回调。

2.1.3 请求回调处理

HttpUrlFetcher#loadData中的 callback.onDataReady(result);经过层层的回调一直到Engine类中。流程图如下:

image.png

以上就是整个Glide加载流程的主脉络。很多细节问题没有去深究。等以后遇到问题再去弄懂细节问题。

这篇文章整整花了4个小时时间抒写。回调章节讲的也是很简单。可以打印下堆栈快速弄懂回调逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容