【学习总结】03 | Auto Layout 是怎么进行自动布局的,性能如何?

1、前言

首先,我认为学习总结,要有所总,所结,就是有归纳后,能用自己的话告诉别人!有所结,就是有所收获输出,一般我认为是思维导图,所以,每篇文章前,我都会先给出文章的脑图:

iOS开发高手课-03-AutoLayout是怎么进行自动布局的,性能如何?.png

2、正文

注意,本系列总结不会引用或提供原课程文章所有的内容或代码,只会作出思维导图,需要学习可购买课程 《iOS开发高手课 - 极客时间》

带问题找答案

  1. Auto Layout 如何实现自动布局的?
  2. 这种布局算法真的会影响性能吗?
  3. 应该选择手动布局还是选择Auto Layout呢?

文中提了3个问题,那么这3个问题怎么解答呢?可以利用第一篇文章说的,一个知识点的方法论来解答。这里就不展开了,具体可以查看 如何建立你自己的开发知识体系 | iHTCboy's blog

Auto Layout

  1. 为什么需要 Auto Layout
  2. 什么是 Auto Layout
  3. 怎么使用 Auto Layout
  4. 使用 Auto Layout 时注意的问题
  5. Auto Layout 的应用领域
  6. Auto Layout 的优缺点
  7. Auto Layout 触类旁通

1. 为什么需要 Auto Layout

为什么需要?一般遇到解答不了的问题,可以试试逆向!,那就反推,就是 没有 Auto Layout 之前是怎么样的 ?没有 Auto Layout 时,我们是通过设置元素的 Frame 来手动指定界面布局的大小和位置。

刚开始,大家的App并不复杂,页面布局也很简单,并且有一个历史原因,就是 iPhone4/s(960x640像素)时代 (更早的 iPhone 3G/S,3G网络,S是速度 Speed,因为08、09年那会,一般国内开发者估计都没有见过,所以这里就简单提一下。) ,iPhone 的宽度都是 640 像素, 开发时用 320 个点计算, 直到 2012年9月发布 iPhone5 (1136x640像素),屏幕高度增长了!4寸,当时三星都开始5寸大屏了,大家当年吐槽iPhone长的图片大家可以搜索看看!此时此刻,适配 iPhone5 依然并没有太大难度,苹果默认针对没有适配 iPhone5 的App,在 iPhone5 打开App时,上下2端黑屏,这样来过度。

就在当年,2012年的 WWDC2012 苹果发布了 Auto Layout 技术,从 iOS6 以后开始支持(Xcode4)。2013年9月发布 iPhone5s,但是大多数的开发者还是习惯使用传统的UI布局方式,大家的开发App布局并不会觉得麻烦,直到2014年才发生变化!

2014年9月,苹果发布了 iPhoe 6(1334x750像素)、iPhone 6 plus(1920x1080像素),屏幕适配工作变得非常必要!如果用计算数值的方法工作量增加了几倍。因为4个尺寸的屏幕不一样!iPhone4/s(960x640像素)、iPhone 5/s (1136x640像素)、iPhoe 6(1334x750像素)、iPhone 6 plus(1920x1080像素),虽然大家还是可以按比例计算做缩放,但是这样并不能解决所有问题,因为如果想像素级还原设计、效果图调整尺寸等,可能都需要重新手动计算一次。所以,这样算过程,应该是2014年后才是 Auto Layout技术被大家广泛应用。我在 GitHub 查看了 iOS 最经典的 Masonry 库是 2013年7月22号 创建的,也是符合这个技术历史的进程啊~ (ps: Masonry 源码值得研究学习,有很多可学习的知识,详细搜索引擎一下,已经有很多好的文章啦~)

那么到此,大家明白了,为什么需要 Auto Layout 了吗?

2. 什么是 Auto Layout

Auto Layout 是一种基于约束的、描述性的布局系统。也就是使用约束条件来描述布局,View 的 Frame 会根据这些描述来进行计算。

3. 怎么使用 Auto Layout

2012年,Xcode4,iOS6 引入了 NSLayoutConstraint 类,并且 VFL (Visual Format Language,视觉格式语言) 的方式创建约束。通过下面2个方式来生成布局约束组:

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(NSDictionary *)metrics views:(NSDictionary *)views;
+(id)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

关于具体使用和 VFL 相关使用可以查看官方文档:Auto Layout Guide: Visual Format Language,所有学习资料官方文档是第一手!

4. 使用 Auto Layout 时注意的问题

Constraint Churn(约束流失)
  1. Avoid removing all constraints (避免删除所有约束)
  2. Add static constraints once (仅一次添加静态约束,且不要再改变它们)
  3. Only change the constraints that need changing(只更改需要更改的约束)
  4. Hide views instead of removing them(隐藏视图而不是删除它们)

注:来自:High Performance Auto Layout - WWDC 2018 - Videos - Apple Developer

Summary
  1. Stack Views help build easily maintainable layouts (堆栈视图有助于构建易于维护的布局)
  2. Use activate and deactivate for constraints (使用激活和停用来约束)
  3. Determine size through constraints (通过约束确定尺寸)
  4. Override intrinsicContentSize judiciously (明智地覆盖内在内容大小)
  5. Use priorities to properly solve your layout (使用优先级来正确解决您的布局)
  6. Alignment goes beyond top, bottom, and center (对齐超出顶部,底部和中心)
  7. Keep localization in mind (记住本地化)

注:来自:Mysteries of Auto Layout, Part 1 - WWDC 2015 - Videos - Apple Developer

以上注意事项来自 WWDC,具体视频可参考文章末尾的引用来源。这里不打算详细解说,因为视频真的说的很好,推荐大家去看看啊。

5. Auto Layout 的应用领域

Auto Layout 其实,除了可以用代码来创建,苹果的可视化布局,也有使用,nibxibstoryboard,那么大家对这3个东西了解熟悉吗?

Interface Builder 工具

Interface Builder 在 Xcode 4 之前,是一个独立的软件,Xcode 4 开始集成到Xcode 中的。这个大家现在比较熟悉,就是可以用鼠标以图形化的方式,拖拉元素来创建UI界面。

NIB、XIB 区别

Xcode 3 前,Interface Builder 创建的文件是 NIB(二进制格式,NeXT Interface Builder),不利于版本控制。

Xcode 3 开始,Interface Builder 使用了一种新的文件格式 XIB(XML文本格式,OS X Interface Builder或XML Interface Builder),XIB在工程编译时被转换成NIB;

XIB 文件

XIB 是一个描述文件,包含了用户界面及相关元素;一个 XIB 文件对应一个ViewController,也可以通过使用XIB来自定义View。

StoryBoard 故事板

iOS 5 (Xcode3)开始,Apple提供了一种全新的布局界面方式 StoryBoard 来拖拉创建界面;StoryBoard 是一组 ViewController 对应的 XIB,以及它们之间的转换方式的集合;在StoryBoard 中,不仅可以看到每个 ViewController 的布局样式,也可以知道各个ViewController 之间的转换关系。

对于2013~2015年,当然非常的缺乏 iOS 开发者,所以一般的公司只有一个 iOS 开发,那么这时候,StoryBoard 就是最快速的创建界面的工具!此时,面对多种设备时,Auto Layout 就是锦上添花,可以大幅提高 UI 开发效率,一次性做出适合所有屏幕尺寸的 UI。

现在,对使用 StoryBoard + Auto Layout 还是使用 代码 + Masonry/SnapKit,依然没有最终的答案,因为各有优缺点。

6. Auto Layout 的优缺点

Auto Layout的优点不用多说,解决手动计算每个屏幕尺寸的布局,提高工程效率!缺点的话,大概就是适当的导致性能降低?

Auto Layout 导致性能降低?是吗?为什么是?为什么不是?前面的“什么是 Auto Layout”只是简单的简介,Auto Layout 是一个 布局系统,没有深入的介绍,不知道大家有没有读到那里时,产生疑问或兴趣呢?

所以,要知道 Auto Layout 的优缺点,还需要深入了解它的原理,才能理解优缺点!否则,死记硬背过后还是不明不白。Auto Layout 是一套 布局引擎系统,叫作 Layout Engine ,是 Auto Layout 的核心。了解 Layout Engine 的布局原理,是理解它的性能(优缺点)的基础。

所以在这里补充一下,主要参考苹果官方的 WWDC 视频来解说 Layout Engine,引用主讲者 Jason Yao 说的:我们并不只想说这样不好,我们相信大家真正的理解它,理解这个过程!剥开表面!了解真正发生了什么?

The Render Loop (渲染循环)
01-The-Render-Loop.jpg

布局引擎是工作流程如上图所示,Update Constraints(更新约束)流,从父视图的约束开始更新,传递到子视图,再到子子视图,这里有2点要说明,一是这个更新约束只是从有约束变化的视图开始,并不是所有视图都更新;二是这个传递过程为什么是从父级开始,因为当一个约束变化时,这个约束是自己与父级的关系或自己与子级的关系,所以会通知给子级视图来响应!Layout(布局)流,是从子视图开始布局,为什么是反向呢?一个视图的布局,它一定是由自己和所有的子视图组成,那自己的布局一定是受子视图的布局和约束影响,所以先确认所有子视图的约束,那自己的视图就确实了,这个可能有点绕,大家可以看看原视频来理解。Display (渲染显示)流,因为子视图布局确定了,那显示的大小和位置就能确定,所以也是从子视图开始显示。

What is updateConstraints?(什么是 updateConstraints ?)
02-The-Render-Loop.jpg

updateConstraints() 是视图的约束更新时会调用的方法,可以重写这个方法来自行设置约束条件。图中列出了 Update Constraints(更新约束)、Layout(布局)、Display (渲染显示) 的对应关系的方法。这里与第一个图并不是对应的关系!而是这3个状态的生命周期分别有对应的方法来响应,要怎么理解?

要理解这个图,最简单是从了解 layoutSubviews()setNeedsLayout()layoutIfNeeded() 三者的关联和作用,明白这3个方法的作用,那么就知道这个生命周期是什么意思。

setNeedsLayout():当一个UIView对象此方法时,实际上等同于做了一个标记,告诉系统需要重新布局,但不会立刻执行,直到 drawing cycle 循环到达该节点时,才会调用layoutSubviews() 方法重新布局。

layoutIfNeeded():允许在 drawing cycle 循环到达该节点之前,就立刻执行布局刷新调用 layoutSubviews() 方法。

layoutSubviews():在上面2个方法调用后,都会被调用。另外,当 addSubviewsize 改变或滑动UIScrollView、旋转Screen等都会触发。

回到 updateConstraints()setNeedsUpdateConstraints()updateConstraintsIfNeeded(),那它们的作用也是对应的关系。现在剩下的问题就是,为什么需要这些方法???我们在开发中,是不是会改变视图后,更马上刷新这个页面就会用到这些方法。那么同时,我们改变(更新)约束后,是不是也希望刷新约束?所以,这些方法的作用就是这样,那么为什么要这样做,有什么好处呢?因为约束更新,需要重新计算一次,那我们可以自己来控制要不要更新,能减少Constraint Churn(约束流失),减少性能损耗。为什么?举例为说,你给一个Lable设置了新的字体,此时要不要马上更新约束?如果你下一句代码是设置新的字体大小呢?所以,你可以控制一组约束,什么时候才更新,现在明白了这些方法和它们对应的关系了吧。剩下的 draw(_:)setNeedsDisplay() 也是同理,只是层级更底,渲染的调用刷新。

Activating a Constraint(激活一个约束)
03-Activating-a-Constraint.jpg

说了这么多,好像还没有说到 Layout Engine 布局引擎的工作流程。这个图就是这个流程,每个 Window 下有一个 Engine(引擎),那么 View(视图) 包含 Variable(变量)和 Constraint(约束),那么 Engine 与 View 之间通过 Equation(等式) 联系。

那么要理解布局引擎的工作,其实很简单,就是Constraint约束描述了View视图的位置和大小(size),那么就通过 Variable(变量)来向引擎获取,需要获取什么?
minY minX height width,那现在引擎需要做的就是把约束解析出这4个值,所以就是通过 Equation(等式)求解。Equation等式其实也很容易理解,就是像下面这些:

 text1.minX = 8
 text1.width = 100
 text2.minX = text1.minX + text1.width +20

最后解出等式,等到结果:

 text1.minX = 8
 text1.width = 100
 text2.minX = 128

所以,布局引擎的核心就是这个等式的计算,我们初中就算了一元二次方程式,后来学习了多次方程式求解,是不是觉得很简单?那么这些数学问题,我们读书时就知道数字题目答案只有一个,但是解法有很多!,所以原文章提到 Cassowary 算法Simplex 算法 就是解决这个方程式求解的问题。我们人可以做一些骚操作,但是要程序操作,一定是通用的解法,这也是算法为什么难!数学为什难!因为找到规律的人,往往是牛逼的人!

Equation(等式)求解的结果,通过 setNeedsLayout() 通知所有的视图,视图通过 UIView.layoutSubviews() 方法从引擎中复制数据到子视图(Copying data from engine to subview)。

所以,这个就是整个引擎工作的过程!不知道我说的明白不明白!理解了这个过程,就能回复前面的 Auto Layout 会导致性能降低?从原理上说,引擎的工作方式和我们开发者手动计算的过程是一样的!导致性能降低的原因,是因为一些约束流失(Constraint Churn)、不可满足约束(Unsatisfiable Constraints)等导致消耗大量计算。可能你会有疑问:为什么引擎不会做傻事呢?比如我自己计算的布局,我都缓存了,引擎做了吗?答案是肯定的,Engine is a layout cache and tracker(引擎是一个布局缓存和约束跟踪器),也就是你设置的布局,如果不必更新是不会更新约束的;同时,约束更新时引擎会知道那些约束需要重新计算,那么不需要再计算!所以,引擎系统导致性能降低本质来说可以忽视,剩下的就是,如何避免我们设置约束时不合理导致性能降低呢?可以参考前面一节:“4. 使用 Auto Layout 时注意的问题”,详细的所有原则这里就不展开了,下面会重点提几个。到此,引擎的原理问题算是解决啦!

Building a More Performant Layout(构建一个更好的性能布局)
04-Building-a-More-Performant-Layout.jpg

这些列举了我们常见的 TableView 滚动卡顿的问题,其实我们知道原因,就是滚动过程中 Cell 要重绘,需要我们知道 Cell 可以重用,但是每个 Cell 的布局和长度可能千变万化,所以卡顿的问题,有一个就是 Cell 变化过程中,重新布局的问题。首先,避免删除所有约束,因为所有约束重新计算可能不是必要的,比如上图的用户头像的位置和大小,固定约束后就不要去改变它啦!那么,可能有一些 Cell 有图片,有一些 Cell 没有时,可以通过 setHidden: 方法 和 noImageConstraints 图片的单独约束来控制,这样,尽最大可以减少约束的计算,导致性能的降低,从而尽可能的避免卡顿!另外,使用 Auto Layout 可以多使用 Compression Resistance PriorityHugging Priority,利用优先级的设置,让布局更加灵活,代码更少,更易于维护。

以上内容来源: High Performance Auto Layout - WWDC 2018 - Videos - Apple Developer

Independent Sibling Views(独立的兄弟视图)
05-Independent-Sibling-Views.jpg

前面说到 如何避免我们设置约束时不合理导致性能降低呢?我们已经知道引擎需要计算方程式求解,对于每个元素视图单独约束,相互不依赖时,其实就是解决一元一次方程式,所以就是一条直线。对于我们开发来说,避免约束的相互依赖就能减少性能消耗,所以我们设置约束时,能不依赖的约束的视图,让他们保持独立!就是最优解!

Dependent Sibling Views(互相依赖的兄弟视图)
06-Dependent-Sibling-Views.jpg

对于相互依赖的约束,它们就构形了多元方程式,依赖关系越多,曲线就越陡峭。所以,我们能做的还是一样,尽量减少多个视图之前的约束依赖!

Nested Views(嵌套的视图)

07-Nested-Views.jpg

对于嵌套的视图,同理,减少视图的嵌套,减少嵌套的层级,都是解决性能的重要手段!

以上内容来源:What's New in Cocoa Touch - WWDC 2018 - Videos - Apple Developer 。更多的技巧,可以参考前面一节:“4. 使用 Auto Layout 时注意的问题”。

Building the Layout(构建布局)
08-Building-the-Layout.jpg

前面只是简单的说明了引擎就是计算出视图的位置和大小,那么具体是怎么计算的呢?这个图片显示了布局引擎的工作流。每个视图在得到自己的布局之前,Layout Engine 会将视图、约束、优先级、固定大小等通过计算转换成最终的位置和大小。

09-Building-the-Layout.jpg

所以,最终的 Layout Engine 计算到布局就是这样的过程,细节点还有很多。还是那句话,授人以鱼不如授人以渔!大家有“渔”后,自然要自己捉鱼!,这里就不详细解进了,可观看 WWDC 视频:Mysteries of Auto Layout, Part 1 - WWDC 2015 - Videos - Apple Developer

最后,关于 Auto Layout 还有很多知识,比如怎么调试 Auto Layout 的 Debug?可以查看 What's New in Auto Layout - WWDC 2016 - Videos - Apple Developer 视频。Auto Layout 的流程(The Layout Cycle),可以查看 Mysteries of Auto Layout, Part 2 - WWDC 2015 - Videos - Apple Developer

7. Auto Layout 触类旁通

UIStackView 与 Flexbox

UIStackView 是2015年 iOS9 苹果推出的一套 API,它可以很好地减轻手动写或拖 constraint 带来的重复繁琐的工作,也可以自动化的处理排列和元素个数的变化。(当年因为需要iOS9+,导致很少有开发者使用,放在2020年的今年,这个控件可以熟悉一下啊!)与 Web 前端的 Flexbox 响应式布局是一个原理。UIStackView 特点有下面4个:

  1. Easy to build(容易构建)
  2. Easy to maintain (容易维护)
  3. Composable Stack Views (可组合的堆栈视图)
  4. Lightweight (轻量级)

这里并不打算讲解 UIStackView 有多利害!确实它很利害。具体可以查看 WWDC 演示的Demo Mysteries of Auto Layout, Part 1 - WWDC 2015 - Videos - Apple Developer

关于 Flexbox 的思想可以看看文章 30 分钟学会 Flex 布局 - 知乎Flex 布局教程:语法篇 - 阮一峰的网络日志,个人觉得文章写得很好。另外关于 UIStackView 的强大就借 UIStackView 入坑指南 - 掘金 文章的一张图来总结吧:

10-UIStackView-Layout.png
SwiftUI
  • 命令式编程(Imperative Programming):命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
  • 声明式编程(Declarative Programming):告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

SwiftUI声明式编程,还有函数式编程响应式编程 等编程思想,这里就不说了。这里提的原因是,SwiftUI 采用不同的布局方式,但是依然使用 Auto Layout,并且 VStack 也是天合之作吧!

3、总结

首先,关于 iOS 、 Xcode 和对应年份的关系,可以梳理一下,2012年 Xcode4 对应 iOS 6 ,2013年 Xcode5 和 iOS 7。它们相差2,所以,2020年将发布 Xcode12 和 iOS 14。这个就是一个数字游戏,可记可不记,就是想说明,记忆可以找规律的。

回到最前面提到的3个疑问题,你是不是已经有了自己的答案了呢?

  1. Auto Layout 如何实现自动布局的?
  2. 这种布局算法真的会影响性能吗?
  3. 应该选择手动布局还是选择Auto Layout呢?

本章内容够不够深不深入?这个大家的了解水平不一样,如果觉得不够深入,还是可以参考本文末的参考扩展了解更多,因为 WWDC 视频中提到了很多细节的东西,有一些很棒,有一些很有趣,这里就不一一列出。因为原文还提到了 Cassowary 算法Simplex 算法,本文并没有把它作为主角色来解读,为什么呢?因为,它并不是我们了解和理解 Auto Layout 最核心的必备知识,并且它对技术有要求,不可能每个人都能看懂,否则文章一上来就会让读者害怕,适得其反。所以,在本文的基础上,如果大家还想深入了解,那这将是一道窗口,而不是一道门口!

以后关于 Auto Layout 的知识,你是不是能更好的跟别人讲解呢?这些就是本文想要做的事情,当然还有更深入的知识可以研究,切记这只是开始!修行在个人~


注:更多关于 iOS 开发和程序开发相关的内容,可以查看系列,目前还在连载中 【学习总结】iOS开发高手课 -- (连载中) | iHTCboy's blog,以上,希望对你有用!

参考

WWDC:

Article:


  • 如有侵权,联系必删!
  • 如有不正确的地方,欢迎指导!
  • 如有疑问,欢迎在评论区一起讨论!


注:本文首发于 iHTCboy's blog,如若转载,请注来源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容