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
調用結果:
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
調用結果小結:
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.邏輯梳理 - 通過讀這個部分,可以讓讀者在不閱讀源碼的情況下,對框架有大致的印象
@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