MJRefresh源码阅读<一>

“下拉刷新”和“上拉加载更多”应该是App标配的功能了,而iOS SDK只提供了UIRefreshControl控件且只有“下拉刷新”功能且只能用于UITableViewController,所以当项目中没有用到UITableViewController或者需要定制“下拉刷新”和“上拉加载更多”时只能自己定制实现,好在这块有好多现成的开源库,国内这块做的比较好的是小码哥的MJRefresh,今天就纪录下学习完MJRefrsh源码后的心得。

原理

总的来说就是给scrollview添加一个头视图(header)和尾视图(footer),通过监听scrollview的contentOffset变化来动态改变header和footer的状态(KVO实现),每种状态预先均已设置了一些监听事件(block实现),处于指定状态时就执行事先设置的事件。

header和footer的状态变化可以是根据scrollview的contentOffset来设置,也可以是刷新结束后主动设置。MJRefresh内部维护一个状态机,分别为MJRefreshStateIdle、MJRefreshStatePulling、MJRefreshStateRefreshing、MJRefreshStateWillRefresh、MJRefreshStateNoMoreData,具体处于哪种状态,默认是MJRefreshStateIdle状态。

以头视图header为例,一开始header的y坐标设置为一个负数这样在默认状态时就看不到header了,然后当下拉srollview时就能看到header了,当下拉到一定位置时也就是说contentOffset达到某个阈值时header就变为刷新状态了,此时就需要设置contentInset了(因为scroolview设置了bounce功能,如果不设置contentInset则下拉松开时scrollview就会回弹到初始原点的位置了,header就看不到了,所以为了让header在刷新状态时停留在顶部就需要设置contentInset)让header停留在顶部,当刷新状态结束时再把contentInset设置为0(原始值),然后header就看不到了,刷新结束。

Runtime

category增加属性

为了给UIScrollView增加header和footer属性,MJRefresh采用category实现,但category不能直接添加属性,但可以通过objc-runtime中objc_getAssociatedObject
和objc_setAssociatedObject方法实现,源码如下:

- (void)setMj_header:(MJRefreshHeader *)mj_header
{
    if (mj_header != self.mj_header) {
        // 删除旧的,添加新的
        [self.mj_header removeFromSuperview];
        [self insertSubview:mj_header atIndex:0];

        // 存储新的
        [self willChangeValueForKey:@"mj_header"]; // KVO
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 mj_header, OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"mj_header"]; // KVO
    }
}

- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

method swizzling

为了方便地在调用UITableView reloadData后执行指定事件,MJRefresh重写了UITableView和UICollectionView的load方法,在load方法中exchange了reloadData和自定义方法,具体技术用的是objc-runtime的method_exchangeImplementations方法。

+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}

+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

- (void)mj_reloadData
{
    [self mj_reloadData];

    [self executeReloadDataBlock];
}

下拉刷新具体实现细节

  1. 重写willMoveToSuperview方法。当header将要添加到目标view时,会调用该方法,在该方法中设置header的宽度、x坐标、最开始的contentInset、设置永远支持垂直弹簧效果及最重要的KVO scrollView的contentOffset、contentInset和panGestureRecognizer属性。
  2. 重写layoutSubviews方法。当header添加到目标view后会调用该方法,在该方法中设置header的y坐标为负的自身高度(高度是在初始话时设置的),这样header在初始状态时看不到,至此整个header的初始化工作就已完成,剩下的就是监听scrollview的属性改变自身状态。