iOS源码阅读 —— MJRefresh

前言:最近应该有很多小伙伴去跳槽面试的吧,相信各位有的已经顺利收到offer了,而有些则是碰壁了,那么我在这里给大家准备了相关面试资料,还有相关算法资料。想了解的可找我拿

MJRefresh几乎是我们开发工作中必用的一款三方库,它提供一套非常简单实用的拖拽执行回调事件的解决方案。下面是官方提供的框架图。

其中最常用的几个默认视图类分别是:

下拉刷新控件:MJRefreshNormalHeader
上拉加载控件:MJRefreshAutoNormalFooter、MJRefreshBackNormalFooter
左滑加载控件:MJRefreshNormalTrailer

下面将对这些类,自上而下地进行分析。

公共基类控件

MJRefreshComponent

通过框架图可以看出所有视图都源于同一个基类——MJRefreshComponent,它为子类提供了公用的属性和事件,主要有:

  • 回调对象和回调方法
  • 拖拽状态定义和控制
  • 通过KVO,对事件(控件偏移、内容尺寸、手势状态)添加监听(回调响应交给子类实现)
  • 其他:
    • 拖拽百分比
    • 根据拖拽比例自动切换透明度

MJRefreshComponent还为子类搭建了基本的逻辑框架:

最后推荐个我的iOS交流圈:[891 488 181]
'有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

视图创建

// 1.初始化
- (instancetype)initWithFrame:(CGRect)frame{;}

// 2.准备工作
- (void)prepare{;}

// 3.视图即将被父视图加入
- (void)willMoveToSuperview:(UIView *)newSuperview{
    // 滚动视图初始值的记录
    // 一些值的更新
    // 监听事件的更新
}

// 4.布局
- (void)layoutSubviews{
    [self placeSubviews];
}

滚动视图状态回调

// 当偏移值发生变化
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
// 当内容大小发生变化
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
// 当点击手势状态发生变化
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

状态设置

// 状态设置
- (void)setState:(MJRefreshState)state{;}

常用方法

// 进入刷新状态
- (void)beginRefreshing{;}
// 结束刷新状态
- (void)endRefreshing{;}

其他

// 自动切换透明度
- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha{;}
- (BOOL)isAutoChangeAlpha{;}
- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha{;}

// 根据拖拽进度实时设置透明度
- (void)setPullingPercent:(CGFloat)pullingPercent{;}

下拉刷新控件(Header)

下拉刷新控件包含四个类:

  • MJRefreshHeader
    • MJRefreshStateHeader
      • MJRefreshNormalHeader
    • MJRefreshGifHeader

MJRefreshHeader

MJRefreshHeader类是一个包含了完整的下拉刷新功能逻辑的空白视图,子类MJRefreshStateHeaderMJRefreshGifHeader只需要再添加一些额外的图片和文字,就能提升使用体验和保持代码的简洁易读性。

实现过程

1.初始化

创建视图,设置高度和位置。

- (void)prepare {
    [super prepare];
    // 设置存储key
    // 设置Header的高度
}

- (void)placeSubviews {
    [super placeSubviews];    
    // 设置Header的位置(y坐标)
}
2.偏移变化:- scrollViewContentSizeDidChange

当用户拖拽滚动控件,是其偏移值发生改变时,会回调- scrollViewContentOffsetDidChange:(NSDictionary *)change方法,在不同的状态下执行对应的逻辑。如果滚动视图已经将Header滚动至屏幕外,则不处理后续逻辑。

- scrollViewContentOffsetDidChange:(NSDictionary *)change方法中,有一些关键的变量值,分别是:

  • 当前滚动的偏移值:offsetY
  • 头部控件刚好出现的偏移值:happenOffsetY
  • 即将刷新的临界点:normal2pullingOffsetY

通过对这些变量值的比较,可以计算出拖拽动作应该被设置为何种状态。

  • 控件正在被拖拽
    • 当拖拽时的偏移量大于临界值,且原状态为闲置时,将状态置为即将刷新
    • 当拖拽时的偏移量小于临界值,且原状态为即将刷新时,将状态重置会闲置
  • 控件未被拖拽,且当前状态为松手进行刷新
    • 执行开始刷新的方法
  • 控件未被拖拽,且为达到执行刷新回调的临界点
if (self.scrollView.isDragging) { // 如果正在拖拽
    self.pullingPercent = pullingPercent;

    // 当拖拽时的偏移量大于临界值,且原状态为闲置时,将状态置为即将刷新
    if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
        // 转为即将刷新状态
        self.state = MJRefreshStatePulling;
    }
    // 当拖拽时的偏移量小于临界值,且原状态为即将刷新时,将状态重置会闲置
    else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
        // 转为普通状态
        self.state = MJRefreshStateIdle;
    }
}
// 原状态为即将刷新,且手已松开
else if (self.state == MJRefreshStatePulling) {
    // 开始刷新
    [self beginRefreshing];
}
// 未达到刷新的偏移量,且手已松开
else if (pullingPercent < 1) {
    // 记录header露出的百分比
    self.pullingPercent = pullingPercent;
}

这里需要注意的是,当Header的状态处于MJRefreshStateRefreshing正在刷新,且控件还在滚动时,会执行- resetInset方法,目的是记录刷新结束后需要调整的上边距值insetTDelta,同时避免 CollectionView 在使用根据 Autolayout 和 内容自动伸缩 Cell, 刷新时导致的 Layout 异常渲染问题。

3.状态设置
- (void)setState:(MJRefreshState)state{
    _state = state;

    // 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件
    MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}

视图刷新被加入了异步队列的主线程中,是为了尽量等空间的属性设置完毕后再进行布局的刷新。

4.开始刷新

执行- beginRefreshing方法,设置状态为MJRefreshStateRefreshing刷新中。 方法调用流程如下:

1.开始刷新方法调用
- (void)beginRefreshing{
   // ...
   self.state = MJRefreshStateRefreshing;
   // ...
}

2.设置状态为正在刷新中
- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        //...        
    } else if (state == MJRefreshStateRefreshing) {
        [self headerRefreshingAction];
    }
}
3.执行刷新动作
- (void)headerRefreshingAction {
    // 主要代码
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
            CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
            // 增加滚动区域top
            self.scrollView.mj_insetT = top;
            // 设置滚动位置
            CGPoint offset = self.scrollView.contentOffset;
            offset.y = -top;
            [self.scrollView setContentOffset:offset animated:NO];
        }
    } completion:^(BOOL finished) {
        [self executeRefreshingCallback];
    }];
}

- headerRefreshingAction方法为滚动视图设置了新的insetoffset,使得Header能在滚动视图的顶部停留,用于展示刷新文字动画之类的。

5.结束刷新

结束刷新需要使用者在耗时操作结束后,主动调用- endRefreshing方法。 方法调用流程如下:

1.结束刷新方法调用
- (void)endRefreshing{
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}
2.设置状态为闲置
- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    // 根据状态做事情
    if (state == MJRefreshStateIdle) {
        if (oldState != MJRefreshStateRefreshing) return;

        [self headerEndingAction];
    } else if (state == MJRefreshStateRefreshing) {
        // ...
    }
}
3.执行结束动作
- (void)headerEndingAction {;}

- headerEndingAction方法将滚动视图的inset重置为刷新状态前的值,将header又隐藏了起来

上拉加载控件(Footer)

下拉刷新控件包含七个类:

  • MJRefreshFooter
    • MJRefreshBackFooter
      • MJRefreshBackNormalFooter
      • MJRefreshBackGifFooter
    • MJRefreshAutoFooter
      • MJRefreshAutoNormalFooter
      • MJRefreshAutoGifFooter

MJRefreshFooter

MJRefreshFooter类不能直接被使用,它仅定义了少量的基础属性和方法,例如构造方法、初始化控件高度,以及无数据加载情况下的处理。

能够直接使用的上拉加载控件是,由MJRefreshFooter衍生出的两个子类,MJRefreshBackFooterMJRefreshAutoFooter,这两个控件的不同之处在于:

  • MJRefreshBackFooter:隐藏在滚动视图的底部边界之外,当拖动至Footer的刷新临界点并放开手,才会执行加载操作。
  • MJRefreshAutoFooter:会紧贴在滚动视图contentSize的边界,如果contentSize的尺寸小于滚动视图的尺寸,用户在不需要滚动的情况下也能看到Footer控件的。它的刷新时机是,用户在拖拽中且达到了Footer刷新临界点。

MJRefreshBackFooter

实现过程

1.初始化

当MJRefreshBackFooter即将被加入父视图时,会走 - willMoveToSuperview: 方法,并在方法体内调用 - scrollViewContentSizeDidChange: 方法。该方法获取了父视图高度和父视图内容的高度,取二者中较大的数,作为Footer的纵坐标值,确保Footer的位置正好隐藏在视图或内容的最底部。

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change
{
    [super scrollViewContentSizeDidChange:change];

    // 内容的高度
    CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom;
    // 表格的高度
    CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom;
    // 设置位置和尺寸
    self.mj_y = MAX(contentHeight, scrollHeight);
}
2.偏移变化:- scrollViewContentSizeDidChange

当用户在滑动控件使offset发生变化时,会触发 MJRefreshKeyPathContentOffset 的监听事件 —— - scrollViewContentOffsetDidChangeMJRefreshBackFooter- scrollViewContentOffsetDidChange方法里的代码逻辑与MJRefreshHeader是几乎相同的,这里不赘述。需要提一点的是,MJRefreshBackFooter视图的临界值计算需要考虑内容的高度与滚动视图之间的高度差问题。

#pragma mark 获得scrollView的内容 超出 view 的高度
- (CGFloat)heightForContentBreakView
{
    CGFloat h = self.scrollView.frame.size.height - self.scrollViewOriginalInset.bottom - self.scrollViewOriginalInset.top;
    return self.scrollView.contentSize.height - h;
}

#pragma mark 刚好看到上拉刷新控件时的contentOffset.y
- (CGFloat)happenOffsetY
{
    CGFloat deltaH = [self heightForContentBreakView];
    // 内容和视图的高度差
    if (deltaH > 0) {
        //  内容高度 > 视图高度
        return deltaH - self.scrollViewOriginalInset.top;
    } else {
        // 内容高度 < 视图高度
        return - self.scrollViewOriginalInset.top;
    }
}
3.状态设置

MJRefreshBackFooter类的- setState方法的主要工作就是在开始刷新和结束刷新的时候,为滚动视图更新对应的offsetinset值。

MJRefreshAutoFooter

实现过程

1.初始化

当MJRefreshAutoFooter即将被加入父视图时,会调用- willMoveToSuperview: 方法,该方法获取了父视图的内容,作为Footer的纵坐标y的值,确保Footer的位置正好紧贴内容的底部。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];

    if (newSuperview) { // 新的父控件
        if (self.hidden == NO) {
            self.scrollView.mj_insetB += self.mj_h;
        }

        // 设置位置
        self.mj_y = _scrollView.mj_contentH;
    } else { // 被移除了
        if (self.hidden == NO) {
            self.scrollView.mj_insetB -= self.mj_h;
        }
    }
}
2.刷新逻辑

MJRefreshAutoFooter控件的位置是紧贴内容的,所以会存在两种情况: 1.当内容高度 < 控件高度,可以直接看到紧贴内容底部的Footer。这种情况下,加载的时机是在用户松手后调用的。

- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];

    if (self.state != MJRefreshStateIdle) return;

    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;

    switch (panState) {
        // 手松开
        case UIGestureRecognizerStateEnded: {
            if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {
                // 内容 < 控件高度
                if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
                    self.triggerByDrag = YES;
                    [self beginRefreshing];
                }
            } else {
                // 内容 > 控件高度
                if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
                    self.triggerByDrag = YES;
                    [self beginRefreshing];
                }
            }
        }
            break;

        case UIGestureRecognizerStateBegan: {
            [self resetTriggerTimes];
        }
            break;

        default:
            break;
    }
}

2.当内容高度 ≥ 控件高度,需要拖动视图到Footer的加载临界值,但此时不需要松开手,只要滚动视图的偏移量突破了临界值,就会触发加载方法。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];

    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;

    // 当autoTriggerTimes被设置成-1(滚动时无限加载)
    // 该方法保证拖动放手后,视图还在滚动的情况下,一直保持加载状态

    // 内容超出控件高度
    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) {

        //  内容高度 - 控件高度 + 控件底部边距 + footer高度 * 百分比 - footer高度
        //  内容高度 - 控件高度 + 控件底部边距
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
            // 防止手松开时连续调用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;

            if (_scrollView.isDragging) {
                self.triggerByDrag = YES;
            }
            // 当底部刷新控件完全出现时,才刷新
            [self beginRefreshing];
        }
    }
}

从代码中可以看出,满足刷新的条件是: 拖动偏移量 ≥ 内容高度 - 控件高度 + 控件底部边距 + footer高度 * 刷新控件露出百分比 - footer高度 当刷新控件露出百分比为默认值1.时,不等式可以简化为: 拖动偏移量 ≥ 内容高度 - 控件高度 + 控件底部边距

3.无限触发

MJRefreshAutoFooter的另一特点就是无限触发,开发者可以设置属性自定义自动刷新的次数。

/** 自动触发次数, 默认为 1, 仅在拖拽 ScrollView 时才生效,

 如果为 -1, 则为无限触发
 */
@property (nonatomic) NSInteger autoTriggerTimes;

当滚动视图在持续地滚动时(内容高度≥控件高度),会不停地调用-scrollViewContentOffsetDidChange:方法,满足加载条件时从而不停的调用-beginRefreshing方法。

- (void)beginRefreshing{
    // 新的拖拽动作 && 剩余触发次数 && 是否无限触发
    if (self.triggerByDrag && self.leftTriggerTimes <= 0 && !self.unlimitedTrigger) {
        return;
    }

    [super beginRefreshing];
}

当前如果支持无限触发autoTriggerTimes == -1,那么在滚动视图停止滚动前,视图到达加载临界点时都会触发加载任务。

4.状态设置

-beginRefreshing在触发的时候,会将state设置成MJRefreshStateRefreshing,并执行加载数据的回调。 通常我们会在加载数据结束的回调方法中去调用-endRefreshing-endRefreshingWithNoMoreData方法,此时state会被设置成MJRefreshStateIdleMJRefreshStateNoMoreData,对应-setState:方法中的代码,我们可以看到无限触发次数是在此处进行了控制。

- (void)setState:(MJRefreshState)state{
    MJRefreshCheckState

    if (state == MJRefreshStateRefreshing) {
        // 执行加载数据回调
        [self executeRefreshingCallback];
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        if (self.triggerByDrag) {
            if (!self.unlimitedTrigger) {
                self.leftTriggerTimes -= 1;
            }
            self.triggerByDrag = NO;
        }

        /** 结束刷新 */
        if (MJRefreshStateRefreshing == oldState) {

            // 当视图开启了分页显示,设置动画和回调
            if (self.scrollView.pagingEnabled) {
                CGPoint offset = self.scrollView.contentOffset;
                offset.y -= self.scrollView.mj_insetB;
                [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                    self.scrollView.contentOffset = offset;

                    if (self.endRefreshingAnimationBeginAction) {
                        self.endRefreshingAnimationBeginAction();
                    }
                } completion:^(BOOL finished) {
                    if (self.endRefreshingCompletionBlock) {
                        self.endRefreshingCompletionBlock();
                    }
                }];

                return;
            }

            // 结束刷新回调
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }
    }
}

左滑加载控件(Trailer)

MJRefreshTrailer 在实现逻辑上与 MJRefreshBackFooter 是完全一样的,只不过是将部分参数从垂直方向换成了水平方向。

State、Normal 子类控件

State类型的控件 的主要特点是添加了不同状态的提示文字和刷新时间的显示。 Normal类型的控件 在 State类型控件 的基础上,添加了箭头图标和刷新的动画。

Gif 子类控件

Gif类型的控件可以在拖拽和刷新时展示精美的动画来提升用户体验。 主要的两个方法是:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 

    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 

    /* 根据图片设置控件的高度 */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; 
    } 
}

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state { 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

刷新控件会根据图片的高度调整自身高度,同时会在没有自定义动画时长的情况下,根据动画的帧数自动设置完整播放一遍动画的时间。

拖拽动画

开发者可以通过拖拽百分比设置用户在拖拽时的动画,具体实现方式是通过计算当前拖拽的百分比在整体动画中对应的某个帧的图片来获取大致的下标。

// 通过拖拽百分比设置 Idle~Pulling状态之间的 对应的动画帧
- (void)setPullingPercent:(CGFloat)pullingPercent
{
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(MJRefreshStateIdle)];
    if (self.state != MJRefreshStateIdle || images.count == 0) return;
    // 停止动画
    [self.gifView stopAnimating];

    // 设置当前需要显示的图片
    NSUInteger index =  images.count * pullingPercent;
    if (index >= images.count) index = images.count - 1;
    self.gifView.image = images[index];
}

刷新动画

刷新动画会在视图状态处于“即将开始刷新”和“刷新中”进行,使用UIImageView的startAnimating对提前设置好的图片组逐帧播放,默认情况下是无限循环播放的。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState

    // 根据状态做事情
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        // 即将刷新 和 刷新中 状态的动画
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;

        [self.gifView stopAnimating];
        if (images.count == 1) { // 单张图片
            self.gifView.image = [images lastObject];
        } else { // 多张图片
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating];
        }
    } else if (state == MJRefreshStateIdle) {
        // 限制状态停止动画
        [self.gifView stopAnimating];
    }
}

总结

MJRefresh清晰整齐的架构为开发者提供了及其丰富的扩展性,而在通常没有定制需求的情况下,默认的控件已经十分够用了。

以下资料在群文件可自行下载

文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。

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

推荐阅读更多精彩内容

  • 前言:最近应该有很多小伙伴去跳槽面试的吧,相信各位有的已经顺利收到offer了,而有些则是碰壁了,那么我在这里给大...
    iOS开发面试总结阅读 1,018评论 0 0
  • MJRefresh是李明杰老师的作品,到现在已经有9800多颗star了,是一个简单实用,功能强大的iOS下拉刷新...
    Style_mao阅读 647评论 1 2
  • MJRefresh 已经很久没有写技术文章了,之前一段时间确实也是很忙,当然这也是一个借口,自己不思进取的成分也有...
    雨雪传奇阅读 1,122评论 0 4
  • 1.前言 MJRefresh 是日常 iOS 开发中使用频率比较高的一款下拉刷新/上拉加载更多的第三方控件,平时似...
    RiverSea阅读 1,365评论 0 10
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,549评论 0 11