高性能iOS翻译-第2章:性能测量

iPhone和iPad在内存方面是资源紧缺的设备,一个app如果超过了最大使用限制它可能被系统干掉的,因此实现一个iOS app管理好内存是非常重要的。

在WWDC 2011大会上面,苹果公司透露,大约90%的设备崩溃都与内存管理有关,轻重最大的原因是内存访问错误或者内存泄露导致的循环引用。

不像java运行时使用的shi是垃圾回收机制,iOS使用的是引用计数,引用计数的缺点就是如果开发者不注意可能重复释放内存和循环引用。

因此在iOS中理解内存管理是非常重要的。

在这一章中我们将学习一下知识:

  • 内存消耗(例如:一个app怎么消耗内存的)
  • 内存管理模型(例如:iOS运行时怎么管理内存)
  • 语法:我们将看看一些我们能看到的OC语法特性。
  • 最小内存实践而不影响用户体验。

内存消耗

内存消耗是指一个app消耗的RAM(随机存取存储器(英语:Random Access Memory,RAM),是与CPU直接交换数据的内部存储器,也叫主存).

iOS虚拟内存模型不包括交换内存,这就意味着,不想桌面应用磁盘不能用于内存分页,这直接导致的是app受到可用RAM的限制,RAM不仅用于前台应用同时也被系统服务或者其他应用的后台任务占用。

在一个app中有两部分的内存消耗:栈大小和堆大小。接下来我们将深入讨论这两点。

栈大小

在应用中的每个新线程栈空间的构成包括预留和初始化两部分,当线程退出的时候栈空间就会被释放,一个线程的最大栈尺寸是很小的,除此之外,他还限制了以下东西:

最大数量方法被递归访问

每个方法都有他自己的栈框架,来消耗整个栈空间大小。例如例子2-1显示,如果我们访问main方法它接着又访问了method1 又访问了method2,所有有三个栈框架来消耗一些字节空间,图2-1显示了一个线程堆栈的样子。

例 2-1

 main() {
        method1();
}
    method1() {
        method2();
}

你可以在一个方法中使用的最大变量个数

栈框架中的变量都会被加载,所以会消耗栈空间。

你可以在嵌套视图结构中使用的最大数量视图

渲染一个复杂视图将会调用layoutSubViews和drawRect方法来递归访问整个层次结构树,如果层次结构太深,那么它可能导致栈溢出

图 2-1

堆大小

一个进程中所有的线程共享同一个堆栈空间。可用的堆空间一般都比设备的RAM小,例如iPhone5s可能有1G的RAM,但是app能使用的堆空间可能只有512M或者更少。app还不能控制堆的分配,他被系统管理。

例如使用NSString、加载图片、创建或使用JSON/XML数据和使用视图都将消耗许多堆内存。如果你的app需要使用到大量的图片(像Flickr和Instagram),你需要特别注意内存的平均值和峰值使用。

图2-2显示了一个在应用中可能出现的一个典型的堆。

在图2-2中主线程在main方法中创建了一个UIApplication,我们假设窗口在某个时间点使用了UITableViewDataSourcelai来展示UITableView,当需要显示一行的时候需要调用tableView:cellForRowAtIndex:.

数据源是引用到一个所有需要显示图片的photos数组。 如果实现不合理,这个数组可能会很大,结果导致高的内存消耗。一个解决方案是存储固定数量的那些显示在用户滚动区域的。这固定的数量将决定你的app的平均内存使用。

图2-2

在数组中的每一个对象都是HPPhoto对象,HPPhoto存储和对象关联的数据,例如图片大小、创建日期、作者、标签、图片URL、本地缓存等等。

从类中创建与对象关联的所有数据都存储在堆中。

这个类中有些值类型实例变量例如int、char或者struct,但是因为对象是创建在堆中,所有他们将消耗堆内存。

当对象创建后赋值,他们可能从栈复制到堆中,同样当一个值是被用在一个方法里面的时候,他们可能从堆复制到栈当中,这是一个非常昂贵的开销,示例2-2中是从栈复制到堆中,反之亦然。

示例2-2 堆和栈

@interface AClass 1
@property (nonatomic, assign) NSInteger anInteger; 2
@property (nonatomic, copy) NSString *aString; 3
@end
//some other class
-(AClass *) createAClassWithInteger:(NSInteger)i
    string:(NSString *)s {4
    AClass *result = [AClass new];
    result.anInteger = i;5
    result.aString = s;6
}
-(void) someMethod:(NSArray *)items {7
NSInteger total = 0;
NSMutableString *finalValue = [NSMutableString string];
for(AClass *obj in items) {
total += obj.anInteger;8
[finalValue appendString:obj.aString];9
} }

  1. AClass类有两个属性
  2. anInteger是一个NSInteger类型,值传递
  3. aString是一个NSString类型,引用传递
  4. createAClassWithInteger: string:方法用于实例化AClass,这个方法提供创建对象的必要值。
  5. 值i是在在栈上面的,当赋值给属性的时候他必须从栈上复制到堆,因为这是结果存储的位置。
  6. 尽管NSString *是引用传递,这个属性是被标记为copy,意味着这个值必须被复制或者克隆,依赖于-NSCopying copyWithZone:方法是怎么实现的。
  7. somethod:方法用于处理AClass对象中的一个数组。
  8. 当anInteger被调用的时候,他的值必须在使用之前复制到栈中,这个值被加到total中。
  9. aString调用的时候,他是引用传递,appendString:方法是用的aString对象的引用。

建议:维持内存不超过可用的RAM是一个好办法,虽然没有硬性规定,但是建议不要超过80%-85%,其余的用于核心操作系统服务,不要忽视didReceiveMemoryWarning信号。

内存管理模型

在本节中我们将学习iOS运行时如何管理内存和影响代码的。

内存管理模型是基于一种拥有关系概念的,只有被一个对象拥有,那么它使用的内存就不能被回收。

当一个对象在方法中被创建的时候,那么这个方法拥有这个对象,如果这个对象从方法中返回,然后被调用,那么调用者也拥有这个对象,这个值被分配到另外一个变量,对应的变量也同样拥有这个对象。

一旦对象的任务完成,你将释放拥有权,这个过程不会转移拥有权,但是会增加或减少对象的拥有计数,当对象的拥有者数量降为0的时候,这个对象将会被回收并释放内存。

拥有关系计数通常被说成ARC,当你自己管理他的时候交MRC,尽管MRC很少用,但是,MRC对于理解内存管理模型是很有用的,Modern-apps是用ARC的我们将讨论ARC在39页。

示例2-3 演示了用引用计数管理内存的基本结构。

示例 2-3 引用计数手动管理内存

NSString *message = @"Objective-C is a verbose yet awesome language";1
NSString *messageRetained = [message retain];2
[messageRetained release];3
[message release];4
NSLog(@"Value of message: %@", message);5
  1. 对象创建,message拥有对象,引用计数为1
  2. messageRetained拥有对像,引用计数为2
  3. messageRetained释放拥有权,引用计数为1
  4. message释放拥有权,引用计数减到0
  5. message的值严格的说是不确定的,可能得到之前一样的值,因为他可能还没有被重用或者重置。

示例 2-4 演示了方法如何影响引用计数

示例 2-4 方法中的引用计数

//part of a class Person
-(NSString *) address {
    NSString *result = [[NSString alloc]
initWithFormat:@"%@\n%@\n%@, %@",
self.line1, self.line2, self.city, self.state]; 1
    return result;
}
-(void) showPerson:(Person *) p {
    NSString *paddress = [p address]; 2
    NSLog(@"Person's Address: %@", paddress);
    [paddress release]; 3
}
  1. 对象被创建,result的引用计数为1
  2. 被paddress引用之后result的引用计数还是1,因为showPerson:是对象所有者创建使用地址按钮,它不应该拥有result;
  3. 释放所有权,引用计数为0;

看示例2-4 showPerson:方法不知道address是应该新建还是复用,但是他确实知道这个对象是引用计数加1之后被返回的,因此他不能拥有address,一旦工作完成,它将释放address,如果这个对象引用计数为1那么他将变为0然后被释放。

苹果的LLVN文档比较喜欢用拥有关系,在本书中拥有关系和引用计数是可以互换的。

自动释放对象

自动释放对象允许延迟释放对象的拥有关系,他有用的一个场景是当我们在一个方法中创建一个对象想要返回他的时候,他能帮助MRC管理对象的生命周期。

严格意义上从OC的语法上讲(谁创建谁释放),在示例2-4中,没有展示address方法拥有返回值,因此调用者showPerson:没有理由来释放这个返回值,因此就存在一个潜在的内存泄露[paddress release]这段代码是为了演示的目的。

那么什么才是address方法的正确写法

有两种方法:

  • 不要用alloc相关联的方法
  • 使用延迟消息返回一个对象

因为使用的是NSString所以修改很容易实现,更新代码像示例2-5

示例 2-5

-(NSString *) address {
    NSString *result = [NSString
stringWithFormat:@"%@\n%@\n%@, %@",
self.line1, self.line2, self.city, self.state]; 1
    return result;
}
-(void) showPerson:(Person *) p {
    NSString *paddress = [p address];
    NSLog(@"Person's Address: %@", paddress);
    2
}
  1. 没有使用alloc方法
  2. 在showPerson:方法中不要使用relead因为他没有创建实例

但是我们如果不是使用的NSString我们将不好修复的,因为通常很难找到合适的方法来为我们所需要。例如我们使用一个第三方的类库或者一个类有多种方法来创建对象,它通常不容易发现哪个方法是维持拥有关系。

因此还有什么办法来处理呢。

NSObject协议定义了一个消息autorelease能用于延迟释放,当我们从一个方法返回对象的时候使用它。

更新代码2-6

示例2-6 使用autorelease引用计数

-(NSString *) address
{
NSString *result = [[[NSString alloc] initWithFormat:@"%@\n%@\n%@, %@",
self.line1, self.line2, self.city, self.state]
autorelease]; 
return result;
}

这段代码能按下面步骤进行分析:

  1. 你拥有通过alloc创建的返回对象
  2. 为了确保没有内存泄露,必须在失去引用前释放拥有权
  3. 因此如果使用了release 对象将会在方法返回前被释放,结果就是返回一个不可用的引用
  4. autorelease的作用就是你想释放拥有权但是同时又想让调用者在这个返回对象释放是之前使用。

使用autorelease当我们创建一个对象从不是alloc方法放回的时候,他能确保调用者一方完成工作,对对象的释放。

Autorelease Pool Blocks 自动释放池块

自动释放池块允许释放对象的拥有权但是避免他不会被立刻被回收。这是非常有用的对于从一个方法中返回一个对象。

他也能确保一个在block中创建的对象在block执行完成的释放,因此放我们需要创建多个对象的时候是非常有用的(for循环里面使用autorelease)。本地块的创建能让对象竟可能早的释放所以能保持内存在一个比较低点。

一个自动释放池kuai块是用@autorelease 标记

如果你打开项目的main.m文件,你会看到示例2-7代码。

示例 2-7 @autoreleasepool block in main.m

int main(int argc, char * argv[]) { @autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([HPAppDelegate class]));
} }

所有包含在block里面的对象都会发送一个autorelease消息同时在autautorelease block的结尾会发送一个release消息。也就是说一个对象被发送autorelease消息那么同时也会发送一个release消息,这是好的,因为他将维持对象的引用计数和加入autorelease块之前一样,如果引用计数为0,对象将会被回收,保持较低的内存占用。

如果你看在main中的方法,你将看到app的入口都包含在autorelease块中,这意味着任何对象在最后都会被释放,不会有内存泄露。

另外的一些block代码,autorelease块是可以被嵌套的,像示例2-8

示例 2-8. Nested autoreleasepool blocks

@autoreleasepool { // some code 
@autoreleasepool {
  // some more code
} }

因为控制从一个方法传递到另外一个方法,autorelease嵌套是常见的,,然而被调用的方法可能有使用autorelease块来回收要提前释放的对象。

自动引用块是无处不在的:Cocoa矿建希望代码都在autorelease块中执行的,否则autorelease的对象是不会被释放的导致app内存泄露,AppKitheUIKit框架都使用autorelease 块来处理事件的迭代循环,因此一般都不要自己创建。

因此有些场合,你可能需要创建autorelease块,例如:

当你有一个循环需要创建许多零时对象

用autorelease pool block在循环代码中来释放每次迭代的内存。 虽然最终的内存使用在迭代前和迭代后释放都是一样的,但是你的app能减少瞬间最大内存。

当你创建一个线程的时候

每个线程都有他自己的autoreleasepool block栈,主线程有,因为他自动生成,但是对于自定义线程,你必须创建自己的autoreleasepool 示例2-10

示例 2-9 Autorelease pool block in a loop

//Bad code  1
{
@autoreleasepool {
NSUInteger *userCount = userDatabase.userCount; for(NSUInteger *i = 0; i < userCount; i++) {
            Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.fname; if(fname == nil) {
                fname = [self askUserForFirstName];
            }
NSString *lname = p.lname; if(lname == nil) {
                lname = [self askUserForLastName];
            }

//...
            [userDatabase updateUser:p];
        }
} }
//Good code 2
{
@autoreleasepool {
NSUInteger *userCount = userDatabase.userCount; for(NSUInteger *i = 0; i < userCount; i++) {
@autoreleasepool {
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.fname; if(fname == nil) {
                    fname = [self askUserForFirstName];
                }
NSString *lname = p.lname; if(lname == nil) {
                    lname = [self askUserForLastName];
                }
//...
                [userDatabase updateUser:p];
            }
} }
}
  1. 这个代码是不好的因为他只有一个autoreleasepool 内存清理在所有的迭代完成之后。
  2. 在这个例子中,有两个autoreleasepool,内部的autoreleasepool确保每次迭代后清理内存,所以需要更少的内存。

示例2-10 Autorelease pool block in custom thread

-(void)myThreadStart:(id)obj { @autoreleasepool {
        //New thread's code
} }
//Somewhere else
{
    NSThread *myThread = [[NSThread alloc] initWithTarget:self
    selector:@selector(myThreadStart:) object:nil];
    [myThread start];
}

ARC

持续跟踪retain、release和autorelease是不容易的,更让人困惑的时候不知道什么时候什么地点该发消息给谁。

苹果是介绍了ARC在WWDC 2011来解决这个问题,Swift是开发iOS app的新语言,也是使用ARC和OC不同的是Swift不支持MRC

ARC是一个编译功能,他评估对象的整个生命周期代码,在适当的位子自动注入内存管理代码,编译器会自动生成dealloc方法,这就意味着跟踪内存使用的困难都解决了。

图2-3展示了MRC和ARC在开发时候的关系,使用ARC开发是更快的,因为他代码更少,

图2-3

你需要确定ARC是在Xcode设置是允许的,从Xcode5开始是默认的 (看图2=4)

图 2-4

不使用ARC依赖:可能很难找到没有使用ARC依赖或者替代方案。但是如果不希望用ARC,那么你需要对一个或多个文件限制ARC功能。
为了禁用ARC,去设置Targets → Build Phases → Compile Sources 选择不需要ARC的文件,添加编译标记-fno-objc-arc,如图2-5

图2-5

这同样的操作能创建一个复合类,可以在分类中使用MRC代码,代码卸载分类中,ARC的代码可以通过这个文件禁用。

ARC规则

ARC是编写代码时候需要遵循的规则,这些规则的目的是提供一个可靠的内存管理模型,在某些情况下,他们只执行最佳实践,在他们简化代码你不必直接参与内存管理,这些规则都有编译器执行,如果有错他们将导致编译错误而不会崩溃,ARC有下面一些编译规则:

  • 你不能实现或调用reatin、release、autorelease或者reatincount方法,这些在对象中和方法都是被限制的,因此[obj release]或者@selector(reatin)会造成编译错误。
  • 你能实现dealloc方法但是不能调用它,不仅对于另外的对象禁用而且父类也是,所以[super dealloc]是会编译错误的。你能用CFReatain、CFRelease,其他Core Foundation风格的方法。
  • 你能使用NSAllocateObject或者NSDeallocateObject,使用alloc创建对象,runtime负责deallocation
  • 不能使用C结构指针
  • 不能再id和void *类型之间随便转换,如果需要,必须强制转换。
  • 不能使用NSAutoreleasePool,用autoreleasepool block替代。
  • 不能使用NSZone内存区域
  • 属性访问名(getter)不能以new开头,为了确保MRC的互操作性,示例2-11是演示的。
  • 尽管通常有些事情要避免的,但是我们还是可以混合使用ARC和MRC,我们已近在40页讨论过了

示例2-11 Accessor name with ARC enabled

//Not allowed
@property NSString * newTitle; //Allowed
@property (getter=getNewTitle) NSString * newTitle;

记住这些规则,我们更新2-5的代码2-12

示例2-12 Updated code with ARC enabled

-(NSString *) address
{
NSString *result = [[NSString alloc] initWithFormat:@"%@\n%@\n%@, %@", self.line1, self.line2, self.city, self.state]; 1
return result; 
    
}
-(void) showPerson:(Person *) p {
    NSString *paddress = [p address];
    NSLog(@"Person's Address: %@", paddress);
    2
}
  1. 不需要autorelease,因为不能使用autorelease或者retain
  2. 不能对paddress调用release方法

引用类型

ARC介绍了一种新的引用类型:弱引用,理解可以使用的引用类型对于内存管理是非常重要的,支持的类型是:

强引用:

强引用是默认创建的,内存被一个强引用引用是不能被释放的,强引用让引用计数加1,延长对象的声明周期

弱引用

弱引用是一个特殊的引用,他不会增加引用计数,(因此不会延长对象的生命周期)弱引用在OC的ARC编程中是非常重要的,接下来我们将讨论。

其他引用类型

OC目前不支持其他类型,但是你可能对下面类型感兴趣:

软引用
软引用是有点像弱引用的,除了他不急于释放对象,一个弱引用对象在下一次垃圾回收周期中会被释放,但是软引用对象通常还会逗留一段时间。

虚引用
他是最弱的引用,他是优先被清理的,一个虚引用对象和一个回收对象是相似的,但是实际上内存没有被回收

这些引用类型对于计数系统是不重要的,他们更适用于垃圾回收器

变量修饰符

ARC是介绍了4种生命周期修饰符:

__strong

它是默认的修饰符,不要明确提及,一个对象只要有强指针指向他,它就一直保存在内存中类似ARC中的retain

__weak

这表明这个引用不会一直保持对象的引用,当没有强引用指向对象的时候,弱引用将设置成nil,类似ARC中的assignment(不设置为nil)操作符,除了添加一个安全的指针,因为对象回收的时候会设置成nil。

__unsafe_unretained

他是和__weak相似的除了当没有强引用指向的时候不设置为nil,它类似ARC中的assignment操作符

__autoreleasing

用于id *类型的参数传递,当参数在方法中传递的时候希望调用autorelease。

像下面用修饰符的语法


TypeName * qualifier variable;

示例 2-13 Using variable qualifiers

Person * __strong p1 = [[Person alloc] init]; 1
Person * __weak p2 = [[Person alloc] init]; 2
Person * __unsafe_unretained p3 = [[Person alloc] init];  3
Person * __autoreleasing p4 = [[Person alloc] init]; 4
  1. 对象创建的时候引用计数为1,对象直到p1指正释放引用才被回收。
  2. 对象创建的时候引用计数为0,将会立刻被回收并且p2被设置nil
  3. 对象创建的时候引用计数为1,将会立刻被回收但是p2不会被设置为nil
  4. 对象创建的时候引用计数为1,当方法返回的时候会自动释放一次。

属性修饰符

引入了两个新的所有权修饰符属性声明,strong和weak,assign的语义也更新了,总之现在有6个修饰符:

strong:默认的,表示一个__strong 关系

weak:便是一个__weak关系

assign:它不是一个新的修饰符,但是语义已经改变了,在ARC之前assign是默认的拥有关系修饰符,在ARC之后assign是__unsafe_untetained

copy:意味着__strong关系,此外,他意味着setter中的复制语义。

retain:表示__strong关系

unsafe_unretained:表示__unsafe_unretained关系

示例2-14展示了这些修饰符的例子,因为assign和unsafe_untetained只复制了值没有进行完整性检查,它应该只能用于值类型(BOOL,NSInteger、NSUInteger等)避免用于引用类型,特别是NSString*和UIView *指针。

示例 2-14 Using property qualifiers

@property (nonatomic, strong) IBOutlet UILabel *titleView;
@property (nonatomic, weak) id<UIApplicationDelegate> appDelegate; 
@property (nonatomic, assign) UIView *danglingReference; 1
@property (nonatomic, assign) BOOL selected; 2
@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) HPPhoto *photo; 3
@property (nonatomic, unsafe_unretained) UIView *danglingReference;
  1. assign错误的指向一个指针。
  2. assign正确的用于值类型
  3. retain是ARC前期的产物,现在很少使用了,这里只是为了例子的完整性。

未完 待续。。。

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

推荐阅读更多精彩内容