iOS 玩转微信——通讯录

概述

  • 2019年初--至今,笔者为求生计,被迫转学Vue开发,老兵不死,只会逐渐凋零,以致于渐渐冷落了iOS开发,毕竟有舍便有得,不逼自己一把,也不知道自己有多优秀。

  • 由于大家对 WeChat 中运用的MVVM + RAC + ViewModel-Based Navigation的模式比较感兴趣,但是此前此项目主要是用于团队内部交流使用,主要介绍了其中使用技巧和实用技术,以及一些细节处理,实用为主,功能为辅。

  • 尽管实现了微信的整体架构,以及朋友圈等功能,但是其中还是充斥着不少测试代码,这让整体项目看起来像个Demo,并且不够优美,随着微信 7.0.0+的出现,整体UI也发生了翻天覆地的变化,所以,只好痛定思痛,重蹈覆辙,重拾iOS,这里先以高仿微信通讯录为例,宣告笔者强势复出,后期争取尽自己最大努力,98%还原真实微信开发,不断剖析其中的技术实现和细节处理。

  • 笔者希望通过学习和实践这个项目,也能够打开学习ReactiveCocoa + MVVM的大门。当然同时也是抛砖引玉,摆渡众生、取长补短,希望能够提供一点思路,少走一些弯路,填补一些细坑,在帮助他人的过程中,收获分享技术的乐趣。

  • 源码地址:WeChat

预览

索引 侧滑
ios_contacts_page_0.png
ios_contacts_page_1.png

GIF

ios_contacts_page.gif

功能

通讯录模块,尽管UI看起来极其简单,但是涵盖不少知识点,也是通讯录模块的功能所在,本篇文章将详述以下知识点以及实现的细节:

  • 汉字转拼音数据排序按字母分组
  • 底部上拉显示白底
  • A-Z 索引Bar索引联动悬停HeaderView渐变
  • Cell 侧滑备注修改侧滑样式

分析

数据处理

首先,主要是将联系人姓名转成拼音,然后取联系人拼音首字母;其次,利用字典(NSDictionary)的key的唯一性,将联系人的首字母插入到字典当中去;最后,取出字典的allKeys进行字母排序,然后遍历数据,进行按字母分组。

这里的核心技术就是汉字转拼音,当然大家可以使用iOS原生库方法PinYin4Objc来实现,这里笔者主要讲讲,iOS原生提供的API:

/// string 要转换的string,比如要转换的中文,同时它是mutable的,因此也直接作为最终转换后的字符串。
/// range是要转换的范围,同时输出转换后改变的范围,如果为NULL,视为全部转换。
/// transform可以指定要进行什么样的转换,这里可以指定多种语言的拼写转换。
/// reverse指定该转换是否必须是可逆向转换的。
/// 如果转换成功就返回true,否则返回false
Boolean CFStringTransform(CFMutableStringRef string, CFRange *range, CFStringRef transform, Boolean reverse);
CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("芈月"));
CFStringTransform(string, NULL, kCFStringTransformMandarinLatin, NO);
NSLog(@"%@",string);
/// 打印结果:mǐ yuè

/// 由于👆正确的输出了拼音,而且还带上了音标。有时候我们不需要音标怎么办?还好CFStringTransform同时提供了将音标字母转换为普通字母的方法kCFStringTransformStripDiacritics。我们在上面的代码基础上再加上这个:

CFStringTransform(string, NULL, kCFStringTransformStripDiacritics, NO);
NSLog(@"%@",string);
/// 打印结果:mi yue

由于后期考虑到,搜索模块需要增加本地搜索联系人的需求,所以本项目这里采用了内部已经封装好 PinYin4ObjcHighlightedSearch,它支持搜索关键字,高亮显示,支持汉字、全拼、简拼搜索,支持多音字搜索。

汉子转拼音API如下:

/// WPFPinYinTools.h
/** 获取传入字符串的第一个拼音字母 */
+ (NSString *)firstCharactor:(NSString *)aString withFormat:(HanyuPinyinOutputFormat *)pinyinFormat;

数据处理整体代码如下:

/// 联系人数据处理
- (void)_handleContacts:(NSArray *)contacts {
    if (MHObjectIsNil(contacts) || contacts.count == 0) return;
    
    // 计算总人数
    self.total = [NSString stringWithFormat:@"%ld位联系人",contacts.count];
    
    
    // 这里需要处理数据
    NSMutableDictionary *tempDict = [[NSMutableDictionary alloc] init];
    
    // 获取首字母
    for(MHUser *contact in contacts){
        // 存到字典中去 <ps: 由于 contacts.json 的wechatId 都是拼音 so...>
        [tempDict setObject:contact forKey:[[contact.wechatId substringToIndex:1] uppercaseString]];
    }
    
    
    //排序,排序的根据是字母
    NSComparator comparator = ^(id obj1, id obj2) {
        if ([obj1 characterAtIndex:0] > [obj2 characterAtIndex:0]) {
            return (NSComparisonResult)NSOrderedDescending;
        }
        if ([obj1 characterAtIndex:0] < [obj2 characterAtIndex:0]) {
            return (NSComparisonResult)NSOrderedAscending;
        }
        return (NSComparisonResult)NSOrderedSame;
    };
    
    // 已经排好序的数据
    NSMutableArray *letters = [tempDict.allKeys sortedArrayUsingComparator: comparator].mutableCopy;
    NSMutableArray *viewModels = [NSMutableArray array];
    /// 遍历数据
    for (NSString *letter in letters) {
        // 存储相同首字母 对象
        NSMutableArray *temps = [[NSMutableArray alloc] init];
        // 存到数组中去
        for (NSInteger i = 0; i<contacts.count; i++) {
            MHUser *contact = contacts[i];
            if ([letter isEqualToString:[[contact.wechatId substringToIndex:1] uppercaseString]]) {
                MHContactsItemViewModel *viewModel = [[MHContactsItemViewModel alloc] initWithContact:contact];
                [temps addObject:viewModel];
            }
        }
        [viewModels addObject:temps];
    }
    
    /// 需要配置 新的朋友、群聊、标签、公众号、
    MHContactsItemViewModel *friends = [[MHContactsItemViewModel alloc] initWithIcon:@"plugins_FriendNotify_36x36" name:@"新的朋友"];
    MHContactsItemViewModel *groups = [[MHContactsItemViewModel alloc] initWithIcon:@"add_friend_icon_addgroup_36x36" name:@"群聊"];
    MHContactsItemViewModel *tags = [[MHContactsItemViewModel alloc] initWithIcon:@"Contact_icon_ContactTag_36x36" name:@"标签"];
    MHContactsItemViewModel *officals = [[MHContactsItemViewModel alloc] initWithIcon:@"add_friend_icon_offical_36x36" name:@"公众号"];
    // 插入到第一个位置
    [viewModels insertObject:@[friends,groups,tags,officals] atIndex:0];
    
    // 插入一个
    [letters insertObject:UITableViewIndexSearch atIndex:0];
    
    self.dataSource = viewModels.copy;
    self.letters = letters.copy;
}

页面展示

当数据处理完,构建好cell,刷新tableView,理论上页面展示和微信页面应该一模一样👍。当然我们滚动到页面的最底部,继续上拉,会露出tableView浅灰色(#ededed)的背景色,但是看看微信的上拉,露出的却是白色的背景色,所以必须把这个细节加上去。

实现逻辑非常简单,只需要设置tableViiew的背景色为透明色,然后添加一个白色背景的UIViewtableView的下面即可,默认隐藏,等有数据时才去显示。实现代码如下:

/// 添加一个tempView 放在最底下 用于上拉显示白底
UIView *tempView = [[UIView alloc] init];
self.tempView = tempView;
// 默认隐藏
tempView.hidden = YES;
tempView.backgroundColor = [UIColor whiteColor];
[self.view insertSubview:tempView belowSubview:self.tableView];

[self.tempView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).with.offset(0);
    make.right.equalTo(self.view).with.offset(0);
    make.bottom.equalTo(self.view).with.offset(0);
    make.height.mas_equalTo(MH_SCREEN_HEIGHT * 0.5);
}];

Cell侧滑备注 功能实现,笔者这里采用iOS 11.0 提供的左滑删除功能,只需实现UITableViewDelegate即可。

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
        return NO;
    }
    return YES;
}
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath  API_AVAILABLE(ios(11.0)){
    
    UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"备注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        completionHandler(YES);
    }];
    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[remarkAction]];
    config.performsFirstActionWithFullSwipe = NO;
    
    return config;
}

由于最新微信侧滑备注浅黑色(#4c4c4c),而系统默认的则是浅灰色的,所以我们需要修改系统的样式,由于每次侧滑,都有调用- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath这个API,而且我们利用Debug view Hierarchy 工具查看层级,发现侧滑是加在tableView上,而不是cell上,所以解决思路如下:一旦调用此API,立即遍历tableViewsubView,然后找到对应的UISwipeActionPullView,修改其内部的UISwipeActionStandardButton 背景色。

但是这里需要指出的是,由于存在两种层级关系如下:

  • iOS 13.0+: UITableView --> _UITableViewCellSwipeContainerView --> UISwipeActionPullView --> UISwipeActionStandardButton
  • iOS 13.0-: UITableView --> UISwipeActionPullView --> UISwipeActionStandardButton

所以最终处理如下:

- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
    /// 注意低版本的Xcode中 不一定是 `_UITableViewCellSwipeContainerView+UISwipeActionPullView+UISwipeActionStandardButton` 而是 `UISwipeActionPullView+UISwipeActionStandardButton`
    
    for (UIView *subView in tableView.subviews) {
        if ([subView isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
            subView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
            for (UIButton *button in subView.subviews) {
                if ([button isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
                    // 修改背景色
                    button.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                }
            }
        } else if ([subView isKindOfClass:NSClassFromString(@"_UITableViewCellSwipeContainerView")]) {
            for (UIView *childView in subView.subviews) {
                if ([childView isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
                    childView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                    for (UIButton *button in childView.subviews) {
                        if ([button isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
                            // 修改背景色
                            button.backgroundColor = MHColorFromHexString(@"#4c4c4c");
                        }
                    }
                }
            }
        }
    }
}

当然点击备注时,也得修改其背景色,否则又会被重置为浅灰色,代码如下:

- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath  API_AVAILABLE(ios(11.0)){
    
    UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"备注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        sourceView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        sourceView.superview.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        // Fixed Bug: 延迟一丢丢去设置 不然无效 点击需要设置颜色 不然会被重置
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            sourceView.backgroundColor = MHColorFromHexString(@"#4c4c4c");
            sourceView.superview.backgroundColor = MHColorFromHexString(@"#4c4c4c");
        });
        
        completionHandler(YES);
    }];
    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[remarkAction]];
    config.performsFirstActionWithFullSwipe = NO;
    
    return config;
}

当然有兴趣的同学也可以借助: MGSwipeTableCell 来实现侧滑备注。


关于,索引条A-Z的实现,笔者这里借助的是:SCIndexView 来实现的,关于其具体的实现,笔者这里就不再一一赘述了,有兴趣的同学可以自行学习。

项目中的配置代码如下,轻松实现微信索引Bar:

/// 监听数据
@weakify(self);
[[RACObserve(self.viewModel, letters) distinctUntilChanged] subscribeNext:^(NSArray * letters) {
    @strongify(self);
    if (letters.count > 1) {
        self.tempView.hidden = NO;
    }
    self.tableView.sc_indexViewDataSource = letters;
    self.tableView.sc_startSection = 1;
}];

#pragma mark - 初始化
- (void)_setup{
    
    self.tableView.rowHeight = 56.0f;
    self.tableView.backgroundColor = [UIColor clearColor];
    
    // 配置索引模块
    SCIndexViewConfiguration *configuration = [SCIndexViewConfiguration configuration];
    // 设置item 距离 右侧屏幕的间距
    configuration.indexItemRightMargin = 8.0;
    // 设置item 文字颜色
    configuration.indexItemTextColor = MHColorFromHexString(@"#555555");
    // 设置item 选中时的背景色
    configuration.indexItemSelectedBackgroundColor = MHColorFromHexString(@"#57be6a");
    /// 设置索引之间的间距
    configuration.indexItemsSpace = 4.0;
    
    self.tableView.sc_indexViewConfiguration = configuration;
    self.tableView.sc_translucentForTableViewInNavigationBar = true;
}

当然通讯录模块中,还有个细节处理,那就是滚动过程中,悬浮HeaderView渐变,主要涉及到背景色的渐变和文字颜色的渐变。当然实现还是比较简单的,就是实现- (void)scrollViewDidScroll:(UIScrollView *)scrollView;方法,计算headerView.mh_y的临界点。实现如下:

// UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    /// 刷新headerColor
    [self _reloadHeaderViewColor];
}

/// 刷新header color
- (void)_reloadHeaderViewColor {
    NSArray<NSIndexPath *> *indexPaths = self.tableView.indexPathsForVisibleRows;
    for (NSIndexPath *indexPath in indexPaths) {
        // 过滤
        if (indexPath.section == 0) {
            continue;
        }
        MHContactsHeaderView *headerView = (MHContactsHeaderView *)[self.tableView headerViewForSection:indexPath.section];
        [self configColorWithHeaderView:headerView section:indexPath.section];
    }
}

/// 配置 header color
- (void)configColorWithHeaderView:(MHContactsHeaderView *)headerView section:(NSInteger)section{
    if (!headerView) {
        return;
    }
    CGFloat insertTop = UIApplication.sharedApplication.statusBarFrame.size.height + 44;
    CGFloat diff = fabs(headerView.frame.origin.y - self.tableView.contentOffset.y - insertTop);
    CGFloat headerHeight = 33.0f;
    double progress;
    if (diff >= headerHeight) {
        progress = 1;
    }else {
        progress = diff / headerHeight;
    }
    [headerView configColorWithProgress:progress];
}



/// MHContactsHeaderView.m
- (void)configColorWithProgress:(double)progress {
    static NSMutableArray<NSNumber *> *textColorDiffArray;
    static NSMutableArray<NSNumber *> *bgColorDiffArray;
    static NSArray<NSNumber *> *selectTextColorArray;
    static NSArray<NSNumber *> *selectBgColorArray;
    
    if (textColorDiffArray.count == 0) {
        UIColor *selectTextColor = MHColorAlpha(87, 190, 106, 1);
        UIColor *textColor = MHColorAlpha(59, 60, 60, 1);
        // 悬浮背景色
        UIColor *selectBgColor = [UIColor whiteColor];
        // 默认背景色
        UIColor *bgColor = MHColorAlpha(237, 237, 237, 1);
        
        selectTextColorArray = [self getRGBArrayByColor:selectTextColor];
        NSArray<NSNumber *> *textColorArray = [self getRGBArrayByColor:textColor];
        selectBgColorArray = [self getRGBArrayByColor:selectBgColor];
        NSArray<NSNumber *> *bgColorArray = [self getRGBArrayByColor:bgColor];
        
        textColorDiffArray = @[].mutableCopy;
        bgColorDiffArray = @[].mutableCopy;
        for (int i = 0; i < 3; i++) {
            double textDiff = selectTextColorArray[i].doubleValue - textColorArray[i].doubleValue;
            [textColorDiffArray addObject:@(textDiff)];
            double bgDiff = selectBgColorArray[i].doubleValue - bgColorArray[i].doubleValue;
            [bgColorDiffArray addObject:@(bgDiff)];
        }
    }
    
    NSMutableArray<NSNumber *> *textColorNowArray = @[].mutableCopy;
    NSMutableArray<NSNumber *> *bgColorNowArray = @[].mutableCopy;
    for (int i = 0; i < 3; i++) {
        double textNow = selectTextColorArray[i].doubleValue - progress * textColorDiffArray[i].doubleValue;
        [textColorNowArray addObject:@(textNow)];
        
        double bgNow = selectBgColorArray[i].doubleValue - progress * bgColorDiffArray[i].doubleValue;
        [bgColorNowArray addObject:@(bgNow)];
    }
    
    UIColor *textColor = [self getColorWithRGBArray:textColorNowArray];
    self.letterLabel.textColor = textColor;
    UIColor *bgColor = [self getColorWithRGBArray:bgColorNowArray];
    self.contentView.backgroundColor = bgColor;
}

- (NSArray<NSNumber *> *)getRGBArrayByColor:(UIColor *)color
{
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char resultingPixel[4];
    CGContextRef context = CGBitmapContextCreate(&resultingPixel, 1, 1, 8, 4, rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipLast);
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, CGRectMake(0, 0, 1, 1));
    CGContextRelease(context);
    CGColorSpaceRelease(rgbColorSpace);
    
    double components[3];
    for (int component = 0; component < 3; component++) {
        components[component] = resultingPixel[component] / 255.0f;
    }
    double r = components[0];
    double g = components[1];
    double b = components[2];
    return @[@(r),@(g),@(b)];
}

- (UIColor *)getColorWithRGBArray:(NSArray<NSNumber *> *)array {
    return [UIColor colorWithRed:array[0].doubleValue green:array[1].doubleValue blue:array[2].doubleValue alpha:1];
}

细节处理:由于要后期需要弹出 搜索模块和收回搜索模块,所以要保证滚动到最顶部时,要确保搜索框完全显示或者完全隐藏,否则就会导致在弹出搜索模块,然后收回搜索模块,会导致动画不流畅,影响用户体验,微信想必也是考虑到如此场景,可以说是,细节满满。

解决方案也比较简单:判断列表停止滚动后scrollView.contentOffset.y 是否在(-scrollView.contentInset.top, -scrollView.contentInset.top + searchBarH) 范围内,判断当前是上拉还是下拉,上拉隐藏,下拉显示。 代码如下:


/// 细节处理:
/// 由于要弹出 搜索模块,所以要保证滚动到最顶部时,要确保搜索框完全显示或者完全隐藏,
/// 不然会导致弹出搜索模块,然后收回搜索模块,会导致动画不流畅,影响体验,微信做法也是如此
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    /// 注意:这个方法不一定调用 当你缓慢拖动的时候是不会调用的
    [self _handleSearchBarOffset:scrollView];
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // 记录刚开始拖拽的值
    self.startDragOffsetY = scrollView.contentOffset.y;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    // 记录刚开始拖拽的值
    self.endDragOffsetY = scrollView.contentOffset.y;
    // decelerate: YES 说明还有速度或者说惯性,会继续滚动 停止时调用scrollViewDidEndDecelerating
    // decelerate: NO  说明是很慢的拖拽,没有惯性,不会调用 scrollViewDidEndDecelerating
    if (!decelerate) {
        [self _handleSearchBarOffset:scrollView];
    }
}

/// 处理搜索框显示偏移
- (void)_handleSearchBarOffset:(UIScrollView *)scrollView {
    // 获取当前偏移量
    CGFloat offsetY = scrollView.contentOffset.y;
    CGFloat searchBarH = 56.0f;
    /// 在这个范围内
    if (offsetY > -scrollView.contentInset.top && offsetY < (-scrollView.contentInset.top + searchBarH)) {
        // 判断上下拉
        if (self.endDragOffsetY > self.startDragOffsetY) {
            // 上拉 隐藏
            CGPoint offset = CGPointMake(0, -scrollView.contentInset.top + searchBarH);
            [self.tableView setContentOffset:offset animated:YES];
        } else {
            // 下拉 显示
            CGPoint offset = CGPointMake(0, -scrollView.contentInset.top);
            [self.tableView setContentOffset:offset animated:YES];
        }
    }
}


以上就是微信通讯录模块所涉及到的全部知识点,且难度一般。当然,通讯录模块还有个重要功能--搜索。⚠️尽管笔者已经在 WeChat 项目中实现了,且效果跟微信如出一撤👍。 但是考虑到其逻辑的复杂性,以及UI的搭建等问题,后期笔者会单独写一篇文章,来详细描述搜索模块的技术实现和细节处理。敬请期待...

期待

  1. 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
  2. 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源码地址:WeChat

主页

GitHub 掘金 CSDN 知乎
点击进入 点击进入 点击进入 点击进入

参考链接

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