iOS进阶--App功耗优化看这篇就够了

一款好的App一定要有非常好的用户体验,这一点已经是大多数开发者的共识。功耗是用户体验中一个重要的组成部分,但这部分因为各种问题,很多时候会被大家忽略。之前公司让我在内部搞个功耗优化的培训,但我发现网上相关的文章非常少,而且大多不系统,也不够权威。索性找到苹果官方文档,边翻译边整理,就有了这边文章。内容有点长,大家可以收藏一下,以后慢慢看🤠

原创文章,如需转载请在下面留言让我知道😊。不留言不在开头标明出处链接的坏同学,1字1元索赔😡


当app更新UI、执行网络操作或者在CPU中运行代码时,都会消耗电能。随着用户越来越依赖电池、app数量的激增,电池能效成了用户体验的重要组成部分。

良好的用户体验需要如下要素:

电池寿命长。随着能效降低,电池寿命也会降低。但用户想让自己的移动设备全天候待命。

速度快。iOS系统处理复杂操作时仍能提供很好的性能。

响应快。同一时刻消耗太多资源会使UI卡顿,响应用户速度变慢。

温度低。app消耗的硬件资源的越多,系统工作越繁重,设备的温度就会逐渐上升。这时系统会通过一些措施降低设备温度。

iOS运用了很多先进的节能技术确保用户有很好的用户体验,包括软硬件配合优化、先进的App调度机制、网络延时操作、任务优先级管理机制等。

App中很小的低效行为在整个系统中累加后,会对电池寿命、性能、响应速度和温度产生明显的影响。作为app开发人员,我们有责任确保我们的app尽可能高效地运行。使用苹果推荐的API,以确保系统可以正确地判断如何更好地管理我们的app和app使用的各种资源。分批、减少网络操作。尽可能避免不需要的UI刷新。功耗大的操作应该在用户的控制之中。比如,如果用户正在玩一个视图非常复杂的大型游戏,电量消耗很快用户是可以理解的。不响应用户操作时,app尽量不要执行任何操作。


基本概念

没有一劳永逸地解决能耗问题的方案。很多技术和操作影响着电量的使用:

CPU. CPU是电能消耗大户,高CPU使用量会迅速消耗掉用户的电池电量。app做的每件事几乎都需要用CPU,所以使用CPU要精打细算,真正有需要时通过分批、定时、有序地执行任务。

设备唤醒。iOS设备通过睡眠来节能。只要设备被唤醒,屏幕和其他的硬件资源就必须通电,会产生很高的间接功耗。如非必须,app要尽量保持闲置,不要推送消息或用其他方式唤醒设备,特别是app在后台的时候。

网络操作。大多数app都需要网络操作。网络通信时,蜂窝数据和Wi-Fi等元器件开始工作就会消耗电能。分批传输、减少传输、压缩数据、恰当地处理错误,你的app省电效果会很显著。

图像、动画、视频。app内容每次更新到屏幕上都需要消耗电能处理像素信息。动画和视频格外耗电。不经意的或者不必要的内容更新同样会消耗电能,所以UI不可见时,应该避免更新其内容。

位置。很多app为了记录用户的活动或者提供基于位置的服务会进行定位。定位精度越高,定位时间越长,消耗电量也就越多。所以app应该尽量降低定位精度、缩短定位时间。不需要位置信息之后立即停止定位。

动作传感器。长时间用不上加速度计、陀螺仪、磁力计等设备的动作数据时,应该停止更新数据,不然也会浪费电能。应按需获取,用完即停。

蓝牙。蓝牙活动频度太高会消耗电能,应该尽量分批、减少数据轮询等操作。

CPU使用量和能耗

CPU使用代价很大。CPU使用量越大,功率越大,电能消耗越多,电池消耗也就越快。功率大小由于设备、处理器、其他硬件资源等会有所不同,表1基于闲置状态(idle state),给出了不同CPU使用量的一个大致的对比。

表1 CPU电量消耗

固定能耗(Fixed Cost)和动态能耗(Dynamic Cost)

iOS设备闲置时很擅长进入低功耗状态。即使是在两次键盘敲击的间隙这种毫秒级范围内,系统闲置时都能关掉很多硬件资源。

闲置时功率消耗很低,对功耗的影响很小。一旦开始执行任务,就会使用系统资源,这些系统资源都需要电能。但是,当设备什么都不干时,零散的任务会让设备进入一种中间状态,既不是闲置也不是活跃状态。在下个任务执行执行前,设备没有足够的时间从这些中间状态进入绝对闲置状态。这种情况下,电能就被白白浪费了。

我们app执行任务有动态能耗(dynamic cost)和固定能耗(fixed cost)。动态能耗也就是app实际工作消耗的能量。固定能耗是在任务执行前后把系统和各种资源调用起来和关闭所消耗的能量。出现大量零散的工作时,在零散的任务之间因为资源可能永远没法真正变为闲置,所以有动态能耗的同时固定能耗也很高。这种情况导致大量电能在相对较小的实际工作中流失了,如图1所示。

图1 固定和动态能耗

为减少固定能耗交换动态能耗

你的app可以通过分批执行、降低执行频率来避免产生零散的任务。例如,不采用同一个线程串行执行一系列任务,而是把任务同时放到多个线程,如图2所示。每次使用CPU,内存、缓存、总线等都得通电。通过分批执行,组件可以只上一次,使用时间也更短。

图2 用多线程交换功率

因为给定时间内做了更多的工作,需要更多能量,这种策略会导致更大的前期动态功耗。作为交换,固定能耗减小了,随着时间推移,这会极大地节省电能。你的app提高了功率,但它更高效,用了更少的时间。这使得CPU回到闲置状态,其他元件也更快地断电。

当开发app时,整个项目中应该全面地思考这种行为,尽量降低固定能耗。


简化、有序地工作

减少后台工作

实现UIApplicationDelegate中的方法,应用进入后台前做好暂停任务,保存数据等工作。如果确实需要完成用户执行的一些任务,应该调用UIApplicationDelegate中的beginBackgroundTaskWithExpirationHandler: 方法,这样后台任务可以继续执行几分钟。任务执行完毕后一定要调用endBackgroundTask:方法,不要等着系统强行挂起进程。

iOS8之后,系统引入了CPU监控机制,以观察后台app的CPU使用量是否超过了限制,如果超出限制,进程可能会被关闭。大多数情况下正常的后台任务不会遇到这种情况,如果遇到了可以查看崩溃日志信息,异常类型为EXC_RESOURCE,子类型为CPU_FATAL。

用QoS分级有序工作

多个app和众多操作需要共享CPU、缓存、网络等资源,为了保持高效,系统需要根据不同任务的优先级智能地管理这些工作。比如更新UI这种重要的事需要多分配资源,而一些后台任务可以延迟一些执行。服务质量(quality of service, 以下简称QoS, iOS8引入)级别可以通过NSOperation, NSOperationQueue, NSThread objects, dispatch queues, 和pthreads (POSIX threads)指定工作的优先级。有4种QoS级别(和2种特殊的级别),如表2所示,划线的两个特殊的级别一般不应该使用,仅作了解即可。最好情况是,用户不交互的时候,90%以上的时间让app运行在Utility或更低级别。

表2 QoS优先级

原来GCD中的全局并发队列(global_queue)用高、默认、低、后台来指定队列的优先级。现在应该改用QoS,两者的对应关系如表3。

表3 GCD全局队列和QoS对应关系

少使用定时器

app经常滥用定时器。想一下你app中的定时器,是否真的有必要存在。抛开具体场景不说,如果定时器触发太频繁,能耗影响是比较大的。

用事件通知代替定时器。有些app用定时器监控文件内容、网络或者其他状态的变化,这会导致CPU无法进入闲置状态而增加功耗。建议使用事件通知来代替定时器,比如使用dispatch source监测文件变化。对于系统提供的服务,尽量使用事件通知,表4是常见的系统通知和对应的监测方法。

表4 系统服务事件通知

GCD里的dispatch queues、dispatch semaphores等同步工具比定时器效率高很多,尽量不要用定时器做同步工具。

所有需要指定一个最后期限的函数或方法都属于定时器,比如:

1. 高级定时器包括dispatch timer sources、CFRunLoopTimerCreate和其他CFRunLoopTimer函数、NSTimer、performSelector:withObject:afterDelay:方法。

2. 底层定时器包括sleep, usleep, nanosleep, pthread_cond_timedwait, select, poll, kevent, dispatch_after, dispatch_semaphore_wait。

如果一定要用定时器,尽量高效地使用,可以参照下列指导方针:

1. 设置一个合适的超时时间。

2. 不再需要时及时关闭重复性定时器。

3. 设置触发公差。

优化I/O访问

app每次执行I/O任务,比如写文件,会导致系统退出闲置模式。而且写入缓存格外耗电。通过下列方法可以提高能效、改善app性能。

1. 减小写入数据。数据有变化再写文件,尽量把多个更改攒到一起一次性写入。如果只有几个字节的数据改变,不要把整个文件重新写入一次。如果你的app经常要修改大文件里很少的内容,可以考虑用数据库存储这些数据。

2. 避免访问存储频度太高。如果app要存储状态信息,要等到状态信息有变化时再写入。尽量分批修改,不要频繁地写入这些小变动。

3. 尽量顺序读写数据。在文件中跳转位置会消耗一些时间。

4. 尽量从文件读写大数据块,一次读取太多数据可能会引发一些问题。比如,读取一个32M文件的全部内容可能会在读取完成前触发内容分页。

5. 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问。

6. 如果你的数据由随机访问的结构化内容组成,建议将其存储在数据库中,可以使用SQLite或Core Data访问。特别是需要操作的内容可能增长到超过几兆的时候。

7. 了解系统如何缓存文件、如何优化缓存的使用。如果你不打算多次引用某些数据,不要自己缓存数据。

低电量模式

iOS9之后,iPhone增加了低电量模式,用户如果希望延长iPhone电池的寿命,可以在设置 > 电池中开启该功能。开启该功能之后iOS会采取一些措施,比如:

1. 降低CPU和GPU性能

2. 暂停随意的和后台的活动,包括网络

3. 降低屏幕亮度

4. 缩短自动锁屏时间

5. 关闭邮件刷新

6. 关闭视角缩放

7. 关闭动态壁纸

你的app也应该做一些事情帮助系统节省电能,比如,可以减少动画、降低帧率、停止位置更新、关闭同步和备份功能等等。可以通过向NSNotificationCenter注册NSProcessInfoPowerStateDidChangeNotification通知监听低电量模式状态。

网络操作

只要app一执行网络操作,就会产生大量间接能耗(overhead cost)。网络硬件,比如蜂窝数据和Wi-Fi电路,为了省电默认是不通电的。为了执行网络操作,这些资源必须通电,之后为了等待接下来可能出现的任务,它们在操作完成后会继续保持一段时间的活跃。零散的网络传输会导致很高的间接能耗,迅速消耗电池电量,如图3所示。

图3 多次网络活动产生间接能耗

缩减网络请求

1. 减少、压缩网络数据。可以降低上传或下载的多媒体内容质量和尺寸等。

2. 使用缓存,不要重复下载相同的数据。

3. 使用断点续传,否则网络不稳定时可能多次传输相同的内容。

4. 网络不可用时不要尝试执行网络请求,尽量只在Wi-Fi情况下联网。

5. 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间。

6. 网络请求失败后用SCNetworkReachability的通知监测网络状态,网络可用后再重试。

延迟联网

分批传输。比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果提供广告,一次性多下载一些,然后再慢慢展示。如果要从服务器下载电子邮件,一次下载多条,不要一条一条地下载。

网络操作能推迟就推迟。如果通过HTTP上传、下载数据,建议使用NSURLSession中的后台会话,这样系统可以针对整个设备所有的网络操作优化功耗。将可以推迟的操作尽量推迟到设备充电状态并且连接Wi-Fi时进行,比如同步和备份工作。

VoIP类应用应该用PushKit而不是长连接。

图像、动画、视频

以下列指导方针优化内容更新:

1. 减少app使用的视图数量。

2. 减少不透明视图的使用,比如视图上显示一个半透明模糊效果。如果要用不透明效果,避免用在内容频繁变化的地方。另外,由于内容变化后背景视图和半透明视图必须同时改变,这也会放大功耗。

3. 避免绘制不可见的内容,比如app的内容被其他视图遮挡、被剪切(clipped)或者出画了。

4. 动画尽可能用较低的帧率。比如,高帧率在玩游戏时有意义,但是菜单画面可能较低的帧率就够了。只有对用户体验有影响时才使用高帧率。

5. 执行动画时不要修改帧率。比如,你的app帧率是60fps,整个动画就保持这个帧率不要变。

6. 避免同时在屏幕上使用多种帧率。比如,你的游戏人物是60fps,天上的云彩移动又是30fps,不要出现这种状况,就算提高其中某一个的帧率,也要用相同的帧率。

7. 开发游戏时使用推荐的framework。这些framework针对性能和功耗是做过优化的:2D游戏用SpriteKit、3D游戏用SceneKit、画面非常逼真的游戏用Mietal。

全屏播放视频时iOS可以通过高效管理各种资源来优化能耗,但是在视频上下额外添加图层会影响功耗优化效果。app尽量不要在全屏视频上添加额外的图层(即使是隐藏的图层)。如果用户有需要,可以通过比如单击这样的方式来显示播放控制之类的UI,不需要了以后应该把这些图层移除掉。

优化定位和动作(Motion)

错误使用定位会阻碍设备进入睡眠模式,让定位硬件部分持续通电而消耗电池电量,这会使用户体验变的很差,下面来看一下如何针对功耗优化定位服务。

如果你的app只是需要快速确定一下用户的位置,最好用CLLocationManager的requestLocation (iOS9引入)方法。定位完成之后会自动让硬件断电。

除了导航,大多数app不需要一直实时更新位置。需要位置服务时开启一下定位,尽量多隔一些时间再进行下次位置更新,更新完了之后马上关掉定位。除非用户在移动的交通工具里,否则不频繁地更新位置一般没多大问题。

尽量降低定位精度。iOS设备默认采用最高精度定位,如果你的app不是确实需要米级的位置信息,不要用最高精度(kCLLocationAccuracyBest)或10米左右的精度(kCLLocationAccuracyNearestTenMeters)。一般来说Core Location提供的精度比你设置的要好,比如你设置为3公里左右的精度,可能会收到100米左右的精度信息。

如果定位精度一直达不到设置的精度时,停止更新位置,稍后再试。

需要后台更新位置时,尽量把pausesLocationUpdatesAutomatically设为YES,如果用户不太可能移动的时候系统会自动暂停位置更新。

后台定位时延时更新位置。如果要做一个健身类的软件追踪用户徒步的距离,可以等用户移动一段距离或者过一段时间之后再更新位置,这样可以让系统优化能耗。

合理使用访问监控(visit monitoring)。访问监控允许app接收用户频繁或长时间访问的场所的进出通知,比如在家、公司或者去喜欢的咖啡馆。

尽量不要用significant-change位置服务,优先考虑用region monitoring、 visit monitoring。

用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。不需要监测设备方向时停止通知。比如用户进入一个只需要竖着显示的画面,及时把方向改变通知关掉。开启动作事件前设置一个比较大的更新间隔。

优化通知

尽量用本地通知(local notification),如果你的app不依赖外部数据,而是需要基于时间的通知,应该用本地通知,可以让设备的网络硬件休息一下。

远程推送有两个级别,一个是立即推送,另一个是针对功耗优化过的延时推送。如果不是真的需要即时推送,尽量使用延时推送。

优化蓝牙通信

1. 没有必要的时候不要扫描蓝牙外设。

2. 扫描外设时一般不要用CBCentralManagerScanOptionAllowDuplicatesKey。

3. 只查找你需要的外设服务。外设可能提供很多服务和特性(characteristic),查找外设的时候可以指定UUID。

4. 不要轮询设备特性值,用通知监测特征值的变化。

5. 特性值不再提供通知或者不再需要通信的时候就断开连接。

Apple Watch

Apple Watch有很多节能的特征,而且watchOS的API也非常高效。但是仍然需要坚持下面的指导方针:

1. 减少iPhone和手表之间的通信,分批通信,用NSURLSession的后台会话延迟联网。

2. 去掉不必要的内容刷新。

3. 尽量用暗色,亮色会显著增加功耗。除了省电,暗色还可以让屏幕边框和显示内容融合得更好

4. 缩小媒体数据大小。如果你的app需要从服务器下载图片,下载适合手表屏幕尺寸的图片,不要下载大图再缩放,这样网络和CPU功耗都更高。

5. 少做工作。如果app需要复杂或者大量的处理任务,考虑将其发送给iPhone处理。

监测功耗

测试或者debug你的app时,注意下列情况:

电池消耗过快

app应该闲置时却活动

响应慢,UI卡顿

主线程执行大量任务

动画使用过多

不透明视图过多

切换应用

内存慢,没有缓存(Memory stalls and cache misses)

内存警告

Lock contention

频繁地切换context

过度使用定时器

频繁绘制屏幕

频繁或者反复执行很小数据的I/O操作

很高的通信间接功耗,比如传输零散的小数据包和缓冲

设备不休眠

用Xcode测量功耗

开发app的过程中是诊断能耗最好的时机。在Xcode中选择View > Navigators > Show Debug Navigator,这里提供了很多仪表用于分析功耗。Energy impact可以查看正在运行的app的功耗,如图4。

图4 Xcode中的功耗仪表

Cost 和 overhead。蓝色的是CPU执行任务消耗的电量,红色的是执行你的app消耗的其他系统资源电能。

CPU。灰色方块表示你的app正在使用CPU执行任务。

Network。灰色方块表示你的app正在进行网络操作。

Location。灰色方块表示你的app正在使用位置服务。

GPU。灰色方块表示你的app正在使用GPU执行图像相关操作,比如绘图或者播放动画。

Background。灰色方块表示你的app处于后台状态,但是让系统仍然保持唤醒状态。

和用户交互时功耗应该比用户选择一个复杂的操作时低,不交互时不应该有功耗。

使用Instruments之前应该先考虑用Xcode中的仪表检查功耗问题。

用Instruments检测功耗

1. 启动Instruments,选择你的设备和要检测的app,打开Energy Log,如图5。推荐使用无线方式连接设备,这样可以完全模拟使用电池工作的真实场景。将设备和Mac用数据线连接好,在图5页面按住⌥选择设备,会出现带有Wireless后缀的设备。

图5 启动Instruments

2. 点击Record按钮或者按⌘+R,开始记录。

3. 在设备上正常使用app,这时会记录功耗数据。

4. 点击Stop按钮或按⌘+R,完成记录。

5. 查看记录的数据有没有异常或者可以优化的地方,如图6。

图6 用Energy Log查看功耗相关记录

提示:app能耗偶尔比较高不一定是app的问题,可能当时的操作本身就很耗电,比如说执行网络操作的时候使用GPS。你应该关注的是峰值、出乎意料的高功耗区域和其他可以优化的地方。

用iOS设备直接记录功耗

不用有线或无线方式连接Instruments,直接用iOS设备记录功耗可以获得更真实的数据。记录工作几乎不耗电,可以全天候使用,即使设备进入睡眠模式也会持续记录。但是如果设备关机后,数据可能会丢失。

1. 在设备上进入设置 > 开发者 > Logging.

2. 开启功耗记录,如图7。

图7 开启功耗记录

3. 点击Start Recording按钮。

4. 正常使用设备。

5. 设备记录完成后返回图6所示页面,点击Stop Recording.

6. 在Instruments中选择好设备,进入Energy Log.

7. 选择File > Import Logged Data from Device。

使用其他模板和仪器检测功耗

有很多因素会影响app的功耗,Engergy Log这个模板可以分析一部分因素,你还可以用其他的模板或工具检查app对功耗的影响。

Activity Monitor. 查看CPU、I/O、网络使用。

Core Animation. 测量图像性能和CPU使用量。

GPU Driver. 测量GPU驱动数据。

Location Energy Impact. 测量Core Location对能耗的影响。

Metal System Trace. 通过追踪app、驱动、GPU的数据,检测iOS Metal应用的性能。

Network. 分析TCP/IP 和UDP/IP 连接。

Time Profiler. 该工具检测app正在运行的线程,隔一段时间采样一次。每个采样都有完整的调用栈(backtrace),你可以找出你的代码中哪里耗费了大量时间。

自定义模板。上面的模板或仪器可以分析app的多个方面。如果你想查看特定的几个方面,可以向Instruments中添加单个的仪器。如果以后可能还要用相同的分析类型,可以把你配置的工具保存成模板。

测试性能

app的性能下降会导致功耗增加,可以用Xcode中的XCTest框架测试你的代码。代码会在性能测试的block中连续运行10次,并给出运行平均时间的标准差。

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