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

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

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

推荐阅读更多精彩内容