美文网首页
开源项目-阅读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