iOS指纹解锁和手势解锁

iOS指纹解锁和手势解锁

[TOC]

前言

一直想写博客来着,一来可以记录一些自己学习和研究的东西,二来也可以将自己写的一些东西分享出去,给他人参考,还可能收到他人的一些建议,从而完善自己的项目和提升自己的技术,这也是一种很好的技术交流方式。但是之前一直不知道怎么去写?怎么去总结?在经过一些观摩和学习后,终于决定先来试试水了😂。下面正式开始我的第一篇博客。

这篇博客是自己基于iOS系统实现的指纹解锁(系统API)和手势解锁(CAShapeLayer)功能。

在之前自学CAAnimation,再加上公司老大说可以预研(之前没有做过)一下各种解锁方式的情况下,想着自己来实现一下现在常用的解锁方式:指纹解锁手势解锁

指纹解锁

基于iOS的指纹解锁其实很简单,因为系统已经提供了API给你,你只需要做一些简单的判断和适时的调用就可以了。

第一步

首先导入头文件#import <LocalAuthentication/LocalAuthentication.h>

判断是否开启了TouchID,如果已经开启,直接校验指纹,如果未开启,则需要先开启TouchID

//判断是否开启了TouchID
[[[NSUserDefaults standardUserDefaults] objectForKey:@"OpenTouchID"] boolValue]

第二步

  • 未开启TouchID,询问是否开启
- (void)p_openTouchID
{
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"是否开启TouchID?" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"YES" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            //开启TouchID
            [[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"OpenTouchID"];
            [[NSNotificationCenter defaultCenter] postNotificationName:@"OpenTouchIDSuccess" object:nil userInfo:nil];
        }]];
        [alertController addAction:[UIAlertAction actionWithTitle:@"NO" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            //不开启TouchID
            [[NSUserDefaults standardUserDefaults] setObject:@(NO) forKey:@"OpenTouchID"];
            [[NSNotificationCenter defaultCenter] postNotificationName:@"OpenTouchIDSuccess" object:nil userInfo:nil];
        }]];
        [self presentViewController:alertController animated:YES completion:nil];
    });
}
  • 已开启TouchID
- (void)p_touchID
{
    dispatch_async(dispatch_get_main_queue(), ^{
        LAContext *context = [[LAContext alloc] init];
        NSError *error = nil;
        //判断是否支持TouchID
        if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
            [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"TouchID Text" reply:^(BOOL success, NSError * _Nullable error) {
                if (success) {//指纹验证成功
                    [[NSNotificationCenter defaultCenter] postNotificationName:@"UnlockLoginSuccess" object:nil];
                }else {//指纹验证失败
                    switch (error.code)
                    {
                        case LAErrorAuthenticationFailed:
                        {
                            NSLog(@"授权失败"); // -1 连续三次指纹识别错误
                            [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil];
                        }
                            break;
                        case LAErrorUserCancel:
                        {
                            NSLog(@"用户取消验证Touch ID"); // -2 在TouchID对话框中点击了取消按钮
                            [self dismissViewControllerAnimated:YES completion:nil];
                        }
                            break;
                        case LAErrorUserFallback:
                        {
                            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                [[NSNotificationCenter defaultCenter] postNotificationName:@"touchIDFailed" object:nil];
                                NSLog(@"用户选择输入密码,切换主线程处理"); // -3 在TouchID对话框中点击了输入密码按钮
                            }];
                            
                        }
                            break;
                        case LAErrorSystemCancel:
                        {
                            NSLog(@"取消授权,如其他应用切入,用户自主"); // -4 TouchID对话框被系统取消,例如按下Home或者电源键
                        }
                            break;
                        case LAErrorPasscodeNotSet:
                            
                        {
                            NSLog(@"设备系统未设置密码"); // -5
                        }
                            break;
                        case LAErrorBiometryNotAvailable:
                        {
                            NSLog(@"设备未设置Touch ID"); // -6
                        }
                            break;
                        case LAErrorBiometryNotEnrolled: // Authentication could not start, because Touch ID has no enrolled fingers
                        {
                            NSLog(@"用户未录入指纹"); // -7
                        }
                            break;
                            
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0
                        case LAErrorBiometryLockout: //Authentication was not successful, because there were too many failed Touch ID attempts and Touch ID is now locked. Passcode is required to unlock Touch ID, e.g. evaluating LAPolicyDeviceOwnerAuthenticationWithBiometrics will ask for passcode as a prerequisite 用户连续多次进行Touch ID验证失败,Touch ID被锁,需要用户输入密码解锁,先Touch ID验证密码
                        {
                            NSLog(@"Touch ID被锁,需要用户输入密码解锁"); // -8 连续五次指纹识别错误,TouchID功能被锁定,下一次需要输入系统密码
                        }
                            break;
                        case LAErrorAppCancel:
                        {
                            NSLog(@"用户不能控制情况下APP被挂起"); // -9
                        }
                            break;
                        case LAErrorInvalidContext:
                        {
                            NSLog(@"LAContext传递给这个调用之前已经失效"); // -10
                        }
                            break;
#else
#endif
                        default:
                        {
                            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                NSLog(@"其他情况,切换主线程处理");
                            }];
                            break;
                        }
                    }
                }
            }];
        }else {
            //不支持
            UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"该设备不支持TouchID" preferredStyle:UIAlertControllerStyleAlert];
            [alertController addAction:[UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleCancel handler:nil]];
            [self presentViewController:alertController animated:YES completion:nil];
        }
    });
}

:代码中的NSNotificationCenter用于不同操作后的界面跳转,重新设置window.rootViewController,可忽略。

到这里指纹解锁就结束了,很简单的一个API调用。


手势解锁

其实在之前还没有接触和刚开始接触iOS开发的时候,觉得手势解锁很难,完全不知道怎么去实现?但是当我在自学CAAnimation的时候,脑海中突然就想到了一个实现手势解锁的方案,下面就开始介绍我的实现方法:

构思

  1. 手势解锁是怎么去验证你滑动的手势是正确的?

    其实手势解锁和输入密码的验证是一样的,在你画UI的时候,你可以给每一个*圆点*一个id,在你设置手势的时候,将滑动到对应*圆点*的id放入一个有序集合中,并保存起来,然后验证登录的时候,用另外一个有序集合记录你当前滑动到的*圆点*id,然后和之前保存在本地的进行对比,就可以达到验证的目的了

  2. 用什么方式去具体实现UI?

    在之前想过几种实现方式,但是都被pass掉了,直到自学CAAnimation的时候,才突然意识到有一个很好的实现方式----CAShapeLayer

其实,当你有了这两个问题的答案的时候,你的手势解锁就已经实现了一大部分,后面的部分就是敲代码了。

实现 (工程代码见文末链接)

先上几张效果图:(由于本人艺术细胞有限,所以为了好看点,界面的UI是参照QQ安全中心的手势解锁)

设置手势

目录结构

手势解锁目录结构
  • GesturesViewController:这个controller用于展示UI,你可以替换成自己controller,
  • GesturesView:用于圆点按钮的初始化和布局,
  • PointView圆点手势按钮。

这里主要介绍一下GesturesView和PointView,主要逻辑也都在这两个类中:

PointView(主要是界面UI,不多介绍,直接上代码)

PointView.h

- (instancetype)initWithFrame:(CGRect)frame
                       withID:(NSString *)ID;

@property (nonatomic, copy, readonly) NSString             *ID;

//选中
@property (nonatomic, assign) BOOL             isSelected;
//解锁失败
@property (nonatomic, assign) BOOL             isError;
//解锁成功
@property (nonatomic, assign) BOOL             isSuccess;
  • -initWithFrame:withID:传入frameID,用于初始化PointView

  • ID:只读,用于外部获取ID

  • isSelected,isError,isSuccess:用于判断PointView的状态以显示不同的UI。

PointView.m

通过懒加载初始化三个CAShapeLayer

#pragma mark - 懒加载
//外层手势按钮
- (CAShapeLayer *)contentLayer
{
    if (!_contentLayer) {
        _contentLayer = [CAShapeLayer layer];
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2.0, 2.0, SELF_WIDTH - 4.0, SELF_HEIGHT - 4.0) cornerRadius:(SELF_WIDTH - 4.0) / 2.0];
        _contentLayer.path = path.CGPath;
        _contentLayer.fillColor = RGBCOLOR(46.0, 47.0, 50.0).CGColor;
        _contentLayer.strokeColor = RGBCOLOR(26.0, 27.0, 29.0).CGColor;
        _contentLayer.strokeStart = 0;
        _contentLayer.strokeEnd = 1;
        _contentLayer.lineWidth = 2;
        _contentLayer.cornerRadius = self.bounds.size.width / 2.0;
    }
    return _contentLayer;
}

//手势按钮边框
- (CAShapeLayer *)borderLayer
{
    if (!_borderLayer) {
        _borderLayer = [CAShapeLayer layer];
        UIBezierPath *borderPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(SELF_WIDTH / 2.0, SELF_HEIGHT / 2.0) radius:SELF_WIDTH / 2.0 startAngle:0 endAngle:2 * M_PI clockwise:NO];
        _borderLayer.strokeColor = RGBCOLOR(105.0, 108.0, 111.0).CGColor;
        _borderLayer.fillColor = [UIColor clearColor].CGColor;
        _borderLayer.strokeEnd = 1;
        _borderLayer.strokeStart = 0;
        _borderLayer.lineWidth = 2;
        _borderLayer.path = borderPath.CGPath;
    }
    return _borderLayer;
}

//选中时,中间样式
- (CAShapeLayer *)centerLayer
{
    if (!_centerLayer) {
        _centerLayer = [CAShapeLayer layer];
        UIBezierPath *centerPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(SELF_WIDTH / 2.0 - (SELF_WIDTH - 4.0) / 4.0, SELF_HEIGHT / 2.0 - (SELF_HEIGHT - 4.0) / 4.0, (SELF_WIDTH - 4.0) / 2.0, (SELF_WIDTH - 4.0) / 2.0) cornerRadius:(SELF_WIDTH - 4.0) / 4.0];
        _centerLayer.path = centerPath.CGPath;
        _centerLayer.lineWidth = 3;
        _centerLayer.strokeColor = [UIColor colorWithWhite:0 alpha:0.7].CGColor;
        _centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor;
    }
    return _centerLayer;
}

设置PointView的UI状态

//根据情况显示三种状态
- (void)setIsSuccess:(BOOL)isSuccess
{
    _isSuccess = isSuccess;
    if (_isSuccess) {
        self.centerLayer.fillColor = RGBCOLOR(43.0, 210.0, 110.0).CGColor;
    }else {
        self.centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor;
    }
}

- (void)setIsSelected:(BOOL)isSelected
{
    _isSelected = isSelected;
    if (_isSelected) {
        self.centerLayer.hidden = NO;
        self.borderLayer.strokeColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor;
    }else {
        self.centerLayer.hidden = YES;
        self.borderLayer.strokeColor = RGBCOLOR(105.0, 108.0, 111.0).CGColor;
    }
}

- (void)setIsError:(BOOL)isError
{
    _isError = isError;
    if (_isError) {
        self.centerLayer.fillColor = RGBCOLOR(222.0, 64.0, 60.0).CGColor;
    }else {
        self.centerLayer.fillColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor;
    }
}

GesturesView(基本所有的逻辑都在这个里面了)

GesturesView.h

//回传选择的id
typedef void (^GestureBlock)(NSArray *selectedID);
//回传手势验证结果
typedef void (^UnlockBlock)(BOOL isSuccess);
//设置手势失败
typedef void (^SettingBlock)(void);

@interface GesturesView : UIView

/**
 设置密码时,返回设置的手势密码
 */
@property (nonatomic, copy) GestureBlock             gestureBlock;
                             
/**
 返回解锁成功还是失败状态
 */
@property (nonatomic, copy) UnlockBlock            unlockBlock;
                             
/**
 判断手势密码时候设置成功(手势密码不得少于四个点)
 */
@property (nonatomic, copy) SettingBlock           settingBlock;

/**
 判断是设置手势还是手势解锁
 */
@property (nonatomic, assign) BOOL         settingGesture;

这里我申明了三个block:

  • GestureBlock:将选择的ID有序集合回传给控制器,
  • UnlockBlock:回传手势验证结果,
  • SettingBlcok:设置手势失败

属性:

  • gestureBlock,unlockBlock,settingBlock:分别是对应block的实例,
  • settingGesture:用于判断是设置手势还是手势解锁

GesturesView.h (最主要的逻辑实现部分)

私有属性部分:

//可变数组,用于存放初始化的点击按钮
@property (nonatomic, strong) NSMutableArray             *pointViews;
//记录手势滑动的起始点
@property (nonatomic, assign) CGPoint                    startPoint;
//记录手势滑动的结束点
@property (nonatomic, assign) CGPoint                    endPoint;
//存储选中的按钮ID
@property (nonatomic, strong) NSMutableArray             *selectedView;
//手势滑动经过的点的连线
@property (nonatomic, strong) CAShapeLayer               *lineLayer;
//手势滑动的path
@property (nonatomic, strong) UIBezierPath               *linePath;
//用于存储选中的按钮
@property (nonatomic, strong) NSMutableArray             *selectedViewCenter;
//判断时候滑动是否结束
@property (nonatomic, assign) BOOL                       touchEnd;

代码实现部分:

初始化startPointendPoint以及9PointView按钮,startPointendPoint默认为0,并设置PointViewID

//初始化开始点位和结束点位
    self.startPoint = CGPointZero;
    self.endPoint = CGPointZero;
        //布局手势按钮(采用自定义的全能初始化方法)
    for (int i = 0; i<9 ; i++) {
        PointView *pointView = [[PointView alloc] initWithFrame:CGRectMake((i % 3) * (SELF_WIDTH / 2.0 - 31.0) + 1, (i / 3) * (SELF_HEIGHT / 2.0 - 31.0) + 1, 60, 60)
                                                         withID:[NSString stringWithFormat:@"gestures %d",i + 1]];
        [self addSubview:pointView];
        [self.pointViews addObject:pointView];
    }

滑动事件:

  • 开始滑动:

如果self.touchEndYES则直接return,为NO开始以下处理:

  1. 首先获取到滑动的点,遍历所有的PointView,判断该点是否在某个手势按钮范围,在范围内记录状态,否则不做处理;
  2. 判断self.startPoint是否为CGPointZero,如果为YES,则将该手势按钮center赋值给self.startPoint;
  3. 判断该手势按钮的center是否包含在self.selectedViewCenter中,如果为YES,忽略此次记录,为NO则记录该中心点,用于画线,同样记录该手势按钮ID,用于记录保存手势密码;
  4. 如果self.startPoint不为CGPointZero,则记录当前滑动到的点为self.endPoint,并划线。
//touch事件
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.touchEnd) {
        return;
    }
    
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self];
    //判断手势滑动是否在手势按钮范围
    for (PointView *pointView in self.pointViews) {
        //滑动到手势按钮范围,记录状态
        if (CGRectContainsPoint(pointView.frame, point)) {
            //如果开始按钮为zero,记录开始按钮,否则不需要记录开始按钮
            if (CGPointEqualToPoint(self.startPoint, CGPointZero)) {
                self.startPoint = pointView.center;
            }
            //判断该手势按钮的中心点是否记录,未记录则记录
            if (![self.selectedViewCenter containsObject:[NSValue valueWithCGPoint:pointView.center]]) {
                [self.selectedViewCenter addObject:[NSValue valueWithCGPoint:pointView.center]];
            }
            //判断该手势按钮是否已经选中,未选中就选中
            if (![self.selectedView containsObject:pointView.ID]) {
                [self.selectedView addObject:pointView.ID];
                pointView.isSelected = YES;
            }
        }
    }
    //如果开始点位不为zero则记录结束点位,否则跳过不记录
    if (!CGPointEqualToPoint(self.startPoint, CGPointZero)) {
        self.endPoint = point;
        [self p_drawLines];
    }
}
  • 画线:

如果self.touchEndYES则直接return,为NO开始画线:

  1. 首先移除self.lineLayer,self.linePath,否则你会发现随着你的滑动,会出现很多条线。
  2. 设置self.linePath的起始点,并遍历self.selectedViewCenter,为self.linePath添加节点,最后将self.endPoint添加上去(为结束滑动的时候,self.endPoint为当前滑动位置的点);
  3. 设置self.lineLayer的相应属性,并添加到self.layer
//画线
- (void)p_drawLines
{
    //结束手势滑动,不画线
    if (self.touchEnd) {
        return;
    }
    //移除path的点和lineLayer
    [self.lineLayer removeFromSuperlayer];
    [self.linePath removeAllPoints];
    //画线
    [self.linePath moveToPoint:self.startPoint];
    for (NSValue *pointValue in self.selectedViewCenter) {
        [self.linePath addLineToPoint:[pointValue CGPointValue]];
    }
    [self.linePath addLineToPoint:self.endPoint];
    
    self.lineLayer.path = self.linePath.CGPath;
    self.lineLayer.lineWidth = 4.0;
    self.lineLayer.strokeColor = RGBCOLOR(30.0, 180.0, 244.0).CGColor;
    self.lineLayer.fillColor = [UIColor clearColor].CGColor;
    
    [self.layer addSublayer:self.lineLayer];
    
    self.layer.masksToBounds = YES;
}
  • 结束滑动:
  1. self.endPoint设置为self.selectedViewCenter.lastObject,如果self.endPoint还是为CGPointZero,则说明未滑动到手势按钮范围,不做任何处理,否则继续以下逻辑处理;
  2. 再次调用-(void)p_drawLines画线;
  3. 判断是设置手势密码还是手势解锁
    1. 设置手势密码
      1. 如果选中的手势按钮数量少于4,设置self.touchEnd = NO使其可以重新设置,return结束此次设置;
      2. 如果设置的手势按钮符合要求则调用self.gestureBlock(self.selectedView)将手势密码回传给控制器
    2. 手势解锁
      1. 获取本地存储的手势密码我这里用的是NSUserDefaults,其实这是不安全的,建议使用Keychain,我也会在后续的更新中使用Keychain 已使用keychain保存密码,具体使用见Demo
      2. 如果self.selectedView和本地手势密码一样,则解锁成功,并设置pointView.isSuccess = YES改变手势按钮样式等,并调用self.unlockBlock(YES),告知控制器结果;
      3. 否则解锁失败,pointView.isError = YES改变手势按钮样式等,并调用self.unlockBlock(NO),告知控制器结果;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //结束手势滑动的时候,将结束按钮设置为最后一个手势按钮的中心点,并画线
    self.endPoint = [self.selectedViewCenter.lastObject CGPointValue];
    //如果endPoint还是为zero说明未滑动到有效位置,不做处理
    if (CGPointEqualToPoint(self.endPoint, CGPointZero)) {
        return;
    }
    [self p_drawLines];
    //改变手势滑动结束的状态,为yes则无法在滑动手势划线
    self.touchEnd = YES;
    //设置手势时,返回设置的时候密码,否则继续下面的操作进行手势解锁
    if (_gestureBlock && _settingGesture) {
        //手势密码不得小于4个点
        if (self.selectedView.count < 4) {
            self.touchEnd = NO;
            for (PointView *pointView in self.pointViews) {
                pointView.isSelected = NO;
            }
            [self.lineLayer removeFromSuperlayer];
            [self.selectedView removeAllObjects];
            self.startPoint = CGPointZero;
            self.endPoint = CGPointZero;
            [self.selectedViewCenter removeAllObjects];
            if (_settingBlock) {
                self.settingBlock();
            }
            return;
        }
        _gestureBlock(self.selectedView);
        return;
    }
    
    //手势解锁
    NSArray *selectedID = [[NSUserDefaults standardUserDefaults] objectForKey:@"GestureUnlock"];
    //解锁成功
    if ([self.selectedView isEqualToArray:selectedID]) {
        //解锁成功,遍历pointview,设置为成功状态
        for (PointView *pointView in self.pointViews) {
            pointView.isSuccess = YES;
        }
        self.lineLayer.strokeColor = RGBCOLOR(43.0, 210.0, 110.0).CGColor;
        if (_unlockBlock) {
            self.unlockBlock(YES);
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:@"UnlockLoginSuccess" object:nil];
        });
    }else {//解锁失败
        //解锁失败,遍历pointView,设置为失败状态
        for (PointView *pointView in self.pointViews) {
            pointView.isError = YES;
        }
        self.lineLayer.strokeColor = RGBCOLOR(222.0, 64.0, 60.0).CGColor;
        if (_unlockBlock) {
            self.unlockBlock(NO);
        }
    }
}

到这里就实现了手势解锁的所有逻辑,在实现之前还在担心有什么问题,结果实现出来之后感觉其实很简单。


最后

希望这篇文章能够帮助到一些人。对于代码博客的一些规范希望大家谅解一下了,后面也会慢慢去优化的。最后的最后附上Demo的链接 Demo-GitHub

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

推荐阅读更多精彩内容