iOS - runtime的一些运用场景

首先,归纳下Runtime的几个使用场景。

  1. 做用户埋点统计
  2. 处理异常崩溃(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的处理)
  3. 按钮最小点击区设置
  4. 按钮重复点击设置
  5. 手势的重复点击处理
  6. UIButton点击事件带多参数
  7. MJRefresh封装
  8. 服务端控制页面跳转
  9. 字典转模型

一 用户埋点

在做app运营的时候, 我们经常会需要接入一些第三方做统计, 例如友盟统计,google统计等。 例如外面需要统计某个页面用户停留的时长, 统计某个页面的展示次数。 通常我们的做法是 : 需要统计A页面停留时长的时候,我们再A页面出现(appear)的时候记录一个时间戳,页面消失(dispear)的时候用当前时间戳与之前的时间戳求出时间间隔,然后上报到分析平台。 如果统计页面展示次数, 就在每次页面出现时调用统计方法。 这样做的坏处是 代码侵入性太强,维护性与易读性都不太好。 假设以后要改需求, 就要进入到代码所在处进行修改。 又或者别人接手你的代码, 根本不知道已经做了哪些埋点, 需求改来改去,时间久了, 项目中全都是垃圾代码。
此时,为了优化统计, 我们使用 Hook (钩子)的思想, 例如Runtime的 Method sweezing(方法交换)去拦截系统方法来实现共计。
首先,我们写一个集成NSObject的工具类,实现方法交换

#import "HookTool.h"
#import <objc/runtime.h>

@implementation HookTool
 
+(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector
{
    Class class = cls;
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method  swizzingMethod = class_getInstanceMethod(class, swizzingSelector);
    
    BOOL addMethod = class_addMethod(class,
                                     originalSelector,
                                     method_getImplementation(swizzingMethod),
                                     method_getTypeEncoding(swizzingMethod));
    
    if (addMethod) {
        class_replaceMethod(class,
                            swizzingSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    }else{
        
        method_exchangeImplementations(originalMethod, swizzingMethod);
    }
}
@end

接着,我们写一个UIViewController的分类, 在Load方法中把系统方法替换掉:

#import "UIViewController+actionAnalysis.h"
#import "HookTool.h"
#import "NSDate+Convenience.h"

@implementation UIViewController (actionAnalysis)
 
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalAppearSelector = @selector(viewWillAppear:);
        SEL swizzingAppearSelector = @selector(user_viewWillAppear:);
        [HookTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
        
        SEL originalDisappearSelector = @selector(viewWillDisappear:);
        SEL swizzingDisappearSelector = @selector(user_viewWillDisappear:);
        [HookTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector];
    });
}
 
 
 
-(void)user_viewWillAppear:(BOOL)animated
{
    //页面出现
 
    [self user_viewWillAppear:animated];
}
 
 
-(void)user_viewWillDisappear:(BOOL)animated
{
    //页面消失
    
    [self user_viewWillDisappear:animated];
}
 
@end

此时还有个问题, 首先你可能并不想对每个页面进行统计, 但是又不想每次添加一个统计就加一个if判断。 这个时候我们就在Xcode中加入一张plist表, plist表里面记录我们所需统计的信息


image.png

此时,我们只需要在hook的方法中去实现统计逻辑

-(void)user_viewWillAppear:(BOOL)animated
{
    NSDictionary * pageenter = [[HookTool getConfig] objectForKey:@"page_enter_anysis"];
    if ([pageenter.allKeys containsObject:NSStringFromClass([self class])]) {
        NSLog(@"%@ 页面展示", NSStringFromClass([self class]));
    }
    
    NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
    if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
        //此处用Userdefault存储只是因为方便书写, 实际用可以用一个单例去存储中间值
        [[NSUserDefaults standardUserDefaults] setDouble:[[NSDate date] timeIntervalSince1970] * 1000 forKey:@"appeartime"];
    }
 
    [self user_viewWillAppear:animated];
}
 
 
-(void)user_viewWillDisappear:(BOOL)animated
{
    //页面停留时间统计
    NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
    if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
        double leaveTime = NSDate.currenMillisecondTimestamp - [[NSUserDefaults standardUserDefaults] doubleForKey:@"appeartime"];
        NSLog(@"%@ 页面的停留时间为 %lf ms", [self class], leaveTime);
    }
    
    [self user_viewWillDisappear:animated];
}

这样的话,以后做页面时长或者页面展示的统计,就只需要维护这个plist表就行了,不需要具体改动代码。
点击事件统计:
与VC的统计类似, 也是利用catagory + hook的思想来实现, 我们可以添加一个UIControl的分类。但是具体需要hook UIControl的哪个方法那 ? 点击进入UIControl的api, 我们很容易发现需要Hook的方法

- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
         接着我们在UIControl的分类中实现方法的交互

@implementation UIControl (actionAnalysis)
 
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzingSelector = @selector(user_sendAction:to:forEvent:);
        [HookTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
    });
}
 
 
-(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld", NSStringFromSelector(action), target, (long)event);
    [self user_sendAction:action to:target forEvent:event];
}

同样的, 我们只需要在plist中添加click的统计所需的参数就可以了


image.png

利用Runtime做用户埋点的就说这么多, 文章只提供思路, 具体plist的结构,或者代码细节根据情况自己做实现就行了。另外, 由于需求变动的原因,造成代码与配置表不匹配(例如可能会出现某个method名字被改变 )从而造成埋点统计失败, 建议写一个单元测试对Plist进行测试,思路: 在单元测试中我们首先读取plist配置文件,遍历所有的页面。在一个页面内遍历所有的ControlEventIDs,对每个响应函数名进行respondsToSelector:判断。 这样可以有效减少埋点失效问题。

二 处理异常崩溃(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的处理)

在开发过程中, 有时候会出现set object for key的时候 object为Nil或者Key为Nil, 又或者初始化array, dic的时候由于数据个数与指定的长度不一致造成崩溃。 此时利用runtime对异常情况进行捕捉,提前return或者抛弃多余的长度。

Dic:

#import "NSDictionary+Safe.h"
#import <objc/runtime.h>
 
@implementation NSDictionary (Safe)
 
+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(dictionaryWithObjects:forKeys:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_dictionaryWithObjects:forKeys:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
+ (instancetype)na_dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt {
    id nObjects[cnt];
    id nKeys[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i] && keys[i]) {
            nObjects[j] = objects[i];
            nKeys[j] = keys[i];
            j++;
        }
    }
    
    return [self na_dictionaryWithObjects:nObjects forKeys:nKeys count:j];
}
 
@end
 
@implementation NSMutableDictionary (Safe)
 
+ (void)load {
    Class dictCls = NSClassFromString(@"__NSDictionaryM");
    Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
    Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (!anObject || !aKey)
        return;
    [self na_setObject:anObject forKey:aKey];
}
 
@end

array:

#import "NSArray+Safe.h"
#import <objc/runtime.h>
 
@implementation NSArray (Safe)
 
+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
    id nObjects[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i]) {
            nObjects[j] = objects[i];
            j++;
        }
    }
    
    return [self na_arrayWithObjects:nObjects count:j];
}
@end
 
@implementation NSMutableArray (Safe)
 
+ (void)load {
    Class arrayCls = NSClassFromString(@"__NSArrayM");
    
    Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
    Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
    method_exchangeImplementations(originalMethod1, swizzledMethod1);
    
    Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
    Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
    method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
 
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_insertObject:anObject atIndex:index];
}
 
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_setObject:anObject atIndex:index];
}
 
@end

三 按钮最小点击区设置

按钮太不好点中了,点击好几次才点击到”, 测试经常会有这样的抱怨, 但是此时按钮图片本身设计就很小。 此时,例如Runtime进行点击区放大, 是个挺好的解决版本

static const void *topNameKey = @"topNameKey";
static const void *rightNameKey = @"rightNameKey";
static const void *bottomNameKey = @"bottomNameKey";
static const void *leftNameKey = @"leftNameKey";
 
 
- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left{
    
    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
 
- (CGRect)enlargedRect
{
    NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge) {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}
 
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds)) {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}

四 按钮的重复点击

这个就不多说了,详细大部分程序员都遇到过, 直接上代码

+ (void)load{
    Method originalMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
    Method swizzledMethod = class_getInstanceMethod([self class], @selector(User_SendAction:to:forEvent:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
 
#pragma mark -- 时间间隔 --
static const void *ButtonDurationTime = @"ButtonDurationTime";
- (NSTimeInterval)durationTime{
    NSNumber *number = objc_getAssociatedObject(self, &ButtonDurationTime);
    return number.doubleValue;
}
- (void)setDurationTime:(NSTimeInterval)durationTime{
    NSNumber *number = [NSNumber numberWithDouble:durationTime];
    objc_setAssociatedObject(self, &ButtonDurationTime, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
   
}
 
- (void)User_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
  
    self.userInteractionEnabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.userInteractionEnabled = YES;
    });
    
    [self User_SendAction:action to:target forEvent:event];
}

五 手势的重复点击处理

手势重复点击有个误区: 不能通过拦截 addTarget:(id)target action:(SEL)action 这个方法来实现,因为这个方法是是添加方法,即使我们交换了,在执行的时候并没有什么变化的。正确的做法是添加一个timeInterval,然后在代理里面根据timeInterval设置UITapGestureRecognizer的enable属性

#import "UITapGestureRecognizer+LOOExtension.h"
#import <objc/runtime.h>
 
@interface UITapGestureRecognizer ()
///时间间隔
@property (nonatomic,assign) NSTimeInterval duration;
 
@end
 
static const void *UITapGestureRecognizerduration = @"GestureRecognizerduration";
 
@implementation UITapGestureRecognizer (LOOExtension)
 
#pragma mark - Getter Setter
 
- (NSTimeInterval)duration{
    NSNumber *number = objc_getAssociatedObject(self, &UITapGestureRecognizerduration);
    return number.doubleValue;
}
 
- (void)setDuration:(NSTimeInterval)duration{
    NSNumber *number = [NSNumber numberWithDouble:duration];
    objc_setAssociatedObject(self, &UITapGestureRecognizerduration, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
 
 
 
/**
 添加点击事件
 
 @param target taeget
 @param action action
 @param duration 时间间隔
 */
- (instancetype)initWithTarget:(id)target action:(SEL)action withDuration:(NSTimeInterval)duration{
    
    self = [super init];
    if (self) {
        self.duration = duration;
        self.delegate = self;
        [self addTarget:target action:action];
    }
    return self;
    
}
 
 
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    self.enabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.enabled = YES;
    });
    
    return YES;
}
 
@end

六 UIButton点击带多参数

UIButton *btn = // create the button  
objc_setAssociatedObject(btn, "firstObject", someObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);   //实际上就是KVC  
objc_setAssociatedObject(btn, "secondObject", otherObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
  
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];  
  
- (void)click:(UIButton *)sender  
{  
    id first = objc_getAssociatedObject(btn, "firstObject");        //取参  
    id second = objc_setAssociatedObject(btn, "secondObject");  
    // etc.  
}  

这么使用runtime感觉有点鸡肋,至少在自己的iOS生涯中,没有必须需要这么做的时候。 其实写个子类,添加个Parameter属性岂不是更简单。

七 MJRefresh的封装

大部分程序员应该都用过MJRefresh这个工具,大部分用法都每次出现tabview初始化后, 都初始化出来一个 mj_header, mj_footer, 并且设置 header与footer后, 把mj_header与mj_footer复制给tableview.mj_header, tableview.mj_footer. 每次去重复创建Header, Footer, 这个是不能容忍的。 我们知道tableview和collectionView都是继承自scrollView,那么我们可以在 scrollView的分类里面添加一些方法,那么我们在以后使用的时候,就不需要一遍一遍的重复写无用代码了,只需要调用scrollView分类方法就可以了。

#import "UIScrollView+JHRefresh.h"
#import <MJRefresh.h>
@implementation UIScrollView (JHRefresh)
/**
 添加刷新事件
 
 @param headerBlock 头部刷新
 @param footerBlock 底部刷新
 */
- (void)setRefreshWithHeaderBlock:(void(^)(void))headerBlock
                      footerBlock:(void(^)(void))footerBlock{
    if (headerBlock) {
        
        MJRefreshNormalHeader *header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            if (headerBlock) {
                headerBlock();
            }
        }];
        header.stateLabel.font = [UIFont systemFontOfSize:13];
        header.lastUpdatedTimeLabel.font = [UIFont systemFontOfSize:13];
       
        self.mj_header = header;
    }
    
    if (footerBlock) {
        MJRefreshBackNormalFooter *footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
            footerBlock();
        }];
        footer.stateLabel.font = [UIFont systemFontOfSize:13];
        [footer setTitle:@"暂无更多数据" forState:MJRefreshStateNoMoreData];
        [footer setTitle:@"" forState:MJRefreshStateIdle];
        self.mj_footer.ignoredScrollViewContentInsetBottom = 44;
        self.mj_footer = footer;
    }
}
 
 
 
/**
 开启头部刷新
 */
- (void)headerBeginRefreshing{
    [self.mj_header beginRefreshing];
}
 
 
/**
 没有更多数据
 */
- (void)footerNoMoreData{
    [self.mj_footer setState:MJRefreshStateNoMoreData];
}
 
/**
 结束刷新
 */
- (void)endRefresh{
    
    if (self.mj_header) {
        [self.mj_header endRefreshing];
    }
    if (self.mj_footer) {
        [self.mj_footer endRefreshing];
    }
}

八 服务端控制页面跳转
项目开发中,我们可能会有这样的需求: 根据服务端推送过来的数据规则,跳转到对应的控制器。 之前我们的做法是这样的: 前端与服务端定义好规则, 例如服务端推送 Push/Live/WatchLive/12, Push: push方式跳转 , Live指的直播模块, WatchLive指的看直播的功能, 12指的房间号, 也就是跳转到12号主播间。 但是这么做坏处就是,必须提前与服务端约定好协议, 每次运营如果加一个新的跳转, 移动端需要改代码,重新上线。扩展性很低。

其实利用Runtime完全可以写成通用的方式来实现跳转。例如外面与服务端定义好推送规则后,服务端推送过来的数据如下:

// 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
NSDictionary *userInfo = @{
                           @"class": @"LiveViewController",     //VC的名字
                           @"property": @{
                                        @"ID": @"123",          //参数名字为 ID , value为 123
                                        @"type": @"12"          //type为附加信息, 根据实际情况定义
                                   }
                           };

接着我们利用Runtime进行跳转

// 类名
    NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
    const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
    
    // 从一个字串返回一个类
    Class newClass = objc_getClass(className);
    if (!newClass)
    {
        return;   //推送的class不存在
    }
    // 创建对象
    id instance = [[newClass alloc] init];
    
    // 对该对象赋值属性
    NSDictionary * propertys = params[@"property"];
    [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // 检测这个对象是否存在该属性
        if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
            // 利用kvc赋值
            [instance setValue:obj forKey:key];
        }
    }];
    
    // 获取导航控制器
    UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
    UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
    // 跳转到对应的控制器
    [pushClassStance pushViewController:instance animated:YES];
检测属性是否存在

- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
    unsigned int outCount, i;
    
    // 获取对象里的属性列表
    objc_property_t * properties = class_copyPropertyList([instance
                                                           class], &outCount);
    
    for (i = 0; i < outCount; i++) {
        objc_property_t property =properties[i];
        //  属性名转成字符串
        NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        // 判断该属性是否存在
        if ([propertyName isEqualToString:verifyPropertyName]) {
            free(properties);
            return YES;
        }
    }
    free(properties);
    
    return NO;
}

九 字典转模型
获取属性的列表的方法是字典转模型的比较核心的方法。常见的字典转模型的三方有 MJExtension, YYModel, JsonModel等, 翻看其源码, 都会发现 Ivar *class_copyIvarList(Class cls, unsigned int *outCount)的使用

MJExtension核心代码摘录


20180503143111683.png

YYModel核心代码摘录


20180503143407891.png

JsonModel json字典转model 摘录


20180503143454842.png

基本上主流的json 转model 都少不了,使用运行时动态获取属性的属性名的方法,来进行字典转模型替换,字典转模型效率最高的(耗时最短的)的是KVC,其他的字典转模型是在KVC 的key 和Value 做处理,动态的获取json 中的key 和value ,当然转换的过程中,第三方框架需要做一些判空啊,镶嵌的逻辑处理, 再进行KVC 转模型.这句代码 [xx setValue:value forKey:key];无论JsonModle,YYKIt,MJextension 都少不了[xx setValue:value forKey:key];这句代码的,不信可以去搜,这是字典转模型的核心方法,

参考:
https://blog.csdn.net/SandyLoo/article/details/80174890

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