先看如下的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个参数).
因为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上。
如下图,官方demo关于UITableView的下拉刷新有好几种,但实现原理都类似,所以这里就以“默认”样式介绍其实现,具体指的是下图的example01例子中的MJRefreshNormalHeader类。
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];
});
}