iOS runtime 五: 方法和消息

Method

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;
    method_t(const method_t &other) = delete;
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

method_t是一个没有成员变量的结构体,只有成员函数,在C++中这种结构体占1个字节.
二级结构big,一个SEL,表示方法名,一个char*表示参数类型和返回值,相当与方法的模板;最后是一个MethodListIMP指针,它是IMP的签名包装.

 struct small {
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP, /*isNullable*/false> imp;
}

还有一个small结构体,这个用来描述方法在image中的存储结构,是三个偏移地址.

 big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }

提供了一个big函数用来获取big结构体.this是形参,实际调用时传的是对象指针.所以它做了强转,返回新的指针,新的指针是struct big类型,大小也不再是一个字节.

    ALWAYS_INLINE SEL name() const {
        if (isSmall()) {
            if (small().inSharedCache()) {
                return (SEL)small().name.get(sharedCacheRelativeMethodBase());
            } else {
                // Outside of the shared cache, relative methods point to a selRef
                return *(SEL *)small().name.get();
            }
        } else {
            return big().name;
        }
    }

    const char *types() const {
        return isSmall() ? small().types.get() : big().types;
    }

    IMP imp(bool needsLock) const {
        if (isSmall()) {
            IMP smallIMP = ptrauth_sign_unauthenticated(small().imp.get(),
                                                        ptrauth_key_function_pointer, 0);
            asm ("": : "r" (smallIMP) :);
            IMP remappedIMP = remappedImp(needsLock);
            if (remappedIMP)
                return remappedIMP;
            return smallIMP;
        }
        return big().imp;
    }

这三个函数用于获取SEL, types和imp.实际内容也没啥,big的话直接取big结构体,small的话调用samll的成员函数取值.

观察一下内存布局

@interface MyClass : NSObject

@property(nonatomic, strong) NSNumber *number;
@property(nonatomic, assign) NSInteger integer;
@property(atomic, assign) NSInteger atomic;
@property(nonatomic, copy) NSString *Str;
@property(nonatomic, weak) NSObject *weak;
@property(nonatomic, strong, readonly) NSObject *readonly;

- (void)instanceMethod;

@end
(lldb) p my.class
(Class) $0 = 0x0000000100008450
(lldb) p (objc_class *)$0
(objc_class *) $1 = 0x0000000100008450
(lldb) p $1->safe_ro()
(const class_ro_t *) $2 = 0x0000000100008348
(lldb) p *$2
(const class_ro_t) $3 = {
  flags = 388
  instanceStart = 8
  instanceSize = 56
  reserved = 0
   = {
    ivarLayout = 0x0000000100003f43 "\U00000001!\U00000011"
    nonMetaclass = 0x0000000100003f43
  }
  name = {
    std::__1::atomic<const char *> = "MyClass" {
      Value = 0x0000000100003f3b "MyClass"
    }
  }
  baseMethods = {
    ptr = 0x00000001000080d8
  }
  baseProtocols = nil
  ivars = 0x0000000100008218
  weakIvarLayout = 0x0000000100003f47 "A"
  baseProperties = 0x00000001000082e0
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $3.baseMethods.ptr
(method_list_t *const) $4 = 0x00000001000080d8
(lldb) p $4->get(0)
(method_t) $5 = {}
(lldb) p $5.big()
(method_t::big) $6 = {
  name = "setWeak:"
  types = 0x0000000100003f59 "v24@0:8@16"
  imp = 0x0000000100003bd0 (KCObjcBuild`-[MyClass setWeak:])
}

(lldb) p sizeof($5)
(unsigned long) $7 = 1
(lldb) p &$5
(method_t *) $9 = 0x00000001000080e0
(lldb) p &$6
(method_t::big *) $10 = 0x00000001000080e0


首先获取class_ro_t,然后输出baseMethods的get(0),是一个空的结构体,然后调用big()可以获取到big结构体.
下面查看空method_t的大小是1字节,并且method_t的地址和big结构体的地址是一样的,和big函数描述的一样.

然后还可以看到SEL是setWeak,这是@property(nonatomic, weak) NSObject *weak的setter.

(lldb) p $4.count
(uint32_t) $11 = 13
(lldb) p *$4
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 13)
}

(lldb) p $12.get(1)->big()
(method_t::big) $13 = {
  name = "instanceMethod"
  types = 0x0000000100003f49 "v16@0:8"
  imp = 0x0000000100003a50 (KCObjcBuild`-[MyClass instanceMethod])
}
  Fix-it applied, fixed expression was: 
    $12.get(1).big()
(lldb) p $12.get(1).big()
(method_t::big) $14 = {
  name = "instanceMethod"
  types = 0x0000000100003f49 "v16@0:8"
  imp = 0x0000000100003a50 (KCObjcBuild`-[MyClass instanceMethod])
}
(lldb) p $12.get(2).big()
(method_t::big) $15 = {
  name = "Str"
  types = 0x0000000100003f51 "@16@0:8"
  imp = 0x0000000100003b50 (KCObjcBuild`-[MyClass Str])
}
(lldb) p $12.get(3).big()
(method_t::big) $16 = {
  name = ".cxx_destruct"
  types = 0x0000000100003f49 "v16@0:8"
  imp = 0x0000000100003c20 (KCObjcBuild`-[MyClass .cxx_destruct])
}
(lldb) p $12.get(4).big()
(method_t::big) $17 = {
  name = "number"
  types = 0x0000000100003f51 "@16@0:8"
  imp = 0x0000000100003a80 (KCObjcBuild`-[MyClass number])
}
(lldb) p $12.get(5).big()
(method_t::big) $18 = {
  name = "setNumber:"
  types = 0x0000000100003f59 "v24@0:8@16"
  imp = 0x0000000100003aa0 (KCObjcBuild`-[MyClass setNumber:])
}
(lldb) p $12.get(6).big()
(method_t::big) $19 = {
  name = "readonly"
  types = 0x0000000100003f51 "@16@0:8"
  imp = 0x0000000100003c00 (KCObjcBuild`-[MyClass readonly])
}
(lldb) p $12.get(7).big()
(method_t::big) $20 = {
  name = "setAtomic:"
  types = 0x0000000100003f6c "v24@0:8q16"
  imp = 0x0000000100003b30 (KCObjcBuild`-[MyClass setAtomic:])
}
(lldb) p $12.get(8).big()
(method_t::big) $21 = {
  name = "atomic"
  types = 0x0000000100003f64 "q16@0:8"
  imp = 0x0000000100003b10 (KCObjcBuild`-[MyClass atomic])
}
(lldb) p $12.get(9).big()
(method_t::big) $22 = {
  name = "weak"
  types = 0x0000000100003f51 "@16@0:8"
  imp = 0x0000000100003ba0 (KCObjcBuild`-[MyClass weak])
}
(lldb) p $12.get(10).big()
(method_t::big) $23 = {
  name = "integer"
  types = 0x0000000100003f64 "q16@0:8"
  imp = 0x0000000100003ad0 (KCObjcBuild`-[MyClass integer])
}
(lldb) p $12.get(11).big()
(method_t::big) $24 = {
  name = "setInteger:"
  types = 0x0000000100003f6c "v24@0:8q16"
  imp = 0x0000000100003af0 (KCObjcBuild`-[MyClass setInteger:])
}
(lldb) p $12.get(12).big()
(method_t::big) $25 = {
  name = "setStr:"
  types = 0x0000000100003f59 "v24@0:8@16"
  imp = 0x0000000100003b70 (KCObjcBuild`-[MyClass setStr:])
}

baseMethods里一共有13个方法,全部输出,6个property,其中一个是readonly,它只有getter没有setter,
还有一个实例方法instanceMethod以及一个来cxx_destruct,这个函数用于释放成员变量.

///注释Myclass的属性再运行
(lldb) p $5.count
(uint32_t) $6 = 1
(lldb) p $5.get(0).big()
(method_t::big) $7 = {
  name = "instanceMethod"
  types = 0x0000000100003fa4 "v16@0:8"
  imp = 0x0000000100003e70 (KCObjcBuild`-[MyClass instanceMethod])
}

假如把Myclass的property都注了,只留一个实例方法,此时cxx_destruct就没了.

SEL与IMP

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一个不透明的objc_selector结构体指针,但是上面观察method_t的时候可以看到它就是一个字符串,或者说可以当做一个char *来看待.
它用于表示方法的名称,从上面的例子也可以看到,name等于方法名,所以它不区分是来自那个类对象或者元类,
这些方法名被添加在一张表中,runtime可以通过sel_registerName或者NSSelectorFormString获取SEL,编译时可以通过编译器指令@selector()来创建SEL.

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP是一个函数指针,它的类型是 id ()(id, SEL, ...)或者void ()(...)

首先回忆一下C++的函数指针.

void aMethod(int a){
    printf("aMethod = %d\n", a);
}

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

        void (*methodPointer)(int) = aMethod;
        methodPointer(1);
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

void (* methodPointer)(int) = aMethod定义了一个函数指针,
指针要有类型,比如int * char*,这个指针的类型是void ()(int)这么一种函数.
void是函数的返回值类型,int是参数列表,methodPointer是指针的名字.
然后= aMethod是让指针methodPointer指向了函数aMethod.

(lldb) p &aMethod
(void (*)(int)) $0 = 0x0000000100003d50 (KCObjcBuild`aMethod at main.m:25)
(lldb) p methodPointer
(void (*)(int)) $1 = 0x0000000100003d50 (KCObjcBuild`aMethod at main.m:25)

调用这个函数,可以是aMethod(1),也可以是methodPointer(1),他们实质是一样的,函数名就像是数组名,访问数组名就是访问数组内存的起始,函数名也代表了这个函数在内存代码区的起始地址.

typedef void (*MY_SEL)(int);
MY_SEL a_sel = aMethod;
a_sel(1);

函数指针也使用typedef来取别名,这里定义了一个函数指针类型,MY_SEL,是void (* )(int).
就类似于typedef MY_INT int;然后就可以MY_INT a = 1;
所以IMP就是void ( * )()类型的指针.
而且不管它指向什么函数,有什么返回值,什么参数,都用这个类型的指针.

int aMethod(int a){
    printf("aMethod = %d\n", a);
    return  a;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        class_addMethod(MyClass.class, @selector(aMethod:), (IMP)aMethod, "i@:i");
        NSLog(@"Hello, World!");
    }
    return 0;

运行断点

(objc_class *) $0 = 0x0000000100008120
(lldb) p $0->data()
(class_rw_t *) $1 = 0x0000000108e27920
(lldb) p $1->methods()
(const method_array_t) $2 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000108e498b1
      }
      arrayAndFlag = 4444166321
    }
  }
}
(lldb) p $2.begin()
(list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>::iterator) $3 = {
  lists = 0x0000000108e498b8
  listsEnd = 0x0000000108e498c8
  m = (entsize = 24, index = 0, element = 0x0000000108e49898)
  mEnd = (entsize = 24, index = 1, element = 0x0000000108e498b0)
}
(lldb) p $3.m
(list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>::iteratorImpl<false>::ListIterator<false>::Type) $4 = (entsize = 24, index = 0, element = 0x0000000108e49898)
(lldb) p $4.element
(entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier>::iteratorImpl<false>::ElementPtr) $5 = 0x0000000108e49898
(lldb) p $5->big()
(method_t::big) $6 = {
  name = "aMethod:"
  types = 0x0000000100003f60 "i@:i"
  imp = 0x0000000100003de0 (KCObjcBuild`aMethod at main.m:25)
}
(lldb) p *$6.imp
(void (*)()) $7 = 0x0000000100003de0 (KCObjcBuild`aMethod at main.m:25)

可以看到IMP是void (*)()类型指针,但是我们给MyClass添加的方法指定的imp是aMethod,类型是int:int

(lldb) p $5.imp
(MethodListIMP) $6 = 0x0000000100003db0 (KCObjcBuild`aMethod at main.m:25)
(lldb) p *$6
(void (*)()) $7 = 0x0000000100003db0 (KCObjcBuild`aMethod at main.m:25)
(lldb) p (int (*)(int))$7
(int (*)(int)) $8 = 0x0000000100003db0 (KCObjcBuild`aMethod at main.m:25)
(lldb) p $8(1)
aMethod = 1
(int) $9 = 1

所以我们想要调用这个函数需要转换指针类型,把(void ( * )())转换成(int (*)(int)).然后就可以调用aMethod了.

addMethod

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;
    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
            result = m->imp(false);
        } else {
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // fixme optimize
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;
        newlist->count = 1;
        auto &first = newlist->begin()->big();
        first.name = name;
        first.types = strdupIfMutable(types);
        first.imp = imp;
        addMethods_finish(cls, newlist);
        result = nil;
    }
    return result;
}

runtime可以动态添加方法,这个函数可以指定是否替换replace原来的方法.
getMethodNoSuper_nolock这个函数用于在类中通过SEL查找方法method_t,这部分属于方法慢速查找流程,之后在细说.
如果找到了,需要替换就替换,替换不替换都是返回原来的那个IMP,因为_method_setImplementation返回旧的IMP.
如果没找到,就创建一个新的method_list_t,给首位元素赋值,SEL,types和IMP.

static void
addMethods_finish(Class cls, method_list_t *newlist)
{
    auto rwe = cls->data()->extAllocIfNeeded();
    if (newlist->count > 1) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(&newlist->begin()->big(), &newlist->end()->big(), sorter);
    }
    prepareMethodLists(cls, &newlist, 1, NO, NO, __func__);
    rwe->methods.attachLists(&newlist, 1);
    flushCaches(cls, __func__, [](Class c){
        return !c->cache.isConstantOptimizedCache();
    });
}

最后调用addMethods_finish,
先对newlist进行了排序,这个排序在查找中会用到.
rwe是objc_class的class_rw_t的rw_ext_t,也就是rw的内容,可以获取到methods,是list_array_tt类型,
所以后面调用了list_array_tt的attachLists来添加新的method_list_t;
然后添加了缓存,runtime添加的方法,添加的时候就已经放在缓存里了.

最后如果addMethod成功添加了一个新的,result就有没有值,如果已经存在旧的SEL,那result就有值,
所以class_addMethod最后才会这么写

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;
    mutex_locker_t lock(runtimeLock);
    return !addMethod(cls, name, imp, types ?: "", NO);
}

如果addMethod返回了有值,那就添加失败了.

objc_msgSend

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这里OBJC_OLD_DISPATCH_PROTOTYPES是0,所以最终objc_msgSend是这么定义的.
但是在使用的时候会进行强制类型转换,来匹配使用场景下的真实返回值和参数.
objc_msgSend是一个函数,

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

比如performSelector的实现是这样的,最终是调用的objc_msgSend函数,不过需要转换类型

(lldb) p objc_msgSend
error: expression failed to parse:
error: <user expression 0>:1:1: 'objc_msgSend' has unknown type; cast it to its declared type to use it

objc_msgSend是一个不完整的类型,如果在这里断点,输出p objc_msgSend,
会报错,让你转换类型再输出

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    id(*my_objc_msgSend)(id, SEL, id) = (id(*)(id, SEL, id))objc_msgSend;
    return my_objc_msgSend(self, sel, obj);
}

(lldb) p my_objc_msgSend
(id (*)(id, SEL, id)) $0 = 0x0000000108f9bf80 (libobjc.A.dylib`objc_msgSend)

我们可以改造一下这个方法,先转换,再调用,也是可以生效的.
objc4中使用objc_msgSend的时候,都进行了强制类型转换.

@interface Myclass : NSObject
@property(nonatomic, strong) NSNumber *myName;
@end

@implementation Myclass
@end

int main(int argc, char * argv[]) {
    id obj = [Myclass alloc];
    Myclass *my = [obj init];
    return  0;
}

修改main.m的代码,随便什么类型的工程都行.添加一个MyClass,然后alloc ,init.

clang -rewrite-objc main.m -o main.cpp

把main.m编译成C++代码.

int main(int argc, char * argv[]) {
    id obj = ((Myclass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Myclass"), sel_registerName("alloc"));
    Myclass *my = ((id (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("init"));
    return 0;
}

找到main函数,
原来的OC代码是Myclass *my = [[Myclass alloc]init];现在被转换成两个objc_msgSend函数调用.
这里(void *)是做了两次类型转换,void 可以指向任意类型的数据,
所以第一句是把objc_msgSend 转换成(Myclass ()(id, SEL)),然后调用,传了两个参数,一个Class指针,一个SEL.
第二句是把objc_msgSend转换成(id (
)(id, SEL))然后调用,传了两个参数obj和一个SEL.
alloc传的第一个参数是类对象,init传的第一个参数是实例对象,这对应了alloc是+方法,init是-方法.

objc_msgSend没有C++的实现,而是直接汇编实现.具体是在source文件夹里的几个.s文件,比如objc-msg-arm64.s
并且它还是一个系列的函数,在message.h中定义了其他很多函数.

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_msgSend_stret(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
    OBJC_ARM64_UNAVAILABLE;

OBJC_EXPORT void
objc_msgSendSuper_stret(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
    OBJC_ARM64_UNAVAILABLE;

这些函数都是汇编实现,和objc_msgSend在相同的位置,
不过特别的是,除了objc_msgSend之外,其他函数没有在汇编之外的地方调用过.
但是他们声明在外面,所以也可以主动调用看看.

objc_msgSendSuper需要一个struct objc_super *,一个SEL

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

objc_super结构体是这样的,指定一个实例对象receiver,还有父类.

//MyClass
- (int)instanceMethod:(NSNumber *)a{
    NSLog(@"instanceMethod = %d", [a intValue]);
    return [a intValue];
}

//main.m
#import <objc/message.h>

MySubClass *sub = [[MySubClass alloc]init];
struct objc_super s;
s.receiver = sub;
s.super_class = [MyClass class];
((id (*)(struct objc_super*, SEL, NSNumber *))objc_msgSendSuper)(&s, sel_registerName("instanceMethod:"), @9);

需要头文件#import <objc/message.h>
objc_msgSendSuper同样也需要强制转换类型,顺便带上参数,
指定objc_super的接受者是sub对象,父类是MyClass.class.
instanceMethod:是父类MyClass的实例方法,MySubClass没有重写.
运行可以正常输出

instanceMethod = 9

performSelector

- (int)instanceMethod:(int)a{
    NSLog(@"instanceMethod = %d", a);
    return a;
}

//- (id)performSelector:(SEL)sel withObject:(id)obj 

假如instanceMethod是这么写的,它接收一个int型的参数,那么performSelector就不好使了,
因为performSelector的参数必须是id,也就是objc_object *类型.
并且performSelector只提供了无参,一个参数和两个参数两种版本.

MyClass *my = [[MyClass alloc]init];
((int (*)(id, SEL, int))objc_msgSend)(my, sel_registerName("instanceMethod:"), 9);

这个只能用objc_msgSend来调用

        MyClass *my = [[MyClass alloc]init];
        int num = 9;
        SEL sel = sel_registerName("instanceMethod:");
        NSMethodSignature *signature = [my methodSignatureForSelector:sel];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        [invocation setArgument:&num atIndex:2];
        invocation.selector = sel;
        invocation.target = my;
        [invocation invoke];
        
        int result;
        [invocation getReturnValue:&result];
        NSLog(@"result = %d", result);

或者使用NSInvocation.关于NSInvocation的细节在之后消息转发中研究.

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

推荐阅读更多精彩内容