美文网首页
开源项目-阅读MJRefresh,你能学到什么(附注释Demo)

开源项目-阅读MJRefresh,你能学到什么(附注释Demo)

作者: 洧中苇_4187 | 来源:发表于2020-06-09 17:30 被阅读0次

    1. 下面两组代码有没有什么区别

    这组没有区别 - const 都是修饰的 MJRefreshSlowAnimationDuration,保证它是常量,不能更改
    const CGFloat MJRefreshSlowAnimationDuration = 0.4;
    CGFloat const  MJRefreshSlowAnimationDuration = 0.4;
    
    这组有区别
    - 前一个 const 修饰的是MJRefreshKeyPathContentOffset
    - 后一个 const修饰的是 * MJRefreshKeyPathContentOffset, 它修饰的是指针,就是说指针是常量,但是它的值可以随意改
    NSString *const MJRefreshKeyPathContentOffset = @"contentOffset";
    const NSString *MJRefreshKeyPathContentOffset = @"contentOffset";
    

    怎么验证??? - 在touchesBegan打一个断点,对每个值进行赋值

    #import "ViewController.h"
    const CGFloat var1 = 0.4;
    CGFloat const var2 = 0.4;
    
    NSString *const var3 = @"var3-String";
    const NSString *var4 = @"var4-String";
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    }
    @end
    

    结果如下: var4可以随意改变,

    (lldb) po var1 = 3.3
    error: <user expression 0>:1:6: cannot assign to variable 'var1' with const-qualified type 'const CGFloat &' (aka 'const double &')
    var1 = 3.3
    ~~~~ ^
    note: variable 'var1' declared const here
    
    (lldb) po var2 = 3.2
    error: <user expression 1>:1:6: cannot assign to variable 'var2' with const-qualified type 'const CGFloat &' (aka 'const double &')
    var2 = 3.2
    ~~~~ ^
    note: variable 'var2' declared const here
    
    (lldb) po var3 = @"123"
    error: <user expression 2>:1:6: cannot assign to variable 'var3' with const-qualified type 'NSString *const &'
    var3 = @"123"
    ~~~~ ^
    note: variable 'var3' declared const here
    
    (lldb) po var4 = @"123"
    123
    
    (lldb) po var4 = @"456"
    456
    
    (lldb) 
    

    2. 创建单例的时候指定某些方法不允许调用 - 交给子类实现

    NS_UNAVAILABLE 不允许外界通过 init 和 new 方法创建单例对象
    - (instancetype)init NS_UNAVAILABLE;
    + (instancetype)new NS_UNAVAILABLE;
    
    
    NS_REQUIRES_SUPER - 子类必须要调用[super xxx]方法,否则会有警告⚠️
    - (void)prepare NS_REQUIRES_SUPER;
    
    

    3. 过期宏的使用

    #define MJRefreshDeprecated(DESCRIPTION) __attribute__((deprecated(DESCRIPTION)))
    

    4. objc_msgSend的使用,和形式扩展后的使用

    1.>MJRefresh中有对objc_msgSend使用,我们先来看看他是怎么使用的

    声明两个宏,
    #define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
    #define MJRefreshMsgTarget(target) (__bridge void *)(target)
    
    这里是使用 - 有没有耳目一新的感觉,卧槽,这个还能这么用
     if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
                MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
            }
    

    2.>接下来写个demo验证一下
    原理:我们知道方法的调用最后都会转化为objc_msgSend(),里面有两个固定参数,self(谁的方法),SEL(方法名),知道这两个参数就可以调用方法了
    我们来看看一个方法转成objc_msgSend是怎么样的,编译语句,你值得拥有xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o viewController.cpp
    多余代码省略,下面是控制器的方法,经过上述指令编译之后,

    @implementation ViewController
    
    - (void)testABC{
        NSLog(@"%s",__func__);
    }
    @end
    
    编译后的方法
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("testABC"));
    
    我们一个个摘出来看
    ((void (*)(id, SEL))  这里定义了一个方法,void * 返回值指针类型,传递两个形参self,SEL
    (void *)objc_msgSend)  objc_msgSend 函数名,
    ((id)self, sel_registerName("testABC")); 两个实参
    
    整一句就是说,把objc_msgSend转成 "(void (*)(id, SEL)"这种类型的函数,并传递两个参数
    

    3.>接着依样画葫芦,定义一个宏,再调用看看#define MJMsg_sendTest(...) ((void (*)(id,SEL,NSString *,int))objc_msgSend)(__VA_ARGS__),这里我定义了四个参数,前面两个固定的,不用多说,后面我传了一个NSStringint.

    注意一点objc_msgSend()要导入#import <objc/message.h>文件

    #define MJMsg_sendTest(...) ((void (*)(id,SEL,NSString *,int))objc_msgSend)(__VA_ARGS__)
    
    @implementation ViewController
    - (void)viewDidLoad{
        [super viewDidLoad];
    
        id refreshingTarget = self;
        SEL refreshingAction = @selector(testABC: arg2:);
        MJMsg_sendTest(refreshingTarget,refreshingAction,@"是我调用的你哈哈哈",24);
    }
    - (void)testABC:(NSString *)string arg2:(int)arg2{
        NSLog(@"%s",__func__);
    }
    @end
    

    调用结果:


    image.png

    4.>如果我搞一个MJPerson会是什么情况,什么意思???
    MJPerson.h,MJPerson.m放到一起,方便说明问题

    #import <Foundation/Foundation.h>
    @interface MJPerson : NSObject
    @end
    
    #import "MJPerson.h"
    
    @implementation MJPerson
    - (void)testABC:(NSString *)string arg2:(int)arg2{
        NSLog(@"%s",__func__);
    }
    @end
    
    控制器 只改了  id refreshingTarget = [MJPerson new];
    @implementation ViewController
    - (void)viewDidLoad{
        [super viewDidLoad];
        id refreshingTarget = [MJPerson new];
        SEL refreshingAction = @selector(testABC:arg2:);
        MJMsg_sendTest(refreshingTarget,refreshingAction,@"是我调用的你哈哈哈",24);
    }
    @end
    
    调用结果 image.png

    小结:
    1.objc_msgSend() 不管你.h文件有没有声明,只要你告诉我self是谁,调用哪个方法,我就能调出来;
    2.通过查看objc_msgSend()的定义,它默认是没有参数,就单纯是一个c函数,通过前面那一串转换的东西,把它转换成你想要的函数,并且可以添加多个参数.

    objc_msgSend(void /* id self, SEL op, ... */ )
    

    3.为什么最开始编译成C++代码的时候,方法会调用sel_registerName("testABC")这个函数,其实这个方法是最底层的方法,不管你是SEL,NSSelectorFromString(),method_getName底层都是调用sel_registerName

    SEL的底层
    SEL有疑问的,可以看看这位简友的 文章,非常详细,有理有据

    @implementation ViewController
    - (void)viewDidLoad{
        SEL refreshingAction = @selector(testABCDEFG);
    }
    @end
    
    编译之后的结果
    // @implementation ViewController
    
    static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
        ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
        SEL refreshingAction = sel_registerName("testABCDEFG");这里这里
    }
    
    
    

    这里有Foundation源码为证

    /**
     * Returns (creating if necessary) the selector whose name is supplied in the
     * aSelectorName argument, or 0 if a nil string is supplied.
     */
    SEL
    NSSelectorFromString(NSString *aSelectorName)
    {
      if (aSelectorName != nil)
        {
          int   len = [aSelectorName length];
          char  buf[len+1];
    
          [aSelectorName getCString: buf
                  maxLength: len + 1
                   encoding: NSASCIIStringEncoding];
          return sel_registerName (buf);  我在这里,看见没
        }
      return (SEL)0;
    }
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx👇下面这个是method_getName()源码
    SEL 
    method_getName(Method m)
    {
        if (!m) return nil;
    
        ASSERT(m->name == sel_registerName(sel_getName(m->name)));这里这里
        return m->name;
    }
    

    5. 对外部变量弱引用,内部重新赋值

    6.逻辑梳理 - 通过读这个部分,可以让读者在不阅读源码的情况下,对框架有大致的印象

    逻辑继承图.png
    @interface MJRefreshComponent : UIView
    {
        /** 记录scrollView刚开始的inset */
        UIEdgeInsets _scrollViewOriginalInset;
        /** 父控件 */
        __weak UIScrollView *_scrollView;
    }
    @end
    

    6.1> MJRefreshComponent 继承自 UIView,那它是怎么拿到父控件(_scrollView)的呢; 任何子控件添加到父控件之前都会调用下面的方法,通过这个方法,可以拿到父控件,这个newSuperview就是UIScrollView类型的,

    - (void)willMoveToSuperview:(UIView *)newSuperview{
        xxxxxxxxxxxxxxxxx
    }
    

    6.2> 我们再来看看MJRefresh的使用,因为header 和 footer的原理相同,就以header为例

     // 下拉刷新
        MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
           这里做下拉刷新的数据处理
        }];
    
        self.tableView.mj_header = header;
    

    6.3> 通过赋值的代码self.tableView.mj_header = header也能猜到,它是通过分类的方式添加了mj_header的属性,把这个header(其实就是MJRefreshComponent类型的View)赋值过去;那么它在这个set_Mj_header方法里又做了什么呢? ⬇️⬇️⬇️⬇️

    - (void)setMj_header:(MJRefreshHeader *)mj_header
    {
        if (mj_header != self.mj_header) {
            // 删除旧的,添加新的
            [self.mj_header removeFromSuperview];
            [self insertSubview:mj_header atIndex:0];
            
            // 存储新的
            objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                     mj_header, OBJC_ASSOCIATION_RETAIN);
        }
    }
    

    6.4> 在分类里存储新的header,设置成新的关联对象,并把header 插入到UIScrollView的最底层,这样就显示在最下面了.

    6.5> 接下来再回到MJRefreshComponent,看看它是怎么监听用户下拉刷新的操作呢 ---> 没错 就是KVO,监听contentOffsetcontentSize , 以及拖拽手势,对contentSize,contentOffset,contentInset有疑问的读者,可以阅读这篇,有图说明,简单易懂 ---->文章

    #pragma mark - KVO监听
    - (void)addObservers
    {
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
        [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
        self.pan = self.scrollView.panGestureRecognizer;
        [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
    }
    

    6.6> 监听后的处理就是通过子类实现父类的方法,得到上下拉刷新的数值变化,再进行相应的处理

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        // 遇到这些情况就直接返回
        if (!self.userInteractionEnabled) return;
        
        // 这个就算看不见也需要处理
        if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
            [self scrollViewContentSizeDidChange:change];
        }
        
        // 看不见
        if (self.hidden) return;
        if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
            [self scrollViewContentOffsetDidChange:change];
        } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
            [self scrollViewPanStateDidChange:change];
        }
    }
    

    6.7> 主要就是实现这三个方法,剩下的就是子类实现一些带gif,处理日期,动画,计算Label文字,菊花位置的操作了

    - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
    - (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
    - (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
    
    通过调用上述几个方法 给 state赋值,对label文字,显示时间等修改
    - (void)setState:(MJRefreshState)state
    {
        MJRefreshCheckState
        // 设置状态文字
        self.stateLabel.text = self.stateTitles[@(state)];
        // 重新设置key(重新显示时间)
        self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
    }
    

    6.8> header 和 footer 的位置布局是在- (void)layoutSubviews之前进行的,它是调用的自己写的方法 --->[self placeSubviews]

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

    6.9> 通过上述的简单介绍,对实现原理,调用流程有了基础认识
    逻辑一梳理,感觉也不是很难,硬着头皮,感觉我也行...

    难点

    1. UICollectionView 和 UITableView 两个大的数据显示列表要在下拉时候做到效果统一,确实有些难度,可能在调试header 和 footer 位置的时候,作者花了较多的时间.
    2. contentOffset的频繁改变,对数据的刷新 和 性能的要求都比较高,在重新布局的时候,容易出现错误.
    3. 带下拉动画的控件,在做动画调试的时候有一定难度,通过源码的动画实现可以看出这点.

    过程中出现了一个小插曲,粗心没有添加两个头文件,出现这个报错,一直找不到原因,self.mj_header不认识,今天早上无意间发现,最终代码完美运行...

    image.png

    Demo位置: MJRefreshTest

    相关文章

      网友评论

          本文标题:开源项目-阅读MJRefresh,你能学到什么(附注释Demo)

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