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__)
,这里我定义了四个参数,前面两个固定的,不用多说,后面我传了一个NSString
和int
.
注意一点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,监听contentOffset
和 contentSize
, 以及拖拽手势,对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> 通过上述的简单介绍,对实现原理,调用流程有了基础认识
逻辑一梳理,感觉也不是很难,硬着头皮,感觉我也行...
难点
- UICollectionView 和 UITableView 两个大的数据显示列表要在下拉时候做到效果统一,确实有些难度,可能在调试header 和 footer 位置的时候,作者花了较多的时间.
- contentOffset的频繁改变,对数据的刷新 和 性能的要求都比较高,在重新布局的时候,容易出现错误.
- 带下拉动画的控件,在做动画调试的时候有一定难度,通过源码的动画实现可以看出这点.
过程中出现了一个小插曲,粗心没有添加两个头文件,出现这个报错,一直找不到原因,self.mj_header
不认识,今天早上无意间发现,最终代码完美运行...
Demo位置: MJRefreshTest
网友评论