③ Runtime面试题相关

1.一个objc对象的isa的指针指向什么?有什么作用?

指向他的类对象,从而可以找到对象上的方法

详解:下图很好的描述了对象,类,元类之间的关系:


图中实线是 super_class指针,虚线是isa指针。

1.Root class (class)其实就是NSObjectNSObject是没有超类的,所以Root class(class)superclass指向nil
2.每个Class都有一个isa指针指向唯一的Meta class
3.Root class(meta)superclass指向Root class(class),也就是NSObject,形成一个回路。
4.每个Meta classisa指针都指向Root class (meta)

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

2.一个 NSObject 对象占用多少内存空间?

(系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得))

受限于内存分配的机制,一个 NSObject对象都会分配 16byte 的内存空间。

但是实际上在 64位 下,只使用了 8byte;
在32位下,只使用了 4byte

一个 NSObject 实例对象成员变量所占的大小,实际上是 8字节

#import <Objc/Runtime>
Class_getInstanceSize([NSObject Class])

本质是

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

获取 Obj-C 指针所指向的内存的大小,实际上是16字节

#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj); 

对象在分配内存空间时,会进行内存对齐,所以在 iOS 中,分配内存空间都是 16字节 的倍数。可以看我们前面介绍的iOS之武功秘籍②:OC对象原理-中(内存对齐和malloc源码分析)

3.说一下对 class_rw_t 的理解?

rw代表可读可写.

Objc类中的属性、方法还有遵循的协议等信息都保存在class_rw_t中:

4.说一下对 class_ro_t 的理解?

存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

3 和 4 具体详情操作可以查看我的iOS之武功秘籍④:类结构分析

5.说一下对 isa 指针的理解, 对象的isa 指针指向哪里?isa 指针有哪两种类型?

isa 等价于 is kind of

  • 实例对象 isa 指向类对象
  • 类对象 isa 指向元类对象
  • 元类对象的 isa 指向元类的基类

isa 有两种类型

  • 纯指针,指向内存地址
  • NON_POINTER_ISA,除了内存地址,还存有一些其他信息

想知道更多详细的isa,可以查看我的iOS之武功秘籍③:OC对象原理-下(isa的初始化和指向分析与对象的本质)

6.说一下 Runtime 的方法缓存?存储的形式、数据结构以及查找的过程?

cache_t增量扩展的哈希表结构。哈希表内部存储的 bucket_t
bucket_t 中存储的是 SELIMP的键值对。

  • 如果是有序方法列表,采用二分查找
  • 如果是无序方法列表,直接遍历查找

关于更多的cache_t分析,请看我的iOS之武功秘籍⑤:cache_t分析

7.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。
详解:

1、调用 -release :引用计数变为零
对象正在被销毁,生命周期即将结束. 
不能再有新的 __weak 弱引用,否则将指向 nil.
调用 [self dealloc]

2、 父类调用 -dealloc 
继承关系中最直接继承的父类再调用 -dealloc 
如果是 MRC 代码 则会手动释放实例变量们(iVars)
继承关系中每一层的父类 都再调用 -dealloc

>3、NSObject 调 -dealloc 
只做一件事:调用 Objective-C runtime 中object_dispose() 方法

>4. 调用 object_dispose()
为 C++ 的实例变量们(iVars)调用 destructors
为 ARC 状态下的 实例变量们(iVars) 调用 -release 
解除所有使用 runtime Associate方法关联的对象 
解除所有 __weak 引用 
调用 free()

8.实例对象的数据结构?

通过runtime的源代码,可以看出:本质上objc_object的私有属性只有一个isa指针.指向类对象的内存地址.

9.什么是method swizzling(俗称黑魔法)

简单说就是进行方法交换
Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。
利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP
换方法的几种实现方式

  • 利用 method_exchangeImplementations 交换两个方法的实现
  • 利用 class_replaceMethod替换方法的实现
  • 利用 method_setImplementation 来直接设置某个方法的IMP

10.什么时候会报unrecognized selector的异常?

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX

11.如何给 Category 添加属性?关联对象以什么形式进行存储?

查看的是 关联对象 的知识点。
详细的说一下 关联对象
关联对象 以哈希表的格式,存储在一个全局的单例中。

@interface NSObject (Extension)

@property (nonatomic,copy  ) NSString *name;

@end


@implementation NSObject (Extension)

- (void)setName:(NSString *)name {
    
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


- (NSString *)name {
    
    return objc_getAssociatedObject(self,@selector(name));
}

@end

12.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
解释如下:
1.因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayoutclass_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;
2.运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

13.类对象的数据结构?

具体可以参看 Runtime 源代码。
类对象就是 objc_class

struct objc_class : objc_object {
  // Class ISA;
  Class superclass; //父类指针
  cache_t cache;             // formerly cache pointer and vtable 方法缓存
  class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 用于获取地址

  class_rw_t *data() { 
      return bits.data(); // &FAST_DATA_MASK 获取地址值
  }

它的结构相对丰富一些。继承自objc_object结构体,所以包含isa指针

  • isa:指向元类
  • superClass: 指向父类
  • Cache: 方法的缓存列表
  • data: 顾名思义,就是数据。是一个被封装好的 class_rw_t

14.runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

  • 每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
  • 也可以这样说:每个selector都与对应的IMP是一一对应的关系,通过selector就可以直接找到对应的IMP

15.runtime 如何实现 weak 变量的自动置nil?知道SideTable吗?

  • runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会 dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil
  • weak修饰的指针默认值是nil(在Objective-C中向nil发送消息是安全的)

更细一点的回答:
1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个 entryweak表中删除,最后清理对象的记录。

SideTable结构体是负责管理类的引用计数表和weak表,

详解:参考自《Objective-C高级编程》一书
1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数。

// 编译器的模拟代码
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
 objc_destroyWeak(&obj1);

通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。

2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。

objc_initWeak函数将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。

obj1 = 0;
obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。

objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

// 编译器的模拟代码
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);

objc_storeWeak函数把第二个参数的赋值对象(obj)的内存地址作为键值,将第一个参数__weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。

由于一个对象可同时赋值给多个附有__weak修饰符的变量中,所以对于一个键值,可注册多个变量的地址。

可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当keynil,将valuenil。在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

1.调用objc_release
2.因为对象的引用计数为0,所以执行dealloc
3.在dealloc中,调用了_objc_rootDealloc函数
4.在_objc_rootDealloc中,调用了object_dispose函数
5.调用objc_destructInstance
6.最后调用objc_clear_deallocating

对象被释放时调用的objc_clear_deallocating函数:

1.从weak表中获取废弃对象的地址为键值的记录
2.将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3.将weak表中该记录删除
4.从引用计数表中删除废弃对象的地址为键值的记录

总结:

其实Weak表是一个hash(哈希)表,Keyweak所指对象的地址,Valueweak指针的地址(这个地址的值是所指对象指针的地址)数组。

16.objc中向一个nil对象发送消息将会发生什么?

如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。

详解:
如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*)floatdoublelong double 或者long long的整型标量,发送给nil的消息将返回0;
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的

17.objc在向一个对象发送消息时,发生了什么?

objc在向一个对象发送消息时,runtime会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现IMP

18.isKindOfClass 与 isMemberOfClass?

下面代码输出什么?

@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
}

答案:1000

详解:
isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于meta classsuper class,不等再继续取super class,如此循环下去。
[NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObjectNSObjectmeta class是否相等,之前讲到meta class的时候放了一张很详细的图,从图上我们也可以看出,NSObjectmeta class与本身不等。接着第二次循环判断NSObjectmeta classsuperclass是否相等。还是从那张图上面我们可以看到:Root class(meta)superclass就是 Root class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES

同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,SarkMeta Class[Sark class]不等,第二次for循环,Sark Meta Classsuper class 指向的是 NSObject Meta Class, 和Sark Class不相等。第三次for循环,NSObject Meta Classsuper class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Classsuper class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO

isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。
第二行isa 指向 NSObjectMeta Class,所以和 NSObject Class不相等。第四行isa指向SarkMeta ClassSark Class也不等,所以第二行res2和第四行res4都输出NO

19.Category 在编译过后,是在什么时机与原有的类合并到一起的?

  • 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init
  • 然后会 map_images
  • 接下来调用 map_images_nolock
  • 再然后就是 read_images,这个方法会读取所有的类的相关信息。
  • 最后是调用 MethodizeClass:,这个方法是方法化的意思。
  • MethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 ClassCategory ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t结构体中。

20.Category 有哪些用途?

  • 给系统类添加方法、属性(需要关联对象)。
  • 对某个类大量的方法,可以实现按照不同的名称归类。

21.Category的实现原理

被添加在了 class_rw_t 的对应结构里。

Category 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的Category,添加了同一个方法,执行的实际上是最后一个。

拿方法列表举例,实际上是一个二维的数组。

Category 如果翻看源码的话就会知道实际上是一个 _catrgory_t 的结构体。

--
例如我们在程序中写了一个 Nsobject+Tools 的分类,那么被编译为 C++ 之后,实际上是:

static struct _catrgory_t _OBJC_$_CATEGORY_NSObject_$_Tools __attribute__ ((used,section),("__DATA,__objc__const"))
{
    // name
    // class
    // instance method list
    // class method list
    // protocol list
    // properties
}

Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 RuntimeCategory 和原来的类才会合并到一起。

mememovememcpy:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的空间,把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就是,越晚参与编译的分类,里面的方法才是生效的那个。

  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

22._objc_msgForward函数是做什么的,直接调用它将会发生什么?

  • _objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
  • 如果手动调用_objc_msgForward,将跳过查找IMP的过程,而是直接触发"消息转发".

详解:_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

  1. List item resolveInstanceMethod:方法 (或resolveClassMethod:)。
  2. List item forwardingTargetForSelector:方法
  3. List item methodSignatureForSelector:方法
  4. List item forwardInvocation:方法
  5. List item doesNotRecognizeSelector: 方法

23.[self class] 与 [super class]下面的代码输出什么?

@implementation Son : Father
   - (id)init
   {
       self = [super init];
       if (self) {
           NSLog(@"%@", NSStringFromClass([self class]));
           NSLog(@"%@", NSStringFromClass([super class]));
       }
       return self;
   }
   @end

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son

详解:这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。

self 是类的隐藏参数,指向当前调用方法的这个类的实例;

super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。不同点在于:super 会告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend;

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class。

objc_msgSendSuper的工作原理应该是这样的:
从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class!

那么objc_msgSendSuper最后就转变成:

// 注意这里是从父类开始msgSend,而不是从本类开始
objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class.  这是类的一个实例
    __unsafe_unretained id receiver;   


// 由于是实例调用,所以是减号方法
- (Class)class {
    return object_getClass(self);
}

由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son。

24.怎么理解Objective-C是动态运行时语言

  • 主要是将数据类型的确定由编译时,推迟到了运行时。这个问题其实浅涉及到两个概念,运行时和多态。
  • 简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。
  • 多态:不同对象以自己的方式响应相同的消息的能力叫做多态。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容