runtime看我就够了!

1.what?

Objective-C具有相当多的动态特性,也就是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding),和动态加载(Dynamic loading)。这些特性都是基于runtime实现的。so,Objective-C的Runtime是一个运行时库(Runtime Libary),它是由C语音和汇编写的库。为C添加了面相对象的能力并创造了 Objective-C,这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。

Objective-C 是面相运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚至可以交换方法的实现,等等。

2.引导--神经病院objc runtime入院考试

看看神经病院的objc runtime的入院考试题。这些个题虽然不会在面试中面到,但是对于runtime的理解是很有帮助的。

@implementation Son : Father

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

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]];
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end

带着思考以及你们的答案,让我们开始runtime之旅。

3. objc中你不能不知道的元素

3.1 从selfsuper开始

首先得理解selfsuper这两个概念。否则无从下手。

self是类的隐藏参数,指向当前调用方法的这个类的实例。
super是一个Magic Keyword,它本质是一个编译器标示符,和self 一样都是指向是一个消息接收者。

3.2 id(对象)

id 定义? (objc.h )
/// A pointer to an instance of a class.
typedef struct objc_object *id;

按照解释,通俗的来说就是一个指针。objc_object又是什么?

id这个struct的定义本身就带了个, 所以我们在使用其他NSObject类型声明实例时需要在前加上, 使用id时却不用 。

什么是objc_object? (objc.h )
/// Represents an instance of a class. 
struct objc_object {
     Class isa  OBJC_ISA_AVAILABILITY;
};

这里给出了解释是说,objc_object代表的是一个类的实例。

这个时候我们知道Objective-C中的object在最后会被转换成C的结构体, 在这个struct中有个isa指针,指向它的类别Class
有一种说法解释isa还是比较容易理解的:is a pointer,是个指针。

3.3 class(类、类对象)

上面出现了class,这又是什么呢?

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

是一个隐式的类型代表OC中的类,那么objc_class是什么?

struct objc_class {
     Class isa;                                 // isa 指针
     Class super_class;                         // 父类,指向父类
     const char *name;                          // 类名
     long version;                              // 版本
     long info;                                 // 类信息
     long instance_size;                        // 实例大小
     struct objc_ivar_list *ivars;              // 参数链表
     struct objc_method_list **methodLists;     // 方法链表
     struct objc_cache *cache;                  // 方法缓存,调用过的方法存入缓存列表,下次调用优先从栈中寻找
     struct objc_protocol_list *protocols;      // 协议链表
}OBJC2_UNAVAILABLE;
Use `Class` instead of `struct objc_class`

objc_class的结构体代表的是一个class。

我们都知道在oc中一切皆为对象。

下载Objc源码,在 objc-runtime-new.h 中,objc_class还有这样的定义

struct objc_class : objc_object {
 // Class ISA;
    Class superclass;
    ...
    ...
}

也就是说,class本身也是一个对象,也有superclass。class是一个指向类对象的指针。这个class也有一个isa指针,这个指针指向的就是元类。

比较绕,举个例子

Son *son = [[Son alloc] init];
// 在这里,我们初始化了一个son的实例变量,就是常说的对象。
// son对象的类是Son。
// 而Son这个class也是一个对象。(类对象)
// Son这个对象的类就是上面说的元类。
Class_MetaClass.jpg

总结:

<b>

  1. 每个Class都有一个isa指针指向一个唯一的Meta Class。
  2. 每一个Meta Class的isa指针都指向最上层的Meta Class(图中的NSObject的Meta Class)
  3. 最上层的Meta Class的isa指针指向自己,形成一个回路
  4. 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身
  5. 最上层的NSObject Class的super class指向 nil。
    </b>

3.4 SEL

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

选择器,一个方法selector。

并没有找到objc_selector的结构定义,selector用于表示一个运行时方法的名字,在运行时,会根据方法的名字、参数序列生产一个唯一的标识,就是SEL,说白了就是方法名的地址,一个字符串。

在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。

不同的类中有相同的selector,这个无所谓,不同的类在执行过程中,会在各自的方法列表中根据selector寻找相应的SEL。
工程中SEL是一个set集合,每一个SEL都是唯一的。SEL只是一个指向方法的指针。可以通过以下三种获取SEL:

  1. sel_registerName函数
  2. Objective-C编译器提供的@selector()
  3. NSSelectorFromString()
SEL func1 = sel_registerName("btnClick");
SEL func2 = @selector(btnClick);
SEL func3 = NSSelectorFromString(@"btnClick");
if ([self respondsToSelector:func1]) {
    [self performSelector:func1 withObject:nil];
}

这个时候会告诉你有警告, "PerformSelector may cause a leak because its selector is unknown"。
为什么会出现这种警告呢⚠️?

如果正常的使用[self btnClick], btnClick这个方法没有实现,系统在编译阶段会报错,否则会走正常的流程调用方法。
如果使用上述方式,一切方法都是在运行时进行处理的,不知道有没有实现该方法,只有在运行时才会知道有没有该方法,有则执行,没有发生crash。

这种警告怎么消除?让我们继续往下看。

3.5 IMP

/// A pointer to the function of a method implementation.
void (*IMP)(id, SEL, ...)     
// id接受消息对象的id,SEL选择器,返回一个void

是一个指针,函数的指针,指向方法实现的首地址。

我们可以根据选择器SEL,获取对应的函数指针IMP,然后我们就可以像调用C的函数一样使用函数指针。通过SEL获取IMP的函数指针,这样我们可以跳出runtime的运行机制消息传递,直接执行IMP的函数实现。这样比直接向实例对象发送消息高效。
我们可以这样获取IMP:

method_getImplementation(Method m);     // 获取任意方法法指针
[self methodForSelector:sel];           // 获取本类中的方法指针

我们可以通过如下转换,将上面提到的警告消除。

SEL sel = NSSelectorFromString(@"btnClick");
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL) = (void*)imp;
func(self, sel);
 
主要去看IMP的定义 void (*IMP)(id, SEL),将现有的函数指针转换成C的形式实现。

3.6 Method

// 方法链表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
     
    int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
    #endif
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}
 
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
 
struct objc_method {
   SEL method_name;         // 方法名
   char *method_types;      // 类型 char指针 存储参数和返回值
   IMP method_imp;          // 函数指针
}

在上面的objc_class的定义中有objc_method_list变量用来存储方法列表,而Method的结构体中,可以看出,存储的是SEL <-> IMP的映射。

3.7 Cache

在看看cache的结构体,cache存储用过的方法

struct objc_cache {
    unsigned int mask // total = mask + 1                    
        OBJC2_UNAVAILABLE;
    unsigned int occupied                                    
        OBJC2_UNAVAILABLE;
    Method buckets[1]                                        
        OBJC2_UNAVAILABLE;
};

mask: 指定分配cache buckets的总数。在方法查找中,runtime使用这个字段确定数组的索引位置。
occupied: 实际占用cache buckets的总数。
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

3.8 Ivar

typedef struct objc_ivar *Ivar;

Ivar代表类中实例变量的类型。

objc_ivar的定义如下:

struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; // 变量名
    char *ivar_type                   OBJC2_UNAVAILABLE; // 变量类型
    int ivar_offset                   OBJC2_UNAVAILABLE; // �基地址偏移字节
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE; // 占用空间
#endif
}

class中提到的objc_ivar_list,定义如下:

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE; // 变量个数
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE; // 占用大小
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE; // 变量数组
}

3.9 objc_property_t

typedef struct objc_property *objc_property_t;

objc_property_t是属性。与之相关联的还有一个objc_property_attribute_t

/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

说了这么多,是不是觉得没毛用?让我们进入正题。

4. 消息

4.1 objc_msgSend 动态绑定

在OC中,消息在运行时才会绑定到方法的实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend,这个函数将消息的接收者和方法名作为参数
objc_msgSend(receiver, selector)

receiver:消息的接收者。
selector:方法的选择器。

<b>这个函数完成了动态绑定的所有事情:</b>

  1. 找到selector对应的方法实现,因为同一个方法可能在不同的的类中有不同的实现,所以我么需要依赖接收者的类来找到确切的实现。
  2. 调用方法的实现,并将接收者对象及方法的所有参数传给它
  3. 将实现返回的值作为自己的返回值。

objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

  1. 当消息发送给一个对象时,首先从运行时系统缓存使用过的方法中寻找(cache),如果找到,执行该方法,如果未找到继续执行
  2. objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法列表中查找对应的selector,如果没有找到则在指向父类的方法列表中寻找,依次、最后到NSObject,如果找到,加入缓存Cache,没有找到,则会走消息转发流程。
  3. 这里需要注意的是,实例方法(-方法)存在对应类的方法列表中,而类方法(+方法)存在对应元类的方法列表中。

4.2 消息转发

也有说是动态方法解析,动态方法决议。都是一个意思。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发


    消息转发.png

当发送一个消息的时候,先从cache列表中寻找,有的话执行,没有从这个实例方法或者类方法的方法列表中查找,有的话执行,没有的话:动态转发第一步

  1. 方法解析。调用+(BOOL)resolveInstanceMethod:(SEL)sel(+resolveClassMethod)这个方法,叫动态方法解析(决议)。在这个方法中,我们有机会为该未知消息新增一个”方法”“。不过使用该方法的前提是我们已经实现了该”方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。或者替换为已知的方法。
  2. 备用接受者。如果返回NO,动态转发第二步
    - (id)forwardingTargetForSelector:(SEL)aSelector,runtime会继续调用这个方法,如果实现了这个方法并返回一个非nil的值,则这个返回值将作为新的消息接收者。如果没有实现该方法, go on 第三步。
  3. 完整转发。- (void)forwardInvocation:(NSInvocation *)anInvocation,如果上一步没有处理消息,则runtime走这个方法。这也是最后一次操作。runtime会在这个方法中将消息转发给其他对象。不过在执行这个方法前会首先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector来请求一个签名,从而生成一个NSInvocation,对消息进行完全转发。

具体事例看本文的demo

总结:
  1. 我们可以通过2、3来模拟“多继承”,一个类中可能会包含其他的类,当这个类不能实现该方法时,将方法的接收方改为其他的类,这样就好像是自己完成了这些操作。
  2. 多继承: 将不同的功能集成到一个对象中,会让这个对象变的很大、涉及到的东西很多。
  3. 消息转发:将功能分解到不同的小的对象中,通过一种特定的方式将它们连接起来,并做消息转发。

常用方法:

// 添加
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); 
// cls 需要动态决议的类
// name 需要动态决议的方法
// imp 需要执行的的方法的指针,(函数指针)
// types 类型 函数类型(字符串) v:返回值void @:参数id类型 ":":SEL对象 i:int d:double

// 获取实例
Method class_getInstanceMethod ( Class cls, SEL name ); 
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name ); 
// 获取所有的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount ); 
//  替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types ); 
//  返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name ); 
//  类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel ); 

5. Category分类

/// An opaque type that represents a category.
typedef struct objc_category *Category;

在objc-runtime-new.h中有如下定义

 struct category_t {
     const char *name;                          // 指的是class_name,不是category_name
     classref_t cls;                            // 是扩展的类对象,编译期间不会被定义,在runtime阶段通过name被指定
     struct method_list_t *instanceMethods;
     struct method_list_t *classMethods;
     struct protocol_list_t *protocols;
     struct property_list_t *instanceProperties;    // 这也是category为什么能添加属性的原因
     
     method_list_t *methodsForMeta(bool isMeta) {
         if (isMeta) return classMethods;
         else return instanceMethods;
     }
     
     property_list_t *propertiesForMeta(bool isMeta) {
         if (isMeta) return nil; // classProperties;
         else return instanceProperties;
     }
 };

使用场景:

  1. 给现有的类添加方法;
  2. 将一个类的实现拆分成多个独立的源文件;
  3. 声明私有的方法。
// .h文件中添加一个属性
@interface People (Add)

@property (nonatomic, copy) NSString *name;

@end

//-------------------------------
// .m中添加方法
static char *PeopleName;

@implementation People (Add)

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, PeopleName, name, OBJC_ASSOCIATION_COPY);
}

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

@end

set方法中调用void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)这个函数:

object 需要关联属性的对象

key 需要关联的key值 一般情况下使用静态变量static char,

value 需要与key对应的数据

policy 关联策略 一种枚举值

get方法中调用id objc_getAssociatedObject(id object, void *key)函数。

精神病院的考试题应该就迎刃而解了。

第一题:

上面的例子中[self class][super class],接受消息的对象都是son这个实例变量。不同的是,self是在本类的方法中寻找,super则告诉编译器在父类中的方法列表中寻找。(意思就是。self直接在本类中寻找,找不到,则在父类中寻找;而super则是直接在父类中寻找)。

使用clang编译两个NSLog之后

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0,
      NSStringFromClass(((Class (*)(id, SEL))
            (void *)objc_msgSend)((id)self, sel_registerName("class"))));
 
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1,
      NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))
            (void *)objc_msgSendSuper)((__rw_objc_super){ 
                (id)self, (id)class_getSuperclass(objc_getClass("Son")) 
            }, sel_registerName("class"))));

这两个方法可以看到:

[self class]---> objc_msgSend

self发消息 ---> id objc_msgSend(id self, SEL op, ...)

[super class]---> objc_msgSendSuper

super发消息 ---> id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数d结构体定义:

struct objc_super {
    __unsafe_unretained id receiver;        // 消息的接收者,就是当前的 self 也就是son实例变量
    __unsafe_unretained Class super_class;  // 当前类的父类
};

// 调用class的方法 函数实现

- (Class)class {
    return object_getClass(self);   // 这里的self指向的是son实例变量
}

看到这里是不是明白了为什么两个输出都是 Son。第一个,调用[self class]直接在Son中查找,发现Son中没有这个函数,则去 Father中查找,还是没有,则去NSObject中查找class,有返回数据. [super class] 则直接从 Father中查找。

如果我们在Father这个类中,重写- (Class)class方法

- (Class)class {
    return nil;
}

会输出什么呢?
nil son

这个时候,估计有人问了,最后都是在 NSObject 中调用的 class 方法,而在super的结构体中这个receiver 上面说的是 self,实例变量son,那是不是super也是从 Son 中开始查找的呢?答案是肯定的不是,看 objc_msgSendSuper 的第二个参数, class_getSuperclass(), 是从父类中寻找的方法。此时是先构造了super的结构体,已经拿到了receiver 指向 self(son实例变量), 这个时候直接从 super 中找方法,不会理会 Son 中的方法重写。

第二题

首先,我们需要理解两个概念:

isKindOfClass: 判断当前对象是不是该该类的实例对象,或者是继承自该类的实例对象。

isMemberOfClass: 只能判断当前对象是不是该类的实例对象。

+ (Class)class {
    return self;
}
// class的类方法返回本身self。
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

根据上面的源码我们看到

isKindOfClass:的实现方法中比较的是类,如果相等返回YES,否则找 superclass 继续比较,直到最后一个 superclass 比较结束

isMemberOfClass: 则是直接比较当前class

所以上面的例子中,第一个是 YES,其它都是NO。
根据上图。我们知道,(id)[NSObject class]这是一个类对象,是由NSObject的元类创建的(isa指向元类),这时比较,两个类不一样,NSObject的元类的superClass指向NSObject本身。所以,第一个为YES。

第三题:

1是类方法,但是并没有实现该类方法。所以在编译的时候相当于把这段方法的声明注销掉了。根据之前的例子,类对象寻找方法是在它的元类中找,NSObject的元类没有此方法,这是在类的分类中添加的一个方法,所以元类中没有,NSObject 的元类的父类是NSObject,在NSObject的方法列表中查找,发现了此方法,存入到元类的方法列表中。输出结果。

2是实例方法,所以在NSObject的方法列表中找,找到,存入缓存,输出结果。

这是在NSObject的分类中添加的方法,如果在自己写的一个类(People: NSObject)的分类中添加这样的方法,输出会怎么样?

@interface People : NSObject

@end

@interface People (Sark)
+ (void)foo;
@end
@implementation People (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[People foo];               // 这里会crash。
[[People new] foo];

之所以会发生crahs,是因为:+foo方法没有在People的元类中,People的元类的SuperClass是NSObject的元类,也没有此法。NSObject的元类的superClass是NSObject,也没有此方法,所以crash。

第四题

具体的内存入栈方式是怎样,没有了解过,之后做处理,先留一个坑,欢迎大神们做解答,谢谢。

可以看原文是怎么解释的,希望能看懂。
传送门

总结

runtime的实际用例:

Method Swizzling,分类添加属性、 字典转模型等。 有空再整理。

引用

objc runtime source code

写的不好,欢迎各位大神指正。喜欢的点赞,加个关注,谢谢!

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

推荐阅读更多精彩内容