使用Xtrace分析MJRefresh技术实现细节(一):View的创建加载过程

写在前面

把简单留给别人,把复杂留给自己。

作为优秀的第三方库,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这款工具,它会打印出所有被追踪类所调用的方法,其使用方法也很简单:

  1. 将Xtrace.h与Xtrace.mm文件拖入工程
  2. 在需要追踪的类中引入Xtrace.h头文件
  3. [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;
}
RefreshHeader运行状态
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];
}

StateHeader调用.png
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中,对此写方法进行了定义,看似代码很多,其实核心逻辑很简单:

  1. 判断当前状态(Idle、Pulling、Refreshing)
  2. 根据状态设定MJRefreshHeader SubViews的视图属性
  3. 执行动画
- (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赋值

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上的位置。

didMoveToSuperView

3. willMoveToWindow & didMoveToWindow


4 .layoutSubviews

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

layoutSubviews

因为layoutSubView只有基类Component进行了覆写,所以会先调用Component的基类方法:

- (void)layoutSubviews
{
    [self placeSubviews];
    
    [super layoutSubviews];
}

- (void)placeSubviews{}

先调用自身的placeSubviews方法,再如此递归向上调用。


流程总结

MJRefresh本身继承自UIView,所以本文在记录其创建→加载过程的同时,也记录了UIView的生命周期:

UIView创建至加载周期

MJRefresh的主要修改的地方有三个:

  • init
  • willMoveToSuperView
  • layoutSubViews

其中init过程流程图如下:

initWithFrame

四、总结

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

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

推荐阅读更多精彩内容