神奇的 BlocksKit (一)
關注倉庫,及時獲得更新: iOS-Source-Code-Analyze
Follow: Draveness · Github
高能預警:本篇文章非常長,因為 BlocksKit 的實現還是比較復雜和有意的。這篇文章不是為了剖析 iOS 開發中的 block 的實現以及它是如何組成甚至使用的,如果你想通過這篇文章來了解 block 的實現,它并不能幫到你。
Block 到底是什么?這可能是困擾很多 iOS 初學者的一個問題。如果你在 Google 上搜索類似的問題時,可以查找到幾十萬條結果,block 在 iOS 開發中有著非常重要的地位,而且它的作用也越來越重要。
概述
這篇文章僅對 BlocksKit v2.2.5 的源代碼進行分析,從框架的內部理解下面的功能是如何實現的:
- 為 NSArray 、 NSDictionary 和 NSSet 等集合類型以及對應的可變集合類型 NSMutableArray 、 NSMutableDictionary 和 NSMutableSet 添加 bk_each: 等方法完成對集合中元素的 快速遍歷
- 使用 block 對 NSObject 對象 KVO
- 為 UIView 對象添加 bk_whenTapped: 等方法快速添加手勢
- 使用 block 替換 UIKit 中的 delegate ,涉及到核心模塊 DynamicDelegate 。
BlocksKit 框架中包括但不僅限于上述的功能,這篇文章是對 v2.2.5 版本源代碼的分析,其它版本的功能不會在本篇文章中具體討論。
如何提供簡潔的遍歷方法
BlocksKit 實現的最簡單的功能就是為集合類型添加方法遍歷集合中的元素。
[@[@1,@2,@3] bk_each:^(id obj) {
NSLog(@"%@",obj);
}];
這段代碼非常簡單,我們可以使用 enumerateObjectsUsingBlock: 方法替代 bk_each: 方法:
[@[@1,@2,@3] enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {
NSLog(@"%@",obj);
}];
2016-03-05 16:02:57.295 Draveness[10725:453402] 1
2016-03-05 16:02:57.296 Draveness[10725:453402] 2
2016-03-05 16:02:57.297 Draveness[10725:453402] 3
這部分代碼的實現也沒什么難度:
- (void)bk_each:(void (^)(id obj))block
{
NSParameterAssert(block != nil);
[self enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {
block(obj);
}];
}
它在 block 執行前會判斷傳進來的 block 是否為空,然后就是調用遍歷方法,把數組中的每一個 obj 傳給 block。
BlocksKit 在這些集合類中所添加的一些方法在 Ruby、Haskell 等語言中也同樣存在。如果你接觸過上面的語言,理解這里方法的功能也就更容易了,不過這不是這篇文章關注的主要內容。
// NSArray+BlocksKit.h
- (void)bk_each:(void (^)(id obj))block;
- (void)bk_apply:(void (^)(id obj))block;
- (id)bk_match:(BOOL (^)(id obj))block;
- (NSArray *)bk_select:(BOOL (^)(id obj))block;
- (NSArray *)bk_reject:(BOOL (^)(id obj))block;
- (NSArray *)bk_map:(id (^)(id obj))block;
- (id)bk_reduce:(id)initial withBlock:(id (^)(id sum,id obj))block;
- (NSInteger)bk_reduceInteger:(NSInteger)initial withBlock:(NSInteger(^)(NSInteger result,id obj))block;
- (CGFloat)bk_reduceFloat:(CGFloat)inital withBlock:(CGFloat(^)(CGFloat result,id obj))block;
- (BOOL)bk_any:(BOOL (^)(id obj))block;
- (BOOL)bk_none:(BOOL (^)(id obj))block;
- (BOOL)bk_all:(BOOL (^)(id obj))block;
- (BOOL)bk_corresponds:(NSArray *)list withBlock:(BOOL (^)(id obj1,id obj2))block;
NSObject 上的魔法
NSObject 是 iOS 中的『上帝類』。
在 NSObject 上添加的方法幾乎會添加到 Cocoa Touch 中的所有類上,關于 NSObject 的討論和總共分為以下三部分進行:
-
AssociatedObject
-
BlockExecution
-
BlockObservation
添加 AssociatedObject
經常跟 runtime 打交道的人不可能不知道 AssociatedObject ,當我們想要為一個已經存在的類添加屬性時,就需要用到 AssociatedObject 為類添加屬性,而 BlocksKit 提供了更簡單的方法來實現,不需要新建一個分類。
NSObject *test = [[NSObject alloc] init];
[test bk_associateValue:@"Draveness" withKey:@" name"];
NSLog(@"%@",[test bk_associatedValueForKey:@"name"]);
2016-03-05 16:02:25.761 Draveness[10699:452125] Draveness
這里我們使用了 bk_associateValue:withKey: 和 bk_associatedValueForKey: 兩個方法設置和獲取 name 對應的值 Draveness .
- (void)bk_associateValue:(id)value withKey:(const void *)key
{
objc_setAssociatedObject(self,key,value,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
這里的 OBJC_ASSOCIATION_RETAIN_NONATOMIC 表示當前屬性為 retain nonatomic 的,還有其它的參數如下:
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t,objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,/**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
上面的這個 NS_ENUM 也沒什么好說的,需要注意的是這里沒有 weak 屬性。
BlocksKit 通過另一種方式實現了『弱屬性』:
- (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key
{
_BKWeakAssociatedObject *assoc = objc_getAssociatedObject(self,key);
if (!assoc) {
assoc = [_BKWeakAssociatedObject new];
[self bk_associateValue:assoc withKey:key];
}
assoc.value = value;
}
在這里先獲取了一個 _BKWeakAssociatedObject 對象 assoc ,然后更新這個對象的屬性 value 。
因為直接使用 AssociatedObject 不能為對象添加弱屬性,所以在這里添加了一個對象,然后讓這個對象持有一個弱屬性:
@interface _BKWeakAssociatedObject : NSObject
@property (nonatomic,weak) id value;
@end
@implementation _BKWeakAssociatedObject
@end
這就是 BlocksKit 實現弱屬性的方法,我覺得這個實現的方法還是比較簡潔的。
getter 方法的實現也非常類似:
- (id)bk_associatedValueForKey:(const void *)key
{
id value = objc_getAssociatedObject(self,key);
if (value && [value isKindOfClass:[_BKWeakAssociatedObject class]]) {
return [(_BKWeakAssociatedObject *)value value];
}
return value;
}
在任意對象上執行 block
通過這個類提供的一些接口,可以在任意對象上快速執行線程安全、異步的 block,而且這些 block 也可以在執行之前取消。
- (id <NSObject,NSCopying>)bk_performOnQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block
{
NSParameterAssert(block != nil);
return BKDispatchCancellableBlock(queue,delay,^{
block(self);
});
}
判斷 block 是否為空在這里都是細枝末節,這個方法中最關鍵的也就是它返回了一個可以取消的 block,而這個 block 就是用靜態函數 BKDispatchCancellableBlock 生成的。
static id <NSObject,NSCopying> BKDispatchCancellableBlock(dispatch_queue_t queue,NSTimeInterval delay,void(^block)(void)) {
dispatch_time_t time = BKTimeDelay(delay);
#if DISPATCH_CANCELLATION_SUPPORTED
if (BKSupportsDispatchCancellation()) {
dispatch_block_t ret = dispatch_block_create(0,block);
dispatch_after(time,queue,ret);
return ret;
}
#endif
__block BOOL cancelled = NO;
void (^wrapper)(BOOL) = ^(BOOL cancel) {
if (cancel) {
cancelled = YES;
return;
}
if (!cancelled) block();
};
dispatch_after(time,queue,^{
wrapper(NO);
});
return wrapper;
}
這個函數首先會執行 BKSupportsDispatchCancellation 來判斷當前平臺和版本是否支持使用 GCD 取消 block,當然一般都是支持的:
-
函數返回的是 YES ,那么在 block 被派發到指定隊列之后就會返回這個 dispatch_block_t 類型的 block
-
函數返回的是 NO ,那么就會就會手動包裝一個可以取消的 block,具體實現的部分如下:
__block BOOL cancelled = NO;
void (^wrapper)(BOOL) = ^(BOOL cancel) {
if (cancel) {
cancelled = YES;
return;
}
if (!cancelled) block();
};
dispatch_after(time,queue,^{
wrapper(NO);
});
return wrapper;
上面這部分代碼就先創建一個 wrapper block,然后派發到指定隊列,派發到指定隊列的這個 block 是一定會執行的,但是怎么取消這個 block 呢?
如果當前 block 沒有執行,我們在外面調用一次 wrapper(YES) 時,block 內部的 cancelled 變量就會被設置為 YES ,所以 block 就不會執行。
-
dispatch_after --- cancelled = NO
-
wrapper(YES) --- cancelled = YES
-
wrapper(NO) --- cancelled = YES block 不會執行
這是實現取消的關鍵部分:
+ (void)bk_cancelBlock:(id <NSObject,NSCopying>)block
{
NSParameterAssert(block != nil);
#if DISPATCH_CANCELLATION_SUPPORTED
if (BKSupportsDispatchCancellation()) {
dispatch_block_cancel((dispatch_block_t)block);
return;
}
#endif
void (^wrapper)(BOOL) = (void(^)(BOOL))block;
wrapper(YES);
}
-
GCD 支持取消 block,那么直接調用 dispatch_block_cancel 函數取消 block
-
GCD 不支持取消 block 那么調用一次 wrapper(YES)
使用 Block 封裝 KVO
BlocksKit 對 KVO 的封裝由兩部分組成:
NSObject
_BKObserver
提供接口并在 dealloc 時停止 BlockObservation
NSObject+BKBlockObservation 這個分類中的大部分接口都會調用這個方法:
- (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)identifier options:(NSKeyValueObservingOptions)options context:(BKObserverContext)context task:(id)task
{
#1: 檢查參數,省略
#2: 使用神奇的方法在分類中覆寫 dealloc
NSMutableDictionary *dict;
_BKObserver *observer = [[_BKObserver alloc] initWithObservee:self keyPaths:keyPaths context:context task:task];
[observer startObservingWithOptions:options];
#3: 惰性初始化 bk_observerBlocks 也就是下面的 dict,省略
dict[identifier] = observer;
}
我們不會在這里討論 #1 、 #3 部分,再詳細閱讀 #2 部分代碼之前,先來看一下這個省略了絕大部分細節的核心方法。
使用傳入方法的參數創建了一個 _BKObserver 對象,然后調用 startObservingWithOptions: 方法開始 KVO 觀測相應的屬性,然后以 {identifier,obeserver} 的形式存到字典中保存。
這里實在沒什么新意,我們在下一小節中會介紹 startObservingWithOptions: 這一方法。
在分類中調劑 dealloc 方法
這個問題我覺得是非常值得討論的一個問題,也是我最近在寫框架時遇到很棘手的一個問題。
當我們在分類中注冊一些通知或者使用 KVO 時,很有可能會找不到注銷這些通知的時機。
因為在 分類中是無法直接實現 dealloc 方法的 。 在 iOS8 以及之前的版本,如果某個對象被釋放了,但是剛對象的注冊的通知沒有被移除,那么當事件再次發生,就會 向已經釋放的對象發出通知 ,整個程序就會崩潰。
這里解決的辦法就十分的巧妙:
Class classToSwizzle = self.class;
// 獲取所有修改過 dealloc 方法的類
NSMutableSet *classes = self.class.bk_observedClassesHash;
// 保證互斥避免 classes 出現難以預測的結果
@synchronized (classes) {
// 獲取當前類名,并判斷是否修改過 dealloc 方法以減少這部分代碼的調用次數
NSString *className = NSStringFromClass(classToSwizzle);
if (![classes containsObject:className]) {
// 這里的 sel_registerName 方法會返回 dealloc 的 selector,因為 dealloc 已經注冊過
SEL deallocSelector = sel_registerName("dealloc");
__block void (*originalDealloc)(__unsafe_unretained id,SEL) = NULL;
// 實現新的 dealloc 方法
id newDealloc = ^(__unsafe_unretained id objSelf) {
//在方法 dealloc 之前移除所有 observer
[objSelf bk_removeAllBlockObservers];
if (originalDealloc == NULL) {
// 如果原有的 dealloc 方法沒有被找到就會查找父類的 dealloc 方法,調用父類的 dealloc 方法
struct objc_super superInfo = {
.receiver = objSelf,
.super_class = class_getSuperclass(classToSwizzle)
};
void (*msgSend)(struct objc_super *,SEL) = (__typeof__(msgSend))objc_msgSendSuper;
msgSend(&superInfo,deallocSelector);
} else {
// 如果 dealloc 方法被找到就會直接調用該方法,并傳入參數
originalDealloc(objSelf,deallocSelector);
}
};
// 構建選擇子實現 IMP
IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);
// 向當前類添加方法,但是多半不會成功,因為類已經有 dealloc 方法
if (!class_addMethod(classToSwizzle,deallocSelector,newDeallocIMP,"v@:")) {
// 獲取原有 dealloc 實例方法
Method deallocMethod = class_getInstanceMethod(classToSwizzle,deallocSelector);
// 存儲 dealloc 方法實現防止在 set 的過程中調用該方法
originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_getImplementation(deallocMethod);
// 重新設置 dealloc 方法的實現,并存儲到 originalDealloc 防止方法實現改變
originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_setImplementation(deallocMethod,newDeallocIMP);
}
// 將當前類名添加到已經改變的類的集合中
[classes addObject:className];
}
}
這部分代碼的執行順序如下:
-
首先調用 bk_observedClassesHash 類方法獲取所有修改過 dealloc 方法的類的集合 classes
-
使用 @synchronized (classes) 保證互斥,避免同時修改 classes 集合的類過多出現意料之外的結果
-
判斷即將調劑方法的類 classToSwizzle 是否調劑過 dealloc 方法
-
如果 dealloc 方法沒有調劑過,就會通過 sel_registerName("dealloc") 方法獲取選擇子,這行代碼并不會真正注冊 dealloc 選擇子而是會獲取 dealloc 的選擇子,具體原因可以看這個方法的實現 sel_registerName
-
在新的 dealloc 中 添加移除 Observer 的方法 , 再調用原有的 dealloc
id newDealloc = ^(__unsafe_unretained id objSelf) {
[objSelf bk_removeAllBlockObservers];
if (originalDealloc == NULL) {
struct objc_super superInfo = {
.receiver = objSelf,
.super_class = class_getSuperclass(classToSwizzle)
};
void (*msgSend)(struct objc_super *,SEL) = (__typeof__(msgSend))objc_msgSendSuper;
msgSend(&superInfo,deallocSelector);
} else {
originalDealloc(objSelf,deallocSelector);
}
};
IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);
-
-
調用 bk_removeAllBlockObservers 方法移除所有觀察者,也就是這段代碼的最終目的
-
根據 originalDealloc 是否為空,決定是向父類發送消息,還是直接調用 originalDealloc 并傳入 objSelf,deallocSelector 作為參數
-
-
在我們獲得了新 dealloc 方法的選擇子和 IMP 時,就要改變原有的 dealloc 的實現了
if (!class_addMethod(classToSwizzle,deallocSelector,newDeallocIMP,"v@:")) { // The class already contains a method implementation. Method deallocMethod = class_getInstanceMethod(classToSwizzle,deallocSelector); // We need to store original implementation before setting new implementation // in case method is called at the time of setting. originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_getImplementation(deallocMethod); // We need to store original implementation again,in case it just changed. originalDealloc = (void(*)(__unsafe_unretained id,SEL))method_setImplementation(deallocMethod,newDeallocIMP); }
-
調用 class_addMethod 方法為當前類添加選擇子為 dealloc 的方法(當然 99.99% 的可能不會成功)
-
獲取原有的 dealloc 實例方法
-
將原有的實現保存到 originalDealloc 中,防止使用 method_setImplementation 重新設置該方法的過程中調用 dealloc 導致無方法可用
-
重新設置 dealloc 方法的實現。同樣,將實現存儲到 originalDealloc 中防止實現改變
-
關于在分類中調劑 dealloc 方法的這部分到這里就結束了,下一節將繼續分析私有類 _BKObserver 。
私有類 _BKObserver
_BKObserver 是用來觀測屬性的對象,它在接口中定義了 4 個屬性:
@property (nonatomic,readonly,unsafe_unretained) id observee;
@property (nonatomic,readonly) NSMutableArray *keyPaths;
@property (nonatomic,readonly) id task;
@property (nonatomic,readonly) BKObserverContext context;
上面四個屬性的具體作用在這里不說了,上面的 bk_addObserverForKeyPaths:identifier:options:context: 方法中調用 _BKObserver 的初始化方法 initWithObservee:keyPaths:context:task: 太簡單了也不說了。
_BKObserver *observer = [[_BKObserver alloc] initWithObservee:self keyPaths:keyPaths context:context task:task];
[observer startObservingWithOptions:options];
上面的第一行代碼生成一個 observer 實例之后立刻調用了 startObservingWithOptions: 方法開始觀測對應的 keyPath :
- (void)startObservingWithOptions:(NSKeyValueObservingOptions)options
{
@synchronized(self) {
if (_isObserving) return;
#1:遍歷 keyPaths 實現 KVO
_isObserving = YES;
}
}
startObservingWithOptions: 方法最重要的就是第 #1 部分:
[self.keyPaths bk_each:^(NSString *keyPath) {
[self.observee addObserver:self forKeyPath:keyPath options:options context:BKBlockObservationContext];
}];
遍歷自己的 keyPaths 然后讓 _BKObserver 作觀察者觀察自己,然后傳入對應的 keyPath 。
關于 _stopObservingLocked 方法的實現也十分的相似,這里就不說了。
[keyPaths bk_each:^(NSString *keyPath) {
[observee removeObserver:self forKeyPath:keyPath context:BKBlockObservationContext];
}];
到目前為止,我們還沒有看到實現 KVO 所必須的方法 observeValueForKeyPath:ofObject:change:context ,這個方法就是每次屬性改變之后的回調:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context != BKBlockObservationContext) return;
@synchronized(self) {
switch (self.context) {
case BKObserverContextKey: {
void (^task)(id) = self.task;
task(object);
break;
}
case BKObserverContextKeyWithChange: {
void (^task)(id,NSDictionary *) = self.task;
task(object,change);
break;
}
case BKObserverContextManyKeys: {
void (^task)(id,NSString *) = self.task;
task(object,keyPath);
break;
}
case BKObserverContextManyKeysWithChange: {
void (^task)(id,NSString *,NSDictionary *) = self.task;
task(object,keyPath,change);
break;
}
}
}
}
這個方法的實現也很簡單,根據傳入的 context 值,對 task 類型轉換,并傳入具體的值。
這個模塊倒著就介紹完了,在下一節會介紹 BlocksKit 對 UIKit 組件一些簡單的改造。
改造 UIKit
在這個小結會具體介紹 BlocksKit 是如何對一些簡單的控件進行改造的,本節大約有兩部分內容:
-
UIGestureRecongizer + UIBarButtonItem + UIControl
-
UIView
改造 UIGestureRecongizer,UIBarButtonItem 和 UIControl
先來看一個 UITapGestureRecognizer 使用的例子
UITapGestureRecognizer *singleTap = [UITapGestureRecognizer bk_recognizerWithHandler:^(id sender) {
NSLog(@"Single tap.");
} delay:0.18];
[self addGestureRecognizer:singleTap];
代碼中的 bk_recognizerWithHandler:delay: 方法在最后都會調用初始化方法 bk_initWithHandler:delay: 生成一個 UIGestureRecongizer 的實例
- (instancetype)bk_initWithHandler:(void (^)(UIGestureRecognizer *sender,UIGestureRecognizerState state,CGPoint location))block delay:(NSTimeInterval)delay
{
self = [self initWithTarget:self action:@selector(bk_handleAction:)];
if (!self) return nil;
self.bk_handler = block;
self.bk_handlerDelay = delay;
return self;
}
它會在這個方法中傳入 target 和 selector 。 其中 target 就是 self ,而 selector 也會在這個分類中實現:
- (void)bk_handleAction:(UIGestureRecognizer *)recognizer
{
void (^handler)(UIGestureRecognizer *sender,UIGestureRecognizerState state,CGPoint location) = recognizer.bk_handler;
if (!handler) return;
NSTimeInterval delay = self.bk_handlerDelay;
#1: 封裝 block 并控制 block 是否可以執行
self.bk_shouldHandleAction = YES;
[NSObject bk_performAfterDelay:delay usingBlock:block];
}
因為在初始化方法 bk_initWithHandler:delay: 中保存了當前手勢的 bk_handler ,所以直接調用在 Block Execution 一節中提到過的 bk_performAfterDelay:usingBlock: 方法,將 block 派發到指定的隊列中,最終完成對 block 的調用。
封裝 block 并控制 block 是否可以執行
這部分代碼和前面的部分有些相似,因為這里也用到了一個屬性 bk_shouldHandleAction 來控制 block 是否會被執行:
CGPoint location = [self locationInView:self.view];
void (^block)(void) = ^{
if (!self.bk_shouldHandleAction) return;
handler(self,self.state,location);
};
====
同樣 UIBarButtonItem 和 UIControl 也是用了幾乎相同的機制,把 target 設置為 self ,讓后在分類的方法中調用指定的 block。
UIControlWrapper
稍微有些不同的是 UIControl 。因為 UIControl 有多種 UIControlEvents ,所以使用另一個類 BKControlWrapper 來封裝 handler 和 controlEvents
@property (nonatomic) UIControlEvents controlEvents;
@property (nonatomic,copy) void (^handler)(id sender);
其中 UIControlWrapper 對象以 {controlEvents,wrapper} 的形式作為 UIControl 的屬性存入字典。
改造 UIView
因為在上面已經改造過了 UIGestureRecognizer ,在這里改造 UIView 就變得很容易了:
- (void)bk_whenTouches:(NSUInteger)numberOfTouches tapped:(NSUInteger)numberOfTaps handler:(void (^)(void))block
{
if (!block) return;
UITapGestureRecognizer *gesture = [UITapGestureRecognizer bk_recognizerWithHandler:^(UIGestureRecognizer *sender,UIGestureRecognizerState state,CGPoint location) {
if (state == UIGestureRecognizerStateRecognized) block();
}];
gesture.numberOfTouchesRequired = numberOfTouches;
gesture.numberOfTapsRequired = numberOfTaps;
[self.gestureRecognizers enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {
if (![obj isKindOfClass:[UITapGestureRecognizer class]]) return;
UITapGestureRecognizer *tap = obj;
BOOL rightTouches = (tap.numberOfTouchesRequired == numberOfTouches);
BOOL rightTaps = (tap.numberOfTapsRequired == numberOfTaps);
if (rightTouches && rightTaps) {
[gesture requireGestureRecognizerToFail:tap];
}
}];
[self addGestureRecognizer:gesture];
}
UIView 分類只有這一個核心方法,其它的方法都是向這個方法傳入不同的參數,這里需要注意的就是。它會遍歷所有的 gestureRecognizers ,然后把對所有有沖突的手勢調用 requireGestureRecognizerToFail: 方法,保證添加的手勢能夠正常的執行。
由于這篇文章中的內容較多,所以內容分成了兩個部分,下一部分介紹的是 BlocksKit 中的最重要的部分動態代理:
關注倉庫,及時獲得更新: iOS-Source-Code-Analyze
Follow: Draveness · Github
來自: https://segmentfault.com/a/1190000005350502