iOS开发经验(11)-内存

目录

  1. 属性
  2. 修饰词
  3. 循环引用
  4. typeof与typedef
1. 属性

objc所有类和对象都是c结构体,category当然也一样
成员变量与实例变量
成员变量就是我们写在花括中的变量, 声明方法的括号中的 NSString *a 和 NSInteger b 都是成员变量。

@interface MyObject : NSObject {  
    NSString *a;
    NSInteger b; 
}  
@end

实例变量是成员变量的一部分,虽然a 和b都是成员变量,但是它们是不同的,a是一个对象指针(前面带*的),a又被称之为实例变量,成员变量包含实例变量。
成员变量中除了b这样的基本数据类型,其它的都是实例变量;

属性本质(@property)

@property (assign, nonatomic) NSString *a;

@property = ivar + getter + setter;

  • 属性就是通过@property 定义的变量,默认会生成带下划线的成员变量,在这里xcode编译器会自动生成一个成员变量 NSString *_a。
  • 属性相比较于成员变量添加了存取方法(通过@synthesize,它是Xcode自动添加的)。
  • 工程师可以使用@dynamic自己实现成员变量的getter和setter方法 , @synthesize 是让 Xcode 帮你生成getter和setter方法 。

@synthesize和@dynamic

  • @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
@synthesize语法来指定实例变量的名字.
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
  • @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
  • @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

@private、@public 、 @protect
属性和成员变量可以被@private、@public 、 @protect 修饰。

  • @private 表示这个类私有的 只允许该类内部和该类的对象访问,其它类和他的子类不累访问。
  • @protect 表示只允许该类和该类的子类访问。
  • @public 表示公共的,所有的对象都能访问。

怎么简单的理解getter和setter

  • setter:给外部提供的一个修改内部属性值的接口,调用setter方法可以做到修改内部属性值。
  • getter:外界提供的一个查看内部变量的接口。
  • self.name和 _name的区别:
    • 前者:self.name = @"happy" 是通过调用setter方法设置属性值,而 NSString *str = self.name 就是通过调用getter 方法获取属性值;
    • 后者:直接去访问成员变量;
2. 修饰词

属性(@property)根据属性中的设定的修饰词(retain, copy, assign)将会生成不同的setter方法。这些不同的setter方法中,对指针和内存地址的处理是不同的,决定了不同的修饰词的用途。

assign

  • assign适用于基本数据类型,weak是适用于NSObject对象,并且是一个弱引用。
  • assign其实也可以用来修饰对象,只是对象的计数器不会+1 ,那么我们为什么不用它呢?因为被assign修饰的对象在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil。如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
    而weak修饰的对象在释放之后,指针地址会被置为nil。所以现在一般弱引用就是用weak。

weak ( ARC )(对象)

  • 弱指针是针对对象的修饰词 , 就是说它不能修饰基本数据类型(int float) 。
  • weak 修饰的引用计数器不会+1 , 也就是直接赋值 。
  • 弱引用是为打破循环引用而生的,比如在Block中,block在copy的时候,会对内部使用到的对象的引用技术+1,如果使用[self 方法名],那么就会有一个强指针指向self所在的class的内存地址,class的引用计数会+1,这样一来会导致class所在的内存地址无法被释放,造成内存泄漏 。
  • 它最被人所喜欢的原因是 它所指向的对象如果被销毁 , 它会指向 nil . 从而不会出现野指针错误 。

注意:什么情况使用 weak 关键字?
在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,
比如: delegate 代理属性
自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,
自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。

strong

  • 直接赋值并且对象的引用计数器 +1 。在ARC下,实例变量本身是强引用,当ARC将传入值赋给实例变量时,它会保留传入的值,释放现有
    实例变量的值。
  • 在 ARC 里替代了 retain 的作用 .

retain ( MRC )

  • release 旧对象( 旧对象计数器 -1 ) , retain 新对象( 新对象计数器 +1 ) , 然后指向新对象 .

copy

  • 如果是不可变的值,行为与strong相同。
  • 如果是可变的值,会将一个副本赋给实例变量。当一个不可变类有一个可变的子类时
    (NSString NSMutableString,NSArray NSMutableArray)可以防止setter 方法传递一个
    可变的子类的对象。会导致我们在不知情的情况下修改对象的值。

注意:
1.修饰的属性本身要不可变的。例如 NSMutableArray 采用 copy 修饰 , 在addObject时会出现Crash, 因为NSMutableArray的对象在copy 过后就会变成NSArray。如果需要copy NSMutableArray对象,用:mutablecopy。
2.遵守 NSCopying 协议的对象使用 .

**nonatomic **

  • 不对set方法加同步锁 .
  • 性能好
  • 线程不安全

atomic

  • 原子属性就是对生成的 set 方法加互斥锁 @synchronized(锁对象) .
    @synchronized(self) { _delegate = delegate;}
  • 需要消耗系统资源 .
  • 互斥锁是利用线程同步实现的 , 意在保证同一时间只有一个线程调用 set 方法 .
  • 其实还有 get 方法 , 要是同时 set 和 get 一起调用还是会有问题的 . 所以即使用了 atomic 修饰 还是不够安全 .

readonly (只读)
1.让 Xcode 只生成get方法 .
2.不想把暴露的属性被人随便替换时 , 可以使用 .

readwrite (读写)(默认)

  • 让 Xcode 生成get/set方法 .
  • 不用 readonly 修饰时 , 默认就是 readwrite .
3. 循环引用

不知道大家有没有听过一句话:人生就像打电话,不是你先挂,就是我先挂。其实用到这里是在合适不过了。假设有两个人打电话,彼此都强引用的对方的电话号码,但是如果双方都不想挂电话的话,那么电话就会一直通着。在OC中也是同样的道理,正常情况下,当一个类或者对象即将释放或者retainCount=0的时候,就会调用dealloc方法,释放相应的内存。但是,当这个对象被其他对象强引用的时候,那么它就不会被回收。此时如果处理不当的话就会造成循环引用。

循环引用对 app 有潜在的危害,会使内存消耗过高,性能变差和 app 闪退等。
循环引用有哪些具体的情况?block ? delegate ? NSTimer?

一、Block
block在copy时都会对block内部用到的对象进行强引用的。
该类又将block作为自己的属性变量,而该类在block的方法体里面又使用了该类本身,此时就很简单的形成了一个环。

引发循环引用,是因为当前self在强引用着block,而block又引用着self,这样就造成了循环引用。而需不需要使用[weak self]就是由循环引用来决定,如果造成了循环引用,就必须使用[weak self]来打破循环.

1.直接强引用
来分析一个自己设计的block模块:

  • 这种情况不必要弱引用
[self oneBlockSucess:^{ 
[self doSomething];
}];
  • 这种情况就有必需用weakself来打破引用环
self.secondBlock = ^{
 [self doSomething];
};

self.secondBlock说明当前self持有了secondBlock这个block属性,
所以如果在block回调内想拿到self去做一些业务处理时,如果直接使用self,就会造成block持有了self,两者互相持有,造成循环引用.打破这个环,就要使用如typeof(self) __weak weakSelf = self; 的weakSelf方式;

2.间接强引用

再来分析一个自己设计的block模块:

控制器self并没有直接持有block属性,但是却强引用了bottomView,bottomView强引用了block属性,这就造成了间接循环引用. block回调内必须使用[weak self]来打破这个循环,否则就会导致这个控制器self永远都不会被释放掉产生常驻内存。如果self.bottomView与bottomView.block有一环不是强引用,就不会产生循环引用问题,因为不会形成一个引用环. 如果一个应用程序里面你有很多循环引用,那么内存占用就会比较大,并且由于一些特殊操作,会产生一个控制器的N个对象实例常驻内存无法释放,造成大量的系统内存泄漏,这当然是谁都不想看到的结果.
打破这个环,就要使用如typeof(self) __weak weakSelf = self;的weakSelf方式;

3. 还有一些例子

//循环引用例子:比如控制器在使用一个Block,这个block又在使用控制器就会出现循环引用
- (void)setcycle
{
    //例子1.
        NSMutableArray *firstArray = [NSMutableArray array];
        NSMutableArray *secondArray = [NSMutableArray array];
        [firstArray addObject:secondArray];
        [secondArray addObject:firstArray];

    //例子2.
    //代码解释:定义一个和self相同数据类型的bself ,并赋值为self,在block中使用
    __weak typeof (self) weakSelf = self;
    
    /*
     注意: typeof 括号中的值和等于后面的值是相同的类型。
     __weak typeof(self.contentView) ws = self.contentView;
     */
    
    self.passValueBlock = ^(NSString *str){
        //循环引用1
        [self test];
        
        //解决方法:
        //[weakSelf test];
        
        //以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要 block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。
        //NSString *localString = self.blockString;//循环引用2
        //NSString *localString = _blockString;//循环引用3
        
        //解决方法
        //NSString *localString = weakSelf.blockString;

    };

    self.passValueBlock(@"s");
    //例子3.
    //宏定义一个block
    typedef void(^blockAct)();
    //利用宏定义来定义变量
    blockAct blockAct_1;
    //定义一个block变量来实现
    blockAct_1 = ^(){
        [self test];
    };
    //调用block
    blockAct_1();
}

- (void)test
{
    NSLog(@"BLOCK");
}

4. 这里直接用一个需求来探究循环引用的问题:
如果我想在Block中延时来运行某段代码,这里就会出现一个问题,看这段代码:

- (void)viewDidLoad {
  [super viewDidLoad];
  MitPerson*person = [[MitPerson alloc]init];
  __weak MitPerson * weakPerson = person;
  person.mitBlock = ^{
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
          [weakPerson test];
      });
  };
  person.mitBlock();
}

直接运行这段代码会发现[weakPerson test];并没有执行,打印一下会发现,weakPerson 已经是 Nil 了,这是由于当我们的 viewDidLoad 方法运行结束,由于是局部变量,无论是 MitPerson 和 weakPerson 都会被释放掉,那么这个时候在 Block 中就无法拿到正真的 person 内容了。
按如下方法修改代码:

- (void)viewDidLoad {
  [super viewDidLoad];
  MitPerson*person = [[MitPerson alloc]init];
  __weak MitPerson * weakPerson = person;
  person.mitBlock = ^{
      __strong MitPerson * strongPerson = weakPerson;
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
          [strongPerson test];
      });
  };
  person.mitBlock();
}

这样当2秒过后,计时器依然能够拿到想要的 person 对象。

深入探究原理
这里将会对每行代码逐步进行说明

1、开辟一段控件存储 person 类对象内容,创建 person 强指针。
MitPerson*person = [[MitPerson alloc]init];
2、创建一个弱指针 weakPerson 指向person对象内容 
__weak MitPerson * weakPerson = person;
person.mitBlock = ^{
3、在 person 对象的 Block 内部创建一个强指针来指向 person 对象,为了保证当计时器执行代码的时候,person 对象没有被系统销毁所以我们必须在系统内部进行一次强引用,并用 GCD 计时器引用 strongPerson,为了保留 person 对象,在下面会对这里更加详细的说明。
  __strong MitPerson * strongPerson = weakPerson;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [strongPerson test];
    });
};
4、执行 Block 代码
  person.mitBlock();

首先需要明白一些关于 Block 的概念:

  • 默认情况下,block 是放在栈里面的
  • 一旦blcok进行了copy操作,block的内存就会被放在堆里面
  • 堆立面的block(被copy过的block)有以下现象:
    • block内部如果通过外面声明的强引用来使用,那么block内部会自动产生一个强引用指向所使用的对象。
    • block内部如果通过外面声明的弱引用来使用,那么block内部会自动产生一个弱引用指向所使用的对象。

我们进行这段代码的目的:

  • 首先,我们需要在 Block 块中调用,person 对象的方法,既然是在 Block 块中我们就应该使用弱指针来引用外部变量,以此来避免循环引用。但是又会出现问题,什么问题呢?就是当我计时器要执行方法的时候,发现对象已经被释放了。
  • 接下来就是为了避免 person 对象在计时器执行的时候被释放掉:那么为什么 person 对象会被释放掉呢?因为无论我们的person强指针还是 weakPerson 弱指针都是局部变量,当执行完ViewDidLoad 的时候,指针会被销毁。对象只有被强指针引用的时候才不会被销毁,而我们如果直接引用外部的强指针对象又会产生循环引用,这个时候我们就用了一个巧妙的代码来完成这个需求。
  • 首先在 person.mitBlock 引用外部 weakPerson,并在内部创建一个强指针去指向 person 对象,因为在内部声明变量,Block 是不会强引用这个对象的,这也就在避免的 person.mitBlock 循环引用风险的同时,又创建出了一个强指针指向对象。
  • 之后再用 GCD 延时器 Block 来引用相对于它来说是外部的变量 strongPerson ,这时延时器 Block 会默认创建出来一个强引用来引用 person 对象,当 person.mitBlock 作用域结束之后 strongPerson 会跟着被销毁,内存中就仅剩下了 延时器 Block 强引用着 person 对象,2秒之后触发 test 方法,GCD Block 内部方法执行完毕之后,延时器和对象都被销毁,这样就完美实现了我们的需求。

二、Delegate

@property (nonatomic, weak) id <TestDelegate> delegate;

说白了就是循环使用的问题,假如我们是写的strong,那么 两个类之间调用代理就是这样的

BViewController *bViewController = [[BViewController alloc] init];
bViewController.delegate = self; //假设 self 是AViewController
[self.navigationController pushViewController:bViewController animated:YES];
假如是 strong 的情况
bViewController.delegate ===> AViewController (也就是 A 的引用计数 + 1)
AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用计数 + 1.
导致: AViewController <======> Delegate ,也就循环引用

Delegate创建并强引用了 AViewController;(strong ==> A 强引用、weak ==> 引用计数不变)

所以用 strong的情况下,相当于 Delegate 和 A 两个互相引用,A 永远会有一个引用计数 1 不会被释放,所以造成了永远不能被内存释放,因此weak是必须的。

** NSTimer**

self.timer = [NSTimer timerWithTimeInterval:_time < 2? DEFAULTTIME: _time target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

是有风险的:

  • timer会强持有target, 同时NSRunLoop会去强持有timer
  • 而你需要注意到, 通常我们是把timer当做某一个类的实例的属性去看待的, 也做了强持有操作
    所以这样做的结果是, 循环引用!于是基于以上代码的写法, 在系统自动dealloc那个实例的时候你会发现, 实例与timer两者都无法free!
    但是,如果你把
[_timer invalidate]; 
_timer = nil;

写在除了该类的dealloc方法的其他地方, 然后在dealloc该类的实例对象之前调用, 再dealloc操作, 那是一点问题都没有.

三、关于NSTimer循环引用的处理方法:
构造一个中介类给NSTimer, 中介类代码如下:

#import <Foundation/Foundation.h>
/// justForText
@interface NSSafeObject : NSObject

- (instancetype)initWithObject:(id)object;
- (instancetype)initWithObject:(id)object withSelector:(SEL)selector;
- (void)excute;

@end
#import "NSSafeObject.h"
@interface NSSafeObject()
{
    __weak id _object;
    SEL _sel;
}
@end
@implementation NSSafeObject
- (instancetype)initWithObject:(id)object
{
    if (self = [super init]) {
        _object = object;
        _sel = nil;
    }
    return self;
}

- (instancetype)initWithObject:(id)object withSelector:(SEL)selector
{
    if(self = [super init])
    {
        _object = object;
        _sel = selector;
    }
    return self;
}

- (void)excute
{
    if (_object && _sel && [_object respondsToSelector:_sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [_object performSelector:_sel withObject:nil];
#pragma clang diagnostic pop
    }
}

调用:
有了这样一个类这样就符合我们编写代码的思维习惯, 你依然可以把NSTimer当做某一个类的对象, 无需考虑循化引用问题, 而只需要在每次创建之初多创建一个中介对象,演示代码如下

NSSafeObject * safeObj = [[NSSafeObject alloc]initWithObject:self withSelector:@selector(autoSendBarrage)];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:safeObj selector:@selector(excute) userInfo:nil repeats:YES];

或者给NSTimer写一个类目, 复写我们经常使用的那个NSTimer的类方法(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)并且将WeakObject这个类的创建封装进去。

5.无需解决循环引用的闭包

  • UIView Block动画
[UIView animateWithDuration:0.2 animations:^{
    self.alpha = 0.0f;
} completion:^(BOOL finished) {
    [self stopLoading];
}];
  • 同步执行
[_array enumerateObjectsUsingBlock:^(TimelineMessageData *nodeData, NSUInteger index, BOOL *stop) {
        [_tableView reloadRow:index inSection:kSectionOffset rowAnimation:UITableViewRowAnimationNone];
}];
  • 大部分GCD方法
    因为self并没有对GCD的block进行持有,没有形成循环引用。目前我还没碰到使用GCD导致循环引用的场景,如果某种场景self对GCD的block进行了持有,则才有可能造成循环引用。
dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});
  • block并不是属性值,而是临时变量
- (void)doSomething {
    [self testWithBlock:^{
        [self test];
    }];
}

- (void)testWithBlock:(void(^)())block {
    block();
}

- (void)test {
    NSLog(@"test");
}
5. typeof与typedef

理解

  • typeof 是一个一元运算,放在一个运算数之前,运算数可以是任意类型。可以理解为:我们根据typeof()括号里面的变量,自动识别变量类型并返回该类型。
    typeof 常见运用于Block中,避免循环引用发生的问题。
    用法:
//定义一个和self相同数据类型的bself ,并赋值为self,在block中使用
__weak typeof (self) weakSelf = self;

注意: typeof 括号中的值和等于后面的值是相同的类型。

__weak typeof(self.contentView) ws = self.contentView;
  • typedef:定义一种类型的别名,而不只是简单的宏替换。
    typedef 常用于命名(枚举和Block)

用法:
用法1-结构体

//结构体
typedef struct Myrect {
    float width;
    float height;
}myRect;

用法2-枚举

//枚举值 它是一个整形(int)  并且,它不参与内存的占用和释放,枚举定义变量即可直接使用,不用初始化.
//在代码中使用枚举的目的只有一个,那就是增加代码的可读性.
typedef NS_ENUM (NSUInteger,direction){
    left=0,
    right=1,
    top =2,
    down =3
};
typedef NS_ENUM (NSUInteger,direction_2){
    left2 = 0,
    right2 = 1 << 0,
    top2 = 1 << 1,
    down2 = 1 << 2
};

用法3-Block

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

推荐阅读更多精彩内容