初探OC对象原理(三)

a5ccf31d7b7cb5ffc693af363e1aad41.jpeg

前言:

这是探究OC对象原理的第三章,也是按照对象的 \color{red}{alloc——}\color{red}{内存分配——}\color{red}{对象的本质} 的底层实现原理顺序来进行的。今天我们探究下对象的本质以及一些拓展内容。在这之前我先介绍一下clangxcrun,因为本文需要用到。请看下图(借鉴前人的总结)

clang.png

简单了解了clang后我们今天需要几个clangxcrun的命令,帮助我们把常用的.m文件转换成c++文件,也就是.cpp文件。

1,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

\color{#FFCB30}{ps:以上命令的环境部分(如:13.0.0、iPhoneSimulator、iPhoneSimulator13.0)需要留意根据自己的实际情况来进行修改。}

2,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` (⼿机)

利用以上命令可以将我们OC的.m文件转成c++的.cpp文件,我们借助.cpp文件 来查看我们生成的对象在c++层面是以怎样的形式存在的。

初步探究

我们先创建一个项目在main.m文件里创建一个类ZYPerson,并且创建一个属性 zyName。如图:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface ZYPerson : NSObject
@property (nonatomic, copy) NSString *zyName;
@end

@implementation ZYPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert  code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

接下来我们执行上面的clang命令 将main.m文件转成main.cpp的文件并且双击打开。利用 command+F搜索ZYPerson

#ifndef _REWRITER_typedef_ZYPerson
#define _REWRITER_typedef_ZYPerson
typedef struct objc_object ZYPerson;
typedef struct {} _objc_exc_ZYPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_ZYPerson$_zyName;
struct ZYPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_zyName;
};

发现: ZYPerson 是以 struct 存在的

回到上面我们的ZYPerson_IMPL代码块,我们看到结构体内struct NSObject_IMPL NSObject_IVARS这句,我们根据这句代码全局搜索下NSObject_IMPL

struct NSObject_IMPL {
    Class isa;
};

发现:NSObject_IVARS其实是成员变量isa.

同时我们在ZYPerson_IMPL结构体中看到我们的属性zyName。我们不妨探究下属性在底层的setter和getter是如何实现的。在ZYPerson_IMPL结构体的正下方紧接着我们看到以下两句代码:

// @property (nonatomic, copy) NSString *zyName;
/* @end */
// @implementation ZYPerson

static NSString * _I_ZYPerson_zyName(ZYPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ZYPerson$_zyName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_ZYPerson_setZyName_(ZYPerson * self, SEL _cmd, NSString *zyName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ZYPerson, _zyName), (id)zyName, 0, 1); }
// @end

我们分析代码发现第一句代码return了一个值,第二句代码却是objc_setProperty设置了一个属性。并且两者都携带了我们经常提到的隐藏参数ZYPerson * selfSEL _cm,再次根据第一行代码返回值的形式,很明显是在拼接地址的方法。这很符合我们iOS在内存中查找值的规律。我们基本可以确定第一句代码就是我们属性zyNamegetter方法,相对的第二句则是它的setter方法。同时我们也明白了为什么我们可以在每个方法里都能调用到self的原因,因为每个方法属性都携带了selfSEL _cm这两个隐藏参数。

回到最初的代码我们还可以看到这样一句代码:typedef struct objc_object ZYPerson,这个objc_object让我很好奇,我们接着查看下它是什么东西,仍然command+F搜索

typedef struct objc_class *Class;

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

typedef struct objc_object *id;

发现:objc_object 是以 struct 存在的,而且我们的ZYPerson是继承的NSObject但是在底层其实是objc_object

在这段代码中,我们看到了熟悉的Classid。并且我们看到他们俩的声明跟ZYPerson的声明有点不一样他们都是以指针形式存在的,看对比:

发现:typedef struct objc_object ZYPerson;
typedef struct objc_class *Class;
typedef struct objc_object *id;
这也是为什么我们在使用id 声明对象的时候为什么不需要*的原因。因为他自己本身就是一个objc_object *结构体指针,他可以定义任何类型的变量。同时我们常用的Class他的类型也是objc_class *结构体指针。

结构体、位域、联合体

接下来我们插入一点预备知识,就是结构体、位域、联合体,分别来看看他们的区别。

实例一

struct ZYComputer1 {
    BOOL iMac; // 0 1
    BOOL macBookPro;
    BOOL airBook;
    BOOL iPad;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYComputer1   computer1;
        NSLog(@"ZYComputer1's size : %ld",sizeof(computer1));
        
    }
    return 0;
}

查看NSLog打印出来的结果:

2021-06-16 17:30:17.916212+0800 ZYProjectTree1[59630:1944971] ZYComputer1's size : 4

可以看到ZYComputer1的大小为 4 字节

示例二

struct ZYComputer2 {
    BOOL iMac: 1;
    BOOL macBookPro : 1;
    BOOL airBook : 1;
    BOOL ipad: 1;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYComputer2   computer2;
        NSLog(@"ZYComputer2's size : %ld",sizeof(computer2));
        
    }
    return 0;
}

查看NSLog打印出来的结果:

021-06-16 17:35:49.180143+0800 ZYProjectTree1[59712:1948611] ZYComputer2's size : 1

可以看到ZYComputer1的大小为 1 字节,这就是位域互斥

示例三

struct ZYPerson1 {
    double      height;
    char        *name;
    long        age;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYPerson1   person1;
        person1.height = 180.0;
        person1.name = "Wayne";
        person1.age = 20;
        
        NSLog(@"person1's height : %f",person1.height);
        NSLog(@"person1's name : %s",person1.name);
        NSLog(@"person1's age : %ld",person1.age);

    }
    return 0;
}

查看NSLog打印出来的结果:

2021-06-16 17:49:01.845235+0800 ZYProjectTree1[60074:1959304] person1's height : 180.000000
2021-06-16 17:49:01.845684+0800 ZYProjectTree1[60074:1959304] person1's name : Wayne
2021-06-16 17:49:01.845719+0800 ZYProjectTree1[60074:1959304] person1's age : 20

可以看到ZYPerson1的所有属性都已经被赋值成功了。所以结构体是共存的。

示例四

union ZYPerson2 {
    char        *name;
    double      height;
    long        age;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        union ZYPerson2   person2;
        person2.name = "Wayne001";
        person2.height = 190.0;
        person2.age = 18;

        NSLog(@"person2's name : %s",person2.name);
        NSLog(@"person2's height : %f",person2.height);
        NSLog(@"person2's age : %ld",person2.age);
    }
    return 0;
}

直接查看NSLog打印出来的结果你会发现会崩溃(具体原因至今没找到),所以我们用断点在lldb下打印看看 (每一次p person2打印对应每一个断点)

(lldb) p person2
(ZYPerson2) $0 = (name = 0x0000000000000000, height = 0, age = 0)
(lldb) p person2
(ZYPerson2) $1 = (name = "Wayne001", height = 2.1220038126150982E-314, age = 4294983532)
(lldb) p person2
(ZYPerson2) $2 = (name = "", height = 190, age = 4640889047261118464)
(lldb) p person2
(ZYPerson2) $3 = (name = "", height = 8.8931816251424378E-323, age = 18)
(lldb) 

如图:

image.png

可以看到ZYPerson2 在几次赋值的过程中始终只保存了一个最后赋予的值。并不像实例三一样每一个属性都被赋值。这就是联合体 union结构体 struct直接的区别。联合体也是互斥的。

总结:

1,结构体 struct的大小是根据内部成员变量的类型和个数来决定的。可以根据实例一证明。
2,对结构体 struct内的成员变量指定位域(如示例二)可以优化结构体 struct的大小。但是最终的大小还是会以字节倍数来计算,如示例二中指定位域1 位,四个成员变量加起来应该是4 位也就是0.5字节,但是最终会分配1字节。因为内存中不存在0.5字节的单位。
3,结构体 struct中的成员变量是共存的,存储的值互不影响,可以通过示例三来证明,每个成员变量都成功赋值。优点是全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
4,联合体 union中的成员变量是互斥的,当某一个成员变量被赋值其他成员变量的地址将不被使用,此时其他成员变量存储的将是脏数据。联合体(union)的缺点就是不够包容;优点是内存使⽤更为精细灵活,也节省了内存空间

initIsa 探索

首先上一段代码 (ps:来源于苹果开源的objc底层源码):

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    /***删除了前段部分代码,删除的代码是前两章探索的alloc、内存分配的部分***/
   //以下判断就是我们要探索的 isa 部分 也就是 对象alloc 以及内存分配后 的步骤

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }
   /***删除了后段部分代码,删除的代码是关于return的内容***/
}

这段代码我们很熟悉,因为在前两章我们经常要跟踪alloc代码到这个方法里。这个方法就包含了对象初始化的一些过程,从alloc内存分配再到现在我们探索的 initIsaclass的绑定。前两步我们已经在前两章探索过了,这里紧接着第二章探索initIsa
我们根据这个if 判断首先看到initInstanceIsa这个方法。我们跟踪进去:

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

    initIsa(cls, true, hasCxxDtor);
}

可以看到这方法 进去后 最终还是调用了initIsa方法。所以我们着重探索initIsa,同样利用command+F可以跟踪到以下方法:

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

NONPOINTER_ISA

继续上面的代码跟踪:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#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
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // 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_t newisa(0)这个方法,发现是个isa的初始化方法,我们看看这个isa_t到底是个什么东西,跟踪进去:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

    private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

发现这是个联合体(union)。我们看到这个联合体里有构造方法isa_t() { }、有uintptr_t bits(ps:这个不是位域里的bits么)、还有我们的对象cls,而且我们发现还有一个ISA_BITFIELD的结构体成员变量。我们看看ISA_BITFIELD是什么

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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 unused            : 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

// SUPPORT_PACKED_ISA
#endif

在这里我们看到分别是 arm64x86_64 两个框架的定义,我们发现这个ISA_BITFIELD原来是位域。而且我们发现ISA_BITFIELD存的内容有nonpointer关联对象(has_assoc)析构函数(has_cxx_dtor)类的指针地址(shiftcls)(magic)弱引用(weakly_referenced)是否使用(unused)散列表(has_sidetable_rc)引用计数(extra_rc)等。这就证明isa_t并不是简单地isa指针和类的信息,而是存了很多其他东西,这就所谓的NONPOINTER_ISA

下面针对这个位域我们看看 arm64架构下的图解:

image.png

0号标志位——nonpointer表示是否对 isa指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等

1号标志位——has_assoc关联对象标志位,0没有,1存在

2号标志位——has_cxx_dtor该对象是否有 C++ 或者Objc 的析构器,如果有析构函数,则需要做析构,如果没有,则可以更快的释放对象

3-35号标志位——shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64架构中有 33 位⽤来存储类指针。

36-41号标志位——magic⽤于调试器判断当前对象是真的对象还是没有初始化的空间。

42号标志位——weakly_referenced志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。

43号标志位——`unused:标志对象是否正在使用

44号标志位——has_sidetable_rc当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位

45-64号标志位——extra_rc当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,
则需要使⽤到下⾯的 has_sidetable_rc。

isa关联对象

上面我们花了很长的时间去跟踪了isa_t一直到位域的分析。现在我们回到代码去看看
我们创建一个ZYPerson类,在main文件里导入头文件,代码如下:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "ZYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        ZYPerson *p = [ZYPerson alloc];
        NSLog(@"%@",p);
    }
    return 0;
}

我们在NSLog(@"%@",p);这句代码上打断点 利用lldbp一下 看看这个对象的地址 以及 类的地址:

image.png

我们看到这里的对象p 和 类ZYPerson 对应的指针地址 0x011d8001000082750x00000001000082700x 后的最高位都是0 这就证明这两个64位地址的内存根本没有用完。我们下面来看看对象和类到底是怎么关联的。我们回到objc源码 方法

inline void 
objc_object::initIsa(Class cls, bool nonpointer,UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor){ }

在这个方法里我们 接着从 isa_t newisa(0)这句代码往下走,可以看到一个 if else 的判断,如果!nonpointer 就直接newisa.setClass(cls, this); 设置了cls。如果不是则走else对上面我们分析的位域进行一些列的赋值 最后也对cls 进行了set 方法。我们跟踪下newisa.setClass(cls, this);

inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    // Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#   if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
    // No signing, just use the raw pointer.
    uintptr_t signedCls = (uintptr_t)newCls;

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
    // We're only signing Swift classes. Non-Swift classes just use
    // the raw pointer
    uintptr_t signedCls = (uintptr_t)newCls;
    if (newCls->isSwiftStable())
        signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
    // We're signing everything
    uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   else
#       error Unknown isa signing mode.
#   endif

    shiftcls_and_sig = signedCls >> 3;

#elif SUPPORT_INDEXED_ISA
    // Indexed isa only uses this method to set a raw pointer class.
    // Setting an indexed class is handled separately.
    cls = newCls;

#else // Nonpointer isa, no ptrauth
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}

我们可以看到这方法里先获取到 newClass

uintptr_t signedCls = (uintptr_t)newCls;

然后获取到位域结构:

 uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

然后进行一系列的位移

shiftcls_and_sig = signedCls >> 3;
shiftcls = (uintptr_t)newCls >> 3;

到这里我们不禁有些疑惑为什么要这样位移来处理呢?这里就涉及到另一个我们需要留意的点那就是 ISA_MASK

ISA_MASK (掩码)

在我们上面探究的位域部分,我们可以留意到以下代码

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

这个就是ISA掩码,他的作用你可以理解为对ISA的一个防护伪装,之前在对象和类绑定的 过程中利用这个ISA_MASK掩码进行了伪装(因为ISA不是单纯的nopointer里面还包含了很多上面我们分析的位域内容等),所以如果我们想要看到ISA对应类的地址就需要经过反掩码的操作来获取。

我们用代码来演示一下,我们知道对象pisa地址0x011d800100008275,也打印出来了其对应的类的地址0x0000000100008270。我们用掩码来验证一下:(也就是让对象的isa地址掩码ISA_MASK 进行 与(&) 操作)

image.png

其实我们想要通过对象得到类的信息,还可以通过ISA位运算直接算出来。下面我们来看看ISA位运算

ISA位运算

我们在之前分析isa_t的时候分析到了他包含的位域部分。在位域里有一个关于类的shiftcls ,上面我们分析的是arm64架构的,但是我们现在电脑跑的代码是在模拟器上所以我们看看shiftclsx86架构上它所占的位是 44位。所以我们下面计算的时候用 44位来计算:

image.png

图解:

ISA位运算图解.jpg

至此,本章内容就算是告一段落了,其中关于对象属性 的settergetter 方法的更详细解析,后面再补充。

遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!

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

推荐阅读更多精彩内容