OC类原理分析(下)

WWDC关于runtime的优化

观看视频

数据结构的变化(Class Data Structures Changes)

在磁盘上,app二进制文件中的类:包含了最常被访问的信息,指向元类父类方法缓存的指针

类的数据结构
image.png
Clean Memory

是指加载后不会发生更改的内存,class_ro_t是属于Clean Memory的。

  • class_ro_t(只读):一个指向更多数据的指针,存储额外信息的结构
  • class_ro_tro代表只读,存放了方法、协议、实例变量的信息:
class_ro_t
image.png
Dirty Memory

是指在进程运行时会发生更改的内存,类的结构一经使用就会变成Dirty Memory,因为运行时会向它写入数据,这里指的是class_rw_t

  • Dirty Memory是这个类被分成两部分的原因,可以保持类加载后不会发生更改的数据越多越好,通过分离永远不会更改的数据,可以把大量的类数据存储为Clean Memory
  • class_rw_t(读写): Methods、Properties、Protocols,当category被加载时,它可以向类中添加新方法,可以根据Method Swizzling方式修改,因为class_ro_t是只读,所以要把这些放在Class_rw_t中。
  1. First Subclass、Next Subling Class:包含了运行时才会生成的信息First Subclass、Next Subling Class,所有的类都会变成一个树状结构,就是通过First Subclass和Next Subling Class指针实现的,它允许运行时遍历当前使用的所有类
  2. Methods、Properties、protocols:包含这3个是因为它们可以在运行时进行修改,当category被加载时,它可以向类中添加新的方法,也可以通过runtime API添加它们
  3. Demangled Name:这个是只有Swift才会使用的字段,因为整个数据结构OC与Swift是共享的,但是Swift类本身并不需要这个字段,是为了有人要访问Swfit的OC名称的时候使用的,利用率比较低。
class_rw_t
image.png
Dirty Memonry与Clean Memory特点比较:
  • Dirty MemonryClean Memory要昂贵的多,只要进程在运行,它就必须一直存在
  • Clean Memory可以进行移除,从而节省更多的内存空间,因为如果需要Clean Memory可以从磁盘中重新加载
Dirty Memory拆分优化原理

Dirty Memonry即类第一次加载就会存在,运行时就会为它分配额外的内存,运行时分配的存储容量是class_rw_t,用于读取-编写数据,但是Dirty Memory里面存在很多Clean Memory,为了更好的空间利用率,拆分就很有必要!

  • 拆分出class_ro_t,运行加载时不会被修改的内存
image.png
  • 这时的class_rw_t还是太大,因为里面包含了Methods、Properties、Protoclos,这3个因素只有使用了category向class中添加方法或使用了Method swizzle才会触发的特性,90%的类不会被使用,所以把它们拆分出是必要的,Demangled Name这个Swift使用的字段拆出去也是必要的,毕竟使用率低,那么Dirty memory内存结构就变成了class_rw_tclass_rw_ext_t两部分。
image.png
如何缩小class_rw_t的结构大小

拆掉那些平时不用的部分,可以将class_rw_t减小一半,对于真的用到了被拆分出去的数据时,可以使用extension来完成这些,添加到类中供其使用(大约90%的类不需要这个扩展)

class_rw_ext_t
image.png
通过终端实际验证微信和Safari的class_rw占用的内存

终端命令

//微信
$ heap WeChat | egrep 'class_ro|COUNT'
   COUNT      BYTES       AVG   CLASS_NAME                                        TYPE    BINARY
     693      55440      80.0   Class.data.readonly (class_ro_t)                  C       libobjc.A.dylib
//Safari
$ heap Safari | egrep 'class_ro|COUNT'
   COUNT      BYTES       AVG   CLASS_NAME                                        TYPE    BINARY
     200      16000      80.0   Class.data.readonly (class_ro_t)                  C       libobjc.A.dylib
class_rw_t与class_ro_t的区别
  • 当有类使用了category的时候,那么此时的类就有了class_rw_t的结构,如果未使用分类,那么类就是一个单纯的class_ro_t的结构。
  • 类的内存结构中,有category Class的时候有class_rw_t,没有的时候只有class_ro_t(clean memonry)

成员变量和属性变量

打开objc4-818源码,创建类LGPerson,LGTeacher内容如下,在LGPerson *p = [[LGPerson alloc] init];处添加断点,运行工程

<!-- LGPerson.h文件 -->
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;

// 方法 - + OC  C/C++ 函数
// 元类
- (void)saySomething;
+ (void)sayNB;

@end

NS_ASSUME_NONNULL_END

<!-- LGTeacher.h文件 -- >
#import <Foundation/Foundation.h>
#import "LGPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGTeacher : LGPerson
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end

NS_ASSUME_NONNULL_END

<!-- main.m文件 -- >
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p = [[LGPerson alloc] init];
        NSLog(@"%@",p);
    }
    return 0;
}

// lldb调试信息
(lldb) p/x LGPerson.class
(Class) $0 = 0x0000000100008408 LGPerson
(lldb) p (class_data_bits_t *)0x0000000100008428
(class_data_bits_t *) $1 = 0x0000000100008428
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100677f00
// 这里的firstSubclass = nil,为什么不是LGTeacher呢?
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000456
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

// 打印LGTeacher.class之后,再次调试$1->data(),下面 firstSubclass = LGTeacher,原因是什么?
(lldb) p LGTeacher.class
(Class) $4 = LGTeacher
(lldb) p $1->data()
(class_rw_t *) $5 = 0x0000000100677f00
(lldb) p *$5 
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000456
    }
  }
  firstSubclass = LGTeacher
  nextSiblingClass = NSUUID
}
疑问?上面第一次打印firstSubclass = nil,执行完p LGTeacher.class再次打印firstSubclass = LGTeacher,原因是什么?这里留下悬念,后面探讨...

上面WWDC视频中提到成员变量存储在class_ro_t中,重新运行上面工程进行调试

KCObjcBuild was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) p/x LGPerson.class
(Class) $0 = 0x0000000100008408 LGPerson
(lldb) p (class_data_bits_t *)0x0000000100008428
(class_data_bits_t *) $1 = 0x0000000100008428
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001010a1770
(lldb) p *$2 
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000456
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $3.ro()
(const class_ro_t *) $4 = 0x0000000100008188
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 0
  instanceStart = 8
  instanceSize = 32
  reserved = 0
   = {
    ivarLayout = 0x0000000000000000
    nonMetaclass = nil
  }
  name = {
    std::__1::atomic<const char *> = "LGPerson" {
      Value = 0x0000000100003f4c "LGPerson"
    }
  }
  baseMethodList = 0x00000001000081d0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100008268
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000082d0
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $5.ivars
(const ivar_list_t *const) $6 = 0x0000000100008268
// 获取到有3个属性
(lldb) p *$6
(const ivar_list_t) $7 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 3)
}
(lldb) p $7.get(0)
(ivar_t) $8 = {
  offset = 0x00000001000083a0
  name = 0x0000000100003e18 "hobby"
  type = 0x0000000100003f55 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $7.get(1)
(ivar_t) $9 = {
  offset = 0x00000001000083a8
  name = 0x0000000100003e28 "_age"
  type = 0x0000000100003f7c "i"
  alignment_raw = 2
  size = 4  //int型占4字节
}
(lldb) p $7.get(2)
(ivar_t) $10 = {
  offset = 0x00000001000083b0
  name = 0x0000000100003e2d "_name"
  type = 0x0000000100003f55 "@\"NSString\""
  alignment_raw = 3
  size = 8    // 字符串内存中占8字节
}
(lldb) 
成员变量和属性变量的区别

创建新工程类的属性与变量main.m中创建LGPerson

// 成员变量 vs 属性 VS 实例变量
// {}中声明的是成员变量,这里的实例变量objc是一种特殊的成员变量
@interface LGPerson : NSObject {
    // STRING   int  double  float char bool
    NSString *hobby; 
    int a;
    NSObject *objc;  // 实例变量
}
// @property声明的是属性变量
@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;
@property (nonatomic, strong) NSString *name;
@property (atomic, strong) NSString *aname;
@end

// 通过clang编译成main.cpp文件查看
$ clang -rewrite-objc main.m -o main.cpp

#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nickName;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nnickName;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_anickName;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_aname;
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *hobby;
    int a;
    NSObject *objc;
    NSString *_nickName;
    NSString *_acnickName;
    NSString *_nnickName;
    NSString *_anickName;
    NSString *_name;
    NSString *_aname;
};
// @property (nonatomic, copy) NSString *nickName;
// @property (atomic, copy) NSString *acnickName;
// @property (nonatomic) NSString *nnickName;
// @property (atomic) NSString *anickName;
// @property (nonatomic, strong) NSString *name;
// @property (atomic, strong) NSString *aname;

/* @end */

// @implementation LGPerson
// self + OBJC_IVAR_$_LGPerson$_nickName 通过内存平移获取
static NSString * _I_LGPerson_nickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);

static NSString * _I_LGPerson_acnickName(LGPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _acnickName), 1); }
static void _I_LGPerson_setAcnickName_(LGPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _acnickName), (id)acnickName, 1, 1); }

static NSString * _I_LGPerson_nnickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nnickName)); }
static void _I_LGPerson_setNnickName_(LGPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nnickName)) = nnickName; }

static NSString * _I_LGPerson_anickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_anickName)); }
static void _I_LGPerson_setAnickName_(LGPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_anickName)) = anickName; }

static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }

static NSString * _I_LGPerson_aname(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_aname)); }
static void _I_LGPerson_setAname_(LGPerson * self, SEL _cmd, NSString *aname) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_aname)) = aname; }
// @end
......
结论
  • 成员变量是生命在类的{}中的
  • 属性变量是用@property方式声明的
  • 属性变量在底层编译阶段会变成_方式成员变量
  • 属性变量会自动生成gettersetter方法

疑问?上面的set方法为什么会有objc_setProperty内存平移赋值两种方式?下面进行探讨...

TypeEncoding

返回实例变量的类型字符串。Apple Documents地址

// 这里的 "c"  "i"  "s"  "q" 都表示编码
void lgTypes(void){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_main_418517_mi_0,"c");
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_main_418517_mi_1,"i");
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_main_418517_mi_2,"s");
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d7_5qn4fnqn0p197t4lkw1bw1p40000gn_T_main_418517_mi_3,"q");
......

// 这里的"@\"NSString\""表示什么?
static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[9];
} _OBJC_$_INSTANCE_VARIABLES_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_ivar_t),
    9,
    {{(unsigned long int *)&OBJC_IVAR_$_LGPerson$hobby, "hobby", "@\"NSString\"", 3, 8},
     {(unsigned long int *)&OBJC_IVAR_$_LGPerson$a, "a", "i", 2, 4},
     {(unsigned long int *)&OBJC_IVAR_$_LGPerson$objc, "objc", "@\"NSObject\"", 3, 8},
......

// "@16@0:8"编码又表示什么?
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[24];
} _OBJC_$_INSTANCE_METHODS_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    24,
    {{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
    {(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
    {(struct objc_selector *)"acnickName", "@16@0:8", (void *)_I_LGPerson_acnickName},
    {(struct objc_selector *)"setAcnickName:", "v24@0:8@16", (void *)_I_LGPerson_setAcnickName_},
    {(struct objc_selector *)"nnickName", "@16@0:8", (void *)_I_LGPerson_nnickName},
    {(struct objc_selector *)"setNnickName:", "v24@0:8@16", (void *)_I_LGPerson_setNnickName_},
......

上面这些全是编码,字符含义可以在Apple Documents官网查到
这里举例@16@0:8的含义

  • @:表示id类型
  • 16:表示占用的内存
  • @:表示id -> self
  • 0:表示从0号位置开始
  • ::表示SEL
  • 8:表示从8号位置开始

再来举例v24@0:8@16含义

image.png

setter方法的底层原理

探讨上面set方法为什么会有objc_setProperty内存平移赋值两种方式?为什么copy修饰的属性使用了objc_setProperty,而strong修饰的没有?

  • LLVM源码中搜索objc_setProperty,找到如下所示的getOptimizedSetPropertyFn方法
image.png

从这里可看出,针对不同修饰符,返回是不同的

  • 上述的几个name分别对应objc4-818.2源码中的如下方法
image.png
  • 通过汇编调试发现,最终都会走到objc_storeStrong
  1. copy修饰的属性汇编调试结果发现执行到objc_storeStrong
  2. strong修饰的属性汇编调试结果执行到objc_storeStrong
  • 源码中搜索objc_storeStrong,有如下源码,主要也是retain新值,release旧值
void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);//retain新值
    *location = obj;
    objc_release(prev);//release旧值
}
  • llvm编译源码中搜索objc_storeStrong,找到EmitARCStoreStrongCall方法,发现copy 和 strong修饰的属性执行的策略是不一致的
  • llvm中搜索EmitARCStoreStrongCall方法,在GenerateCopyHelperFunction方法有调用,然后在这里发现了strongweak的不同处理
image.png
  • 如果是weak修饰,执行EmitARCCopyWeak方法,weak在底层的调用是objc_initWeak
  • 如果是strong修饰,执行EmitARCStoreStrongCall方法
得出结论
  • copy和strong修饰的属性在底层编译的不一致,主要还是llvm中对其进行了不同的处理的结果。copy的赋值是通过objc_setProperty,而strong的赋值时通过self + 内存平移(即将指针通过平移移至name所在的位置,然后赋值),然后还原成strong类型
  • strong & copy 在底层调用objc_storeStrong,本质是新值retain,旧值release
  • weak 在底层调用objc_initWeak

类方法存储的api方式

上面我们探索类方法存储的时候,使用的是方式是源码+lldb调试,还有一种方式是通过系统api来获取,代码如下。lgInstanceMethod_classToMetaclass(pClass);添加断点进行调试

// 用于获取类的方法列表
void lgObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        LGLog(@"Method, name: %@", key);
    }
    free(methods);
}
// 用于获取类的实例方法
void lgInstanceMethod_classToMetaclass(Class pClass){
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
// 用于获取类的类方法
void lgClassMethod_classToMetaclass(Class pClass){
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
//    - (void)sayHello;
//    + (void)sayHappy;
    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    LGLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
// 用于获取方法的实现
void lgIMP_classToMetaclass(Class pClass){
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));// 0
    // sel -> imp 方法的查找流程 imp_farw
    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); // 0
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // LGTeacher *teacher = [LGTeacher alloc];
        LGPerson *person = [LGPerson alloc];
        Class pClass     = object_getClass(person);
        lgObjc_copyMethodList(pClass);
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        NSLog(@"*************");
        lgObjc_copyMethodList(metaClass);
        lgInstanceMethod_classToMetaclass(pClass);
        lgClassMethod_classToMetaclass(pClass);
        lgIMP_classToMetaclass(pClass);
        NSLog(@"Hello, World!");
    }
    return 0;
}

// 打印信息
// 类中打印的方法
Method, name: sayHello
Method, name: name
Method, name: .cxx_destruct  // 添加的cxx析构方法
Method, name: setName:
Method, name: obj
Method, name: setObj:
2021-07-18 23:03:44.702431+0800 002-类方法归属分析[38880:4168691] *************
// 元类中打印的方法
Method, name: sayHappy
lgInstanceMethod_classToMetaclass - 0x1000081c0-0x0-0x0-0x100008158
lgClassMethod_classToMetaclass-0x0-0x0-0x100008158-0x100008158
2021-07-18 23:23:14.862891+0800 002-类方法归属分析[38880:4168691] 0x100003ac0-0x7fff201faac0-0x7fff201faac0-0x100003b00
2021-07-18 23:23:14.863444+0800 002-类方法归属分析[38880:4168691] lgIMP_classToMetaclass
得出结论
  • 底层没有获取类方法的方法,获取类方法的底层还是获取实例方法,只是获取的是元类的实例方法
  • class_getInstanceMethod:获取实例方法,如果指定的类或其父类不包含带有指定选择器的实例方法,则为NULL
  • class_getClassMethod:获取类方法,如果指定的类或其父类不包含具有指定选择器的类方法,则为NULL。
  • class_getMethodImplementation:获取方法的具体实现,如果未查找到,则进行消息转发
    这也论证了元类中为什么会有类对象的类方法
遗留面试题?iskindOfClass & isMemberOfClass 的理解
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容