衆所周知Block已被廣泛用於iOS編程。它們通常被用作可併發執行的邏輯單元的封裝,或者作爲事件觸發的回調。Block比傳統回調函數有2點優勢:
允許在調用點上下文書寫執行邏輯,不用分離函數
Block可以使用local variables.
基於以上種種優點Cocoa Touch越發支持Block式編程,這點從UIView的各種動畫效果可用Block實現就可見一斑。而BlocksKit是對Cocoa Touch Block編程更進一步的支持,它簡化了Block編程,發揮Block的相關優勢,讓更多UIKit類支持Block式編程。BlocksKit是一個block的大雜燴,它給Fundation和UIKit框架裏很多的類都做了擴展,可以通過調用相關類的擴展的方法簡單的實現一下幾個功能:
- 1.通過block傳入事件處理函數
- 2.創建動態代理,傳入block給想要實現的方法。
- 3.在很多基礎的類上增加額外的方法
block可以幫助我們組織獨立的代碼段,並提高複用性和可讀性。而BlocksKit可以很簡單的實現block,實現回調,和通信,可以大大減少工作量。
BlocksKit目錄結構
BlocksKit代碼存放在4個目錄中分別是Core、DynamicDelegate、MessageUI、UIKit。其中:
- Core 存放Foundation Kit相關的Block category
- DynamicDelegate動態代理(一種事件轉發機制)相關代碼
- MessageUI 存放MessageUI相關的Block category
- UIKit 存放UIKit相關的Block category
Core相關代碼分析
Core文件夾下面的代碼可以分爲如下幾個部分:
- 1、容器相關(NSArray、NSDictionary、NSSet、NSIndexSet、NSMutableArray、NSMutableDictionary、NSMutableSet、NSMutableIndexSet)
- 2、關聯對象相關
- 3、邏輯執行相關
- 4、KVO相關
- 5、定時器相關
使用
做好上面的步驟之後,在代碼中使用就更簡單了,使用之前導入相應的頭文件
//Foundation框架:
#import "BlocksKit.h"
//UIKit框架:
#import "BlocksKit+UIKit.h"
//QuickLook框架:
#import "BlocksKit+QuickLook.h"
//MessageUI框架:
#import "BlocksKit+MessageUI.h"
1、Core舉例
(1)、容器相關的BlocksKit
不管是可變容器還是不可變容器,容器相關的BlocksKit代碼總體上說是對容器原生block相關函數的封裝。容器相關的BlocksKit函數更加接近自然語義,有一種函數式編程和語義編程的感覺。
部分不可變容器的BlocksKit聲明:
//串行遍歷容器中所有元素
- (void)bk_each:(void (^)(id obj))block;
//併發遍歷容器中所有元素(不要求容器中元素順次遍歷的時候可以使用此種遍歷方式來提高遍歷速度)
- (void)bk_apply:(void (^)(id obj))block;
//返回第一個符合block條件(讓block返回YES)的對象
- (id)bk_match:(BOOL (^)(id obj))block;
//返回所有符合block條件(讓block返回YES)的對象
- (NSArray *)bk_select:(BOOL (^)(id obj))block;
//返回所有!!!不符合block條件(讓block返回YES)的對象
- (NSArray *)bk_reject:(BOOL (^)(id obj))block;
//返回對象的block映射數組
- (NSArray *)bk_map:(id (^)(id obj))block;
//查看容器是否有符合block條件的對象
//判斷是否容器中至少有一個元素符合block條件
- (BOOL)bk_any:(BOOL (^)(id obj))block;
//判斷是否容器中所有元素都!!!不符合block條件
- (BOOL)bk_none:(BOOL (^)(id obj))block;
//判斷是否容器中所有元素都符合block條件
- (BOOL)bk_all:(BOOL (^)(id obj))block;
使用
NSString* str = [arr bk_match:^BOOL(id _Nonnull obj) {
return ((NSString *)obj).length == 1;
}];
NSArray* arr_01 = [arr bk_select:^BOOL(id _Nonnull obj) {
return ((NSString *)obj).length == 1;
}];
NSArray* arr_02 = [arr bk_reject:^BOOL(id _Nonnull obj) {
return ((NSString *)obj).length == 1;
}];
NSLog(@"str = %@",str);
NSLog(@"arr_01 = %@",arr_01);
NSLog(@"arr_02 = %@",arr_02);
打印:
2016-06-24 14:54:12.085 BlocksKitDemoTwo[12443:438922] str = 1
2016-06-24 14:54:12.085 BlocksKitDemoTwo[12443:438922] arr_01 = (
222,
433
)
2016-06-24 14:54:12.086 BlocksKitDemoTwo[12443:438922] arr_02 = (
222,
433
)
- 部分可變容器的BlocksKit聲明
/** Filters a mutable array to the objects matching the block.
@param block A single-argument, BOOL-returning code block.
@see <NSArray(BlocksKit)>bk_reject:
*/
//刪除容器中!!!不符合block條件的對象,即只保留符合block條件的對象
- (void)bk_performSelect:(BOOL (^)(id obj))block;
//刪除容器中符合block條件的對象
- (void)bk_performReject:(BOOL (^)(id obj))block;
//容器中的對象變換爲自己的block映射對象
- (void)bk_performMap:(id (^)(id obj))block;
(2)、關聯對象相關的BlocksKit
關聯對象的作用如下:
在類的定義之外爲類增加額外的存儲空間。使用關聯,我們可以不用修改類的定義而爲其對象增加存儲空間。這在我們無法訪問到類的源碼的時候或者是考慮到二進制兼容性的時候是非常有用。關聯是基於關鍵字的,因此,我們可以爲任何對象增加任意多的關聯,每個都使用不同的關鍵字即可。關聯是可以保證被關聯的對象在關聯對象的整個生命週期都是可用的(ARC下也不會導致資源不可回收)。
關聯對象的例子,在我們的實際項目中的常見用法一般有category中用關聯對象定義property,或者使用關聯對象綁定一個block。
關聯對象相關的BlocksKit是對objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects這幾個原生關聯對象函數的封裝。主要是封裝其其內存管理語義。
部分函數聲明如下
//@interface NSObject (BKAssociatedObjects)
//以OBJC_ASSOCIATION_RETAIN_NONATOMIC方式綁定關聯對象
- (void)bk_associateValue:(id)value withKey:(const void *)key;
//以OBJC_ASSOCIATION_COPY_NONATOMIC方式綁定關聯對象
- (void)bk_associateCopyOfValue:(id)value withKey:(const void *)key;
//以OBJC_ASSOCIATION_RETAIN方式綁定關聯對象
- (void)bk_atomicallyAssociateValue:(id)value withKey:(const void *)key;
//以OBJC_ASSOCIATION_COPY方式綁定關聯對象
- (void)bk_atomicallyAssociateCopyOfValue:(id)value withKey:(const void *)key;
//弱綁定
- (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key;
//刪除所有綁定的關聯對象
- (void)bk_removeAllAssociatedObjects;
(3)、邏輯執行相關的BlocksKit
所謂邏輯執行,就是Block塊執行。邏輯執行相關的BlocksKit是對dispatch_after函數的封裝。使其更加符合語義。
主要函數如下
//@interface NSObject (BKBlockExecution)
//主線程執行block方法,延遲時間可選
- (BKCancellationToken)bk_performAfterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
//後臺線程執行block方法,延遲時間可選
- (BKCancellationToken)bk_performInBackgroundAfterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
//所有執行block相關的方法都是此方法的簡化版,該函數在指定的block隊列上以指定的時間延遲執行block
- (BKCancellationToken)bk_performOnQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
//取消block,非常牛逼!!!一般來說一個block加到block queue上是沒法取消的,此方法實現了block的取消操作(必須是用BlocksKit投放的block)
+ (void)bk_cancelBlock:(id <NSObject, NSCopying>)block;
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
//cancelled是個__block變量,使得該block在加入queue後能夠邏輯上取消。注意,僅僅是邏輯上取消,不能把block從queue中剔除。
__block BOOL cancelled = NO;
//在外部block之上加一層能夠邏輯取消的代碼,使其變爲一個wrapper block
//當調用wrapper(YES)的時候就讓__block BOOL cancelled = YES,使得以後每次block主體都被跳過。
void (^wrapper)(BOOL) = ^(BOOL cancel) {
//cancel參數是爲了在外部能夠控制cancelled _block變量
if (cancel) {
cancelled = YES;
return;
}
if (!cancelled) block();
};
//每個投入queue中的block實際上是wraper版的block
dispatch_after(time, queue, ^{
//把cancel設置爲NO,block能夠邏輯執行
wrapper(NO);
});
//返回wraper block,以便bk_cancelBlock的時候使用
return wrapper;
}
+ (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
//把cancel設置爲YES,修改block中_block cancelled變量,如果此時block未執行則,block在執行的時候其邏輯主體會被跳過
void (^wrapper)(BOOL) = (void(^)(BOOL))block;
wrapper(YES);
}
(4)、KVO相關BlocksKit
KVO主要涉及兩類對象,即“被觀察對象“和“觀察者“。
與“被觀察對象”相關的函數主要有如下兩個:
//添加觀察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
//刪除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
//與“觀察者“相關的函數如下:
//觀察到對象發生變化後的回調函數(觀察回調)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
通常的KVO做法是先對“被觀察對象”添加“觀察者”,同時在“觀察者”中實現觀察回調。這樣每當“被觀察對象”的指定property改變時,“觀察者”就會調用觀察回調。
KVO相關BlocksKit弱化了“觀察者”這種對象,使得每當“被觀察對象”的指定property改變時,就會調起一個block。具體實現方式是定義一個_BKObserver類,讓該類實現觀察回調、對被觀察對象添加觀察者和刪除觀察者。
_BKObserver類定義如下:
@interface _BKObserver : NSObject {
BOOL _isObserving;
}
//存儲“被觀察的對象”
@property (nonatomic, readonly, unsafe_unretained) id observee;
@property (nonatomic, readonly) NSMutableArray *keyPaths;
//存儲回調block
@property (nonatomic, readonly) id task;
@property (nonatomic, readonly) BKObserverContext context;
- (id)initWithObservee:(id)observee keyPaths:(NSArray *)keyPaths context:(BKObserverContext)context task:(id)task;
@end
static void *BKObserverBlocksKey = &BKObserverBlocksKey;
static void *BKBlockObservationContext = &BKBlockObservationContext;
@implementation _BKObserver
- (id)initWithObservee:(id)observee keyPaths:(NSArray *)keyPaths context:(BKObserverContext)context task:(id)task
{
if ((self = [super init])) {
_observee = observee;
_keyPaths = [keyPaths mutableCopy];
_context = context;
_task = [task copy];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
//觀察者回調,在KV改變的時候調用相關block
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;
}
}
}
}
//開啓KV觀察
- (void)startObservingWithOptions:(NSKeyValueObservingOptions)options
{
@synchronized(self) {
if (_isObserving) return;
[self.keyPaths bk_each:^(NSString *keyPath) {
//observee的被觀察對象,observer是自己,
[self.observee addObserver:self forKeyPath:keyPath options:options context:BKBlockObservationContext];
}];
_isObserving = YES;
}
}
//停止KV觀察
- (void)stopObservingKeyPath:(NSString *)keyPath
{
NSParameterAssert(keyPath);
@synchronized (self) {
if (!_isObserving) return;
if (![self.keyPaths containsObject:keyPath]) return;
NSObject *observee = self.observee;
if (!observee) return;
[self.keyPaths removeObject: keyPath];
keyPath = [keyPath copy];
if (!self.keyPaths.count) {
_task = nil;
_observee = nil;
_keyPaths = nil;
}
[observee removeObserver:self forKeyPath:keyPath context:BKBlockObservationContext];
}
}
@end
使用BlocksKit
- (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)token options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSString *keyPath, NSDictionary *change))task;
- (void)bk_removeObserverForKeyPath:(NSString *)keyPath identifier:(NSString *)token;
(5)、定時器相關BlocksKit
NSTimer有個比較噁心的特性,它會持有它的target。比如在一個controller中使用了timer,並且timer的target設置爲該controller本身,那麼想在controller的dealloc中fire掉timer是做不到的,必須要在其他的地方fire。這會讓編碼很難受。具體參考《Effective Objective C》的最後一條。 BlocksKit解除這種噁心,其方式是把timer的target設置爲timer 的class對象。把要執行的block保存在timer的userInfo中執行。因爲timer 的class對象一直存在,所以是否被持有其實無所謂。
實現代碼如下:
//"Replaced with -bk_performAfterDelay:usingBlock:"
+ (id)bk_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
{
NSParameterAssert(block != nil);
return [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
}
+ (id)bk_timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
{
NSParameterAssert(block != nil);
return [self timerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
}
+ (void)bk_executeBlockFromTimer:(NSTimer *)aTimer {
void (^block)(NSTimer *) = [aTimer userInfo];
if (block) block(aTimer);
}
2、動態代理樣例
代理是objective c裏常用的模式,主要用來做邏輯切分,一個類做一類事情,讓代碼的耦合度減少。但他不方便的地方在於,要創建一個代理,就要定義一個類,聲明這個類遵循那些接口,然後實現這些接口對應的函數。動態代理(Dynamic delegate)則讓我們能夠在code裏,on the fly的創建這樣一個代理,通過block定義要實現的方法。
例如:
- (void)annoyUser
{
// 創建一個alert view
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:@"Hello World!"
message:@"This alert's delegate is implemented using blocks. That's so cool!"
delegate:nil
cancelButtonTitle:@"Meh."
otherButtonTitles:@"Woo!", nil];
// 獲取該alert view的動態代理對象(什麼是動態代理對象稍後會說)
A2DynamicDelegate *dd = alertView.bk_dynamicDelegate;
// 調用動態代理對象的 - (void)implementMethod:(SEL)selector withBlock:(id)block;方法,使得SEL映射一個block對象(假設叫做block1)
[dd implementMethod:@selector(alertViewShouldEnableFirstOtherButton:) withBlock:^(UIAlertView *alertView) {
NSLog(@"Message: %@", alertView.message);
return YES;
}];
// 同上,讓映射-alertView:willDismissWithButtonIndex:的SEL到另外一個block對象(假設叫做block2)
[dd implementMethod:@selector(alertView:willDismissWithButtonIndex:) withBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
NSLog(@"You pushed button #%d (%@)", buttonIndex, [alertView buttonTitleAtIndex:buttonIndex]);
}];
// 把alertView的delegate設置爲動態代理
alertView.delegate = dd;
[alertView show];
}
// 那麼,alert view在顯示的時候收到alertViewShouldEnableFirstOtherButton:消息調用block1;alert view在消失的時候收到alertView:willDismissWithButtonIndex:消息,調用block2
從上面的代碼我們可以直觀地看到:dd(動態代理對象)直接被設置爲alert view的delegate對象,那麼該alert view的UIAlertViewDelegate消息直接傳遞向給了dd。然後dd又通過某種方式把對應的SEL調用轉爲對應的block調用。我們又可以作出如下猜測:
- 1、dd內部可能有個dic一樣的數據結構,key可能是SEL,value可能是與之對應的block,通過implementMethod:withBlock:這個方法把SEL和block以鍵值對的形式建立起dic映射
- 2、Host對象(本例是alertView)向dd發delegate消息的時候傳遞了SEL,dd在內部的dic數據結構查找對應的block,找到後,調用該block。
3、UIKit相關的Block
拿UIControl打比方,要想處理一個事件:
- 以前:
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;1
需要通過上述方法將某一個對象的某一個selector傳入,一般的做法是在viewcontroller裏定義一個方法專門處理某一個按鈕的點擊事件。
- 現在:
- (void)bk_addEventHandler:(void (^)(id sender))handler forControlEvents:(UIControlEvents)controlEvents;1
通過上述方法將一個block註冊上去,不需要單獨定義方法。
例如:
[btn bk_addEventHandler:^(id _Nonnull sender) {
NSLog(@"111");
} forControlEvents:UIControlEventTouchUpInside];