写在前面
把简单留给别人,把复杂留给自己。
作为优秀的第三方库,MJRefresh充分贯彻了这句话。
但我们不光是用户,我们还是创作者。
所以,深入了解其背后的实现细节,既能学习优秀的编程思维,还能为我们将来自定义提供方便。
要说分析别人的代码的话,光看源码,切来切去既影响效率还容易出错。
好在,有Xtrace这款神器。
整体思路
如果觉得一个东西太复杂,那是因为还没有抽象到一定高度去分析,然后,针对每一个子模块,肢解到最简单去分析。
--大象:Thinking in UML
我对上面这句话的理解:
抽象:抛开具体实现细节,将目标概括提取。
高度:决定你分析的层级,也就是你准备从多大的粒度开始分析。
一、总体结构
先分析一下MJRefresh的总体构成。
第一层,MJRefresh

这是对MJRefresh最高层级的抽象了,它就是它,我知道它是做什么的就行。
简单点来说,就是当我们在目录里看到这个词的时候,知道它是刷新控件。
第二层,MJRefreshHeader & MJRefreshFooter

这时候,我们知道,MJRefresh中包含了下拉刷新(Header)和上拉加载(Footer)两个子控件,
我们日常使用是这样子的:
self.tableView.mj_header = [MJRefreshHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
self.tableView.mj_footer = [MJRefreshFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
第三层,MJRefreshCustomView

顾名思义,CustomView可以让我们根据自己的需求,自定义控件。
MJ也为我们提供了基础的CustomView供我们使用,基本能满足大部分日常需求。
小结
至此,MJRefresh的总体结构已经抽象完毕了,可以看到,仅仅只有三层而已。
二、代码结构
面向对象编程中,绝大部分对象,我都偏向于抽象成两个部分:
- 初始化:这部分的代码跟运行时没有关系,或者关系轻微(比如在布局时,根据SuperView的相关参数对自身进行设置)
- 运行时:只有发生事件(比如KVO、Gesture等)时,才会调用的部分。
当然,不是说所有的代码非此即彼,肯定会存在一些模棱两可的部分,这时候的处理完全看个人喜好,毕竟我们所做的一切都是以理清思路为目的的。
同样需要说明的是:
我们这里先不进行代码的具体功能分析,因为这属于比较低层次的抽象部分,我们这里的主要目的是搞清楚MJRefresh或者说UIView的加载过程。
MJRefrsh类结构

MJ本人提供的类图结构,在第三层(CustomView)与第二层(Header & Footer)的中间插入了更细分的类,方便我们进行半自定义。
MJRefreshComponent
MJRefreshComponent作为基类,定义了MJRefresh的整体流程,其它子类只是在此流程的基础上,通过覆写基类的方法,实现定制。

MJRefreshComponent继承自UIView,所以其初始化部分,基本都是覆写了UIView的方法。
其添加的自定义方法为:
- (void)prepare;
- (void)placeSubviews;
这两个方法的调用,分别写在了init与layoutSubViews的覆写方法中
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 准备工作
[self prepare];
// 默认是普通状态
self.state = MJRefreshStateIdle;
}
return self;
}
- (void)prepare
{
// 基本属性
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.backgroundColor = [UIColor clearColor];
}
- (void)layoutSubviews
{
[self placeSubviews];
[super layoutSubviews];
}
- (void)placeSubviews{}
MJRefreshHeader

Header部分,主要是对Component的进一步具象,通过覆盖prepare、placeSubViews方法,更进一步的实现RefreshView的具体细节。
我们进行完全自定义的时候,最好是直接继承自MJRefreshHeader类,因为MJ在此类上提供了完整的流程控制和极简的构造方法。
Footer部分与Header部分一样,只是具体的逻辑部分会稍有不同。
CustomView部分则是进一步具象了,就不进行重复内容的介绍了。
三、初始化流程
函数实现部分,只是孤零零的存在,缺失了情景(上下文)的支持,没有任何意义。
因此我们需要将函数代入具体的流程中,才能理解,为什么函数内部要这么写。
我在这里使用的代码,就是MJRefresh提供的demo,有兴趣的童鞋可以自己用Xtrace追踪下试试。
(一) Xtrace
首先,简单介绍下Xtrace这款工具,它会打印出所有被追踪类所调用的方法,其使用方法也很简单:
- 将Xtrace.h与Xtrace.mm文件拖入工程
- 在需要追踪的类中引入Xtrace.h头文件
- [specific class xtrace]即可
一般是在AppDelegate 方法中调用,因为这样可以捕捉到完整的调用链。
#import "Xtrace.h"
#import "MJRefreshNormalHeader.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[MJRefreshNormalHeader xtrace];
}
不过在此追踪MJRefresh的话,会提示animate帧太大,使用[Xtrace excludeMethod:]方法排除animate方法时却会报错,这里我也没搞懂怎么回事,如果有熟悉Xtrace的童鞋,希望指导一下。
所以这里我只能在类初始化的时候调用Xrace了,不过好在,影响不大。
MJRefresh的初始化VC是MJExampleViewController:
- (void)viewDidLoad
{
[Xtrace showReturns:NO];
[MJRefreshNormalHeader xtrace];//在初始化MJRefresh类之前调用Xtrace
[super viewDidLoad];
__unsafe_unretained UITableView *tableView = self.tableView;
// 下拉刷新
tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 结束刷新
[tableView.mj_header endRefreshing];
});
}];
……
……
(二) 具体流程
1. init

VC调用MJRefreshHeader的构造方法,该构造方法调用自身init
#pragma mark - 构造方法
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
MJRefreshHeader *cmp = [[self alloc] init];
cmp.refreshingBlock = refreshingBlock;
return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
MJRefreshHeader *cmp = [[self alloc] init];
[cmp setRefreshingTarget:target refreshingAction:action];
return cmp;
}
1.1 init : [super initWithFrame]
子类中并没有覆写基类的init方法,所以默认还是调用基类的init
#pragma mark - 基类(Component) Init
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// 准备工作
[self prepare];
// 默认是普通状态
self.state = MJRefreshStateIdle;
}
return self;
}
1.2 init : [self prepare]
基类init定义,会直接调用[self prepare]。
self prepare 是这么定义的:
#pragma mark - NormalHeader prepare
- (void)prepare
{
[super prepare];
...
...
}
所以会一层一层优先调用父类的prepare方法:

1.2.1 Header prepare:
#pragma mark - MJRefreshHeader prepare
- (void)prepare
{
[super prepare];
// 设置key
self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
// 设置高度
self.mj_h = MJRefreshHeaderHeight;
}

1.2.2 StateHeader prepare:
#pragma mark - MJRefreshStateHeader prepare
- (void)prepare
{
[super prepare];
// 初始化间距
self.labelLeftInset = MJRefreshLabelLeftInset;
// 初始化文字
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
[self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}


1.2.3 NormalHeader prepare:
#pragma mark - 重写父类的方法
- (void)prepare
{
[super prepare];
self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
NormalHeader调用
Prepare小结:
至此,我们只完成了基类init方法(忘了的童鞋请返回去重新看一下)中的一小步,及[self prepare]。
接下来还有一个方法self.state = .....
注意,这里会涉及到写属性setState,之所以要介绍这个写属性,是因为其实现代码里涉及到了子视图的加载。
1.3 init : [self setState]
MJ在MJRefreshHeader中,对此写方法进行了定义,看似代码很多,其实核心逻辑很简单:
- 判断当前状态(Idle、Pulling、Refreshing)
- 根据状态设定MJRefreshHeader SubViews的视图属性
- 执行动画
- (void)setState:(MJRefreshState)state
{
MJRefreshCheckState
// 根据状态做事情
if (state == MJRefreshStateIdle) {
if (oldState == MJRefreshStateRefreshing) {
self.arrowView.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
self.loadingView.alpha = 0.0;
} completion:^(BOOL finished) {
// 如果执行完动画发现不是idle状态,就直接返回,进入其他状态
if (self.state != MJRefreshStateIdle) return;
self.loadingView.alpha = 1.0;
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
}];
} else {
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformIdentity;
}];
}
} else if (state == MJRefreshStatePulling) {
[self.loadingView stopAnimating];
self.arrowView.hidden = NO;
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
}];
} else if (state == MJRefreshStateRefreshing) {
self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行
[self.loadingView startAnimating];
self.arrowView.hidden = YES;
}
}
因为涉及到了子视图属性的设置,所以会加载子视图,调用流程如下:


setState 小结
setState写方法,在我们自定义HeaderView的过程中十分重要,并且这里触发了子视图的懒加载。
1.4 init : return MJRefreshNormalHeader

Block赋值完毕,我们的MJRefreshNormalHeader也就创建完毕了,MJRefreshHeader构造函数至此执行完毕,该return了。
注意,此时我们仅仅是完成了下面语句的 “=” 的右边部分,还没有将创建完毕的NormalHeader加载到TableView.mj_header上,所以接下来会执行赋值语句,也就是MoveToSuperView。
tableView.mj_header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
// 模拟延迟加载数据,因此2秒后才调用(真实开发中,可以移除这段gcd代码)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 结束刷新
[tableView.mj_header endRefreshing];
});
}];
2. willMoveToSuperView & didMoveToSuperView
MJRefresh所有类中,只有基类Component覆写了这个方法:
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
if (newSuperview) { // 新的父控件
// 设置宽度
self.mj_w = newSuperview.mj_w;
// 设置位置
self.mj_x = 0;
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加监听
[self addObservers];
}
}


这一步,主要是设置MJRefresh自己在SuperView上的位置。

3. willMoveToWindow & didMoveToWindow


4 .layoutSubviews
UIView在didMoveToWindow之后,才完全加入到UIView Hierarchy,在此之后,才会进行layoutSubViews。

因为layoutSubView只有基类Component进行了覆写,所以会先调用Component的基类方法:
- (void)layoutSubviews
{
[self placeSubviews];
[super layoutSubviews];
}
- (void)placeSubviews{}
先调用自身的placeSubviews方法,再如此递归向上调用。
流程总结
MJRefresh本身继承自UIView,所以本文在记录其创建→加载过程的同时,也记录了UIView的生命周期:

MJRefresh的主要修改的地方有三个:
- init
- willMoveToSuperView
- layoutSubViews
其中init过程流程图如下:

四、总结
学习的路上,有人指路当然最好,但是往往我们并没有那么幸运。
这时候,我们只能靠自己。
而我认为,学习代码的最好办法,就是去看牛人写的代码。
但是怎么才能看明白别人的代码呢?
我的方法就是:适度的抽象 + 流程分析。
类方法实现细节看不懂?没关系,细节暂时抛开,先搞明白流程。
流程搞懂了,再慢慢回过头来看细节代码。
全局到局部,是我认为比较合适的阅读复杂代码的方式。
网友评论