oc 顺滑的列表倒计时工具 支持分页

关于代码与导入:
pod 'PYBaseCountDownHandler'
demo

列表倒计时需求概述

image.png

经常会出现一个列表中,有多个商品卡片需要有倒计时,这时候会有几个重点需要特别关注:

  • 多个商品卡片的刷新逻辑(复用逻辑)
  • 加载更多数据时,定时器数据的准确性
  • 用户切换到后台,在进入页面,定时器的数据准确性
  • 单个商品倒计时结束后,定时器移除单个商品逻辑
  • 定时器销毁逻辑

思考 && 思路

  1. 列表中有n个cell需要倒计时,并且列表需要支持上拉加载、下拉刷新。

在刷新数据源时持有所有的model,并且计算每个model的倒计时时差。
(倒计时总计时间) - (新model加入时的已经倒计时时间) = (model倒计时)

  1. cell的更新
  1. countDownHandler需要弱持有屏幕上的cell,并且在cell更新数据源后,根据cell获取到model
  2. 把(model加入时已经倒计时时间)传递给model,让model来计算倒计时。
  1. 切入后台后,定时器需要继续执行(或停止)。
  1. 进入后台前会调用
    - (void)applicationDidEnterBackground:(UIApplication *)application
  2. 回到前台后会调用
    - (void)applicationWillEnterForeground:(UIApplication *)application
  3. 在这两个地方计算时间差,然后累计到countDownHandler的已倒计时时间

框架构思

一、 定时器管理model

image.png

由定时器管理工具对网络数据model进行集中管理计时,然后由model通知cell进行刷新数据

但这种会出现一些明显的不足:

  • model数量过多,大多数都没展示在屏幕上,未展示的model不需要刷新UI,
  • 通过model获取cell,需要通过tableViewcellForIndexPath方法,造成了一定的资源浪费,如果 model 增加一个回调,直接在回调里引用cell 对cell进行刷新就会有复用问题(会导致一个model引用多个cell)
image.png

二、 定时器管理cell

image.png

反向思考,定时器直接管理cell,并刷新UI,我们可以根据cell获取到model,在看上面几个问题:

  • cell 的数量等于屏幕在展示的Cell与复用池中的cell,数量要比model数量少很多,且可以标记cell是否在屏幕上进行缩小范围
  • 通过cell 获取model,即使在复用池中的cell获取的model不对,也不影响展示,因为在cell将要出现时,会赋值正确的model,刷新UI

三、 定时器对分页数据的处理

数据初始化的时候,定时管理器,会为每个需要定时的数据进行绑定一个start属性,来记录定时器已计时时间

model剩余时间 = model.end + model.start - 当前倒计时;

image.png

四、 定时器回显

定时器通过代理方法,获取cell引用的model,并通过model的start计算出model真实的已倒计时时间,并通过代理回调给cell,cell在通过model的截止日期,计算需要展示的倒计时文本,如果倒计时为0,则通知countDown,移除cell

image.png

整体流程

image.png

实践:

  1. 根据cell,获取
    分别设置声明两种代理,一个用于数据源代理方法,一个对于倒计时刷新View中Text的代理方法
  2. CountDownHandlerDataSource
  • 利用registerCountDownEventWithDataSources 储存modelArray
  • 其中modelArray中的model必须继承CountDownHandlerDataSource代理
    在储存modelArray数组时,会向model中添加一个CGFlaot属性(countDownHandler_startCountDown),用来记录此时 CountDownHandler 计时器已经计时时间(currentTime)。
  • countDownHandler_startCountDown: 在计算剩余倒计时时间时,会用到。 剩余时间 = model的总倒计时时间-(CountDownHandler.currentTime - countDownHandler_startCountDown);
/** 针对于model的delegate方法 */
@protocol CountDownHandlerDataSource<NSObject>
/**
 当需要这条数据显示的时候,会进行调用
 @param handler handler
 @param until 当前已经倒计时了多少时间【剩余时间 = 倒计时总时间 - until】
 */
- (void) countDownHandler: (CountDownHandler *)handler andDataSourceCurrenUntil: (CGFloat)until;
@end
  1. CountDownHandlerViewDelegate
  • 利用CountDownHandlerViewDelegate刷新UI
  • 在调用- (void) countDownHandler: (CountDownHandler *)handler andDataSource: (id <CountDownHandlerDataSource>)dataSource;方法前,会先调用- (id <CountDownHandlerDataSource>) getViewDelegateMapDataSource;方法,获取到相应的model,并调用model的代理方法- (void) countDownHandler: (CountDownHandler *)handler andDataSourceCurrenUntil: (CGFloat)until;来保证model的数据刷新
/** 针对于视图的delegate方法 */
@protocol CountDownHandlerViewDelegate<NSObject>
/**
在每次倒计时事件触发后调用与调用`registerCountDownEventWithDelegate`后都会触发该代理方法
 */
- (void) countDownHandler: (CountDownHandler *)handler andDataSource: (id <CountDownHandlerDataSource>)dataSource;

/**
 获取视图所对应的Model
 @return model
 */
- (id <CountDownHandlerDataSource>) getViewDelegateMapDataSource;
@end

列表实践

  1. 需要自行保证CountDownHandler生命周期

  2. 如果需求为tableViewcell中有倒计时:

  3. 必须 在数据源数组的set方法中 调用registerCountDownEventWithDataSources方法,进行model的批量注册,无需判断是否重复注册,方法内部进行了排除

  4. 在model需要实现CountDownHandlerDataSource相关代理方法,进行倒计时计算

  5. 在tableView中持有CountDownHandler,并且需要在tableViewDataSource方法cellFroRowAtIndexPath中,调用 registerCountDownEventWithDelegate,把cell,作为delegate,在代理方法中修改UI

具体实现

.h


@interface PYCountDownHandler : NSObject

/**
 倒计时 时间 间隔 (秒单位) 默认为1
 */
@property (nonatomic, assign) CGFloat timeInterval;

/**
  现在已经进行时间 (负数 秒单位) 默认为0 
 */
@property (nonatomic, assign) CGFloat currentTime;

/**
 最多同时存在多少个需要倒计时的model
 @warning 最好是两个屏幕所能盛放的cell的数量), 默认为100
 */
@property (nonatomic, assign) NSInteger targetMaxCount;

/**
 开始倒计时 创建 dispatch_source_t
 */
- (void) start;

/**
 结束倒计时 把timer赋值为nil 不会删除所需要倒计时的model
 */
- (void) end;

/**
 注册倒计时事件
 @bug 注册事件前,需要确保 delegate 中有正确的数据源,否则会数据错乱
 */
- (void) registerCountDownEventWithDelegate: (id <CountDownHandlerViewDelegate>)delegate;

/**
 批量添加delegate,

 @param dataSources dataSource数组 如果数中有元素已经添加,那么将不再添加
 @bug 在有上拉加载的需求中,如果依然 依据当前self.currentTime计算时间的话,会出现差错,因为新返回的数据,需要从0开始倒计时,而不是直接减去currentTime
 
    所以在添加到注册列表的过程中,在dataSource中记录了此时的currentTime(记做delegateCurrentTime),
 
    在进行倒计时时候,会利用currentTime - delegateCurrentTime, 得到需要真正的倒计时间
 
 @bug 需要在网络请求下来后,立即把modelArray注册到dataSources中,以保倒计时准确
 */
- (void)registerCountDownEventWithDataSources: (NSArray<id <CountDownHandlerDataSource>>*)dataSources;

/**
 注册单个的DataSource

 @param dataSource dataSource
 */
- (void) registerCountDownEventWithDataSource: (id<CountDownHandlerDataSource>)dataSource;
/**
 不再相应倒计时
 @param delegate 注销修改视图的delegate
 */
- (void) removeDelegate: (id)delegate;


/** 移除相应的 dataSource */
- (void) removeDataSource: (id<CountDownHandlerDataSource>)dataSource;

/**
 获取delegates
 */
- (NSArray *) getCurrentDelegates;
/**
 获取所有的dataSource
 */
- (NSArray *) getCurrentDataSource;
/**
 清除所需要倒计时的View delegate
 */
- (void) removeAllDelegate;

/**
 清除所需要倒计时的dataSource
 */
- (void) removeAllDataSource;

/**
* 停止后台计时
*/
- (void)stopBackstageTimeing;

/**
 * 开始后台计时
 */
- (void)startBackgroundTiming;

/**
 进入后台返回前台的回调 如果自定义实现这个方法,则 isStopWithBackstage 失效
*/
- (void) applicationWillEnterForegroundWithCurrentDate:(void(^)(CGFloat currentTimeDifferent, PYCountDownHandler *countDownHandler))currentTimeDifferentBlock;


/// 进入后台 又回到前台的时间差
+ (CGFloat) currentTimeDifferent;

/// 所有 进入后台 又回到前台的(currentTimeDifferent)时间差
+ (CGFloat) totalTimeDifferent;

+ (void) applicationDidEnterBackgroundWithCurrentDate: (NSDate *)date;

+ (void) applicationWillEnterForegroundWithCurrentDate: (NSDate *)date;
@end

.m

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

CGFloat STATIC_CURRENT_TIME_DIFFERENCE = 0.0f;
CGFloat STATIC_CURRENT_TIME_TOTAL_DIFFERENCE = 0.0f;
NSDate *STATIC_APPLICATION_DID_ENTER_BACKGROUND = nil;
NSDate *STATIC_APPLICATION_DID_BECOME_ACTIVE = nil;

static NSString *const K_countDownHandler_startCountDown = @"K_countDownHandler_startCountDown";
static NSString *const K_countDownHandler_startCountDown_becomeActive_notification = @"K_countDownHandler_startCountDown_becomeActive_notification";

@interface PYCountDownHandler()
@property (nonatomic,strong) NSMutableArray <id<CountDownHandlerViewDelegate>>*delegates;
@property (nonatomic,strong) NSMutableArray <id<CountDownHandlerDataSource>>*dataSources;
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic,strong) void(^currentTimeDifferentBlock)(CGFloat currentTimeDifferent, PYCountDownHandler *countDownHandler);
/**
进入后台后,是否停止倒计时 默认为false
实现`applicationWillEnterForegroundWithCurrentDate`方法后 该属性失效
*/
@property (nonatomic,assign) BOOL isStopWithBackstage;
@end

@implementation PYCountDownHandler

+ (void)applicationWillEnterForegroundWithCurrentDate:(NSDate *)date {
    STATIC_APPLICATION_DID_BECOME_ACTIVE = date;
    STATIC_CURRENT_TIME_DIFFERENCE = -1;
    [[NSNotificationCenter defaultCenter] postNotificationName:K_countDownHandler_startCountDown_becomeActive_notification object:nil];
}

+ (void) applicationDidEnterBackgroundWithCurrentDate: (NSDate *)date {
    STATIC_APPLICATION_DID_ENTER_BACKGROUND = date;
    STATIC_CURRENT_TIME_DIFFERENCE = -1;
}

+ (CGFloat) totalTimeDifferent {
    return STATIC_CURRENT_TIME_TOTAL_DIFFERENCE;
}

+ (CGFloat)currentTimeDifferent {
    return STATIC_CURRENT_TIME_DIFFERENCE;
}

- (instancetype) init {
    if (self = [super init]) {
        self.timeInterval = 1;
        self.currentTime = 0;
        self.targetMaxCount = 100;
    }
    return self;
}

- (void) start {
    if (!self.timer) {
        [self createTimer];
    }
}

- (void) end {
    if (self.timer) {
        dispatch_cancel(self.timer);
        self.timer = nil;
    }
}

- (void) registerCountDownEventWithDataSources:(NSArray<id<CountDownHandlerDataSource>> *)dataSources {
    __weak typeof (self)weakSelf = self;
    [dataSources enumerateObjectsUsingBlock:^(id<CountDownHandlerDataSource>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [weakSelf registerCountDownEventWithDataSource:obj];
    }];
}

- (void) registerCountDownEventWithDataSource:(id<CountDownHandlerDataSource>)dataSource {
    
    if (!dataSource) {
        return;
    }
    __weak typeof(dataSource) weakDataSource = dataSource;
    __weak typeof(self) weakSelf = self;
    if ([self getDataSourceStartCountDownTime:weakDataSource] < 0) {
        [self setDataSourceStartCountDownTime:weakDataSource];
    }
    if([weakDataSource respondsToSelector:@selector(countDownHandler:andDataSourceCurrenUntil:)]) {
        CGFloat currentUntil = weakSelf.currentTime - [weakSelf getDataSourceStartCountDownTime:weakDataSource];
        [weakDataSource countDownHandler:weakSelf andDataSourceCurrenUntil:currentUntil];
    }
    if ([self.dataSources containsObject:dataSource]) {
        return;
    }
  
    [self lock:^{
        [weakSelf.dataSources addObject:dataSource];
    }];
}

- (void)registerCountDownEventWithDelegates:(NSArray<id<CountDownHandlerViewDelegate>> *)delegates {
    __weak typeof(self)weakSelf = self;
    [delegates enumerateObjectsUsingBlock:^(id<CountDownHandlerViewDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [weakSelf registerCountDownEventWithDelegate:obj];
    }];
}

- (void) registerCountDownEventWithDelegate: (id <CountDownHandlerViewDelegate>)delegate{
    if (!delegate) {
        NSLog(@".\
              \n   🌶:【%@】注册代理失败,代理为nil\
              \n   🌶: 如果出现倒计时复用问题:\
              \n     必须要在`registerCountDownEventWithDelegate`之前,保证delegate数据源存在\
              \n   也就是确保`getViewDelegateMapDataSource`可以获取到正确的值\
              \n.",NSStringFromClass([self class]));
        return;
    }
    __weak typeof(delegate) weakDelegate = delegate;
    __weak typeof(self) weakSelf = self;
    if([weakDelegate respondsToSelector:@selector(countDownHandler:andDataSource:)]) {
        id <CountDownHandlerDataSource> dataSource;
        if([weakDelegate respondsToSelector:@selector(getViewDelegateMapDataSource)]) {
            [weakSelf registerCountDownEventWithDataSource:dataSource];
            
            dataSource = [weakDelegate getViewDelegateMapDataSource];
            
            if (!dataSource) {
                [weakSelf logError_NotDataSource];
            }
            if ([dataSource respondsToSelector:@selector(countDownHandler:andDataSourceCurrenUntil:)]) {
                CGFloat currentUntil = weakSelf.currentTime - [weakSelf getDataSourceStartCountDownTime:dataSource];
                [dataSource countDownHandler:weakSelf andDataSourceCurrenUntil:currentUntil];
            }
        }
        [weakDelegate countDownHandler:weakSelf andDataSource:dataSource];
    }
    if ([self.delegates containsObject:delegate]) {
        return;
    }
    
    if (self.delegates.count > self.targetMaxCount) {
        [self removeDelegate:self.delegates.firstObject];
    }
    
    [self lock:^{
        [weakSelf.delegates addObject:weakDelegate];
    }];
}

- (void) removeDelegate: (id)delegate {
    if (!delegate) {
        NSLog(@"\n🌶:【%@】注册代理失败,代理为nil\n",NSStringFromClass([self class]));
        return;
    }
    __weak typeof(self)weakSelf = self;
    [self lock:^{
        [weakSelf.delegates removeObject: delegate];
    }];
}
- (void)removeDataSource:(id<CountDownHandlerDataSource>)dataSource {
    if (!dataSource) {
        NSLog(@"\n🌶:【%@】注册代理dataSource,dataSource为nil\n",NSStringFromClass([self class]));
        return;
    }
    __weak typeof(self)weakSelf = self;
    [self lock:^{
        [weakSelf.dataSources removeObject:dataSource];
    }];
}

- (void)timerAction {
    [self lock:^{
        self.currentTime += self.timeInterval;
        __weak typeof (self)weakSelf = self;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            [self.delegates enumerateObjectsUsingBlock:^(id<CountDownHandlerViewDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                
                if ([obj respondsToSelector:@selector(countDownHandler:andDataSource:)]) {
                    id <CountDownHandlerDataSource>dataSource;
                    if ([obj respondsToSelector:@selector(getViewDelegateMapDataSource)]) {
                        dataSource = [obj getViewDelegateMapDataSource];
                    }
                    if ([dataSource respondsToSelector:@selector(countDownHandler:andDataSourceCurrenUntil:)]) {
                        CGFloat currentUntil = weakSelf.currentTime - [weakSelf getDataSourceStartCountDownTime:dataSource];
                        [dataSource countDownHandler:weakSelf andDataSourceCurrenUntil:currentUntil];
                    }
                    [obj countDownHandler:weakSelf andDataSource:dataSource];
                }
            }];
        });
    }];
}

- (void) lock: (void(^)(void))block {
//    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    if (block) {
        block();
    }
//    dispatch_semaphore_signal(self.semaphore);
}

- (void) createTimer {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
      dispatch_time_t t = self.isStopWithBackstage ? DISPATCH_TIME_NOW : dispatch_walltime(NULL,0);
    
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
    /*
    第一个参数:定时器对象
    第二个参数:DISPATCH_TIME_NOW 表示从现在开始计时相对时间 dispatch_walltime 绝对时间
    第三个参数:间隔时间 GCD里面的时间最小单位为 纳秒
    第四个参数:精准度(表示允许的误差,0表示绝对精准)
    */
    dispatch_source_set_timer(_timer,t,1.0*NSEC_PER_SEC, 0);
    
    self.timer = _timer;

    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.timer, ^{
        [weakSelf timerAction];
    });
    
    dispatch_resume(self.timer);
}

- (void)dealloc {
    NSLog(@"✅销毁:%@",NSStringFromClass([self class]));
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (NSArray *) getCurrentDelegates {
    return self.delegates.copy;
}

- (NSArray *) getCurrentDataSource {
    return self.dataSources.copy;
}

- (void) removeAllDelegate {
    __weak typeof(self)weakSelf = self;
    [self.delegates enumerateObjectsUsingBlock:^(id<CountDownHandlerViewDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [weakSelf removeDelegate:obj];
    }];
}

- (void)removeAllDataSource {
    __weak typeof(self)weakSelf = self;
    [self lock:^{
        [weakSelf.dataSources removeAllObjects];
    }];
}

// MARK: - get && set
- (NSMutableArray <id<CountDownHandlerViewDelegate>>*) delegates {
    if (!_delegates) {
        _delegates = [NSMutableArray new];
    }
    return _delegates;
}

- (NSMutableArray <id<CountDownHandlerDataSource>> *)dataSources {
    if (!_dataSources) {
        _dataSources = [NSMutableArray new];
    }
    return _dataSources;
}

- (dispatch_semaphore_t) semaphore {
    if (!_semaphore) {
        _semaphore = dispatch_semaphore_create(1);
    }
    return _semaphore;
}

- (void) setDataSourceStartCountDownTime: (id<CountDownHandlerDataSource>)dataSource {
    if (!dataSource) return;
    objc_setAssociatedObject(dataSource, &K_countDownHandler_startCountDown, @(self.currentTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (CGFloat) getDataSourceStartCountDownTime: (id<CountDownHandlerDataSource>)dataSource {
    NSNumber *obj = objc_getAssociatedObject(dataSource, &K_countDownHandler_startCountDown);
    if (![obj isKindOfClass:[NSNumber class]]) {
        return -1;
    }
    return obj.integerValue;
}

- (void) logError_NotDataSource {
    
        NSLog(@".\
              \n   🌶:【%@】注册代理失败,代理为nil\
              \n   🌶: 如果出现倒计时复用问题,必须要在`registerCountDownEventWithDelegate`之前,保证delegate数据源存在\
              \n   也就是确保`getViewDelegateMapDataSource`可以获取到正确的值\
              \n.",NSStringFromClass([self class]));
}

- (void) applicationWillEnterForegroundWithCurrentDate:(void (^)(CGFloat, PYCountDownHandler *))currentTimeDifferentBlock {
    self.currentTimeDifferentBlock = currentTimeDifferentBlock;
}

- (void)setCurrentTimeDifferentBlock:(void (^)(CGFloat, PYCountDownHandler *))currentTimeDifferentBlock {
    if (currentTimeDifferentBlock) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive) name:K_countDownHandler_startCountDown_becomeActive_notification object:nil];
    }else{
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        NSLog(@"⚠️ currentTimeDifferentBlock 为nil 不再回调applicationDidBecomeActiveWithTimeDifferent");
    }
    _currentTimeDifferentBlock = currentTimeDifferentBlock;
}

- (void) didBecomeActive {
    
    [self setupTimeOffset];
    
    if (self.currentTimeDifferentBlock) {
        self.currentTimeDifferentBlock([PYCountDownHandler currentTimeDifferent],self);
    }
    if (self.isStopWithBackstage) {
        self.currentTime += [PYCountDownHandler currentTimeDifferent];
    }
}

- (void) setupTimeOffset {
    if (STATIC_CURRENT_TIME_DIFFERENCE <= 0 && STATIC_APPLICATION_DID_BECOME_ACTIVE && STATIC_APPLICATION_DID_ENTER_BACKGROUND) {
           STATIC_CURRENT_TIME_DIFFERENCE = STATIC_APPLICATION_DID_BECOME_ACTIVE.timeIntervalSince1970 - STATIC_APPLICATION_DID_ENTER_BACKGROUND.timeIntervalSince1970;
           STATIC_CURRENT_TIME_TOTAL_DIFFERENCE += STATIC_CURRENT_TIME_DIFFERENCE;
       }else{
           STATIC_CURRENT_TIME_DIFFERENCE = 0.0;
           STATIC_CURRENT_TIME_TOTAL_DIFFERENCE = 0.0;
       }
}

- (void)startBackgroundTiming {
    self.isStopWithBackstage = true;
}

- (void)stopBackstageTimeing {
    self.isStopWithBackstage = false;
}
@end

相应的属性

属性/方法 描述 默认值
timeInterval 倒计时的时间间隔(秒) 1
currentTime 当前已经进行的时间(负数,秒单位) 0
targetMaxCount 最多同时存在多少个需要倒计时的cell 100
isStopWithBackstage 进入后台后,是否停止倒计时,默认为false false
start 开始倒计时,创建 dispatch_source_t -
end 结束倒计时,将计时器设为nil,不会删除需要倒计时的cell -
registerCountDownEventWithDelegate: 注册事件前,需要确保 delegate 中有正确的数据源,否则会数据错乱 -
registerCountDownEventWithDataSources: 批量添加数据源(网络数据model)数组 -
registerCountDownEventWithDataSource: 注册单个数据源(网络数据model) -
removeDelegate: 不再相应倒计时,注销cell -
removeDataSource: 移除指定的数据源(网络model) -
getCurrentDelegates 获取当前注册的所有cell -
getCurrentDataSource 获取当前注册的所有数据源(网络model) -

说再多不如demo上手
pod 'PYBaseCountDownHandler'

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 左边是输出台,右边是tableView,点击后modal了一个控制器,停止了计时器 一、主要功能 对于tableV...
    LiYaoPeng阅读 1,109评论 1 6
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,118评论 29 470
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 一钟醉,相思尽染心儿碎,浊酒一杯岁月催,晓寒深处,窗前冷榻,与君何时会。 二钟醉,青丝成雪人憔悴,对影成双泪化灰,...
    墨疏璃阅读 256评论 0 0