美文网首页iOS开发资料收集区iOS进阶iOS 项目实战相关的教程
使用Xtrace分析MJRefresh技术实现细节(一):Vie

使用Xtrace分析MJRefresh技术实现细节(一):Vie

作者: ZZZEoEv | 来源:发表于2016-08-15 17:15 被阅读954次

    写在前面

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

    作为优秀的第三方库,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

    四、总结

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

    相关文章

      网友评论

      本文标题:使用Xtrace分析MJRefresh技术实现细节(一):Vie

      本文链接:https://www.haomeiwen.com/subject/yihusttx.html