任意方法的Swizzle的应用之一AOP

在上篇博客曾聊过对任意方法Swizzle有多种应用,其中之一就是对多个方法的开始或者结束添加统一的切面调用。很有名的Aspect库利用类似于KVO实现原理来实现,在运行时给需要Swizzle的类动态添加子类,同时该对象isa指针指向创建的子类,然后hook子类的forwardInvocation关联到函数__ASPECTS_ARE_BEING_CALLED__。之后再将要hook的method的实现替换为_objc_msgForward,这样对该method调用会就转发到__ASPECTS_ARE_BEING_CALLED__,然后利用NSInvocation转发该调用即可。而有了对任意方法Swizzle的方案后就可以另辟蹊径来解决这个问题。

上篇博客给了个简单的demo实现,不过功能较弱,这次花了一天多时间,写了一个较为成熟的实现,总共三百行左右,其中一半是汇编。

先上代码,先吓走一票人再说。

#import <objc/runtime.h>

typedef NS_OPTIONS(NSInteger, ZWInvocationOption) {
    ZWInvocationOptionReplace = 1,
    ZWInvocationOptionBefore = 1 << 1,
    ZWInvocationOptionAfter = 1 << 2,
    ZWInvocationOptionOnly = 1 << 3,
};

#if defined(__arm64__)

static NSMutableDictionary  *_ZWBeforeIMP;
static NSMutableDictionary  *_ZWOriginIMP;
static NSMutableDictionary  *_ZWAfterIMP;
static NSLock  *_ZWLock;

OS_ALWAYS_INLINE void ZWStoreParams(void) {
    asm volatile("str    d7, [x11, #0x88]\n\
                 str    d6, [x11, #0x80]\n\
                 str    d5, [x11, #0x78]\n\
                 str    d4, [x11, #0x70]\n\
                 str    d3, [x11, #0x68]\n\
                 str    d2, [x11, #0x60]\n\
                 str    d1, [x11, #0x58]\n\
                 str    d0, [x11, #0x50]\n\
                 str    x8, [x11, #0x40]\n\
                 str    x7, [x11, #0x38]\n\
                 str    x6, [x11, #0x30]\n\
                 str    x5, [x11, #0x28]\n\
                 str    x4, [x11, #0x20]\n\
                 str    x3, [x11, #0x18]\n\
                 str    x2, [x11, #0x10]\n\
                 str    x1, [x11, #0x8]\n\
                 str    x0, [x11]\n\
                 ");
}
OS_ALWAYS_INLINE void ZWLoadParams(void) {
    asm volatile("ldr    d7, [x11, #0x88]\n\
                 ldr    d6, [x11, #0x80]\n\
                 ldr    d5, [x11, #0x78]\n\
                 ldr    d4, [x11, #0x70]\n\
                 ldr    d3, [x11, #0x68]\n\
                 ldr    d2, [x11, #0x60]\n\
                 ldr    d1, [x11, #0x58]\n\
                 ldr    d0, [x11, #0x50]\n\
                 ldr    x8, [x11, #0x40]\n\
                 ldr    x7, [x11, #0x38]\n\
                 ldr    x6, [x11, #0x30]\n\
                 ldr    x5, [x11, #0x28]\n\
                 ldr    x4, [x11, #0x20]\n\
                 ldr    x3, [x11, #0x18]\n\
                 ldr    x2, [x11, #0x10]\n\
                 ldr    x1, [x11, #0x8]\n\
                 ldr    x0, [x11]\n\
                 ");
}

OS_ALWAYS_INLINE void ZWGlobalOCSwizzle(void) {
    asm volatile("stp    x29, x30, [sp, #-0x10]!");
    
    asm volatile("mov    x29, sp\n\
                 sub    sp, sp, #0xb0");
    
    asm volatile("mov    x11, sp");
    asm volatile("bl    _ZWStoreParams");

    asm volatile("mov    x0, sp");
    asm volatile("bl    _ZWBeforeInvocation");
    
    asm volatile("mov    x0, sp");
    asm volatile("bl    _ZWInvocation");
    
    asm volatile("str    x0, [sp, #0xa0]");
    asm volatile("str    d0, [sp, #0xa8]");

    asm volatile("mov    x0, sp");
    asm volatile("bl    _ZWAfterInvocation");
    
    asm volatile("ldr    x0, [sp, #0xa0]");
    asm volatile("ldr    d0, [sp, #0xa8]");
    
    asm volatile("mov    sp, x29");
    asm volatile("ldp    x29, x30, [sp], #0x10");
}

OS_ALWAYS_INLINE NSString *ZWGetMetaSelName(SEL sel) {
    return [@"__META_" stringByAppendingString:NSStringFromSelector(sel)];
}

OS_ALWAYS_INLINE id ZWGetInvocation(NSDictionary *dict, id obj, SEL sel) {
    Class class = object_getClass(obj);
    NSString *className = NSStringFromClass(class);
    NSString *selName = class_isMetaClass(class) ? ZWGetMetaSelName(sel) : NSStringFromSelector(sel);
    [_ZWLock lock];
    id Invocation = dict[className][selName];
    [_ZWLock unlock];
    return Invocation;
}

OS_ALWAYS_INLINE NSUInteger ZWGetInvocationCount(NSDictionary *dict, id obj, SEL sel) {
    id ret = ZWGetInvocation(dict, obj, sel);
    if ([ret isKindOfClass:[NSArray class]]) {
        return [ret count];
    } else if ([ret isKindOfClass:NSClassFromString(@"NSBlock")]) {
        return 1;
    }
    return 0;
}

IMP ZWGetOriginImp(id obj, SEL sel) {
    id Invocation = ZWGetInvocation(_ZWOriginIMP, obj, sel);
    if ([Invocation isKindOfClass:[NSValue class]]) {
        return [Invocation pointerValue];
    }
    return NULL;
}

IMP ZWGetCurrentImp(id obj, SEL sel) {
    id Invocation = ZWGetInvocation(_ZWOriginIMP, obj, sel);
    if ([Invocation isKindOfClass:NSClassFromString(@"NSBlock")]) {
        if (!Invocation) return NULL;
        uint64_t *p = (__bridge void *)(Invocation);
        return (IMP)*(p + 2);
    }
    return NULL;
}

IMP ZWGetAopImp(NSDictionary *Invocation, id obj, SEL sel, NSUInteger index) {
    if (!obj || !sel) return NULL;
    id block = ZWGetInvocation(Invocation, obj, sel);
    if ([block isKindOfClass:[NSArray class]]) {
        block = block[index];
    }
    if (!block) return NULL;
    uint64_t *p = (__bridge void *)(block);
    return (IMP)*(p + 2);
}


void ZWAopInvocation(void **sp, NSDictionary *Invocation, ZWInvocationOption option) {
    id obj = (__bridge id)(*sp);
    SEL sel = *(sp + 1);
    if (!obj || !sel) return;
    NSInteger count = ZWGetInvocationCount(Invocation, obj, sel);
    __autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(option)];
    for (int i = 0; i < count; ++i) {
        ZWGetAopImp(Invocation, obj, sel, i);
        asm volatile("cbz    x0, LBB_20181107");
        asm volatile("mov    x17, x0");
        asm volatile("ldr    x11, %0": "=m"(sp));
        asm volatile("ldr    x12, %0": "=m"(arr));
        asm volatile("bl    _ZWLoadParams");
        asm volatile("mov    x1, x12");
        asm volatile("blr    x17");
        asm volatile("LBB_20181107:");
    }
}

void ZWAfterInvocation(void **sp) {
    ZWAopInvocation(sp, _ZWAfterIMP, ZWInvocationOptionAfter);
}
void ZWInvocation(void **sp) {
    __autoreleasing id obj;
    SEL sel;
    void *obj_p = &obj;
    void *sel_p = &sel;
    
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x10, %0": "=m"(obj_p));
    asm volatile("ldr    x0, [x11]");
    asm volatile("str    x0, [x10]");
    asm volatile("ldr    x10, %0": "=m"(sel_p));
    asm volatile("ldr    x0, [x11, #0x8]");
    asm volatile("str    x0, [x10]");
    
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x0, [x11]");
    asm volatile("ldr    x1, [x11, #0x8]");
    asm volatile("bl    _ZWGetOriginImp");
    asm volatile("cbnz    x0, LZW_20181105");
    
    __autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(ZWInvocationOptionReplace)];

    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("ldr    x0, [x11]");
    asm volatile("ldr    x1, [x11, #0x8]");
    asm volatile("bl    _ZWGetCurrentImp");
    asm volatile("cbz    x0, LZW_20181106");
    asm volatile("mov    x17, x0");
    asm volatile("ldr    x12, %0": "=m"(arr));
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("bl    _ZWLoadParams");
    asm volatile("mov    x1, x12");
    asm volatile("blr    x17");
    asm volatile("b LZW_20181106");

    asm volatile("LZW_20181105:");
    asm volatile("mov    x17, x0");
    asm volatile("ldr    x11, %0": "=m"(sp));
    asm volatile("bl    _ZWLoadParams");
    asm volatile("blr    x17");
    asm volatile("LZW_20181106:");
}
void ZWBeforeInvocation(void **sp) {
    ZWAopInvocation(sp, _ZWBeforeIMP, ZWInvocationOptionBefore);
}

OS_ALWAYS_INLINE Method ZWGetMethod(Class cls, SEL sel) {
    unsigned int count = 0;
    Method retMethod = NULL;
    Method *list = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; ++i) {
        Method m = list[i];
        SEL s = method_getName(m);
        if (sel_isEqual(s, sel)) {
            retMethod = m;
        }
    }
    free(list);
    return retMethod;
}

OS_ALWAYS_INLINE void ZWAddInvocation(NSDictionary *dict, NSString *className, NSString *selName, id block,  ZWInvocationOption options) {
    NSArray *tmp = dict[className][selName];
    if (options & ZWInvocationOptionOnly) {
        dict[className][selName] = block;
    } else {
        if ([tmp isKindOfClass:[NSArray class]]) {
            dict[className][selName] = [tmp arrayByAddingObject:block];
        } else if (!tmp) {
            dict[className][selName] = @[block];
        }
    }
}

void ZWAddAop(id obj, SEL sel, ZWInvocationOption options, id block) {
    if (options == ZWInvocationOptionOnly || !obj || !sel || !block) return;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _ZWOriginIMP = [NSMutableDictionary dictionary];
        _ZWBeforeIMP = [NSMutableDictionary dictionary];
        _ZWAfterIMP = [NSMutableDictionary dictionary];
        _ZWLock = [[NSLock alloc] init];
    });
    
    Class class = object_getClass(obj);
    Method method = NULL;
    NSString *className = NSStringFromClass(class);
    ZWGetMethod(class, sel, &method);//class_getInstanceMethod(class, sel)会获取父类的方法
    NSString *selName = class_isMetaClass(class) ? ZWGetMetaSelName(sel) : NSStringFromSelector(sel);
    
    [_ZWLock lock];
    if (!_ZWOriginIMP[className]) {
        _ZWOriginIMP[className] = [NSMutableDictionary dictionary];
        _ZWBeforeIMP[className] = [NSMutableDictionary dictionary];
        _ZWAfterIMP[className] = [NSMutableDictionary dictionary];
    }

    if (options & ZWInvocationOptionReplace) {
        _ZWOriginIMP[className][selName] = block;
    } else {
        IMP originImp = method_getImplementation(method);
        if (originImp != ZWGlobalOCSwizzle) {
            _ZWOriginIMP[className][selName] = [NSValue valueWithPointer:originImp];
        }
    }
    
    if (options & ZWInvocationOptionBefore) {
        ZWAddInvocation(_ZWBeforeIMP, className, selName, block, options);
    }
    if (options & ZWInvocationOptionAfter) {
        ZWAddInvocation(_ZWAfterIMP, className, selName, block, options);
    }
    method_setImplementation(method, ZWGlobalOCSwizzle);
    [_ZWLock unlock];
}
#else
void ZWAddAop(id obj, SEL sel, ZWInvocationOption options, id block) {
}
#endif

这里只写了arm64下的实现,arm32机器已经比较少,x86_64汇编比较麻烦,就不实现了。

入口函数ZWAddAop,前俩参数就不多说了,第三个参数是一个block,这个block在定义的时候,一定要注意参数类型要对应,Method的前两个参数签名为固定的“@:”,而block第一个参数签名是固定的“@?”,就是block本身,所以这里本来是需要一个占位的参数SEL和原始方法对应,后续的参数一一对应即可,如果不需要使用某些参数可以省略声明。但需要注意的是这里的占位参数被我改变成了NSArray,其会将当前调用的对象和方法,以及调用位置封装成数组传递,方便使用者。(简易实现,所以直接用了公共容器)。另外需要说明的是这里我并没有校验block和原始方法签名是否对应,需要的可以自行添加。

- (int )aMethod3:(NSString *)str;
对应
^(NSArray *info, NSString *str) {};
如果不使用str, ^(NSArray *info){};不使用info,也可以不声明

第四个参数ZWInvocationOption,表示需要在哪些位置插入该block调用。其中ZWInvocationOptionOnly不能单独使用,其他情况都可以单独使用或者复合使用。Only和Before,After复合使用时表示当前添加的block是当前位置唯一的调用,其他调用会被替换掉,否则在同一个方法的同一个位置可以添加多个block调用。Only和Replace复合没有意义,因为我觉得没有必要替换多次,因为从功能上讲Before就可以完成这项需求,如果有需求的话可以自行修改。

ZWAddAop函数实现比较容易看懂,需要说明一下的是,这里我操作NSMutableDictionary时我加了锁,如果有大量方法需要替换时,可以采用串行调用而不是加锁的方式,这样速度会提高。如果是元类方法会在存储的时候添加上__META_头和实例方法区分开。另外class_getInstanceMethod会获取父类的实现,所以这里class_copyMethodList来搜索,因为存在内存拷贝,所以效率不太好。

注意:优化点,将所有NSDictionary的selector key由NSString变成NSNumber,该地址是一个常量,而class对象是可以直接作为key的。AOP切面调用极为频繁,这就会带来极大的性能提升,优化后甚至可以提高一个数量级。

以上是添加AOP调用的过程,接下来说明一下调用过程。

所有添加AOP的方法,IMP会修改为ZWGlobalOCSwizzle方法,其声明没有参数没有返回值,也不涉及任何OC和C的代码,所以编译器会给一个空的实现,不会添加多余代码,可以方便的添加汇编代码(这种叫C桩(stub)函数)。其逻辑还是很容易看懂的。

我大概说一下,这里我抽离了参数入栈的的汇编到ZWStoreParams,这个函数会被多个地方调用,所以不能直接使用sp,我这里使用了x11,调用的时候需要设置x11的值(复杂的情况下使用寄存器,需要先暂存其值到栈上,方便之后恢复供后续使用,只不过比较麻烦,会涉及到初始栈空间的分配,这里其他地方不会使用x11存数据就简单处理了),调用ZWStoreParams,将参数存在栈上,同时将栈指针放入x0,传递给ZWBeforeInvocation使用。

接下来调用ZWBeforeInvocation,其接收了栈指针sp,上面有所有参数,其内部调用ZWAopInvocation。

在ZWAopInvocation中获枚举当前方法当前位置所有添加的AOP block。下面代码会将一些信息封装到数组中,后面写x1也就是block调用的第二个参数(info)。

__autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(option)];

for循环所有调用,并依次调用ZWGetAopImp获取所有block入口地址,ZWGetAopImp在字典中_ZWBeforeIMP或者_ZWAfterIMP获取存入的Block,并计算出入口地址返回。

使用cbz指令, 如果x0==0就跳转标签LBB_20181107,就是末尾。否则将入口地址写入x17,接下来加载sp指针到x11,加载arr到x12,调用ZWLoadParams加载参数到寄存器,将arr写入x1,最后跳转到x17执行block方法。

note:标签LZW_20181107似乎有一些要求,不能随便乱写,似乎要L打头(L表示局部标签),后面就随便了,确保唯一即可。

回到ZWGlobalOCSwizzle函数,调用完ZWBeforeInvocation后,调用ZWInvocation,也就是原方法或是其替换方法。该方法也接收sp参数,然后我们声明obj,sel,及其指针obj_p,sel_p,接下来将sp和obj,sel的地址加载到寄存器,然后将ps,ps+8,也就是栈上的第一第二个参数写入obj,sel中。这样就把汇编的数据转移到了C语言的变量里面,这也是内联汇编和OC/C要数据交互方法。

接下来将sp,sp+8写入x0,x1,然后调用ZWGetOriginImp,其会尝试获取_ZWOriginIMP字典中的原始IMP并返回,汇编cbnz x0, LZW_20181105如果原始IMP不为NULL,则跳转标签LZW_20181105。

再看标签LZW_20181105,其实现也比较简单,将原始IMP写入x17,同时加载sp到x11,调用ZWLoadParams恢复参数到寄存器上,然后跳转到x17执行原始IMP后就结束了(变量声明都是autoreleasing的,所以不会插入release也就不会破坏x0,d0)。

如果原始IMP为NULL,则表明其被替换过。则将obj,sel,调用位置ZWInvocationOptionReplace封装成数组arr待用。加载sp,sp+8到x0,x1,然后调用ZWGetCurrentImp,获取替换后的实现。其是一个block,计算出IMP并返回。cbz x0, LZW_20181106,如果返回值为NULL,则直接跳转到标签LZW_20181106结束调用,否则继续执行,将IMP写入x17,加载arr,sp到x12,x11,调用ZWLoadParams,恢复所有的参数,这里和调用原始IMP不一样的时候将第二个参数x1替换为数组arr。然后blr x17执行IMP,之后跳转到标签LZW_20181106,这里不能直接使用汇编的"ret"返回。因为该句之后会调用___stack_chk_guard来检查栈有没有被破坏,不过幸运的是该调用没有破坏x0,d0寄存器。

最后再次回到ZWGlobalOCSwizzle函数,调用完ZWInvocation后,将可能存储返回值的寄存器x0,d0写入sp的+0xa0,+0xa8的位置,然后加载sp为参数调用ZWAfterInvocation,其逻辑和ZWBeforeInvocation基本一致就不再多说。

然后从sp的+0xa0,+0xa8的位置恢复数据到x0,d0,然后恢复sp,x29,x30寄存器,最后返回。

怎么使用呢?这里给一些例子

- (void)viewDidLoad {
    [super viewDidLoad];

    ZWAddAop(self, @selector(aMethod1:), ZWInvocationOptionBefore | ZWInvocationOptionOnly, ^(NSArray *info){
        NSLog(@"before1 : ");
    });
    ZWAddAop(self, @selector(aMethod1:), ZWInvocationOptionBefore | ZWInvocationOptionReplace  | ZWInvocationOptionAfter , ^(NSArray *info, int a){
        NSLog(@"before1 | replace1 | after1 : %d", a);
    });

    ZWAddAop(self, @selector(aMethod2:), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str){
        NSLog(@"after2: %@", str);
    });

    ZWAddAop(self, @selector(aMethod2:), ZWInvocationOptionReplace | ZWInvocationOptionOnly, ^(NSArray *info, NSString *str){
        NSLog(@"replace2 | after2: %@ \n%@",info, str);
    });

    ZWAddAop(self, @selector(aMethod3::), ZWInvocationOptionReplace, ^int (NSArray *info, NSString *str){
        NSLog(@"replace3: %@", str);
        return 11034;
    });

    ZWAddAop(self, @selector(aMethod3::), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str){
        NSLog(@"after31: %@ \n %@", info, str);
    });

    ZWAddAop(self, @selector(aMethod3::), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str){
        NSLog(@"after32: %@", str);
    });
    
    ZWAddAop([self class], @selector(aMethod4:), ZWInvocationOptionReplace, ^(id info , int a){
        NSLog(@"META replace4:");
    });

    [self aMethod1:8848];
    [self aMethod2:@"test str"];
    int r = [self aMethod3:@"咋不上天呢" :@[@1,@2]];
    NSLog(@"return int: %d",r);
    [ViewController aMethod4:12358];
}

- (NSRange)aMethod1:(int)a {
    NSLog(@"method1: %d",a);
    return (NSRange){0,1};
}
- (void)aMethod2:(NSString *)str {
    NSLog(@"method2: %@", str);
}
- (int )aMethod3:(NSString *)str :(NSArray *)array{
    NSLog(@"method3: %@", str);
    return 11;
}
+ (void)aMethod4:(int )obj {
    NSLog(@"method4: %d", obj);
}

本来只是想写一个demo供大家参考的,但发现很多东西还是要过手了才有说服力,于是前前后后(主要是想简单实现基本功能即可,后来发现很多功能特性还是得添加,于是乎就不断涂涂改改...)花了两天时间写了个较完善些的实现。如果对稳定性要求较高,可以多测试一下,汇编和OC/C混编确实容易出bug,写的时候稍不注意就出现莫名其妙的问题,需要不断的修改和改进,另外我测试的case也不太足。

最后说一下这套方案的优缺点吧。

优点:1、简洁,虽然可能汇编细节不太懂,但大致的实现思路还是很容易看懂。
2、效率较高,大量使用汇编,减少函数调用,当然也有一个龟速代表class_copyMethodList拖慢了效率,当然也仅仅只是指针拷贝,而且大部分类的方法也就几个十几个。主要是没有其他类似的方法,要替换class_copyMethodList就得自己访问Class的MethodList,这比较麻烦,需要大量的代码才能实现,不过class_copyMethodList调用不多。每个hook函数调用过程中AOP的开销才是重点关注的地方,目前主要是内存分配(OC对象创建)。

缺点,改进点:
1、追求简洁,抛弃了一些细节问题。比如传入的block和原始方法的签名没有校验是否匹配,再比如:block的传回的第一个参数使用NSArray来存储obj,sel和调用位置,其实最好可以使用model对象,同时也可以传回更多的信息。
2、整型参数大于6个或者浮点参数大于8个则无法处理。因为多出来的参数在栈上,目前我还没有想出除了拷贝栈以外的简洁解决方案,而拷贝栈比较麻烦(我大概思考了一下,比较麻烦的部分是确定栈上存了多少数据,这个得根据签名和参数传递的规则去计算)。如果需要处理这类函数就需要自己另外处理了。

Github源码地址

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

推荐阅读更多精彩内容