OC底层之 消息发送机制

1、编译时

编译阶段,会通过编译器将语言转化成机器码,然后直接在CPU上执行机器码,效率
更高.

OC便是使用编译器进行处理,生成可执行文件;
而像Python等语言则是使用解释器,在运行时解释执行代码,将代码翻译成目标代
码,然后再一句一句的去执行目标代码,所以相对来说效率就低,但是跑起来后不用
重启编译代码,直接修改代码就可以看到效果。

编译过程的详细信息请移步

2、为什么说OC是动态语言

静态语言,是在编译期已经决定了一切,在运行时只是遵守在编译器确定好的指令
在进行而已;
动态语言,只是在编译期做语义语法的检查,而不会分配内存,在编译期只关心对
象是否调用这个方法,而不会关心这个对象是否为nil, 也不会关心这个方法的具体
实现以及这个对象的本质是否能调用该方法,更不会关心有没有这个方法实现,这些
事情都是在运行时进行的.

OC的动态性体现在三个方面:动态类型动态绑定动态加载

  • 动态类型,在运行时才确定对象的类型。如'NSString *data = [[NSData alloc] init]' 编译时是字符串类型,运行时确定真正类型是data类型
  • 动态绑定,基于动态类型,在对象被确定后,其类型也被确定了,则该对象对应的属性和响应小时也就确定了
  • 动态加载,根据需求加载所需资源,让程序在运行时添加代码模块以及其他资源, 用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件

3、对象的底层结构

isa指针,实例对象通过它的isa指针找到它的类,类对象通过它找到它的元类,元类指向根类
isa 指针确定所属类,super_class 确定继承关系。

/// 实例对象的底层结构
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// 类对象的底层结构
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() const {
        return bits.data();
    }
};

/// isa
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

/// 对象结构
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    uintptr_t isaBits() const;

    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    // initClassIsa(): class objects
    // initProtocolIsa(): protocol objects
    // initIsa(): other objects
    void initIsa(Class cls /*nonpointer=false*/);
    void initClassIsa(Class cls /*nonpointer=maybe*/);
    void initProtocolIsa(Class cls /*nonpointer=maybe*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);

    // changeIsa() should be used to change the isa of existing objects.
    // If this is a new object, use initIsa() for performance.
    Class changeIsa(Class newCls);

    bool hasNonpointerIsa();
    bool isTaggedPointer();
    bool isBasicTaggedPointer();
    bool isExtTaggedPointer();
    bool isClass();

    // object may have associated objects?
    bool hasAssociatedObjects();
    void setHasAssociatedObjects();

    // object may be weakly referenced?
    bool isWeaklyReferenced();
    void setWeaklyReferenced_nolock();

    // object may have -.cxx_destruct implementation?
    bool hasCxxDtor();

    // Optimized calls to retain/release methods
    id retain();
    void release();
    id autorelease();

    // Implementations of retain/release methods
    id rootRetain();
    bool rootRelease();
    id rootAutorelease();
    bool rootTryRetain();
    bool rootReleaseShouldDealloc();
    uintptr_t rootRetainCount();

    // Implementation of dealloc methods
    bool rootIsDeallocating();
    void clearDeallocating();
    void rootDealloc();

private:
    void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);

    // Slow paths for inline control
    id rootAutorelease2();
    uintptr_t overrelease_error();

#if SUPPORT_NONPOINTER_ISA
    // Unified retain count manipulation for nonpointer isa
    id rootRetain(bool tryRetain, bool handleOverflow);
    bool rootRelease(bool performDealloc, bool handleUnderflow);
    id rootRetain_overflow(bool tryRetain);
    uintptr_t rootRelease_underflow(bool performDealloc);

    void clearDeallocating_slow();

    // Side table retain count overflow for nonpointer isa
    void sidetable_lock();
    void sidetable_unlock();

    void sidetable_moveExtraRC_nolock(size_t extra_rc, bool isDeallocating, bool weaklyReferenced);
    bool sidetable_addExtraRC_nolock(size_t delta_rc);
    size_t sidetable_subExtraRC_nolock(size_t delta_rc);
    size_t sidetable_getExtraRC_nolock();
#endif

    // Side-table-only retain count
    bool sidetable_isDeallocating();
    void sidetable_clearDeallocating();

    bool sidetable_isWeaklyReferenced();
    void sidetable_setWeaklyReferenced_nolock();

    id sidetable_retain();
    id sidetable_retain_slow(SideTable& table);

    uintptr_t sidetable_release(bool performDealloc = true);
    uintptr_t sidetable_release_slow(SideTable& table, bool performDealloc = true);

    bool sidetable_tryRetain();

    uintptr_t sidetable_retainCount();
#if DEBUG
    bool sidetable_present();
#endif
};

从上面的结构体信息,可以知道,每个对象的本质都是一个结构体,都有一个isa指
针指向对应的类

4、消息发送的基础数据

SEL,方法选择器,是一个方法的selector指针,用于表示运行时下方法的名称,定义如下:

typedef struct objc_selector *SEL;

1、底层维护一张选择子表(SEL
2、OC编译时,会依据每个方法的名称和参数生成一个唯一整型标识(int类型的地
址),这个标识就是SEL
3、两个类之间,不管有没有关系,只要方法名相同,对应的SEL地址是一样的,所以
在OC同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也
不行(同一个类使用相同的方法名,即便参数不同,编译时也会报错)
4、不同类方法名可以相同,因为方法查找会根据SEL从各自的类方法列表中查询对应
IMP

/// SEL方法调用
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );

/* 
向Runtime系统中注册一个方法,如果已注册则直接返回对应的选择器,如果未注册,
则将方法名映射到一个选择器,并返回这个选择器
*/
SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );

// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );

IMP 函数指针,指向方法实现的首地址

id (*IMP)(id, SEL, ...)

Method 表示类定义中的方法

struct objc_method {
/// 方法名
    SEL _Nonnull method_name       OBJC2_UNAVAILABLE;
/// 方法类型
    char * _Nullable method_types  OBJC2_UNAVAILABLE;
/// 方法实现
    IMP _Nonnull method_imp        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
/// Method 方法调用
// 调用指定方法的实现,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快
id method_invoke ( id receiver, Method m, ... );

// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );

// 获取方法名
SEL method_getName ( Method m );

// 返回方法的实现
IMP method_getImplementation ( Method m );

// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );

// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );

// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );

// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );

// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );

// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );

// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );

// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );

// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );

5、消息发送机制

消息发送流程:

消息发送流程
  • 1、编译器将消息发送转化成objct_msgSend(id , SEL, ...),通过汇编代码
    一步步处理最终会走到这个方法lookUpImpOrForward(最新的源码)
  • 2、先判断当期那对象是否为nil,是则直接返回nil
  • 3、在运行时去执行时,该对象(实例或类)会先通过isa指针指向对应的类(类或元
    类)
  • 4、通过SEL签名在cache_t中进行查找,有则返回
  • 5、没有再去method_list中进行查找,有则添加到缓存中,并回调
  • 6、没有就通过super_class找到父类,继续进行查找,一直循环遍历,直到根类NSObject
  • 7、以上都未能找到,则进行动态解析,最后进入消息转发
异常提示IMP:
__attribute__((noreturn, cold)) void
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);
}
/// 截取部分汇编源码,可以看到lookUpImpOrForward调用的基本信息
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward


IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    /* 
    _objc_msgForward_impcache 这里通过汇编发现最终执行了
    objc_defaultForwardHandler方法调用,主要用来进行异常回调
    */
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;

    IMP imp = nil;
    Class curClass;

    ///添加线程锁,防止多线程操作
    runtimeLock.assertUnlocked();

    //如果当前的行为是查找缓存,直接查找当前方法缓存
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    runtimeLock.lock();

    /// 类的懒加载(为查找方法做准备条件,如果类没有初始化时,初始化类和父类、元
    /// 类等),对是否有swift进行不同的处理
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

/// 类的初始化操作
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;

/// 循环遍历类-父类进行查找
    for (unsigned attempts = unreasonableClassCount();;) {
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
        // 查找当前类中的方法列表,如果方法存在,则跳转到缓存插入处理
            imp = meth->imp;
            goto done;
        }

        /// 对当前类进行重新赋值,查找父类
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver did not help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but do not cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            /// 查询到父类的缓存中存在该方法,则直接插入到当前类缓存中
            goto done;
        }
    }

    //这里进入消息转发
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
/// 缓存插入
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
  • 从上面lookUpImpOrForward方法查找过程中,我们可以看到对类或者父类方法列表
    进行方法查询用到了getMethodNoSuper_nolock这个方法:
  • getMethodNoSuper_nolock,对search_method_list_inline进行遍历,查询对应的方法
static method_t *
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)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

  • search_method_list_inline,在所有方法列表中(自身,categorys)使用二分法或遍历逐一寻找以name属性值为sel的method_t(Method),如果找到,以sel为键把method存入cache_t, 直接执行mehtod里的IMP;
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        // 有序的话 进行二分查找(折半遍历查找)
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        // 无序则进行线性遍历查找
        for (auto& meth : *mlist) {
            /// 对比方法名的地址
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
  • findMethodInSortedMethodList,二分查找的具体实现
ALWAYS_INLINE 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;
}

方法缓存相关内容请移步

6、消息转发

消息转发流程
  • 动态方法解析resolveMethod_locked

/// 如果这个类不是元类则执行__class_resolveInstanceMethod,如果是元类
/// 的话则执行__class_resolveClassMethod,所以如果是类方法的话执行resolveClassMethod,
/// 那么会一直查找最后还会执行一次resolveinstancemethod,但是这个方法是
/// 执行的元类的resolveinstancemethod而不是类的,因为类是元类对象如果
/// 元类找不到就会往上层查找,元类的上层是根元类,根元类的父类指向NSObject,
/// 所以最后还会执行一次resolveInstanceMethod最终的实例方法。

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
1、尝试调用自身的_class_resolveMethod动态为类对象或元类对象里添加方法实
现。如果成功添加了method,记录已经添加过
2、看是否在resolveClassMethod  或者 resolveInstanceMethod方法中为sel
提供了动态方法决议(可通class_addMethod方法添加),如果没有提供则进入下
一步
3、解析完成后继续调用lookUpImpOrForward完成以下操作
if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
  • 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector
  • 该方法只能让我们把消息转发到另一个能处理这个消息的对象,但是无法处理消息
    的内容,比如参数和返回值

  • 该方法用于把消息转发到另外一个可以处理该消息的对象,如果该aSelector
    另一个对象中实现了,则该对象就作为该消息新的接收值,直接返回该对象。

  • 完整消息转发

运行时系统在这一步让消息接收者将消息转发给其他对象。当前对象会将对应的sel -ector,目标(target)和参数打包成anInvocation对象转发给其他对象

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([XXX instancesRespondToSelector:aSelector]) {
            signature = [XXX instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

forwardInvocation:方法的实现有两个任务:
(1)定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所
有未知消息。
(2)使用anInvocation作为参数,将消息发送到选中的对象。anInvocation
会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    XXX *x = [XXX new];
    if ([XXX instancesRespondToSelector:anInvocation.selector]) {
          [anInvocation invokeWithTarget:x];
    }
}
如果以上均未实现,则无法识别selector,并抛出异常

7、runtime实践:

1、获取对象的属性、成员变量、方法和协议列表
2、通过消息转发机制进行方法的拦截
3、动态添加方法(class_addMethod
4、方法的动态交换(method-swizzling
5、将当前类与自定义类建立映射(Class object_setClass(id obj, Class cls),由runtime的说明可知该函数的作用是为一个对象设置一个指定的类,object内部有一个叫做isa的变量指向它的class。这个变量可以被改变,而不需要重新创建。然后就可以添加新的ivar和方法了。可以通过以下命令来修改一个object的class)
例: object_setClass([NSBundle mainBundle],[ModifyBundle class])
NSBundle与ModifyBundle建立映射关系,将bundle的isa指针指向[ModifyBundle class]类,在自定义Bundle类中重写加载bundle
对应的加载自建的XML类文件、加载xib storyboard plist等文件、加载本地多
语言文件(.strings)等方法,实现全局的多语言处理

8、疑难解析

为什么要设计元类

  • 如果没有元类,则类对象的信息和实例对象的信息都会存储在类中,这样对于查找对
    应的信息,会更加复杂且混乱(如类方法和实例方法一样的情况下,需要区分查找对
    象的类型)

  • 依次类推,实例对象的方法列表在类对象的结构体中,如果直接放在实例对象中,那
    么同一个类对象可能生成多个实例对象,这些实例对象调用对应的方法时,内存将会被
    爆掉的

  • 综上,元类的设计符合单一原则和可扩展性,实例对象管理自己的属性信息,实例对
    象的方法列表由类管理,类对象方法列表由元类管理,各司其职,减少了对于对象类
    型和方法类型的判断,增加了查找效率;不同种类的方法走同一套流程,大大节约了
    维护成本。

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

推荐阅读更多精彩内容