1.Android 大热门开源框架<一>之Glide源码(2024精华版)

目录:

1.Glide图片加载的总体流程介绍

2.Glide缓存机制做了哪些优化?

3.Glide做了哪些内存优化?

4.Glide如何管理生命周期?

5. Glide加载一个一兆的图片(100100),是否会压缩后再加载,放到一个200200的view上会怎样,1000*1000呢,图片会很模糊,怎么处理?

6.Glide怎么做大图加载?

7.Glide的缺点是啥?

1.Glide图片加载的总体流程介绍

1.1 面试官:如何实现一个图片加载框架?

概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,压缩,显示等操作。

  • 封装参数: 从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;
  • 解析路径: 图片的来源有多种,格式也不尽相同,需要规范化;
  • 读取缓存: 为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;
  • 查找文件/下载文件: 如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;
  • 解码: 这一步是整个过程中最复杂的步骤之一
  • 变换: 解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等);
  • 缓存: 得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;
  • 显示: 显示结果,可能需要做些动画(淡入动画,crossFade等)
  • 流程图“”: https://juejin.cn/post/6844904002551808013

封装参数--------->下载图片(缓存)--------->解码------>图片变换(圆角等等)--------->显示到控件上

设计glide.jpg
1.2 面试官:有看过它的源码吗?怎么看的?
完整的架构.jpg
glide设计.jpg

Glide最基本的用法就是三步走,先with(),再load(),最后into()

Glide.with(this).load(url).into(imageView);前面2步是最简单的,后面最复杂

1.2.1 with()

绑定生命周期,glide最大的优势,glide最大的优点

就是对bitmap的管理是跟随生命周期去发生改变的。其它的框架,当Activity销毁的时候,是不会释放之前加载图片占用的所有内存。

glide的优势就是当Activity销毁的时候,之前加载的所有图片的内存都释放了

Glide.with(Activity) 主要做了 线程池 + 缓存 + 请求管理与生命周期绑定+其它配置初始化的构建

面试官:生命周期具体是怎么绑定的?如果感知生命周期的? lifecycyle?如何避免内存泄露的?

空白fragment的创建,是如何取的,怎么保证不为空,通过map存,因为fragment的创建是消息创建的,消息是异步的

1.2.2 load()

load() 调用的是 RequestManager.load()

返回一个请求:request=load(String string)

public DrawableTypeRequest<String> load(String string) {
    return (DrawableTypeRequest<String>) fromString().load(string);
}

总结;最终load()方法返回的其实就是一个DrawableTypeRequest对象

Glide的缓存功能,大部分都是在load()方法中进行的

1.2.3 into()

2个队列,一个用set方法,一个用list方法。为什么?

请求:和okhttp一样,有2个队列,运行和准备队列。 在不同生命周期会调用队列的一些处理方法。

private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());
// A set of requests that have not completed and are queued to be run again. We use this list to maintain hard
// references to these requests to ensure that they are not garbage collected before they start running or
// while they are paused. See #346.
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final List<Request> pendingRequests = new ArrayList<Request>();

总结:请求服务器,变换,显示都是这里处理的。

into()就是加载资源完成后作什么处理,它接受三种参数:

面试官:那么具体的,Glide比较关注的有哪些?如何加载图片的?
  • 异步加载:线程池
  • 切换线程:Handler,没有争议吧
  • 缓存:LruCache、DiskLruCache
  • 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
  • 内存泄露:注意ImageView的正确引用,生命周期管理
  • 列表滑动加载的问题:加载错乱、队满任务过多问题

2.Glide缓存机制做了哪些优化?

2.1 面试官:为什么要用缓存?
  • 1)、减少流量消耗,加快响应速度;
    1. 、Bitmap 的创建/销毁比较耗内存,可能会导致频繁GC;使用缓存可以更加高效地加载 Bitmap,减少卡顿。

为什么用内存缓存?:增加图片读取的速度,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,造成内存资源浪费。

为什么用硬盘缓存? 而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

2.2 面试官:Glide缓存机制是怎样的?

磁盘缓存+二级内存缓存

面试官:为什么内存缓存用2层?为什么不能只使用软引用?

弱引用它是一个随时可能被回收的资源,只使用软引用作为图片缓存的手段效率是比较低的,因为不能控制软引用的图片什么时候被系统回收。而需要被频繁使用的图片也可能被回收,导致要重新从网络上下载。因此,不推荐单独使用软引用作为App中缓存图片的唯一形式。

问题: 为什么有Lrucache,还需要弱引用

之所以需要 activeResources,它是一个随时可能被回收的资源,memory 的强引用频繁读写 可能造成内存激增频繁 GC,而造成内存抖动。资源在使用过程中保存在 activeResources 中, 而 activeResources 是弱引用,随时被系统回收,不会造成内存过多使用和泄漏(弱引用不容易导致内存抖动)

之所以要设计两种内存缓存的原因是为了防止加载中的图片被LRU回收。ActiveResources 就是一个弱引用的 HashMap ,用来缓存正在使用中的图片,使用 ActiveResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉

2.3 面试官:存是怎么存的?,取是怎么取的?(不同的版本不一样?)比如郭霖大神那个版本就不是

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;
}
加载顺序.jpg

源码中我没有看到去硬盘中取,因为磁盘不是内存缓存!!!

WeakReference---LruCache-----------DiskCache

如何保证弱引用是有数据的!!!

4.8版本大致总结一下: 首先从弱引用读取缓存,没有的话通过Lru读取,[有则取,并且加到弱引用中,有问题吧,会不会重复】

其中存储被LruCache移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁,因此软引用是不会影响系统运行的。

如果没有会开启EngineJob进行后面的图片加载逻辑。并且也加到弱引用中!!!

图片是如何存放的?(存图片)和上面相反

网络 --> DiskLruCache--> LruCache-->弱引用????感觉有点问题!

存数据
很明显,这是加载图片之后的事情。通过EngineJob开启线程池去加载图片,取到数据之后,会回调到主线程,把图片存到弱引用。

当图片不再使用的时候,比如说暂停请求或者加载完毕或者清除资源时,就会将其从弱引用中转移到LruCache缓存池中。

内存缓存加载顺序如下:

1).根据图片地址,宽高,变换,签名等生成key
2).第一次加载没有获取到活动缓存。
3).接着加载内存资源缓存,先清理掉内存缓存,在添加进行活动缓存。
4).第二次加载活动缓存已经存在。
5).当前图片引用为 0 的时候,清理活动资源,并且添加进内存资源。
6).又回到了第一步,然后就这样环环相扣。

总结一下,就是正在使用中的图片使用弱引用来进行缓存,暂时不用的图片使用LruCache来进行缓存的功能;同一张图片只会出现在弱引用LruCache中的一个。

发现:弱引用引用计数为0,也就是没有图片使用,会把它添加到lrucache中

https://www.136.la/jingpin/show-125769.html

问题: glide中对Bitmap做了哪些操作?三级缓存?为何在有了内存缓存后,还要持有ActivityRef这个呢?

三级缓存.jpg

面试官: Glide哪里用到了弱应用?哪里用到了软应用?

LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,(LruCache本身是强引用,里面包的对象是软应用)

LruCache算法,Least Recently Used,又称为近期最少使用算法。主要算法原理就是把最近所使用的对象的强引用存储在LinkedHashMap上,并且,把最近最少使用的对象在缓存池达到预设值之前从内存中移除。

    private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){
        @Override
        protected int sizeOf(String key, SoftReference<Bitmap> value) {
            //默认返回1,这里应该返回Bitmap占用的内存大小,单位:K

            //Bitmap被回收了,大小是0
            if (value.get() == null){
                return 0;
            }
            return value.get().getByteCount() /1024;
        }
    };

3.Glide做了哪些内存优化?

3.1 Glide 优点

链式调用,生命周期,解耦

3.2 Glide做了哪些内存优化?

1). 尺寸优化 inSampleSize

2). 图片格式优化

  1. 内存复用优化:BitmapPool
面试官:BitmapPool 了解吗?Glide 是如何实现 Bitmap 复用的?

应该是在下载图片的是用,其他情况都是用缓存处理的!

Android Bitmap inBitmap 可以复用图片,看了下源码,很迷糊,inBitmap指向缓存图片,这个跟复用有啥关系呢?看了c直接返回了inBitmap,多做这一步干嘛呢?为啥内存缓存有了还要做这一步

主要就是指的复用内存块,不需要在重新给这个bitmap申请一块新的内存,避免了一次内存的分配和回收,从而改善了运行效率。

Glide 的 BitmapPool。采用策略模式。根据不同版本采用不同的。

private static LruPoolStrategy getDefaultStrategy() {
  final LruPoolStrategy strategy;
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    strategy = new SizeConfigStrategy();
  } else {
    strategy = new AttributeStrategy();
  }
  return strategy;
}

try {
      // 数据加载
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ...
      if (options.inBitmap != null) {
        try {
          // 输入流重置
          is.reset();
          bitmapPool.put(options.inBitmap);
          // 清理掉 inBitmap 并进行第二次加载
          options.inBitmap = null;
          // 再次调用进行加载
          return decodeStream(is, options, callbacks, bitmapPool);
        } catch (IOException resetException) {
          throw bitmapAssertionException;
        }
      }

Glide 首先会通过设置 inBitmap 复用的方式加载图片。如果这个过程中出现了异常,因为此时 inBitmap 不为空,所以将会进入异常处理流程,此时会清理掉 inBitmap,再次调用 decodeStream 方法二次加载,这个时候就不是 Bitmap 复用的了。所以,Glide 内部会通过错误重试机制进行 Bitmap 复用,当复用并出现错误的时候,会降级为非复用的方式第二次进行加载。

方法1:LruCache缓存大小设置,

方法2:onLowMemory-- -----内存状态

当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。

Glide 则根据系统内存紧张级别(level)进行 memoryCache / bitmapPool / arrayPool 的回收,而 RequestManager 在 TRIM_MEMORY_MODERATE 级别会暂停请求。

图片格式优化:

Picasso的默认质量是 ARGB_8888 ,Glide的默认质量则为 RGB_565

值得注意的是在Glide4.0之前,Glide默认使用RGB565格式,比较省内存但是Glide4.0之后,默认格式已经变成了ARGB_8888格式了,这一优势也就不存在了

4.Glide如何管理生命周期?

4.1 Glide如何管理生命周期?

绑定生命周期,glide最大的优势,glide最大的优点

就是对bitmap的管理是跟随生命周期去发生改变的。其它的框架,当Activity销毁的时候,是不会释放之前加载图片占用的所有内存。

glide的优势就是当Activity销毁的时候,之前加载的所有图片的内存都释放了

Glide.with(Activity) 主要做了 线程池 + 缓存 + 请求管理与生命周期绑定+其它配置初始化的构建

面试官:生命周期具体是怎么绑定的?如果感知生命周期的? lifecycyle?如何避免内存泄露的?

空白fragment的创建,是如何取的,怎么保证不为空,通过map存,因为fragment的创建是消息创建的,消息是异步的

Glide.with(this)绑定了Activity的生命周期。在Activity内新建了一个无UI的Fragment,这个Fragment持有一个Lifecycle,通过Lifecycle在Fragment关键生命周期通知RequestManager进行相关从操作。在生命周期onStart时继续加载,onStop时暂停加载,onDestory时停止加载任务和清除操作。

关键的几个类:

RequestManager

RequestManagerRetriever

SupportRequestManagerFragment ,

Lifecycle

LifecycleListener

4.2 面试官:glide调用在子线程会怎么样?

在UI线程中调用Glide,在子线程使用的话会和使用ApplicationContext一样;

可以理解为不对请求的生命周期进行管理

传入Application Context或者在子线程使用:调用getApplicationManager(context);这样Glide的生命周期就和应用程序一样了。可以理解为不对请求的生命周期进行管理

4.3 面试官:glide如何防止内存泄漏的?

简单说一下内存泄漏的场景,如果在一个页面中使用Glide加载了一张图片,图片正在获取中,如果突然关闭页面,这个页面会造成内存泄漏吗?ImageView 内存泄露曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,原因是SDK中有个加载网络头像的方法,持有ImageView引用导致的。

当然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。

事实上,这种方式虽然解决了内存泄露问题,但是并不完美,例如在界面退出的时候,我们除了希望ImageView被回收,同时希望加载图片的任务可以取消,队未执行的任务可以移除。

Glide的做法是监听生命周期回调,看 RequestManager 这个类

public void onDestroy() {
    targetTracker.onDestroy();
    for (Target<?> target : targetTracker.getAll()) {
      //清理任务
      clear(target);
    }
    targetTracker.clear();
    requestTracker.clearRequests();
    lifecycle.removeListener(this);
    lifecycle.removeListener(connectivityMonitor);
    mainHandler.removeCallbacks(addSelfToLifecycle);
    glide.unregisterRequestManager(this);
  }

5. Glide加载一个一兆的图片(100100),是否会压缩后再加载,放到一个200200的view上会怎样,1000*1000呢,图片会很模糊,怎么处理?

根本原因:保存的时候,URl+宽高保存的。

当我们调整imageview的大小时,Picasso会不管imageview大小是什么,总是直接缓存整张图片,而Glide就不一样了,它会为每个不同尺寸的Imageview缓存一张图片,也就是说不管你的这张图片有没有加载过,只要imageview的尺寸不一样,那么Glide就会重新加载一次,这时候,它会在加载的imageview之前从网络上重新下载,然后再缓存。

缓存的key:

缓存一般通过键值对的形式,Glide以一个EngineKey对象当作key,缓存的键包括图片的宽、高、signature等参数。而OriginalKey对象其实就存了EngineKey的真实key,并重写其equals方法,所以这个对象就可以作为source资源的key,从source磁盘缓存读取时就以这个为key而写入磁盘缓存时,会将无论是EngineKey还是OriginalKey的字段通过SHA-256加密并转成16进制字符串当作名字存入本地

具体应用场景:缩图和全的时候!

面试官: 如何管理生命周期?Cache?如果一张下载一张大图,有两个尺寸不同的View去加载,在内存中几份缓存?

应该是2分

面试官:glide缓存策略?同一个图片跟size有关么

面试官:磁盘的缓存策略是怎么样的?

高效的缓存策略

A. 支持Memory和Disk图片缓存

B. Picasso 只会缓存原始尺寸的图片,而 Glide 缓存的是多种规格,也就意味着 Glide 会根据你 ImageView 的大小来缓存相应大小的图片尺寸

比如你 ImageView 大小是200200,原图是 400400 ,而使用 Glide 就会缓存 200200 规格的图,而 Picasso 只会缓存 400400 规格的。这个改进就会导致 Glide 比 Picasso 加载的速度要快,毕竟少了每次裁剪重新渲染的过程,非常灵活 & 加载速度快

硬盘缓存:这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:

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

DiskCacheStrategy.SOURCE: 表示只缓存原始图片。

DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。

DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在后面学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。

5.2.1 为什么需要两种磁盘缓存

举个例子,同一张图片,我们先在100*100View是展示,再在200*200View上展示
如果不缓存变换后的类型相当于每次都要进行一次变换操作,如果不缓存原始数据则每次都要去重新下载数据
如下可以看出,两种缓存的key不一样

面试官:如果修改了,缓存原始图片会怎样?

面试官:当图片没变,但是 图片的链接一直在变的时候,怎么缓存?

比如使用七牛云的时候,会在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:

http://url.com/image.jpg?token=d9caa6e02c990b0a

而使用Glide加载这张图片的话,也就会使用这个url地址来组成缓存Key。

但是接下来问题就来了,token作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果token变了,那么图片的url也就跟着变了,图片url变了,缓存Key也就跟着变了。结果就造成了,明明是同一张图片,就因为token不断在改变,导致Glide的缓存功能完全失效了。

解决办法就是重写 GlideUrl 的 getCacheKey() 方法,把会变的一部分的值给干掉,就可以解决问题。

public class MyGlideUrl extends GlideUrl {
    private String mUrl;
    public MyGlideUrl(String url) {
        super(url);
        mUrl = url;
    }

    @Override
    public String getCacheKey() {
        return mUrl.replace(findTokenParam(), "");
    }
    private String findTokenParam() {
        String tokenParam = "";
        int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
        if (tokenKeyIndex != -1) {
            int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
            if (nextAndIndex != -1) {
                tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
            } else {
                tokenParam = mUrl.substring(tokenKeyIndex);
            }
        }
        return tokenParam;
    }
}
//使用
Glide.with(this).load(new MyGlideUrl(url)).into(imageView);

我们需要在load()方法中传入这个自定义的MyGlideUrl对象,而不能再像之前那样直接传入url字符串了。不然的话Glide在内部还是会使用原始的GlideUrl类,而不是我们自定义的MyGlideUrl类。

6.Glide怎么做大图加载?

对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等
首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中
所以这种情况的优化思路一般是局部加载,通过BitmapRegionDecoder来实现
这种情况下通常Glide只负责将图片下载下来,图片的加载由我们自定义的ImageView来实现

7.Glide的缺点是啥?

问题: gilde是如何加载git图片的?会出现什么问题

Glide加载gif的卡顿优化思路分析

至此我们就知道了Glide加载Gif图片的原理了,就是将gif根据每一帧解析成很张图片,然后在依次设置给ImageView。

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

推荐阅读更多精彩内容