Objective-C,不用也要知道的runtime知识(简单易懂)

写在前面,对runtime的学习,已经悄悄地进行了很长时间,知识结构,也由之前的模模糊糊,变得越来越清晰,网上已经有很多关于runtime的学习研究篇章了,但是,别人的东西,不是我的东西(包括我的东西,也不是你的东西,如果你看了我的文章,对你有一点启发就好,别的不多求),每个人都会自己的学习记忆体系,把知识转变为自己熟悉的套路,那,在日后回看的时候,也会觉得,还是熟悉的味道,上手,也会极快滴.

1.runtime是什么?(俗一点的说法:说说你对runtime的理解.)

runtime,国内叫运行时,是一套底层的C语言API,objective-c代码,底层都是基于它来实现的.

感受一下发送消息的代码:

#import "ViewController.h"
#import "Person.h"//项目中创建好的一个Person类,它有一个working方法
#import <objc/message.h>
- (void)viewDidLoad {
    [super viewDidLoad];
    //Person *p = [Person alloc];
    //查找build setting -> 搜索msg->设置为NO,才会有msg代码提醒
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    //p = [p init];
    p = objc_msgSend(p, sel_registerName("init"));
    //[p working];
    objc_msgSend(p, @selector(working));
    //只有对象才能发消息,所以发消息的前缀用obj
}

2.运行时和编译时特性的对比.

(知识储备)源码的解释流程:预编译->编译->链接->运行
编译:C语言在编译阶段就要切确知道被调用函数的真实类型了,如果在编译时不能确定真实类型,则会报错.
运行时:OC在调用方法/定义类/定义成员变量时,在编译时还是不能知道他们的真实类型.OC把这一切行为推迟到运行时.也就是意味着,有类型不匹配的情况,在运行时才会抛出异常.OC中调用方法,也叫消息发送.注意区别C语言中的函数调用.
注意:编码时,报错信息尽量在编译阶段就调试出来.

3.runtime的相关术语

SEL :方法选择器,他对应方法的名字,OC中方法的名字包括冒号.
id :指向某个类的实例的指针.
Class :指向 objc_class 结构体的指针.
Method :代表类中的某个方法,它存储了方法名(SEL),方法类型(参数类型和返回值类型),方法实现(IMP).
IMP :指向函数的指针,代表了方法的实现.
Cache : 专门用来缓存方法的实现的(IMP).
Property :属性

4.OC中的隐形参数

开发中,我们经常会用到一个全局self,但是你没有想过他是怎么来的呢?其实,这个self,是每个方法中都带有的隐形参数,跟他一起的,还有一个_cmd参数,他是SEL类型的变量,这两个参数,都是苹果在运行时,悄悄咪咪地添加进去的.

有关这两个隐形参数:
①观察这两个表达式typedef id (*IPM) (id, SEL,...)objc_msgSend(id, SEL,...),我们可以确定,一组idSEL参数,就能确定唯一的方法实现地址,相反,一个确定的方法,也只有唯一的一组idSEL 参数.读到这里,你心中的这个谜团,有没有解开了呢?那就是:OC的一个类中,不能有同名的方法.
②开发中,除了上面点出的两个变量之外,你还用过哪些变量感觉"情不知何起,而一往情深的"? 看代码,说出你心中的执行结果.

@implementation Dog : Animal
- (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
//打印结果
NSLog(@"%@", NSStringFromClass([self class])); =>Dog
NSLog(@"%@", NSStringFromClass([super class])); =>Dog

很多人会想当然的认为“ superself类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实super 是一个 Magic Key Word, 它本质是一个编译器标示符,和 self是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。

super使用的理性解释:当super接到消息时,编译器会添加一个objc_super 结构体:

struct objc_super {
id receiver; //receiver仍然是self本身
Class class;//Class是指向objc_class结构体的指针,结构体内有指向父类的指针的成员变量,所以规定了`super`直接找父类方法
}

编译器将指向selfid指针和classSEL传递给objc_msgSendSuper函数(参数又进行了一次传递,内容没有变哦),而class方法只有在NSObject才能找到,OC底层将class方法转为object_getClass(),紧接着又会被编译器将代码转为objc_msgSend(objc_super->receiver,@selector(class)),因为,一开始就是self去调用class方法,打印出来的,也就是Dog,而不是Animal.读到这,如果不是很理解,那你多读几遍,毕竟文字嘛,理解起来是没那么形象.实在理解不了,就记住那段"感性的super"吧,开发中,够用的了.

5.消息发送

本来想偷偷懒,用语言描述消息发送的步骤的,但是,写着写着,自己都绕晕了,so,a picture speaks all.


消息发送步骤.png

6.动态添加方法

当一个消息被发送出来后,会经过一系列的查找,其中有一个步骤就是判断有没有动态添加方法.那么runtime提供什么样的接口给外界进行动态添加方法呢?

+ (BOOL)resolveInstanceMethod:(SEL)se//动态对象方法
+ (BOOL)resolveClassMethod:(SEL)sel;//动态类方法
通过重写上面方法,调用class_addMethod();函数来动态添加方法,同时返回YES即可,如果返回NO,则会进入消息转发机制.
eg:

#import "ViewController.h"
#import <objc/runtime.h>
@implementation ViewController
//没有返回值没参数类型的函数
void dynamicMethIMP (id self, SEL _cmd){
    NSLog(@"我是动态添加的方法实现");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(resolveThisMethodDynamically)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(resolveThisMethodDynamically)) {        
        /**
         @param sel __unsafe_unretained Class cls 类的类型(需要添加方法的类)
         @param SEL name 方法选择器(方法的名称)
         @param IMP  方法的实现
         @param const char *types 函数类型
         */
        class_addMethod([self class], sel, (IMP)dynamicMethIMP, "v@");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
@end

7.消息转发

消息转发,前提是没人要的消息,才会被转发.我个人认为是runtime比较精彩的部分.

1)重定向

消息转发之前,runtime系统允许外界替换消息的接受者为其他对象,通过-(id)forwardingTargetForSelector:(SEL)aSelector;该方法不能指定对象为self了.如果没有指定其他对象来发送这个信息,则进入消息转发机制.

2)转发

在转发机制里面,执行的方法是- (void)forwardInvocation:(NSInvocation *)aInvocation;这个方法需要传入一个NSInvocation对象,这个对象系统会自动调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;返回一个方法签名对象,用来生成NSInvocation对象当做参数传进去使用,所以在重写forwardInvocation:方法来做各种处理(eg:修改方法实现,修改响应对象)的同时也要重写methodSignatureForSelector:,否则会抛异常.

如果没有实现forwardInvocation:方法,系统会调-(void)doesNotRecognizeSelector:(SEL)aSelector方法,如果外界也没有实现这个方法,那么程序就会crash.到此,消息转发就告一段落了.

假如发送了消息之后,没有方法实现,那么,有多少次机会补救呢?我的理解是3次.

8.方法交换
#import "NSObject+CHLog.h"
#import <objc/runtime.h>
#import "CHTools.h"

@implementation NSObject (CHLog)

+(void)load{
    //1.获取系统的description对象方法类型
    Method instanceDescription = class_getInstanceMethod(self, @selector(description));
    //2.获取myLog对象的方法类型
    Method ch_instanceDescription = class_getInstanceMethod(self, @selector(myLog));
    //3.交换方法
    method_exchangeImplementations(instanceDescription, ch_instanceDescription);
    
    //1.获取系统的description类对象方法类型
    Method classDescription = class_getClassMethod(self, @selector(description));
    //2.获取系统的myLog类对象方法类型
    Method ch_classDescription = class_getClassMethod(self, @selector(myLog));
    
    //3.交换方法
    method_exchangeImplementations(classDescription, ch_classDescription);
}

- (NSString *)myLog{
    NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函数名:%s], " "[行号:%d], [时间:%@]\n打印内容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [LXHTools getTodayDetailDateString]] ;
    
    return str;
}
+ (NSString *)myLog{
    NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函数名:%s], " "[行号:%d], [时间:%@]\n打印内容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [CHTools getTodayDetailDateString]] ;    
    return str;
}
@end

介绍方法交换,主要是想大家有一个思想在,就是在没有.m文件的情况下,想修改一个类的方法,除了使用继承和分类暴力抢先之外,还可以利用runtime来实现.并且runtime有一个好处就是只需要修改一次就能一劳永逸.你设想,你接触一个很老的项目,而项目的需求是要你在系统的方法上添加新的功能,你难道要为系统的类写一个分类,再去每个使用了该方法的类中去导入头文件,再手动把方法替换?如果你不会使用方法交换,那写分类,确实是一个解决的方法.

交换方法的实现,其实就是OC中Method Swizzle的实践,除了method_exchangeImplementations,我们还可以利用class_replaceMethod来修改类,利用method_setImplementation来直接设置某个方法的IMP,归根到底,都是偷换了selector的IMP.so far,有没有觉得runtime非常牛逼,但是,runtime虽好,使用需谨慎啊.
如下面的使用的时候,调用description方法已经被替换成了调用我的myLog方法。

//项目中用到description方法的地方都会偷偷变成我的myLog方法的实现
- (void)viewDidLoad {
    [super viewDidLoad]; 
    // description => myLog 交互这两个方法实现
    NSLog(@"%@", [Person description]); 
    Person *p = [[Person alloc] init]; 
    NSLog(@"%@", [p description]); 
}
2016-02-23 16:42:11.599 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函数名:+[NSObject(CHLog) myLog]], [行号:38], [时间:2016-02-23 16:42:11]
打印内容:
2016-02-23 16:42:11.600 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函数名:-[NSObject(CHLog) myLog]], [行号:33], [时间:2016-02-23 16:42:11]
打印内容:
9.动态添加属性

在这之前,你是不是也认为category中只能添加方法,不能添加属性?
但是,利用runtime,添加属性,也变成了可能!下面是我在UIView的分类中添加的一个字符串nameTag属性,以后直接通过点语法,可以设置/获取控件的字符串tag,设置控件的tag就不再拘泥于NSInteger了.

#import "UIView+CHFrame.h"
#import <objc/runtime.h>
@implementation UIView (CHFrame)
static char nametag_key;

- (void)setNameTag:(NSString *)NameTag {
    objc_setAssociatedObject(self, &nametag_key, NameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)nameTag {
    return objc_getAssociatedObject(self, &nametag_key);
}
- (UIView *)viewWithNameTag:(NSString *)aName {
    if (!aName) return nil; 
    // Is this the right view? 查找view
    if ([[self nameTag] isEqualToString:aName])
        return self;
    // Recurse depth first on subviews;
    for (UIView *subview in self.subviews) {
        UIView *resultView = [subview viewNamed:aName];
        if (resultView) return  resultView;
    }
    // Not found
    return nil;
}

- (UIView *)viewNamed:(NSString *)aName {
    if (!aName) return nil;
    return [self viewWithNameTag:aName];
}
@end
10.字典转模型的安全实现原理

字典转模型,字典,才需要转成模型(将字典中的key,跟模型的属性名一一对应起来,在开发中直接通过点语法来取值,会更方便开发和利于纠错).

1) 自动生成模型属性

通常,服务器返回的字段会比较多,有些字段我们用不上,开发中,多数时候我们都是去字典中一个一个找,然后再去模型中定义对应的属性,下面介绍一个方便设计模型的方法--遍历字典中所有的key,并以@property(nonatomic,strong) NSString *name的形式拼接打印出来,拷贝去使用就可以了.

//给NSDictionary写一个分类
#import "NSDictionary+Property.h"

@implementation NSDictionary (Property)
// isKindOfClass:判断是否是当前类或者子类
- (void)createModelPropertyFormatter
{  
    NSMutableString *formatter = [NSMutableString string];
    // 遍历字典
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {       
        NSString *formatterKind;
        if ([value isKindOfClass:[NSString class]]) {
            formatterKind = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;",key];
        } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
            formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
        } else if ([value isKindOfClass:[NSNumber class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
        } else if ([value isKindOfClass:[NSArray class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
        } else if ([value isKindOfClass:[NSDictionary class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
        }     
        [formatter appendFormat:@"\n%@\n",formatterKind];     //换行
    }];    
    NSLog(@"%@",formatter);    
}
@end
//我的经验:将服务器获取到的字段转成plist文件,层级更加清晰,获取字段中的字典数组,遍历数组,得到单个的字典,通过调createModelPropertyFormatter方法,把字典的key统统打印出来,拷贝到模型中,删除多余的字段,此时,模型已经创建好了.
//如果不懂使用这个快捷方法,可以留言,代码这里就不贴出来了.

2) KVC--Key Value Coding

苹果已经提供了一个方法,可以快速进行字典转模型了

#import "CHTagItem.h"

@implementation CHTagItem
+ (instancetype)tagWithDict:(NSDictionary *)dict{
    CHTagItem *tagItem = [[self alloc] init];
    [tagItem setValuesForKeysWithDictionary:dict];
    return tagItem;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
@end

setValuesForKeysWithDictionary:dict快速赋值
setValue:forUndefinedKey:重写系统方法(空实现),以防找不到一一对应的字段时抛异常.
上面那种方法,只适用比较简单的模型设计.如果服务器返回的字段中字典有嵌套关系,只是简单地重写setValue:forUndefinedKey方法使程序是不抛异常,你会发现,模型设计得并不是你想要的结果.下面举个:

#import <Foundation/Foundation.h>
#import "CHUserItem.h"

@interface CHCategoryItem : NSObject
@property (nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *id;
/** 用户信息模型的集合*/
@property (nonatomic, strong)NSArray<CHUserItem *> *users;
/**用户信息的模型*/
@property (nonatomic, strong)CHUserItem *user;
@end

#import <Foundation/Foundation.h>
@interface CHUserItem : NSObject
@property (nonatomic, strong)NSString *screen_name;
@property (nonatomic, strong)NSString *header;
@property (nonatomic, strong)NSString *fans_count;
@end

在调用setValuesForKeysWithDictionary:方法时,系统流程如下:
1.遍历模型的属性,调属性的setter方法赋值--setName:
2.如果该属性没有setter方法,系统会去找不带下划线的属性赋值--name.
3.如果该属性没有不带下划线的属性,那系统会去找带下划线的属性去赋值--_name.
4.如果第3步还是没有找到对应的属性赋值,系统就会调setValue:forUndefinedKey抛出异常,告诉外界找不到该字段去赋值.
根据上面的流程,我们可以通过重写属性的setter方法,拦截数据处理好了再赋值,这样才可以保证字典转模型成功.

- (void)setUsers:(NSArray *)users{
    //保存字典数据,这一步是写setter方法的规范写法
    _users = users;
    //字典转模型
    NSMutableArray *arrayM = [NSMutableArray array];
    for (NSDictionary *dict in users) {
       //前提:在CHUserItem模型中已经实现userItemsWithDict:方法
        CHUserItem *item = [CHUserItem userItemsWithDict:dict];
        [arrayM addObject:item];
    }
    //保存模型数据
    _users = arrayM;
}
-(void)setUser:(CHUserItem *)user{
    _user = user;
    NSDictionary *dict = (NSDictionary *)user;
    
    CHUserItem *item = [CHUserItem userItemsWithDict:dict];
    _user = item;   
}
3) MJExtension的简单实现--runtime的运用

苹果因为不知道你的模型是怎样设计的,所以他只能做到像KVC那样快速赋值.但是我们自己设计好了模型之后,我们可以通过遍历模型的属性,去字典中找对应的字段去赋值,这样不但减少了遍历的次数,而且还排除了找不到未定义的key的错误.MJExtension的实现原理便是这样,下面,我贴出不是那么严谨的MJExtension实现代码来共大家理解.

- (void)test{
    // 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"category.plist" ofType:nil];
    
    NSDictionary *categoryDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSArray *array = categoryDict[@"list"];
    NSMutableArray *m = [NSMutableArray array];
    for (NSDictionary *dict in array) {
        
        CHCategoryItem *categoryItem = [CHCategoryItem modelWithDictionay:dict];
        [m addObject:categoryItem];
        NSLog(@"%@,%@,%@",categoryItem.name, categoryItem.users[0].screen_name,categoryItem.user);
    }
  }
//2016-02-27 23:28:07.931 runtime[12395:6880878] 网红,中二函,<CHUserItem: 0x608000026580>
2016-02-27 23:28:07.932 runtime[12395:6880878] 精品,A号逗比,<CHUserItem: 0x6000000273a0>
2016-02-27 23:28:07.932 runtime[12395:6880878] 搞笑,梁猛搞笑视频,<CHUserItem: 0x6000000276c0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 创意,小越女simida,<CHUserItem: 0x6080000268a0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 视频,说方言的王子涛涛,<CHUserItem: 0x608000026bc0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 图文,阿葩罩爷,<CHUserItem: 0x6000000279e0>
2016-02-27 23:28:07.934 runtime[12395:6880878] 潜力,大哥vip,<CHUserItem: 0x600000027d00>
2016-02-27 23:28:07.934 runtime[12395:6880878] 生活,草莓阿三,<CHUserItem: 0x600000028020>
2016-02-27 23:28:07.934 runtime[12395:6880878] 原创,5毛团队,<CHUserItem: 0x600000028340>
#import <Foundation/Foundation.h>

@protocol ClassNameInItemArray <NSObject>
/**
 return 字典,key是模型中的名称,value则是数组所装的模型的名称
 */
+ (NSDictionary *)classNameInItemArray;
@end

@interface NSObject (DictToModel)<ClassNameInItemArray>

/**字典转模型的runtime实现*/
+ (instancetype)modelWithDictionay:(NSDictionary *)dict;
@end
#import "NSObject+DictToModel.h"
#import <objc/runtime.h>

@implementation NSObject (DictToModel)
// 获取类里面所有方法
// class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
// 获取类里面的Property
//  class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)

// Ivar:实例变量 以下划线开头,Property:属性
+ (instancetype)modelWithDictionay:(NSDictionary *)dict
{
    id objc = [[self alloc] init];//创建模型对象
    unsigned int count = 0;    // count:实例变量个数
    // 获取实例变量的数组集合
    Ivar *ivarLists = class_copyIvarList(self, &count);
    // 遍历数组
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarLists[i];     // 1.取出单个实例变量
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];  // 2.获取实例变量名字(带下划线的)
        NSString *key = [name substringFromIndex:1]; // 截串,_name -->name
        // 获取实例变量类型,下面需要用到的
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 断点查看可看到type为@\"CHUserItem\" 截串获取:CHUserItem
        type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        type = [type stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 根据key去服务器返回的字典中查找对应的value
        id value = dict[key];
        //取出来的value是字典,并且模型属性类型是自定义的(不是NS开头)
        //@property (nonatomic, strong)CHUserItem *user;类型是自定义的类型
        if ([value isKindOfClass:[NSDictionary class]] && ![type hasPrefix:@"NS"]) {
            // 字典转换成自定义的模型
            Class modelClass = NSClassFromString(type);// 使用映射,得到类对象
            if (modelClass) {
                value = [modelClass modelWithDictionay:value];
            }
            
        }
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 字典数组转换成模型数组.
            // 校验self对应的类中,是否实现我的协议方法(该方法的作用请看头文件里的具体说明)
            if ([self respondsToSelector:@selector(classNameInItemArray)]) {                
                id idSelf = self;// 转换成id类型,就能调用任何对象的方法
                // 获取数组中字典对应的模型字符串
                NSString *type = [idSelf classNameInItemArray][key];
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDictionay:dict];
                    [arrM addObject:model];
                }             
                // 把模型数组赋值给value
                value = arrM;
                
            }
        }
        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }    
    return objc;
}
@end

plist文件的概览图如下:


categoryPlist.png

如果你英语水平不错,可以点击runtime官网文档进行自己的学习研究.
对文章有什么建议或意见,欢迎纠正.
写文不易,如果觉得本文章对你有用请点个喜欢/关注,谢谢!

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

推荐阅读更多精彩内容