iOS 题目详解 部分一

主要记录一些题目所关联的知识点, 详解

iOS 题目详解 部分一
iOS 题目详解 部分二
iOS 题目详解 部分三

iOS 题目简述 部分一

题目1 关于打印self 和 super

三个类的继承关系如下NSObject->Dog->Doggy;下列代码打印结果是什么?

#import "Doggy.h"
@implementation Doggy
- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", [self class]);
        NSLog(@"%@", [super class]);
    }
    return  self;
}
@end

打印结果是:

2020-07-06 11:43:28.341267+0800 Topic[8721:101914] Doggy
2020-07-06 11:43:28.341389+0800 Topic[8721:101914] Doggy

对于 [self class]没什么疑问就是当前类, 所以打印Doggy, 但是为什么[super class]也是Doggy而不是Dog呢, 下面从源码角度验证此问题;
我们都知道 iOS中调用方法最后都会转变为objc_msgSend(receiver, sel)的方式, 而且从objc4-781的 runtime版本 中可以看到 NSObject.mm的源码有如下, 有此可以得知, class方法是得到当前类的类对象;

+ (Class)class {
    ///类方法直接返回本身
    return self;
}
- (Class)class {
    ///通过实例对象获取类对象
    return object_getClass(self);
}

因此可以得知不论是[self class]还是[super class] 最终返回的是调用者 revceiver 的类对象;
通过指令将文件转化为C++文件;得到的代码如下:

static instancetype _I_Doggy_init(Doggy * self, SEL _cmd) {
    self = ((Doggy *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Doggy"))}, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8m_1nrsxxrn5ln9zflyd82pvypc0000gn_T_Doggy_d4a898_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8m_1nrsxxrn5ln9zflyd82pvypc0000gn_T_Doggy_d4a898_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Doggy"))}, sel_registerName("class")));
    }
    return self;
}

将源码简化下

static instancetype _I_Doggy_init(Doggy * self, SEL _cmd) {
    self = objc_msgSendSuper)({(id)self, [[Doggy class] superClass]}, sel);
    if (self) {
        NSLog(objc_msgSend)(self, sel));
        NSLog(objc_msgSendSuper)({self,  [[Doggy class] superClass]}, sel);
    }
    return self;
}

因此我们只需要弄清楚objc_msgSendSuper()的内部逻辑即可, 我们从源码objc-msg-arm.s中可以得知此方法的定义如下, 通过汇编实现, 但是我们不需要知道具体逻辑,通过注释即可找到我们所需要的;objc_msgSendSuper()的入参也是两个参数, 第一个参数是一个结构体objc_super; 结构体中两个变量第一个是消息接收者, 第二个是接收者的父类类对象; 至此我们可以得知为什么调用[super class]也会得到当前类的的类对象,因为它的消息接收者仍然是当前类的实例对象;

/********************************************************************
 * id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
 *
 * struct objc_super {
 *      消息接收者
 *     id receiver;
*      从那个类开始搜索方法
 *     Class cls;   // the class to search
 * }
 ********************************************************************/
    ENTRY _objc_msgSendSuper
    ldr r9, [r0, #CLASS]    // r9 = struct super->class
    CacheLookup NORMAL, _objc_msgSendSuper
    // cache hit, IMP in r12, eq already set for nonstret forwarding
    ldr r0, [r0, #RECEIVER] // load real receiver
    bx  r12         // call imp

    CacheLookup2 NORMAL, _objc_msgSendSuper
    // cache miss
    ldr r9, [r0, #CLASS]    // r9 = struct super->class
    ldr r0, [r0, #RECEIVER] // load real receiver
    b   __objc_msgSend_uncached
    END_ENTRY _objc_msgSendSuper

通过 clang语句将 OC文件转化为C++文件, 并不是100%实际底层执行逻辑, 只是大概的逻辑, 就如这里, 其实真正调用的方法是objc_msgSendSuper2()这个方法, 不过不论哪个方法对我们的分析结构影响不大;

1.1 题目1衍生问题, 分析调用分类方法为什么先调用[super sel];

分析如下子类调用父类方法为什么先写[super sel];

///父类方法的实现
- (void)eatSomething {
    NSLog(@"Dog Like Bones");
}
///子类的方法实现
- (void)eatSomething {
    [super eatSomething];
    NSLog(@"Doggy like Bones Too");
}

子类调用父类方法先调用[super sel]通过上面的底层分析我们可以得知这样写的调用流程为, 通过结构体objc_super我们可以得知实际的调用者是当前类, 但是查找方法的过程是从父类开始查找的, 因从当前类开始查找会造成死循环;方法调用顺序, 具体可以通过objc_msgSend()流程得知

 struct objc_super {
     id receiver;
     Class cls; // the class to search
 }

题目2 :isKindOfClass 和 isMemberOfClass 的区别;

如下代码的打印结果

Doggy *doggy = [[Doggy alloc] init];
Dog *dog = [[Dog alloc] init];
BOOL case1 = [doggy isKindOfClass:[Dog class]];
BOOL case2 = [doggy isMemberOfClass:[Dog class]];
BOOL case3 = [dog isKindOfClass:[NSObject class]];
BOOL case4 = [dog isMemberOfClass:[NSObject class]];
BOOL case5 = [[Doggy class] isMemberOfClass:object_getClass([Dog class])];
BOOL case6 = [[Doggy class] isKindOfClass:object_getClass([Dog class])];

打印结果为

case1 = 1, case2 = 0, case3 = 1, case4 = 0, case5 = 0, case6 = 1

下面通过源码分析为什么是这个结果, 通过NSObject.mm中我们可以得知isMemberOfClassisKindOfClass的方法实现为


+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

首先得知这俩方法都有实例方法和类方法;分别用来判断类对象元类对象相等或者包含逻辑;

  • 实例方法: 是判断类对象是否相等或是其子类;
    - (BOOL)isMemberOfClass:(Class)cls :判断[当前实例对象的类对象]是否等于[类对象 cls]; (判断相等)
    - (BOOL)isKindOfClass:(Class)cls : 判断[当前实例对象的类对象]是否属于 [类对象cls] 的子类或者等于 [类型cls];(判断相等或包含)
  • 类方法: 是判断元类对象是否相等或是其子类;
    + (BOOL)isMemberOfClass:(Class)cls: 判断[当前类对象指向的元类对象]是否等于 [元类对象cls]; (判断相等)
    + (BOOL)isKindOfClass:(Class)cls : 判断[当前类对象指向元类对象] 是否属于 [元类对象 cls子类]或者等于[元类对象cls], 注意这里有个隐藏点, 基类的元类对象的 superclass指向基类;(判断相等或包含)

得知方法的底层实现后即可得知为什么打印结果是那样;


题目3 以下代码能否执行, 如果能执行, 结果是什么;

如下代码

///自定义的Model.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Model : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
- (void)printName;
- (void)printAge;
+ (void)printSuperClass;
@end
NS_ASSUME_NONNULL_END

///Model.m文件
#import "Model.h"
@implementation Model
- (void)printName {
    NSLog(@"name is : %@", self.name);
}
- (void)printAge {
    NSLog(@"age is : %@", self.age);
}
+ (void)printSuperClass {
    NSLog(@"superclass is : %@", [self superclass]);
}
@end

以下代码能不能执行, 如果能执行, 结果是什么?

    NSString *testAge = @"TheAge";
    NSString *testName = @"TheName";
    id cls =  [Model class];
    void *clsPoint = &cls;
    [(__bridge id)clsPoint printAge];

首先说下答案, 是可以编译, 可以执行, 而且不会报错; 打印结果是

2020-07-07 22:37:37.526402+0800 Topic[7827:1339842] age is : TheAge

此问题牵扯知识点比较多, 一点一点梳理;

  • 3.1 首先是一个结构体的的首个变量的地址就是结构的地址, 并且通过内存地址的加减偏移获得其他的变量地址, 关于验证过程可以看结尾的补充1部分;
  • 3.2 实例对象和类对象的底层结构都为结构体, 并且首个元素是isa指针, 因此实例对象的地址就是isa的地址, 因为在64位架构之前isa直接就是指向的类对象, 64位架构开始isa&ISAac_MASK可以得到类对象, 因此我们可以简单的只要拿到isa就可以拿到类对象的地址, 进而可以查找到整个类的实例方法, 因为实例方法存储在类对象中,可以看这篇文章;
  • 3.3 方法的调用最后都会转化为objc_msgSend(receiver, sel)的方式, 以实例对象为例, 则实例对象receiver的首地址就是isa, 通过首地址可以找到对应的类对象, 进而查找调用方法;因此我们可以推测只要是这种指向结构的均可以查找并调用方法;
  • 3.4 iOS 中采用小端模式, 验证过程可以看结尾的补充2部分

通过这个图我们可以看出当实例方法中调用_age的变量时, 是实例对象地址(结构体, 向下寻找)加上16个字节就是_name的地址, 所以通过clsPoint查找到方法并调用时内存地址(向上寻找)加16个字节得到的是 testAge变量的值; 至此, 方法调用结束, 注意如果地址(向上寻找)加上16个字节找不到合适的变量, 则会崩溃;


题目4 日常开发中哪些地方用到 runtime?

OC 是动态 语言, 它的动态性就是由runtime来支撑的;平时编写的 OC代码基本上底层都是转化为了runtime来调用的;
具体运用:

  • 利用关联对象技术来跟分类添加属性;
  • 遍历类的所有成员变量(例如之前可以直接修改 UITextField占位文字颜色背景色之类的操作);
  • 方法交换,替换系统方法的实现;
  • 利用消息转发处理找不到方法的异常(动态添加方法);

题目5 如何通过runtime替换系统的 UIButton 的点击事件;

例如我们需要打印出来每个 UIButton的点击事件时的信息, 可以为其添加一个分类将点击后调用的方法替换掉;
直接为 UIControl添加一个 Category将原本方法替换掉, 一旦执行了方法替换, 则方法查找, 缓存会被清空;

@implementation UIControl (Extention)
+ (void)load {
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(x_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}
- (void)x_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    ///如果是 UIButton则处理下
    if ([self isKindOfClass:[UIButton class]]) {
        NSLog(@"UIButton 点击着的信息action: %@__target: %@__event: %@", NSStringFromSelector(action), target, event);
        /*
        如果原本的方法中仍然想让执行, 则此处要这样调用一下, 为什么这样不会造成死循环
        因为方法的实现(IMP)已经被替换了;源码中可以得知这一点; 
        */
        [self x_sendAction:action to:target forEvent:event];
    }
}
@end
===>
void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;
    mutex_locker_t lock(runtimeLock);
    ///将方法的 IMP 替换
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;
    ///将方法的缓存清空
    flushCaches(nil);
    adjustCustomFlagsForMethodChange(nil, m1);
    adjustCustomFlagsForMethodChange(nil, m2);
}



题目6 关于 Tagged Pointer

有如下两段代码, 请问分别能不能执行, 执行结果如何
情况1:

for (int i = 0; i < 1000; i ++) {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
    dispatch_async(queue, ^{
        NSString *name = [NSString stringWithFormat:@"abcdefghhijklmnopqrst"];
//            NSLog(@"name = %p", name);    
    self.name = name;
    });
}

情况2

for (int i = 0; i < 10000; i ++) {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
    dispatch_async(queue, ^{
        NSString *name = [NSString stringWithFormat:@"abc"];
//            NSLog(@"name = %p", name);
        self.name = name;
    });
}

运行结果是, 情况1会崩溃,情况2能正常运行;
首先我们知道self.name = name;的本质是调用其setter方法, 而ARC下也是有内存管理这个概念, 只不过系统帮忙做了这个工作, 所以不论是ARC还是MRC最后的setter方法都会执行如下

- (void)setName:(NSString *)name {
  if (_name != name)
    [_name release];
    _name = [name retain];
}

情况1为什么会崩溃, 因为NSString *name = [NSString stringWithFormat:@"abcdefghhijklmnopqrst"];执行多少次就在堆区创建了多少个新对象, 通过打印即可确定; 所以我们可以知道if (_name != name)会一直成立, 这样的话[_name release];_name = [name retain];也会每次都执行, 由于是多线程, 同时修改_name崩溃风险;
解决方案1, 使用原子操作:
@property (nonatomic, strong) NSString *name;
改为@property (natomic, strong) NSString *name;
解决方案2: 为 setter的赋值过程加锁;

为什么情况2可以正常运行, 那是因为在64架构以后使用了Tagged Pointer技术, 针对小的NSNumber, NSString做了优化, 将值直接存在指针内, 所以不论多少次NSString *name = [NSString stringWithFormat:@"abc"];name中存储的始终是一个地址(abc值则直接封装在这个地址中), 通过打印即可验证;
因为始终是同一个地址, 所以判断条件if (_name != name)只会在第一次成立, 后续就不再成立, 即使再多线程也没风险;


题目7 OC的内存管理
  • iOS中使用引用计数的概念来管理内存;
  • 新创建的对象引用计数为1, 引用计数为0时对象会被销毁, 释放其所占内存;
  • 调用retain会使引用计数加1, 调用release会使引用计数减1;
  • 调用alloc, new, copy, mutablecopy创建新对象, 引用计数变为1; 调用release, autorelease释放对象;
7.1 引用计数存储在哪里?

答案: 引用计数存放在 isa指针中, 如果不够存放则存放在一个散列表SideTable中;
通过在runtime部分学习isa指针的结构可以得知这个结论;
下面通过源码来验证, 使用runtime的版本为objc4-781版本;
找到NSObject.mm的实现, 找到retainCount, 可以找到SideTable的结构为

struct SideTable {
    spinlock_t slock;
    ///存放的是引用计数reference counts;
    RefcountMap refcnts;
    ///弱指针引用表, weak 修饰的对象, 释放时使用这个表;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }
...
}

我们知道查看引用计数使用的是retainCount, 所以可以得到

///引用计数入口
- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

===>
_objc_rootRetainCount(id obj) {
    ASSERT(obj);
    return obj->rootRetainCount();
}

===>
/*
这里需要注意, 你会找到两个rootRetainCount()方法, 需要注意的是一个是
区别一个支持NONPOINTER_ISA, 一个是不支持NONPOINTER_ISA;
现在的64位架构都是使用SUPPORT_NONPOINTER_ISA, 具体验证过程可以点击#if SUPPORT_NONPOINTER_ISA宏去查看, 
不再贴出验证过程;
*/
#if SUPPORT_NONPOINTER_ISA
...

inline uintptr_t 
objc_object::rootRetainCount()
{
    ///如果是TaggedPointer 直接返回, 因为这是一个指针, 不是对象, 不需要考虑引用计数;
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    ///获取 isa 指针的内容
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    ///如果使用了nonpointer, 64架构值是1
    if (bits.nonpointer) {
        /*
        rc 就是1加上存在 isa 中的extra_rc;
          跟之前的 runtime 中讲解extra_rc的对应上. extra_rc存的值是 rc 的值减1;
        */
        uintptr_t rc = 1 + bits.extra_rc;
        /// 跟之前的 runtime 中讲解has_sidetable_rc对应上, 如果是1,  则使用散列表存储引用计数;
        if (bits.has_sidetable_rc) {
             ///rc 的值为1加上散列表中 rc 的值
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
// SUPPORT_NONPOINTER_ISA
#else
// not SUPPORT_NONPOINTER_ISA

....
#endif

===>
size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
     ///散列表SideTable, 第二个变量存储的是引用计数
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

至此可以验证引用计数的存储位置;

7.2 weak 指针的原理是什么?

跟引用计数一样, weak指针会被存放在散列表中; 在对象执行dealloc的时候将这个散列表遍历置为nil;
通过源码查看流程, 入口为NSObjectdealloc函数;

- (void)dealloc {
    _objc_rootDealloc(self);
}

===>
void _objc_rootDealloc(id obj){
    ASSERT(obj);
    obj->rootDealloc();
}

===>
inline void
objc_object::rootDealloc()
{
    ///判断是否是 TaggedPointer 如果是则不考虑内存管理
    if (isTaggedPointer()) return;  // fixme necessary?
    /*
    判断是否有弱引用, 关联对象, C++析构函数, 是否散列表存储
    如果没有这些则会释放的更快
    */
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        ///有弱引用, 关联对象, C++析构函数, 散列表存储之类
        object_dispose((id)this);
    }
}

===>
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);
        if (assoc) _object_remove_assocations(obj);
        ///处理需要 dealloc 的对象
        obj->clearDeallocating();
    }
    return obj;
}

===>
inline void 
objc_object::clearDeallocating()
{
    sidetable_clearDeallocating();
}

===>
void 
objc_object::sidetable_clearDeallocating()
{
    ///通过当前为 key 取出散列表
    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()) {
        ///通过&SIDE_TABLE_WEAKLY_REFERENCED 获取弱引用散列表 然后清理
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            ///清理弱引用指针,  通过弱引用散列表
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

至此可以确定weak的原理;




补充和验证部分

补充1

结构体的首个变量的地址就是结构体的地址, 并且可以通过通过内存地址的加减获得其他变量的地址;
例如有如下结构体:

struct Person {
    int age;
    int height;
};
struct Man {
    struct Person person;
    int weight;
};

则下述的操作进和判断都是合理的

//**验证代码如下**//
    struct Man man = {
        {30, 180},
        100
    };
    void *ageAddress = &(man.person.age);
    ///第一个元素的的地址就是结构体的地址   地址: 0x00007ffee44e4b80
    void *manAddress = &(man);
    ///地址:  0x00007ffee44e4b80
    void *firstParaAddress = &(man.person);
    ///拿到 man 结构体中第一个元素的地址+8就是 man.weight 的地址; firstParaAddress + 8 = 0x00007ffee44e4b88;
    void *weightAddress = firstParaAddress + 8;
    NSLog(@"结构体中第一个元素地址: %p",ageAddress );
    NSLog(@"结构体中第一个元素地址: %p",manAddress );
    NSLog(@"结构体中地址: %p",manAddress );
    NSLog(@"首地址+8 就是weight的地址: %p == %p", &(man.weight), weightAddress);
//**打印结果如下**//
2020-07-07 22:37:37.526033+0800 Topic[7827:1339842] 结构体中第一个元素地址: 0x16f73f840
2020-07-07 22:37:37.526191+0800 Topic[7827:1339842] 结构体中第一个元素地址: 0x16f73f840
2020-07-07 22:37:37.526240+0800 Topic[7827:1339842] 结构体中地址: 0x16f73f840
2020-07-07 22:37:37.526290+0800 Topic[7827:1339842] 首地址+8 就是weight的地址: 0x16f73f848 == 0x16f73f848
补充2

iOS 采用小段模式,存储在栈区的变量的内存地址是递减的;

    ///&str1 = 0x00007ffee71a6ed8
    NSString *str1 = @"abc";
    ///&str2 = 0x00007ffee71a6ed0
    NSString *str2 = @"111";
    ///&str3 = 0x00007ffee71a6ec8
    NSString *str3 = @"123asdf";
    ///&num1 = 0x00007ffee71a6ec4
    int num1 = 1;
    ///&num3 = 0x00007ffee71a6ec0
    int num2 = 2;
    ///num3Address = 0x00007ffee71a6ebc;
    int num3 = 3;
    void *num3Address = &num3;

通过上面我们可以看到在这个方法中开辟的变量在栈中的地址是递减存在的; 每次是递减8个字节或者4个字节;


文中测试代码


参考文章和下载链接
Apple 一些源码的下载地址
大小端模式
iOS 判断大小端字节序
方法的查找顺序
LP64 结构数据占据多少位
LP64什么意思
isa 的结构
iOS Tagged Pointer技术

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