MJRefresh源码阅读笔记

读源码之前需要掌握的基础概念

UIView的生命周期

先看如下的demo代码


@interface GreenView : UIView
@end
@implementation GreenView

- (instancetype)init //2
{
    self = [super init];
    if (self) {
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame //3
{
    self = [super initWithFrame:frame];
    if (self) {
    }
    return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview { //5
    NSLog(@"%s, superView=%@", __func__, newSuperview); //newSuperview是vc.view
}
- (void)didMoveToSuperview { //6
    NSLog(@"%s", __func__);
}
- (void)willMoveToWindow:(UIWindow *)newWindow { //7
    NSLog(@"%s, window=%@", __func__, newWindow);
}
- (void)didMoveToWindow {//8
    NSLog(@"%s", __func__);
}
@end

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    GreenView *greenView = [[GreenView alloc] init]; //1
    greenView.backgroundColor = UIColor.greenColor;
    greenView.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:greenView]; //4 内部会一次调用GreenView的willMoveToSuperview()和didMoveToSuperview()
}

@end

给代码打上断点,运行结果如下图。可以得出结论:当调用GreenView(UIView的子view)init方法时,该方法里面会调用initWithFrame()方法。
在这里插入图片描述
在上图的基础上,跳到下一个断点,结果如下图。可以得出结论:调用[self.view addSubview:greenView];时,GreenView的willMoveToSuperview()方法会被执行。
在这里插入图片描述
在上图的基础上,跳到下一个断点,结果如下图。可以得出结论:调用[self.view addSubview:greenView];时,GreenView的willMoveToSuperview()方法先被执行,然后GreenView的didMoveToSuperview()方法才被执行。
在这里插入图片描述
在上图的基础上,跳到下一个断点,结果如下图。可以得出结论:GreenView的willMoveToWindow()和didMoveToWindow()方法会被依次调用。当didMoveToWindow()被调用则意味着该GreenView被添加到window上了(看下面第2张图和第3张图的api描述)。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

关联对象的原理

如下图,关联对象是通过两个AssociationsHashMap(你可以把该类理解为java的HashMap或者OC的NSMutableDictionary)来实现的。当app进程启动时,内存就已经存在一个_map的静态变量,该变量指向一个AssociationsHashMap实例,假设该实例名为A。A的key是实例对象地址经过DISGUISE()算法运算后得到的一个hash值(同一个实例对象的hash值相同),这里的实例对象就是你调用objc_setAssociatedObject()方法时所传入的第1个参数,而A的value是另一个AssociationsHashMap实例,假设该实例名为B。B的key是一个“void类型的东西”,这里的“void类型的东西”就是你调用objc_setAssociatedObject()方法时所传入的第2个参数,而B的value是ObjcAssociation类的实例对象,ObjcAssociation类有两个属性,一个是id类型(存储的是你调用objc_setAssociatedObject()方法时所传入的第3个参数),另一个是objc_associationPolicy类型(存的是你调用objc_setAssociatedObject()方法时所传入的第4个参数).
在这里插入图片描述

UISCrollView的contentInset、contentOffset、contentSize。

因为UICollectionview和UITableView都是UIScrollView的子类,所以contentInset、contentOffset、contentSize属性在这3个类中的语义和表现都是相同的。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

模板方法模式

模板方法是一种设计模式,具体可以查看这篇博客:https://blog.csdn.net/jason0539/article/details/45037535

源码的结构

官网 https://github.com/CoderMJLee/MJRefresh 的demo有很多,刷新有两类:下拉刷新(pullRefresh)和上拉加载更多(loadMore)。这两类又都能运用在UITableView、UICollectionview、UIWebView和WKWebView上。

UITableView的下拉刷新

如下图,官方demo关于UITableView的下拉刷新有好几种,但实现原理都类似,所以这里就以“默认”样式介绍其实现,具体指的是下图的example01例子中的MJRefreshNormalHeader类。
在这里插入图片描述

使用场景

1 点击上图模拟器中的默认按钮,即可进入下图的页面。
在这里插入图片描述

2 在上图的界面中下拉,如下图。
在这里插入图片描述
3 当上图的下拉超过一定距离后,结果就如下图:顶部会提示“Release to refresh”。
在这里插入图片描述
4 上图的下拉刷新的结果如下图
在这里插入图片描述

头部的下拉刷新的相关类图结构

在这里插入图片描述
由上图可知,MJRefreshNormalHeader本质上就是个UIView。

在这里插入图片描述
下图来源于官网:https://github.com/CoderMJLee/MJRefresh。红色字体表示我们可以直接在项目中使用的类。
在这里插入图片描述

自动下拉刷新的场景分析

1 如下图,当点击“默认”按钮时,即将进入“默认样式”的MJTableViewController时,tableView的mj_header会被赋值。
在这里插入图片描述
2 在上图的基础上,跳到下一个断点(本文指的是指向[weakSelf loadNewData];的断点)时,模拟器的界面是正在加载的画面。
在这里插入图片描述
MJRefreshNormalHeader类的headerWithRefreshingBlock()方法的实现如下图,该方法只是创建一个该类的实例对象,然后保存你自定义的block,然后返回。
在这里插入图片描述

UITableView有mj_header属性??答:我们知道,系统UITableView并没有mj_header属性,那么就可以猜出这大概率是通过分类实现的。如下图,mj_header属性是在UIScrollView+MJRefresh文件中定义的,当你把一个MJRefreshNormalHeader实例对象赋值给UITableView实例对象的mj_header属性时,MJRefreshNormalHeader实例对象(本质上就是一个UIView)就会被插入到tableView的子view数组中,即MJRefreshNormalHeader实例对象就成了tableView的子view。
在这里插入图片描述
接下来看[self.tableView.mj_header beginRefreshing];的调用。beginRefreshing()方法是在MJRefreshComponent类里面实现的。如下图所示,self.window不为空就意味着MJRefreshComponent这个view(本例中具体指的是MJRefreshNormalHeader实例对象)就已经被添加到view tree上了。从左边的调用栈可以知道,MJTableViewController的viewDidLoad()方法被调用时,说明MJTableViewController的view(就是我们平常见到的UIViewController的view属性)才刚被创建出来,此时并没有被添加到view tree上,即该view还没有被挂载在跟view为UIWindow的某棵view tree上。从下图的执行情况可以知道此时MJRefreshNormalHeader实例对象的window的值为空,该实例对象的state被赋予MJRefreshStateWillRefresh,然后setNeedsDisplay()被调用,而setNeedsDisplay()方法会触发系统来调用drawRect(),所以我们就可以在MJRefreshComponent的drawRect()里面加个断点。
在这里插入图片描述
在上张图的断点基础上,跳到下一个断点,结果就停在了断点drawRect()方法上,如下图所示,该方法只是简单的修改state的值为MJRefreshStateRefreshing。不知道你看到这里有没有疑问,反正我有:奇怪,这里并没有触发下拉刷新啊,到底是在哪里触发header视图的下拉呢?可除了这里,还会有哪里呢?聪明的你,应该就会想到KVO、set方法的重写。。。于是开始在代码里面开始往这两个方向寻找证据。
在这里插入图片描述
很快,果然,MJRefreshComponent重写了setState()方法。此时在该方法添加一个断点,然后继续跳到该断点上。
这里面先是给_state成员变量赋值,然后会切到UI线程中,让UI线程执行[self setNeedsLayout];。我们知道,setNeedsLayout()会触发系统来调用该view的layoutSubviews()方法。咦,图中的左边的调用栈有些“深”,原来是MJRefreshComponent的好几个子类都重写了setState()方法啊。那接下来就按照栈的顺序并通过回溯的方式看这些方法咯。
在这里插入图片描述
(附加:上图的MJRefreshDispatchAsyncOnMainQueue宏的定义如下图。)
在这里插入图片描述
在上图的调用栈的基础上,退出栈顶的栈帧(说白了就是跳到MJRefreshHeader.m的setState()方法的断点上),然后执行结果如下图。
在这里插入图片描述
我们接着进入headerRefreshingAction()方法瞅瞅,原来这里就是“下拉刷新动画”的执行代码。。。因为MJRefreshDispatchAsyncOnMainQueue宏是把“下拉刷新动画”的代码的执行放到下一个runloop中,所以这里我们先跳过这个方法不讲,然后讲栈里面其它的setState()方法,最后再讲这个方法吧。
在这里插入图片描述
在上图的调用栈基础上,再回溯到MJRefreshStateHeader的setState()方法上,如下图。父类的setState()方法调用居然写在MJRefreshCheckState这个宏里面(如下面第2张图所示)。。下图的setState()方法里面,除了调用父类的setState()方法外,还会设置header的文字描述、最后更新时间,此时的具体值请看下面第3张图。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在上图的调用栈基础上,再回溯到MJRefreshNormalHeader的setState()方法上,如下图。菊花的动画就是这里设置的。
在这里插入图片描述
最后再回过头来看看“正在刷新”的界面的实现原理:设置scrollView(UIScrollView的子类有UITableView和UICollectionview)的contentInset和contentOffset变量,进而让“正在刷新的区域”(具体界面如下面第1张图)能够在手指离开屏幕后也能展示出来。当“刷新区域”的展示动画结束后,就会触发你的自定义刷新请求,具体实现请看第2、第3和第4张图。

在这里插入图片描述
在这里插入图片描述
(下图是附加的,旨在说明mj_insetT属性是如何实现的)
在这里插入图片描述

在这里插入图片描述
在上图的基础上,跳到下一个断点,就是你的自定义刷新逻辑了。
在这里插入图片描述
本文分析的下拉刷新的请求如下图所示,其实就是模拟耗时操作,添加5条数据,然后睡眠两秒钟,然后调用[tableView reloadData];来刷新列表,接着调用[tableView.mj_header endRefreshing];来结束下拉刷新。
在这里插入图片描述
断点进入endRefreshing()方法,发现该方法就只是改变了state状态,哦,那又会调用前面所介绍的那几个类的setState()方法。
在这里插入图片描述
和前面的setState()的分析方法一样,我们还是按照栈的顺序并通过回溯的方式看这些方法。
在这里插入图片描述
在上图的调用栈的基础上,退出栈顶的栈帧(说白了就是跳到MJRefreshHeader.m的setState()方法的断点上),然后执行结果如下图。
在这里插入图片描述

我们接着进入headerEndingAction()方法瞅瞅,原来这里就是“下拉刷新区域上移隐藏 动画”的执行代码。该方法先保存上次刷新的时间,然后通过设置self.scrollView.mj_insetT来改变scrollView(本例具体指的是MJTableViewController类的tableView)的contentInset,从而隐藏“刷新区域”。
在这里插入图片描述

在上图的调用栈基础上,再回溯到MJRefreshStateHeader的setState()方法上,如下图。父类的setState()方法调用居然写在MJRefreshCheckState这个宏里面。。下图的setState()方法里面,除了调用父类的setState()方法外,还会设置header的文字描述、最后更新时间。
在这里插入图片描述
在上图的调用栈基础上,再回溯到MJRefreshNormalHeader的setState()方法上,如下图。该方法会隐藏菊花,停止菊花动画。
在这里插入图片描述

该场景的时序图
- (void)example01
{
    __weak __typeof(self) weakSelf = self;
    
    // 设置回调(一旦进入刷新状态就会调用这个refreshingBlock)
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        [weakSelf loadNewData]; //你的自定义刷新逻辑
    }];
    
    // 马上进入刷新状态
    [self.tableView.mj_header beginRefreshing];
}

上面代码的内部处理流程如下面的时序图所示。
在这里插入图片描述


- (void)loadNewData
{
    // 1.添加假数据
    for (int i = 0; i<5; i++) {
        [self.data insertObject:MJRandomData atIndex:0];
    }
    
    // 2.模拟2秒后刷新表格UI(真实开发中,可以移除这段gcd代码)
    __weak UITableView *tableView = self.tableView;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(MJDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 刷新表格
        [tableView reloadData];
        
        // 拿到当前的下拉刷新控件,结束刷新状态
        [tableView.mj_header endRefreshing];
    });
}

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值