IOS底层(八): alloc相关: isa与类关联源码分析

OC底层源码/原理合集

建议先看下
IOS底层(三): alloc相关1.初探 alloc, init, new源码分析

我们先看一个main项目(创建只有main.m项目)

main项目

其中我们在main中加了一个TestObj

// 声明
@interface TestObj : NSObject
@property (nonatomic, copy) NSString *name;
@end

// 实现
@implementation TestObj
@end

接下来介绍一个重要的编辑器Clang

Clang

Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器

2013年4月, Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。

Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。

介绍的目的是为了将main.m转成main.cpp, 便于我们探索源码

  • 打开终端
cd 进入项目文件夹
clang -rewrite-objc main.m -o main.cpp //将 main.m 编译成 main.cpp
main.m转成main.cpp

我们再看项目会生成一个cpp文件


cpp文件1.1
  • 打开cpp文件
cpp文件1.2
  • 搜索TestObj

代码很多, 我们就找自定义类的关键词就行, 搜索TestObj

cpp文件1.3

可看到, 我们自定义的TestObj对象在底层会被编译成结构体struct (OC对象的本质其实就是结构体)

其中name是我们自定义的一个NSString属性,

NSObject_IMPL 也可搜索到

struct NSObject_IMPL {
    Class isa;
};

补充个知识点: 结构体是可以继承的, 在C++是可以继承的, 在C可以伪继承, 伪继承的方式是直接将NSObject结构体定义为 TestObj 中的第一个属性, 意味着 TestObj 拥有 NSObject中的所有成员变量。

所以NSObject_IVARS 等效于 NSObject中的 isa

cpp文件1.4

而这里我们可看到是自定属性的get, set方法, 这里的set会调用一个objc_setProperty方法, 这是一个Runtime的API

objc_setProperty

  • 回到objc源码看一下objc_setProperty方法
objc_setProperty
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue); // 设置isa指向
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue); // 对新值的retain
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;    // 设置新值   
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;     // 设置新值   
        slotlock.unlock();
    }

    objc_release(oldValue);  // 对旧值release
}

这里可看到 objc_setProperty原理就是对旧值的release, 对新值的retain

这里对objc_setProperty总结下

objc_setProperty相当于一个承上启下的接口, 上层的许多个set方法(setName, setAge......), 如果直接对接底层llvm会有生成很多临时变量(sel等), 当你如果想要查找某一个时候会很麻烦。

所以设计了一个objc_setProperty令所有属性的set方法都经过它, 区分根据cmd, 以便达到无论上层怎么变,下层都是不变的, 以及下层的变化也无法影响上层,主要是达到上下层接口隔离的目的。

objc_setProperty原理图

cls 与 类 的关联原理

alloc关键三步的最后一个指针关联

obj->initInstanceIsa(cls, hasCxxDtor);

看下 initInstanceIsa源码

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

    initIsa(cls, true, hasCxxDtor);
}
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类型的, isa_t又是什么呢? 点击进入

isa_t (0~63)

#include "isa.h"

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);
};

我们可以看到isa_t是一个union联合体

这里我们先看下联合体

联合体 union

常见的构造数据类型有两种 结构体(struct), 联合体/共用体(union)

  • 结构体 : 不同的数据结构整合成一个整体, 其变量是共存, 变量不管是否使用, 都会被分配内存。

优点: 包容性强且成员之间不会互相影响。

缺点: 所有属性都被分配内存, 比较浪费。
例如: 假设定一个4个int型属性, 但是我只用了1个4字节, 却开辟了16字节(128位)内存空间, 浪费12字节。

  • 联合体 : 不同的数据结构整合成一个整体, 其变量是互斥, 所有成员共占用一段内存。而且联合体采用内存覆盖技术, 同一时间段只能保存一个成员值, 如果对新成员赋值, 就会将原来的成员覆盖掉。

优点: 内存精细灵活, 节省内存空间。

缺点: 包容性弱。

  • 结构体与联合体对比

内存占用情况:
结构体: 各个成员占用不同段内存, 互不影响
联合体: 所有成员占用同一段内存, 修改一个成员会影响其他成员

内存分配大小:
结构体: 大于等于所有成员的内存总和(成员之间可能有缝隙)
联合体: 占用的内存等于最大成员占用的内存

isa_t也是基于内存优化考虑使用联合体, isa通过 char + 位域(即二进制中每一位均可代表不同信息)的原理实现。通常来说isa占用8字节(64位), 足够存储很多信息, 这样的极大节省内存, 以提高性能。

其中bitscls两个成员变量是互斥关系, 如果是 nonpointer isa 就会有bit结构
如果这个类不是, 就当前的地址就是类地址。(联合体,同一时间,只会存储一个值,如果在存储另一个,会将前面存储的覆盖,所以是互斥的)

ISA_BITFIELD 这个是ISA位域信息 (arm64是真机的, x86_64是Mac或者模拟器的我们看arm64即可)

# 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

上面的判断针对于simulators模拟器, 我们重点看下真机的下面判断

  • nonpointer: 是否对isa指针开启指针优化占1位

    • 0: 纯isa指针
    • 1: 不只是类对象地址, isa中包含类信息, 对象引用计数等
    • 大部分自定义的类都为, nonpointer isa
  • has_assoc: 是否有关联对象占1位

    • 0: 没有关联对象
    • 1: 存在关联对象
  • has_cxx_dtor: 该对象是否有C++/OC的析构器(类似于dealloc)占1位

    • 0: 无, 可以更快释放对象
    • 1: 有, 需要做析构逻辑(析构函数就是dealloc)
  • shiftcls: 存储类指针的值。开启指针优化的情况下, 用来存储类指针(即类信息)

    • arm64: 占33位
    • arm64-not-e: 和sig 合一起占52位
    • x86_64: 占44位
  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间占6位

  • weakly_referenced: 指对象是否被指向或者曾经指向一个ARC弱变量, 没有弱引用可以更快释放(如果有弱引用对象, 需要引用计数移除)

  • deallocating: 标志对象是否正在释放内存

  • has_sidetable_rc: 是否有外挂的散列表, 当对象引用计数大于10时, 则需要借用该变量存储进位

  • extra_rc: 额外的引用计数, 表示该对象的引用计数值, 实际上是引用计数值减1

    • 例如: 如果对象的引用计数为10,那么extra_rc为9,真机上的 extra_rc 是使用 19位来存储引用计数的
    • 对象的引用计数存在isa里面,
isa_t
isa_t存储

关于dealloc补充点只是点, 我们先看下dealloc源码

dealloc

- (void)dealloc {
    _objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

可看到dealloc主要做了移除 引用计数, 关联对象, 散列表等, 之后再做了些销毁处理

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj); // c++析构
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true); // 移除关联对象
        obj->clearDeallocating();
    }

    return obj;
}
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating(); // 散列表的释放
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}
void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this); // weak 弱引用表的释放
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

dealloc源码中也能看出来, 这些对象是否能被释放其实都是通过标识位来进行读取的, 对象信息都已经存在isa中, 而dealloc相当于做了最后的熄火

验证 isa

  • main中定义自定义一个类SATest
验证isa1.1
  • class_createInstanceFromZone这里打个断点, 跟下initInstanceIsa这个流程(同时别忘确保进来的类确实是SATest)
验证isa1.2
  • 看到自定义类走了initInstanceIsa方法, 依次 initInstanceIsainitIsa 继续跟进
验证isa1.3
  • initIsa加个断点 p一下初始化的isa, 可看到
验证isa1.4
  • 走过newisa.bits = ISA_MAGIC_VALUE;, 这个方法主要是对isa位域赋给一个初始值/默认值, 再 p一下, 可看到
验证isa1.5

其中

__x86_64__
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
  • nonpointer变成1: 不难理解, isa中需要包含类信息, 对象引用计数等而且大部分自定义的类都为nonpointer isa

  • magic变成59: 首先magic是用于调试器判断当前对象是真的对象还是没有初始化的空间, 很明显有。nonpointer占1位, has_assoc占1位, has_cxx_dtor占1位, shiftcls x86_64 占44位, 那么从47起数6位(magic 占6位), 其中cls = 0x001d800000000001转成2进账从从47起数6位有111011 而这个就是10进制59。这块最好需要结合计算器一起看, 如图

验证isa1.6
  • 继续走, 走过newisa.setClass(cls, this);p一下, 可看到
验证isa1.7

newisa.setClass(cls, this)其实就是cls类指针关联方法, 进入源码可看到

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; // 右移3位, 赋值到shiftcls位
#endif
}

重点这段代码 shiftcls = (uintptr_t)newCls >> 3; , 将cls信息转换赋值给isa中的shiftcls位(shiftcls主要用来存储类指针(即类信息))

同时当前的cls已经变为SATest, 即isa指针关联起来

这里验证下

验证isa1.8
验证isa1.9
验证isa2.0
验证isa2.1

当然我们也可以这样验证
(上面例子不小心让我关了, 重新运行个, 原理一样)

x86_64 模拟器下, shiftcls占44位, 后面3位, 前面17位(一共64位, 8字节)

验证isa2.2

验证isa2.3
  • 当前obj为SATest指针, 我们x/4gx一下, 第一个是isa

  • isa, 右移3位, 去掉后面3个

    右移3位

  • 左移20位, 去掉前面17个


    左移20位
  • 右移17位, 得到shiftcls

    右移17位

我们将它与cls对比一下可发现相等, 也可以验证clsisa已经关联

总结

cls 与 isa 关联原理就是isa指针中的shiftcls位域中存储了类信息, 其中initInstanceIsa的原理就是将calloc开辟出来的obj指针与当前进行关联

补充

应该有些人会有疑问, isa继承isa_t, 而由于isa_t是个联合体, 呢么isa应该也是联合体, 而所有自定义类都是继承NSObject, 而NSObject底层

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

这里面isa不是Class么, 两者有矛盾

其实这里是为了方便开发人员开发做了一层小小的转换, 看下源码(下面是781源码, 个人感觉781这块写的更直接一点)

- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
#if SUPPORT_TAGGED_POINTERS

inline Class 
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}
inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

可看到(Class)(isa.bits & ISA_MASK);, 本来返回的是isa相关信息, 但是做了一层强转, 转成Class类型返回。

下面是818源码, 改变一些不过原理相同, 都是强转Class

inline Class
objc_object::ISA(bool authenticated)
{
    ASSERT(!isTaggedPointer());
    return isa.getDecodedClass(authenticated);
}
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
    if (nonpointer) {
        return classForIndex(indexcls);
    }
    return (Class)cls;
#else
    return getClass(authenticated);
#endif
}
inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
    return cls;
#else

    uintptr_t clsbits = bits;

#   if __has_feature(ptrauth_calls)
#       if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    // Most callers aren't security critical, so skip the
    // authentication unless they ask for it. Message sending and
    // cache filling are protected by the auth code in msgSend.
    if (authenticated) {
        // Mask off all bits besides the class pointer and signature.
        clsbits &= ISA_MASK;
        if (clsbits == 0)
            return Nil;
        clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
    } else {
        // If not authenticating, strip using the precomputed class mask.
        clsbits &= objc_debug_isa_class_mask;
    }
#       else
    // If not authenticating, strip using the precomputed class mask.
    clsbits &= objc_debug_isa_class_mask;
#       endif

#   else
    clsbits &= ISA_MASK;
#   endif

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

推荐阅读更多精彩内容