iOS runtime

一.什么是runtime?

runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数。

二.runtime能做什么?

可以在程序运行时创建,检查,修改类、对象和它们的方法。
具体应用举例:

- 获取类的方法列表/参数列表;
- 方法调用;
- 方法拦截、动态添加方法;
- 方法替换: method swizzling
- 关联对象,动态添加属性;

再具体些:

1.对象关联
2.解归档
3.字典与模型互转
4.方法的调用,交换,插入,拦截,动态添加。
。。。。。。。

三.基础知识

实践出真理,开始吧。
先从Objective-C 中的方法调用说起。其实 [receiver message] 会被编译器转化为: objc_msgSend(receiver, selector)。
例:
创建一个名为testClass的新类

#import "testClass.h"

@implementation testClass
-(instancetype)init{
    if (self = [super init]) {
        [self runTimeTestAction];
    }
    return self;
}

-(void)runTimeTestAction{
    NSLog(@"runTime");
}
@end

在终端使用 clang 重写命令,得到testClass.cpp文件:

 clang -rewrite-objc /Users/apple/Desktop/runTimeTest/runTimeTest/TestClass/testClass.m 

双击打开,command+F 搜索“static instancetype”,可以看到被编译器转化后的Init方法


static instancetype _I_testClass_init(testClass * self, SEL _cmd) {
    if (self = ((testClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("testClass"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));
    }
    return self;
}

可以看到[self runTimeTestAction];被编译后的样子:
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));

objc_msgSend 函数被定义在 objc/message.h 目录下,其函数原型为

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

可以看到该函数有两个参数,一个id类型,一个SEL类型,再来看看这两个类型被如何定义,他们都被定义在objc/objc.h目录下:

SEL:

typedef struct objc_selector *SEL;
//这是个映射到方法的C字符串,你可以用 Objective-C 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。

id:

typedef struct objc_object *id;
//id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。
//objc_object 结构体定义如下:
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

这就是所谓的对象,isa为其一个Class类型的成员变量,isa指定对象所属的类。
那么再来看看Class是如何定义的:

Class:

Class 也是一个结构体指针类型:

typedef struct objc_class *Class;

objc_class 结构体:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

·Class 也有一个 isa 指针,指向其所属的元类(meta).
·super_class:指向其超类.
·name:类名.
·version:类的版本信息.
·info:类的详情.
·instance_size:该类的实例对象的大小.
·ivars:指向该类的成员变量列表.
·methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因.
·cache:Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高.
·protocols:指向该类的协议列表.

PS

methodLists 指向该类的实例方法列表实例方法-方法,那么类方法(+方法)存储在哪儿呢?类方法被存储在元类中,Class 通过 isa 指针即可找到其所属的元类.


上图实线是 super_class 指针,虚线是 isa 指针。根元类的超类是NSObject,而 isa 指向了自己。NSObject 的超类为 nil,也就是它没有超类。

现在结合上图来看下方法调用的整体流程:

1.首先,Runtime 系统会把方法调用转化为消息发送,即 objc_msgSend,并且把方法的调用者,和方法选择器,当做参数传递过去

2.此时,方法的调用者会通过 isa 指针来找到其所属的类,然后在 cache 或者 methodLists 中查找该方法,找得到就跳到对应的方法去执行

3.如果在中没有找到该方法,则通过 super_class 往上一级超类查找(如果一直找到 NSObject 都没有找到该方法的话,这种情况,我们放到后面消息转发的时候再说)

4.如果一直找到 NSObject 都没有找到该方法的话就进入消息转发了,消息转发的大致过程如图(我们可以利用这一过程,添加动态方法)
消息转发的大致过程如图:


①进入 resolveInstanceMethod: 方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕.

②resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步,返回某个对象,则会调用该对象的方法.

③若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步.

④当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等.

⑤如果到最后,消息还是没有得到响应,程序就会crash
一般情况下程序会crash,错误信息类似下面这样:

unrecognized selector sent to instance 0x7fd0a141afd0

四.实践及应用

上面说了这么多只是根据自己的理解通过Runtime的消息发送机制初步整理了下,runtime基础原理和内部结构,插图及解释大部份摘自网络。下面开始说些具体应用。

1.对象关联

对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态给对象增加成员变量。相对的,对象的方法定义都保存在类的可变区域中。

methodList,通过修改methodList指针所指向的指针的值,我们可以实现为类动态增加方法实现。

因此,我们可以实现动态为一个类增加成员方法,但是却不能直接为类增加成员变量,这就是category的实现原理。

对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

·object 是源对象.
·value 是被关联的对象.
·key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象.
·policy 是一个枚举值,表示关联对象的行为:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示关联对象copy,不是线程安全的
};

要取出被关联的对象使用 objc_getAssociatedObject 方法。
要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可:

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象.
举个栗子,假如我们要给 UIButton 添加一个监听单击事件的 block 属性,新建 UIButton 的 Category:


#import <UIKit/UIKit.h>
typedef void(^whenClickBlock)(void);
@interface UIButton(ClickBlock)
@property (nonatomic,copy) whenClickBlock whenClickBlock;
@end

#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>
static const void *associatedKey = "associatedKey";
@implementation UIButton(ClickBlock)

-(void)setWhenClickBlock:(whenClickBlock)whenClickBlock{
    objc_setAssociatedObject(self, associatedKey, whenClickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    if (whenClickBlock) {
        [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    }
}

-(whenClickBlock)whenClickBlock{
    return objc_getAssociatedObject(self, associatedKey);
}

-(void)buttonClick{
    if (self.whenClickBlock) {
        self.whenClickBlock();
    }
}

@end

调用:

    //用对象关联在Category内加属性
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = self.view.bounds;
    [self.view addSubview:button];
    __weak UIButton * weakbutton=button;
    button.whenClickBlock = ^{
        //随机改变颜色
        weakbutton.backgroundColor=randomColor;
    };
    
    

2.交换方法,动态加载方法

方法交换

关于这部分操作,都被定义在#import <objc/runtime.h>目录下,下面介绍几个常用的,其他的感兴趣的话可以自行查看:

    class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)// 替换类方法的定义
    method_exchangeImplementations(Method m1, Method m2) //交换两个方法的实现
    method_setImplementation(Method m, IMP imp)  //设置一个方法的实现

PS:class_replaceMethod 试图替换一个不存在的方法时候,会调用class_addMethod为该类增加一个新方法
例:

#import "ViewController+login.h"
#import <objc/runtime.h>

@implementation UIViewController (login)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class=[self class];
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzledViewDidAppear:);
        swizzleMethod(class, originalSelector, swizzledSelector);
       
    });
    
    
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
     // Method : 包含了一个方法的  方法名 + 实现 + 参数个数及类型 + 返回值个数及类型 等信息
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //IMP:方法的地址
    IMP swizzledImp = method_getImplementation(swizzledMethod);
    char *swizzledTypes = (char *)method_getTypeEncoding(swizzledMethod);
    IMP originalImp = method_getImplementation(originalMethod);
    char *originalTypes = (char *)method_getTypeEncoding(originalMethod);
    BOOL success = class_addMethod(class, originalSelector, swizzledImp, swizzledTypes);
    //避免UIViewController父类实现swizzledViewDidAppear影响
    if (success) {
       //替换
        class_replaceMethod(class, swizzledSelector, originalImp, originalTypes);
    }else {
        // 失败,表明已经有这个方法,直接交换
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}



- (void)swizzledViewDidAppear:(BOOL)animation {
    [self swizzledViewDidAppear:animation];
    NSLog(@"YLZ:%@ viewDidAppear", NSStringFromClass([self class]));
}
@end

调用:

    ViewController * VC=[ViewController new];

动态加载方法

首先看下消息转发,上文也提到过,具体过程:

//  第一步,消息接收者没有找到对应的方法时候,会先调用此方法,可在此方法实现中动态添加新的方法
//  返回YES表示相应selector的实现已经被找到,或者添加新方法到了类中,否则返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

//  第二步, 如果第一步的返回NO或者直接返回了YES而没有添加方法,该方法被调用
//  在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象, 注意如果返回self就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;
}


//  第三步, 如果forwardingTargetForSelector:返回了nil,则该方法会被调用,系统会询问我们要一个合法的『类型编码(Type Encoding)』
//  若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 在这里可以改变方法选择器
    [anInvocation setSelector:@selector(unknown)];
    // 改变方法选择器后,需要指定消息的接收者
    [anInvocation invokeWithTarget:self];
}

- (void)unknown {
    NSLog(@"unkown method.......");
}

// 如果没有实现消息转发 forwardInvocation  则调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"unresolved method :%@", NSStringFromSelector(aSelector));
}

根据消息转发过程,一般我们在resolveInstanceMethod:(SEL)sel动态添加方法:
构建一个Cat ,动态添加miaow:方法,我们可以这样做,例:

#import "cat.h"
#import <objc/runtime.h>
@implementation cat

- (void)eat{
    NSLog(@"cat eat....");
}

- (void)jump{
    NSLog(@"cat jump....");
}
//prama:传人参数,可自定义
void  miaow(id self ,SEL _cmd,NSString * param){
    NSLog(@"%@",param);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel==NSSelectorFromString(@"miaow:")) {
        /**
         *  动态添加方法
         *
         *  @param self cls:为哪个类添加方法
         *  @param sel  SEL:添加方法的方法编号(方法名)是什么
         *  @param IMP  IMP:方法实现
         *  @param const char * types方法类型
         *
         */
        BOOL isSuccess = class_addMethod(self, sel, (IMP)miaow, "v@:@");
        return isSuccess;
    }
        return [super resolveInstanceMethod:sel];
        }

@end


调用:

    [[cat new]performSelector:@selector(miaow:) withObject:@"喵~"];

自动解归档

在归档的过程中,当属性较多时,用普通的解归档操作,比较麻烦,🔎很多人深有体会,通过runtime可以简化这一操作,具体思路为:
-使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.
-使用 ivar_getName 方法获取成员变量的名称.
-通过 KVC 来归档,解档.
代码:



#import <Foundation/Foundation.h>
@interface myModel : NSObject<NSCoding>
@property (nonatomic, copy) NSNumber *sex;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, copy) NSNumber *height;
@property (nonatomic, copy) NSNumber *weight;
@end

///////////////////////////////////
#import "myModel.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation myModel
- (id) initWithCoder: (NSCoder *)aDecoder{
    if (self=[super init]) {
        unsigned int outcount=0;
        Ivar * vars=class_copyIvarList([self class], &outcount);
        for (int i=0; i<outcount; i++) {
            
            Ivar var=vars[i];
            const char * name=ivar_getName(var);
            NSString * key=[NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        //释放vars
           free(vars);
    }
    return self;
}
- (void) encodeWithCoder: (NSCoder *)aCoder{
    unsigned int outcount=0;
    Ivar * vars=class_copyIvarList([self class],&outcount );
    for (int i=0 ; i<outcount; i++) {
        Ivar var=vars[i];
        const char * name=ivar_getName(var);
        NSString * key=[NSString stringWithUTF8String:name];
        id value=[self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(vars);
    
}
@end

3.字典模型互转

跟归档遇到的问题一样,当有很多属性的时候,常规方法会比较麻烦
说下利用runtime做字典模型互转的思路:

字典转模型:

1.根据字典的 key 生成 setter 方法.
2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC).

模型转字典:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性.
2.调用 property_getName 获取属性名称.
3.根据属性名称生成 getter 方法.
4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC).

代码:

#import <Foundation/Foundation.h>

@interface NSObject (KeyValues)

+(id)objectWithKeyValues:(NSDictionary *)aDictionary;

-(NSDictionary *)keyValuesWithObject;

@end
//////////////////////////////////////////////////////////////

#import "NSObject+KeyValues.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (KeyValues)
//字典转模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
    id objc = [[self alloc] init];
    for (NSString *key in aDictionary.allKeys) {
        id value = aDictionary[key];
     
        /*判断当前属性是不是Model*/
        objc_property_t property = class_getProperty(self, key.UTF8String);
        unsigned int outCount = 0;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        objc_property_attribute_t attribute = attributeList[0];
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];
        if ([typeString isEqualToString:@"@\"TestModel\""]) {
            value = [self objectWithKeyValues:value];
        }
        /**********************/
        
        //生成setter方法,并用objc_msgSend调用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
        SEL setter = sel_registerName(methodName.UTF8String);
        if ([objc respondsToSelector:setter]) {
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
        free(attributeList);
    }
    return objc;
}

//模型转字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];
        
        //生成getter方法,并用objc_msgSend调用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
            
            /*判断当前属性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }
            /**********************/
            
            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }
        
    }
    free(propertyList);
    return dict;
}
@end

*-_

写在最后:

到这里,日常常用的基本都有了。一直想写一篇简单的介绍runtime的短文,供自己和大家借鉴学习,工作繁忙,前后断断续续写了几个星期,也总算完成了,严格来说这篇文章算所有人共同完成的,借鉴了大量网络资源,作者也算是在自己理解的基础上对网络上现有资源的初步整理,非常感谢各位大神的分享🙏。后面如果有机会再补充一些别的用例,供大家参考。
参考:
Runtime全方位装逼指南🙏
代码:
runTimeTest

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

推荐阅读更多精彩内容