isa 结构分析

我们要进行isa的分析首先掌握的知识
1、联合体(共用体)
1.1、使用位运算 进行 存取 数据
1.2、位域 简介
1.3、结构体位域优化代码
1.4、联合体优化代码
2、clang
2.1 clang 简介
2.2 简单用法
2.3 clang 源码 查看线索
3、isa_t联合体

1、联合体union

联合体union的定义方式与结构体一样,但是二者有根本区别。

什么是联合体(union)呢?联合体是一种特殊的类,也是一种构造类型的数据结构。完全就是共用一个内存首地址,并且各种变量名都可以同时使用,操作也是共同生效。所以也叫共用体。并且联合体(union)中是各变量是“互斥”的,但是内存使用更为精细灵活,也节省了内存空间。

在结构体��中各成员有各自的内存空间,一个结构变量总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间一个联合变量的长度等于各成员中最长的长度

1.1、使用位运算 进行 存取 数据

废话不多 我们定义一个LHCar类 这个类 有up down left right 四个代表方向的BOOL类型的属性

@interface LHCar : NSObject

@property (nonatomic, assign) BOOL up;
@property (nonatomic, assign) BOOL down;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
 
@end

进行打印

2020-09-09 23:41:04.086760+0800 isa结构分析[4982:341540] 16

输出为16字节 其中包括 isa 和4个BOOL类型的属性 共 8+1+1+1+1 = 12 内存对齐 为 16 字节
我们知道 BOOL值 只有两种情况 01 , 一个字节8个二进制位 ,并且二进制 只有 01 想到这里 那么我们完全可以使用一个二进制位来表示 一个BOOL值 。也就是这四个BOOL属性 我们可以用4个二进制位来表示如下图 ,大大节省了内存空间

截屏2020-09-10上午9.20.36.png

按照我们的臆想来实现代码,首先分别声明 up down left right 掩码 mask ,来方便我们位运算取值赋值

#define LHDirectionUpMask    0b00001000
#define LHDirectionDownMask  0b00000100
#define LHDirectionLeftMask  0b00000010
#define LHDirectionRightMask 0b00000001

定义 char 类型 的成员变量

@interface LHCar(){
     
    char _upDownLeftRight;
}
@end

初始化

- (instancetype)init
{
    self = [super init];
    if (self) {
        _upDownLeftRight = 0b00000001;
    }
    return self;
}

自定义setter


-(void)setUp:(BOOL)up
{
    if (up) {
        /// 如果需要将值置为1,将源码和掩码进行按位或运算
        _upDownLeftRight |= LHDirectionUpMask;
    }else{
        /// 如果需要将值置为0 // 将源码和按位取反后的掩码进行按位与运算
        _upDownLeftRight &= ~LHDirectionUpMask;
    }
}

-(void)setDown:(BOOL)down
{
    if (down) {
        _upDownLeftRight |= LHDirectionDownMask;
    }else{
        _upDownLeftRight &= ~ LHDirectionDownMask;
    }
}

- (void)setLeft:(BOOL)left
{
    if (left) {
        _upDownLeftRight |= LHDirectionLeftMask;
    } else {
        _upDownLeftRight &= ~LHDirectionLeftMask;
    }
}

- (void)setRight:(BOOL)right
{
    if (right) {
        _upDownLeftRight |= LHDirectionRightMask;
    } else {
        _upDownLeftRight &= ~LHDirectionRightMask;
    }
}

getter

-(BOOL)isUp
{
    return !!(_upDownLeftRight & LHDirectionUpMask);
}
-(BOOL)isDown
{
    return !!(_upDownLeftRight & LHDirectionDownMask);
}

-(BOOL)isLeft
{
    return !!(_upDownLeftRight & LHDirectionLeftMask);
 
}
-(BOOL)isRight
{
    return !!(_upDownLeftRight & LHDirectionRightMask);

}

按照图上示意 及我们的想法 这时候调用getter方法打印 初始化值

 LHCar * car = [[LHCar alloc]init];
 NSLog(@"up:%d down:%d left:%d right:%d",car.isUp,car.isDown,car.isLeft,car.isRight);
截屏2020-09-10上午9.47.22.png

我们屏蔽之前定义的属性 调用自定义的setter方法进行赋值

      LHCar * car = [[LHCar alloc]init];
        [car setUp:YES];
        [car setDown:NO];
        [car setLeft:YES];
        [car setRight:NO];
        NSLog(@"up:%d down:%d left:%d right:%d",car.isUp,car.isDown,car.isLeft,car.isRight);
截屏2020-09-10上午9.44.56.png

发现我们用一个字节的里的4个二进制位 就完成了 之前的 占有4个字节的 4个属性的读写

1.2、位域 简介

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有01两种状态,用1位 二进制位即可。为了节省存储空间并使处理简便,C语言又提供了一种数据结构,称为"位域"或"位段"

所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的定义和位域变量的说明
struct 位域结构名

{

位域列表

};

例如:

struct bs

{

int a:8;

int b:2;

int c:6;

}data;


说明 data 为 bs 变量,共占两个字节,其中位域a占8位,位域b占2位,位域 c 占6位。

位域定义说明

1、 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域
2、由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。(如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。)
3、位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的

1.3、结构体位域优化代码

我们了解了位域的基本信息那么我们可以用结构体位域来优化代码


#import "LHCar.h"

//#define LHDirectionUpMask    0b00001000
//#define LHDirectionDownMask  0b00000100
//#define LHDirectionLeftMask  0b00000010
//#define LHDirectionRightMask 0b00000001

@interface LHCar(){
//
//    char _upDownLeftRight;
    
    struct{
        char up   : 1;
        char down : 1;
        char left : 1;
        char right: 1;
    }_upDownLeftRight;
    
    
    
}
@end

@implementation LHCar
- (instancetype)init
{
    self = [super init];
    if (self) {
//        _upDownLeftRight = 0b00000001;
        
        
        
    }
    return self;
}

-(void)setUp:(BOOL)up
{
//    if (up) {
//        /// 如果需要将值置为1,将源码和掩码进行按位或运算
//        _upDownLeftRight |= LHDirectionUpMask;
//    }else{
//        /// 如果需要将值置为0 // 将源码和按位取反后的掩码进行按位与运算
//        _upDownLeftRight &= ~LHDirectionUpMask;
//    }
    
    _upDownLeftRight.up = up;
    
}

-(void)setDown:(BOOL)down
{
//    if (down) {
//        _upDownLeftRight |= LHDirectionDownMask;
//    }else{
//        _upDownLeftRight &= ~ LHDirectionDownMask;
//    }
      _upDownLeftRight.down = down;
}

- (void)setLeft:(BOOL)left
{
//    if (left) {
//        _upDownLeftRight |= LHDirectionLeftMask;
//    } else {
//        _upDownLeftRight &= ~LHDirectionLeftMask;
//    }
    _upDownLeftRight.left = left;
    
}

- (void)setRight:(BOOL)right
{
//    if (right) {
//        _upDownLeftRight |= LHDirectionRightMask;
//    } else {
//        _upDownLeftRight &= ~LHDirectionRightMask;
//    }
    _upDownLeftRight.right = right;
}
 
-(BOOL)isUp
{
//    return !!(_upDownLeftRight & LHDirectionUpMask);
    
    return !!_upDownLeftRight.up;
}
-(BOOL)isDown
{
//    return !!(_upDownLeftRight & LHDirectionDownMask);
    return !!_upDownLeftRight.down;
}

-(BOOL)isLeft
{
//    return !!(_upDownLeftRight & LHDirectionLeftMask);
    return !!_upDownLeftRight.left;
 
}
-(BOOL)isRight
{
//    return !!(_upDownLeftRight & LHDirectionRightMask);
    return !!_upDownLeftRight.right;

}
@end

这样是可以正常存取 但是去掉了 掩码mask 和初始化代码 导致可读性非常差 这时联合体出来了

1.4、联合体优化代码

#import "LHCar.h"

#define LHDirectionUpMask    0b00001000
#define LHDirectionDownMask  0b00000100
#define LHDirectionLeftMask  0b00000010
#define LHDirectionRightMask 0b00000001

@interface LHCar(){
    union{
        char bits;
        struct{
            char up   : 1;
            char down : 1;
            char left : 1;
            char right: 1;
        };
        
    }_upDownLeftRight;
 
    
    
}
@end

@implementation LHCar
- (instancetype)init
{
    self = [super init];
    if (self) {
        _upDownLeftRight.bits = 0b00000001;
        
        
        
    }
    return self;
}

-(void)setUp:(BOOL)up
{
    if (up) {
 
        _upDownLeftRight.bits |= LHDirectionUpMask;
    }else{
        _upDownLeftRight.bits &= ~LHDirectionUpMask;
    }
 
    
}

-(void)setDown:(BOOL)down
{
    if (down) {
        _upDownLeftRight.bits |= LHDirectionDownMask;
    }else{
        _upDownLeftRight.bits &= ~ LHDirectionDownMask;
    }
     
}

- (void)setLeft:(BOOL)left
{
    if (left) {
        _upDownLeftRight.bits |= LHDirectionLeftMask;
    } else {
        _upDownLeftRight.bits &= ~LHDirectionLeftMask;
    }
    
    
}

- (void)setRight:(BOOL)right
{
    if (right) {
        _upDownLeftRight.bits |= LHDirectionRightMask;
    } else {
        _upDownLeftRight.bits &= ~LHDirectionRightMask;
    }
    
}
 
-(BOOL)isUp
{
    return !!(_upDownLeftRight.bits & LHDirectionUpMask);
    
  
}
-(BOOL)isDown
{
    return !!(_upDownLeftRight.bits & LHDirectionDownMask);
 }

-(BOOL)isLeft
{
     return !!(_upDownLeftRight.bits & LHDirectionLeftMask);
    
 
}
-(BOOL)isRight
{
    return !!(_upDownLeftRight.bits & LHDirectionRightMask);
 
}
@end

测试发现依旧可以 完成 存取

其中 _upDownLeftRight 联合体只 占用了一个字节 因为结构体中
updownleftright、都只占用一位二进制空间,这就是 4 个二进制空间 而 char 类型 bits 也只占用了一个字节 他们都在联合体中 因此 共用一个字节的内存

总结: 通过掩码进行位运算来增加 效率 通过联合体结构 可以 节省内存空间

2、clang用法

2.1、简介

Clang是一个C语言、C++、Objective-C、C++语言的轻量级编译器。源代码发布于BSD协议下。也是Xcode 第一的编译器

2.2 简单使用

clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件 UIKit报错问题

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了 一些封装,要更好用一些

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp (手机)

clang就说这么多 具体深究请自行查阅 注意:查看模拟器版本 以及 源文件

准备一段代码 clang 其本质

#import <Foundation/Foundation.h>
 
@interface LHPerson : NSObject
{
    NSString * nickName;
}

@property (nonatomic, copy) NSString *name;
 
@end

@implementation LHPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
    }
    return 0;
}

clang过后 我们根据LHPerson为深入线索,进行搜索

struct LHPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *__strong nickName;
    NSString *__strong _name;
};


// @property (nonatomic, copy) NSString *name;



/* @end */


// @implementation LHPerson

static NSString * _I_LHPerson_name(LHPerson * self, SEL _cmd) { return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LHPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LHPerson_setName_(LHPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LHPerson, _name), (id)name, 0, 1); }
// @end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}

NSObject_IMPL又是什么?

struct NSObject_IMPL {
    __unsafe_unretained Class isa;
};

通过上面总结
1、一个类的声明或创建 底层实现 就是 一个结构体
2、Class其实就是一个指针 指向了 objc_class类型的结构体
3、LHPerson_IMPL 结构体中有3个成员变量 isanickName _name
4、属性自动生成 getter setter 方法 并且帮我们转成 _name
5、nickName并没有生成getter setter
6、selfSEL _cmd 为默认参数

3、isa_t联合体

union isa_t


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
};

由上面的介绍联合体概念可以知道,cls和bits之间是互斥的,即有cls就没有bits,有bits就没有cls。

initInstanceIsa

我们知道alloc的流程 最重要的 是 三部曲

1、 size = cls->instanceSize(extraBytes); ///计算 需要多少内存空间
2、 obj = (id)calloc(1, size); // alloc 开辟内存的地方
3、 obj->initInstanceIsa(cls, hasCxxDtor);///关联对应的类

objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

这就很好地解释了为什么上面的源码在初始化isa的时候会用nonpointer来区分开。所以isa的大小占8个字节,64位。其中这64位中分别存储了什么呢?通过ISA_BITFIELD位域

ISA_BITFIELD 位域
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

这两种是分别在arm64和x86系统架构下的,但是都是64位的,本文的说明是在x86下介绍的。

nonpointer: 表示是否对isa指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa中包含了类信息,对象的引用计数等
has_assoc:关联对象标志位,0没有,1存在
has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象

shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。x86_64 下 44 位
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放
deallocating:标志对象是否正在释放内存
has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

lldb指令验证

截屏2020-09-10下午2.46.41.png

x/4gx person : 打印出地址
p/x LHPerson.class : 打印类的内存地址

通过源码 搜索 object_getClass

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}
inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

从源码中可以知道返回的isa最终是(Class)(isa.bits & ISA_MASK)。
p/x :0x00007ffffffffff8ULL & 0x001d80010000233d

截屏2020-09-10下午3.02.08.png

最终发现
1和
2的内存值是一样的,所以isa是关联着对象与类的。

根据上面我们对联合体的大概了解 我们也知道 那么我们是不是可以不用面具mask 直接通过<<< >>>也可以拿到类的信息 x86下类信息在 isa 的位域 44
截屏2020-09-10下午3.49.56.png

继续p/x 做位移运算


截屏2020-09-10下午4.33.46.png

我的天 isa 指针中 真的包含 类的信息

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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