方法的本质

探索方法的本质

一个最基本的方法调用代码

void run(){
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];;
        [person sayNB];
    }
    return 0;
}

方法的调用底层到底是个什么东西呢
我们可以利用clang的一些命令 clang -rewrite-objc main.m -o main.cpp,将main文件编译成c之后的代码
cd /Users/caoxiang/Desktop/dealloc/dealloc

  • xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m
    扩展一下

  • 指定真机

xcrun -sdk iphoneos clang -rewrite-objc ViewController.m

  • 指定模拟器

xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m

  • 指定SDK版本

xcrun -sdk iphonesimulator10.3 clang -rewrite-objc ViewController.m

clang之后的代码

其中有个问题
run()方法直接调用,而我们的oc方法被编译成一个objc_msgSend函数(runtime里的消息发送机制)
run函数在编译器就确定了函数的调用与实现,
因此,oc的方法的本质就是objc_msgSend(或者objc_msgSendSuper等函数)的调用
objc_msgSend两个参数,第一个参数是对象是哪个对象的操作,第二个参数就是sel也就是方法,通过sel找到imp的实现,完成方法的调用.就叫做消息发送机制.

objc_msgSend流程

方法的查找流程分为两种

  • 快速查找:利用汇编直接从缓存中找
  • 慢速查找:快速查找没有命中,从方法表中查找
    现在方法调用处打一个断点


    打断点

然后debug->debug WorkFlow ->Always Show Disassembly

查看汇编

然后进入汇编
汇编

然后按住cotrol + stepIn
源码位置

可以看到objc_msgSendlibobjc

打开源码搜索objc_msgSend,我们直接看汇编,找到.s文件,现在架构大部分都是arm64,所以我们直接看objc-msg-arm64.s文件
看汇编重要的一点事看ENTRY表示入口,如下图

找到文件

image.png
// person - isa - 类
    ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

p13为isa,因为x0为首地址,[]就是首地址的值,首地址就是isa指针
GetClassFromIsa_p16是一个宏,下边是实现

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

SUPPORT_INDEXED_ISAindexIsa一般不常用,watchos开发是indexIsa
and p16, $0, #ISA_MASK这句才是重点,$0是传进来的参数,也就是p13 isa 拿$0与isa_mask进行&运算得到类,在前两篇文章中介绍了对象和类之间是怎么联系起来的,所以我们的p16是一个类
拿到isa之后进行下边操作

LGetIsaDone:
    CacheLookup NORMAL  

CacheLookup是一个宏定义,下边是实现代码

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro
1:   cmp    p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

b.ne 2f如果bucket的sel != _cmd找2,否则直接命中返回$0
快速查找流程结束,如果没有命中就进入慢速查找流程

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f              //进行3主要是为了保存一份方便下次查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask   
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket

CheckMiss也是一个宏

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我们这里传进来的是normal,那么找__objc_msgSend_uncached
我们搜索__objc_msgSend_uncached找到他的入口

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

来看一下MethodTableLookup是什么东西
他也是一个宏定义

.macro MethodTableLookup
...
bl  __class_lookupMethodAndLoadCache3
...

这里只展示了一个最重要的一行代码,bl跳转到__class_lookupMethodAndLoadCache3
我们根据以往经验,汇编会自动在前边加一个_,那么我们去掉一个下划线全局搜_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

来一个demo


@interface LGPerson : NSObject

- (void)sayNB;
+ (void)sayHappay;

@end
@implementation LGPerson

- (void)sayNB{
    NSLog(@"%s",__func__);
}

+ (void)sayHappay{
    NSLog(@"%s",__func__);
}
@end
@interface LGStudent : LGPerson
- (void)sayHello;
+ (void)sayObjc;
@end
@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s",__func__);
}

+ (void)sayObjc{
    NSLog(@"%s",__func__);
}
@end
@interface NSObject (LG)
- (void)sayMaster;
+ (void)sayEasy;
@end
@implementation NSObject (LG)

- (void)sayMaster{
    NSLog(@"%s",__func__);
}
+ (void)sayEasy{
    NSLog(@"%s",__func__);
}

@end

LGStuednt继承与LGPerson,LGPerson继承与NSObject
然后调用

LGStudent *student = [[LGStudent alloc] init];
        // 对象方法
        // 自己有 - 返回自己
        [student sayHello];
        // 自己没有 - 老爸 -
        // [person sayNB]; // CACHE
        
        [student sayNB];
        // 自己没有 - 老爸没有 - NSObject
        [student sayMaster];
        // 自己没有 - 老爸没有 - NSObject 没有
        // unrecognized selector sent to instance 0x103000450
       [student performSelector:@selector(saySomething)];
  • 自己有的时候返回自己的方法
  • 自己没有的时候找父类
  • 老爸没有时找NSObject
  • 都没有,抛出异常
    实例方法的查找会根据继承连去一层一层的找

类方法的调用

// 类方法
        // 自己有 - 返回自己
        [LGStudent sayObjc];
        // 自己没有 - 老爸 -
        [LGStudent sayHappay];
        // 自己没有 - 老爸没有 - NSObject
        [LGStudent sayEasy];
        // 自己没有 - 老爸没有 - NSObject 没有

这里会有一个问题如果我这样调用会不会崩溃

 [LGStudent performSelector:@selector(sayMaster)];

会打印

-[NSObject(LG) sayMaster]

发现不会蹦,因为类方法存在元类里边,我们调用类方法

  • 首先去元类里边找,
  • 元类里边没有找到就招父元类,
  • 没有找到继续根据继承链找,
  • 最后找到根元类(NSObject的元类),根元类又继承与NSObject

isa流程图.png

最后找到sayMaster,所以不会崩溃
看下源码来验证下到底是不是这个流程
我们上边看汇编,如果缓存没有命中就来到慢速查找流程,也就是_class_lookupMethodAndLoadCache3方法

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
  • 当调用实例方法的时候,cls就是当前类,sel就是调用的方法,obj就是当前实例
  • 当调用类方法的时候,cls则代表的是当前类的元类(MetaClass),因为静态方法是存放在元类里边的
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

因为是慢速查找,这里传进来的cache为NO,所以下边这几行代码不看,

if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

这里加了一把锁,是防止多线程情况下产生错乱,比如同时调用a方法和b方法,那么这里在调用a的时候返回一个b的imp,会产生问题
checkIsKnownClass,是判断类是否合法

 if (!cls->isRealized()) {
        realizeClass(cls);
    }

这行代码是拿到父类元类,以及类的data里边的rw里的ro里的methodlist等等一系列信息,是为方法查找做准备条件,这里不做重点研究.

// Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

先在本类里边找,getMethodNoSuper_nolock

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

拿到类的data里的methodList进行循环遍历,
search_method_list进行二分法查找,查找速度更快,下边为查找算法,这里不做过多研究

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通过一些列算法找到方法,然后看

if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }

如果找到了method那么下次还慢速查找么,苹果爸爸肯定不会这样蠢的,如果找到就调用log_and_fill_cache进行缓存,下次直接利用汇编快速查找,缓存的操作跟我们研究cache_t结构时是一模模一样样的.
goto done 然后查找结束

如果找不到呢,那么就去找父类Try superclass caches and method lists.根据代码注释我们也可以看出来接下来要找父类了.
如果找父类,就不开始跳汇编了,因为刚开始父类元类等条件我们已经准备好了,那么我们现在直接找父类的cache.

// Superclass cache.
            imp = cache_getImp(curClass, sel);

如果找到imp,直接goto done直接返回,如果找不到,然后找到父类的方法列表进行查找(流程同在本类中查找流程)
如果都找不到方法呢,直接报错我们很熟悉的一个错误+[LGStudent sayLove]: unrecognized selector sent to class 0x1000012e8.
imp = (IMP)_objc_msgForward_impcache;看这句代码.发现点不进去.那么按照国际惯例,全局搜索
然后选择objc_msg_arm64.s没错又是恶心人的汇编,为什么找他呢,因为其他地方都是调用,没有实现,按照经验STATIC_ENTRY __objc_msgForward_impcache ,是不是很熟悉

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

__objc_forward_handler是个啥玩意儿啊,来搜一下(经验告诉我搜不到的时候去掉一个下划线),

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

发现他是个这么个玩意儿objc_defaultForwardHandler,点进去看一下我的天啊,出现了好熟悉的代码

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

这不就是经常出现的报错原因么
但是找不到方法直接报错,体验很不好,有没有什么方法补救一下呢?
苹果爸爸告诉我们,可以的,在给你一次机会.可以利用消息转发机制
https://www.jianshu.com/p/03383d2d395d

补充:为什么要设计meteClass

1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
4、如果到基类依然没有找到该方法则触发动态方法解析流程。
5、还是找不到就触发消息转发流程
走到这里一套方法发送的流程就都走完了,那这跟元类的存在有啥关系?我们都知道类方法是存储在元类中的,那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?
答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。
倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, ...)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的...就是各式各样的参数。
通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。

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

推荐阅读更多精彩内容