聊聊分类 Category

在Objective-C 2.0中,提供了Category 这个语言特性,可以动态地为已有类添加新行为。那么我们在使用Category 时,有没有想过 Category 是什么?它是如何实现的?我们今天就来研究下Category 的底层实现!

1、Category 的本质

我们全局搜索category 可以找到它的声明与定义:

//Category 本质是一个结构指针,指向结构体实例 category_t
typedef struct category_t *Category;;

// 结构 category_t 
struct category_t {
    const char *name;//分类所属的类的名称
    classref_t cls;//分类所属的类
    struct method_list_t *instanceMethods;//分类里添加的实例方法列表
    struct method_list_t *classMethods;//分类添加的类方法列表
    struct protocol_list_t *protocols;//分类实现的协议列表
    struct property_list_t *instanceProperties;//分类添加的实例属性列表
    struct property_list_t *_classProperties;  //分类添加的类属性列表
    
    //获取方法列表
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    //获取属性列表
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

Category 结构成员看出可以在 Category中添加实例方法、类方法,实现某些协议,添加属性;但是不可以在 Category 中添加实例变量。

那么 Objective-C 是如何实现 Category 中添加的实例方法、类方法,实现的协议,添加的属性呢?

2、Category的源码转换过程

2.1、 Objective-C 源码

在程序某处声明的一个协议ObjectDelegate

@protocol ObjectDelegate <NSObject>
- (void)logObjectInfo;
@end

使用 Objective-C 写一段 Category 的源码:

@interface SuperModel (Test)
<ObjectDelegate>
@property (nonatomic ,strong) NSString *nickName;
@property (nonatomic ,strong) NSString *age;
- (void)logNickName;
+ (void)logHello;
@end
@implementation SuperModel (Test)

- (void)logNickName{
    NSLog(@"class_getName  ==== %s",class_getName(self.class));
}

+ (void)logHello{
    NSLog(@"class_getName  ---- %s",class_getName(self.class));
}

- (void)setNickName:(NSString *)nickName{
    objc_setAssociatedObject(self, @selector(nickName), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)nickName{
    return objc_getAssociatedObject(self, _cmd);
}

#pragma mark - ObjectDelegate

- (void)logObjectInfo{
    NSLog(@"实现协议 ObjectDelegate 的 -logObjectInfo 方法");
}
@end
2.2、转为 C++ 源码
xcrun -sdk iphonesimulator clang -rewrite-objc main.m

使用上述指令将上文的 Objective-C 代码转为 C++ 代码,可以生成一个 .cpp 文件,该文件有几万行代码,很多代码并不是本节我们研究的方向所以暂不关注!在该文件搜索关键字 Test 查找与分类有关的实现代码:

2.2.1、一些方法的实现

.cpp 文件找到关于 Category 中添加的实例方法、类方法的关键代码:

//实例方法 - (void)logNickName 的实现函数
static void _I_SuperModel_Test_logNickName(SuperModel * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6___xgbfnx57zz3j5cdqx19dtnm0000gn_T_Model_ff5446_mi_15,class_getName(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
}

//类方法 + (void)logHello 的实现函数
static void _C_SuperModel_Test_logHello(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6___xgbfnx57zz3j5cdqx19dtnm0000gn_T_Model_ff5446_mi_16,class_getName(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
}

//属性 nickName 的 set 函数
static void _I_SuperModel_Test_setNickName_(SuperModel * self, SEL _cmd, NSString *nickName) {
    objc_setAssociatedObject(self, sel_registerName("nickName"), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

//属性 nickName 的 get 函数
static NSString * _I_SuperModel_Test_nickName(SuperModel * self, SEL _cmd) {
    return objc_getAssociatedObject(self, _cmd);
}

//实现协议的函数
static void _I_SuperModel_Test_logObjectInfo(SuperModel * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6___xgbfnx57zz3j5cdqx19dtnm0000gn_T_Model_ff5446_mi_17);
}

//分类的实例方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[4];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_SuperModel_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    4,
    {{(struct objc_selector *)"logNickName", "v16@0:8", (void *)_I_SuperModel_Test_logNickName},
        {(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_SuperModel_Test_setNickName_},
        {(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_SuperModel_Test_nickName},
        {(struct objc_selector *)"logObjectInfo", "v16@0:8", (void *)_I_SuperModel_Test_logObjectInfo}}
};

//分类的类方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_SuperModel_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"logHello", "v16@0:8", (void *)_C_SuperModel_Test_logHello}}
};

我们可以看到在Category中添加的实例方法、类方法的IMP指向的函数的具体实现;然后生成两个方法列表:

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_SuperModel_$_Test用来存储Category中添加的所有实例方法;
  • _OBJC_$_CATEGORY_CLASS_METHODS_SuperModel_$_Test用来存储Category中添加的所有类方法;
2.2.2、一些属性的实现

.cpp文件找到关于Category中添加的属性的关键代码:

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;//列表中元素的数量
    struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_SuperModel_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    2,//2 个属性
    {{"nickName","T@\"NSString\",&,N"}},
    {"age","T@\"NSString\",&,N"}}
};

我们可以看到在Category中添加的属性被存储在属性列表_OBJC_$_PROP_LIST_SuperModel_$_Test中。
Category中添加了两个属性,但是在上节中没有发现关于属性 agesetget 方法。也就是说:Category中添加的属性系统并不会动态的创建setget 方法,需要开发者去实现setget 方法

2.2.3、一些协议的实现

.cpp文件找到关于Category中实现的协议方法的关键代码:

//实现的 ObjectDelegate 协议的方法列表
static struct{
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;//实现的方法数量
    struct _objc_method method_list[1];//实现的方法列表
} _OBJC_PROTOCOL_INSTANCE_METHODS_ObjectDelegate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"logObjectInfo", "v16@0:8", 0}}
};

//表示协议的结构体 _protocol_t
struct _protocol_t {
    void * isa;  // NULL
    const char *protocol_name;//协议名称
    const struct _protocol_list_t * protocol_list; //遵循的父协议
    const struct method_list_t *instance_methods;//必须实现的实例方法列表
    const struct method_list_t *class_methods;//必须实现的类方法列表
    const struct method_list_t *optionalInstanceMethods;//可选的实例方法列表
    const struct method_list_t *optionalClassMethods;//可选的类方法列表
    const struct _prop_list_t * properties;//属性列表
    const unsigned int size;  // 该结构体实例的内存大小
    const unsigned int flags;  // = 0
    const char ** extendedMethodTypes;
};

//ObjectDelegate 协议的结构体实例
struct _protocol_t _OBJC_PROTOCOL_ObjectDelegate __attribute__ ((used)) = {
    0,
    "ObjectDelegate",//协议名称
    (const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_ObjectDelegate,//遵循的父协议
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_ObjectDelegate,//必须实现的实例方法列表
    0,//必须实现的类方法列表
    0,//可选的实例方法列表
    0,//可选的类方法列表
    0,//属性列表
    sizeof(_protocol_t),// 该结构体实例的内存大小
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_ObjectDelegate
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_ObjectDelegate = &_OBJC_PROTOCOL_ObjectDelegate;

/* 实现的协议列表。
 * 注意:不是实现的协议方法列表
 */
static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit
    struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_SuperModel_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1,
    &_OBJC_PROTOCOL_ObjectDelegate//实现的 ObjectDelegate 协议
};

关于协议 _protocol_t并不是本章研究的内容,在这里我们只是观察下协议 _protocol_t是如何添加到 Category 的:

  • Category中实现的协议,生成一个个_protocol_t结构体实例:如实现的协议ObjectDelegate转为结构体实例_OBJC_PROTOCOL_ObjectDelegate
  • 实现的协议列表存储着实现的一个个协议:如 协议列表_OBJC_CATEGORY_PROTOCOLS_$_SuperModel_$_Test存储实现的协议ObjectDelegate的内存地址;

2.2.4、Category的结构体实例

截止到目前,我们已经研究了Category中添加实例方法、类方法,实现某些协议,添加属性的过程;接下来研究Category的结构体实例:

//分类转为结构体:对结构体赋值
static struct _category_t _OBJC_$_CATEGORY_SuperModel_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
    "SuperModel",//分类所属的类的名称
    0, // &OBJC_CLASS_$_SuperModel, //分类所属的类
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_SuperModel_$_Test,//分类里添加的实例方法列表
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_SuperModel_$_Test,//分类添加的类方法列表
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_SuperModel_$_Test,//分类实现的协议列表
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_SuperModel_$_Test,//分类添加的属性列表
};

static void OBJC_CATEGORY_SETUP_$_SuperModel_$_Test(void ) {
    _OBJC_$_CATEGORY_SuperModel_$_Test.cls = &OBJC_CLASS_$_SuperModel;
}

__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
    (void *)&OBJC_CATEGORY_SETUP_$_SuperModel_$_Test,
};

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_SuperModel_$_Test,
};

我们可以在结构体实例 _OBJC_$_CATEGORY_SuperModel_$_Test的结构成员分别是前文分析的 :

  • 实例方法列表地址:_OBJC_$_CATEGORY_INSTANCE_METHODS_SuperModel_$_Test
  • 类方法列表:_OBJC_$_CATEGORY_CLASS_METHODS_SuperModel_$_Test
  • 实现的协议列表:_OBJC_CATEGORY_PROTOCOLS_$_SuperModel_$_Test
  • 属性列表:_OBJC_$_PROP_LIST_SuperModel_$_Test

此时 .cpp文件研究完毕,我们清楚的了解 Category是如何实现它所添加的方法、属性、协议的!但是我们的问题并没有找到答案:系统是如何加载Category中的方法、属性、协议?

接下来去 Runtime源码 查询答案。

3、Category如何加载 ?

load方法调用栈.png
3.1、Runtime 的入口函数

Objective-C 的运行是依赖于 Runtime 的:在 Runtime 库objc-os.mm文件找到入口函数如下:

void _objc_init(void){
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    environ_init();//环境初始化,读取影响运行时的环境变量; 如果需要,还可以打印环境变量帮助。
    tls_init();//初始化线程存储的键
    static_init();//运行 C++ 静态构造函数
    lock_init();// 锁的初始化
    exception_init();//初始化 libobjc 的异常处理系统

    /* 注册dyld事件的监听:
     * 注册 unmap_image,以防某些 +load 取消映射
     * map_images 函数是初始化的关键,内部完成了大量 Runtime 环境的初始化操作。
     */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

Runtime 的入口函数 _objc_init()主要功能:

  • 1、环境初始化,读取影响运行时的环境变量; 如果需要,还可以打印环境变量帮助;
  • 2、初始化线程存储的键 _objc_pthread_key
  • 3、运行 C++ 静态构造函数;
  • 4、锁的初始化
  • 5、初始化 libobjc 的异常处理系统;
  • 6、通过 dyld 调用 map_images() 函数,load_images() 函数,unmap_image() 函数

Category被附加到类上是dyld通过_dyld_objc_notify_register()调用 map_images()时发生的;
我们去看下map_images()函数:

/* 处理被映射到 dyld 的指定镜像;
 * 在加锁后,将任务交给 map_images_nolock() 函数
 */
void map_images(unsigned count, const char * const paths[],const struct mach_header * const mhdrs[]){
    mutex_locker_t lock(runtimeLock);
    //加了个锁,然后调用 map_images_nolock() 函数
    return map_images_nolock(count, paths, mhdrs);
}

/* 处理被映射到 dyld 的一些镜像 mhdrs[],主要功能:
 * 1、首次调用,初始化共享缓存
 * 2、统计所有的 header_info ,统计所有的 class 数量,未优化的 class 数量;
 * 3、首次调用,注册内部使用的选择器,初始化自动释放池与哈希表
 * 4、第 2 步获取的信息,调用 _read_images() 函数来处理
 */
void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]){
      //...代码省略
      _read_images(hList, hCount);
      //...代码省略
}

map_images_nolock() 函数根据入参 mhdrs[]统计所有的header_info ,统计所有的 class 数量,未优化的 class 数量;然后调用_read_images() 函数去处理统计出来的信息。

3.2、_read_images() 函数
void _read_images(header_info **hList, uint32_t hCount){
     //....以上代码省略

    // 发现类别。
    for (EACH_HEADER) {
        category_t **catlist =  _getObjc2CategoryList(hi, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category的目标类丢失了(可能是弱链接的)。
                //推翻对此 Category 的任何了解。
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with  missing weak-linked target class",  cat->name, cat);
                }
                continue;
            }

            // 这个类别的过程:首先,向其目标类注册类别。
            // 然后,如果实现了类,则重建类的方法列表。
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols   ||  cat->instanceProperties)  {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 将Category的method、protocol、property添加到Class
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", cls->nameForLogging(), cat->name,  classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  ) {
                /* addUnattachedCategoryForClass() 为类添加独立的类别
                 * 该函数会对 Class 和 Category 做一个映射关联
                 */
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    
                    /* remethodizeClass() 会把 Category 中的方法列表加到 Class 的methed_list_t里面去;
                     * 而且是插入到Class方法列表的前面
                     * 这就是 Category 中重写主类的方法导致的方法覆盖的原因
                     */
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");

    // Category 必须是最后一个查找,以避免潜在的竞争
    // 当其他线程在此线程完成修复之前调用新类别代码时:
    // +load handled by prepare_load_methods()

    //....以下代码省略
}
3.3、remethodizeClass() 函数
static void remethodizeClass(Class cls){
    category_list *cats;
    bool isMeta;
    runtimeLock.assertLocked();
    isMeta = cls->isMetaClass();    
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s",cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        //将分类中的方法列表、属性列表和协议列表添加到类;并清空方法缓存
        attachCategories(cls, cats, true /*flush caches*/);
        free(cats);
    }
}

主要实现了attachCategories() 函数!

3.4、attachCategories() 函数
/// 将分类中的方法列表、属性列表和协议列表添加到类:
static void attachCategories(Class cls, category_list *cats, bool flush_caches){
    if (!cats) return;
    
    if (PrintReplacedMethods) printReplacements(cls, cats);
    bool isMeta = cls->isMetaClass();
    
    /* 为列表分配内存
     * 列表为二维数组:第一维数组的元素是一个指向列表的指针;如 mlists 的第一维数组的元素是一个指向方法列表的数组
     * 第二维数组的元素是一个个方法、属性、协议
     */
    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)malloc(cats->count * sizeof(*protolists));
    
    // 倒序遍历分类列表,最先得到最新的分类
    int mcount = 0;//方法数量
    int propcount = 0;//属性数量
    int protocount = 0;//协议数量
    int i = cats->count;
    bool fromBundle = NO;//是否来自 Bundle 中的类
    while (i--) { /// 从分类的最后一个元素开始, 逆序遍历
        auto& entry = cats->list[i];//entry 是个结构 locstamped_category_t
        //获取方法列表:如果是元类,获取类方法列表;否则获取实例方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;// 将方法列表存入二维数组 mlists 中
            fromBundle |= entry.hi->isBundle();// 分类的头部信息中存储了是否是 bundle,将其记住
        }
        //获取属性列表:如果是元类返回 nil ;否则返回实例属性列表
        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;//将属性列表存入二维数组 proplists中
        }
        
        protocol_list_t *protolist = entry.cat->protocols;//分类实现的协议列表
        if (protolist) {
            protolists[protocount++] = protolist;// 将协议列表存入二维数组 protolists 中
        }
    }
    
    auto rw = cls->data();//取出 cls 的 class_rw_t 数据
    // 将新方法列表添加到 rw 中的方法列表中
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);// 释放 mlists
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

从上述函数得出结论:

  • 编译在最后的分类,添加至数组的最前方;
  • 数组最前方的方法覆盖了数组中后面的方法;
  • 分类表中的同名方法谁能生效取决于编译顺序;
3.5、attachLists() 函数
   void attachLists(List* const * addedLists, uint32_t addedCount) {
       if (addedCount == 0) return;
       
       if (hasArray()) {
           // many lists -> many lists
           uint32_t oldCount = array()->count;/// 列表中原有元素总数
           uint32_t newCount = oldCount + addedCount; /// 拼接后元素总数
           setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));///重新分配内存
           array()->count = newCount;
           /// 内存移动:腾出前面的内存空间
           memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
           /// 内存拷贝:将新添加的列表元素添加至前方
           memcpy(array()->lists, addedLists,addedCount * sizeof(array()->lists[0]));
       }
       else if (!list  &&  addedCount == 1) {
           // 0 lists -> 1 list
           list = addedLists[0];
       }
       else {
           // 1 list -> many lists
           List* oldList = list;
           uint32_t oldCount = oldList ? 1 : 0;
           uint32_t newCount = oldCount + addedCount;
           setArray((array_t *)malloc(array_t::byteSize(newCount)));
           array()->count = newCount;
           if (oldList) array()->lists[addedCount] = oldList;
           memcpy(array()->lists, addedLists,addedCount * sizeof(array()->lists[0]));
       }
   }

从上述函数得出结论:

  • 分类添加的方法可以覆盖原有方法 ;
  • 分类表中的同名方法谁能生效取决于编译顺序

4、关联对象

/** 根据指定的键和关联策略为指定对象设置关联值
 *
 * @param object 源对象
 * @param key 键
 * @param value 要与源对象的键关联的值; 传递nil以清除现有关联;
 * @param policy 关联策略
 */
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)

/** 根据指定的键获取与源对象关联的值
 * @param object 源对象
 * @param key 键
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

/** 删除源对象的所有关联
 * @param object 源对象
 * @note 该函数将源对象退回到原始状态;尽量使用 objc_setAssociatedObject() 设置 nil 值清除一个关联
 */ 
void objc_removeAssociatedObjects(id _Nonnull object)
关联对象
关联对象

5、扩展与分类Category

区别 扩展 分类Category
加载时机 编译时决议 在运行期决定
能否添加变量 可以添加实例变量 无法添加实例变量;因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局
是否依赖于该类源码 以声明的方式存在,依赖于 .m 文件 不需要知道类的源码即可使用
用处 一般用来隐藏类的私有信息 用来分散类的实现;为原有类增加功能等

参考文章
Mach-O文件结构
深入理解Objective-C:Category

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

推荐阅读更多精彩内容