目標越接近,困難越增加。但願每一個人都像星星一樣安詳而從容地不斷沿着既定的目標走完自己的路程。
一. Block的基本概念
1. 什么是Block
蘋果在Mac OS X10.6 和iOS 4之后引入了block語法。這一舉動對於許多OC使用者的編碼風格改變很大。對於block蘋果文檔上有這樣的描述,block對象是一個C語言結構體,可以並入到C和Objective-C代碼中。block對象本質上是一個匿名函數,以及與該函數一起使用的數據,其他語言中有時稱為閉包或lambda。 block特別適用於回調,或者是在你為了讓代碼看起來具有更清晰的邏輯進行代碼的組合時使用。
上面的解釋,告訴我們: 首先,block是一個OC中的對象;並且,這個對象是一個C語言的結構體,它可以使用在C語言和OC中;同時,block本質上是一個匿名函數和其包含的數據集中。這應該就是block的定義了。
2. 為什么要使用Block
在iOS中,在以下情況下通常使用block(摘自於蘋果官方文檔):
- 代替代理和委托方法;
- 作為回調函數的替代;
- 一次性編寫多次處理的任務;
- 方便對集合中的所有項執行任務;
- 與GCD隊列一起執行異步任務;
這里告訴我們在以上情況下,我們都能夠使用block。我感受最深刻的是使用block進行回調。很多情況下,我們可能只需要對某個事件進行一個單一的回調,也許僅僅就一次,如果我使用代理的話,我需要創建類、編寫協議,僅僅對於一個小地方的回調成本很高,那么block登場就恰到好處了。 除此之外,block的特性可以讓代理集中在某處,我們只需要在一個地方就可以完成回調之前和回調時的代碼,相比,使用回調函數和代理都沒有這個優勢!另外,我們可以想到,OC中封裝好了一些集合的方法,比如,數組的排序,仔細會發現,這里就使用block進行回到操作的。
3. Block聲明和定義
我們先看一張蘋果官方文檔上的圖:
3.1)聲明一個Block變量
returnType (^blockName)(parameter1, parameter2, ...);
- returnType:
block
返回值類型,如果沒有返回值使用void
; - 必須包含blockName並且以
^
開頭,^
符號是block
的標志; - 參數列表可以和聲明函數一樣,只寫出形參類型不需寫出形參名稱;
3.2)定義一個Block基礎語法
^ retutnType (parameter1, parameter2, ...){
//block code
};
block
的標志就是^
,所有的block
必須以^
符號開頭;returnType
表示返回值類型,可以省略不寫,一般都省略不寫,編譯器會根據上下文自動補充返回值類型;- 參數列表包括參數類型和形參名稱;
3.3)定義Block的四種形式
// 1.無返回值、無參數
void (^notReturnNotParameter)(void) = ^{
};
// 2.無返回值、有參數
void (^notReturnIsParameter)(int) = ^(int parameter){
};
// 3.有返回值、無參數
NSString* (^isReturnNotParameter)(void) = ^{
return @"isReturnNotParameter";
};
// 4.有返回值、有參數
NSString* (^isReturnIsParameter)(NSString *) = ^(NSString *name){
return name;
};
3.4)Block類型
returnType (^blockName)(parameter1, parameter2, ...);
上面語法格式為聲明了一個block變量blockName
,那么blockName
變量的的類型應該寫為:returnType (^)(parameter1, parameter2, ...)
。
我們常使用typedef
定義塊變量類型,其語法為:
typdef returnType (^blockTypeName)(parameter1, parameter2,..);
這里的blockTypeName
為塊變量類型名,它區別於聲明時候的塊變量名。定義了塊變量類型就能夠重復定義塊變量。
3.5)Block調用
調用一個block,與C語言調用函數一致;
blockName();
二. Block捕獲變量
block捕獲變量總結:
-
auto變量,自動變量,平時我們定義
int age = 3
,前面有個auto,auto int age = 3
,系統幫我們默認的加上了一個auto。自動變量auto
修飾的變量,是有捕捉到block內部,並且是屬於值傳遞。 -
靜態變量
static
修飾的變量,例如static int age = 3
;static 修飾的age,是會捕獲進block內部,並且捕獲的是age的地址(指針傳遞),所以外面age改變的時候,block內部的age也會跟着改變,因為外部和block內部的age指向的是同一地址。 -
全局變量,沒有捕獲到block的內部,因為全局變量,作用域是所有函數,生命周期是程序結束,所以哪里都能改,哪里都能輸出它的值,所以沒必要捕獲進block的內部,並且無論block上面時候回調,全局變量的值都能正常輸出。
- (void)blockTest {
// Block捕獲變量
int age = 28;
void (^printAgeBlock)(void) = ^{
NSLog(@"age1 = %ld", age); // age1 = 28
};
NSLog(@"age2 = %ld", age); // age2 = 28
age = 30;
NSLog(@"age3 = %ld", age); // age3 = 30
printAgeBlock();
}
從輸出結果可以看出,執行block
之前進行的變量值修改並沒有影響到block
塊內的代碼,這是由於在定義block
塊的時候編譯器已經在編譯期將外部變量值賦給了block
內部變量(稱為“值捕獲”),在這時候進行了一次值拷貝,而不是在運行時賦值,因此外部變量的修改不會影響到內部block
的輸出。
如果捕獲是一個指針類型的變量則外部的修改會影響到內部,就和函數傳遞形參是一樣的道理,這個時候block
內部或持有這個對象,並增加引用計數,在block
結束釋放后,也會釋放持有的對象,減少引用計數,這里需要注意循環引用的問題,在后文中會講解。
上述block
塊內注釋了一段給age
重新賦值的代碼,因為在block
內部不允許修改捕獲的變量。
- (void)blockTest {
NSMutableString *nameMutString = [NSMutableString stringWithString:@"nutName"];
void (^printMutNameBlock)(void) = ^{
// nutName1 = nutName good!
NSLog(@"nutName1 = %@", nameMutString);
};
// nutName2 = nutName
NSLog(@"nutName2 = %@", nameMutString);
[nameMutString appendString:@" good!"];
// nutName3 = nutName good!
NSLog(@"nutName3 = %@", nameMutString);
printMutNameBlock();
}
三. __block的使用
如果希望block
捕獲的變量在外部修改后也可以影響block
內部,或是想在block
內部修改捕獲的變量,可以使用__block
關鍵字定義變量。
- (void)blockTest {
// __block
__block NSUInteger height = 65;
void (^printHeightBlock)(void) = ^{
// height1 = 70
NSLog(@"height1 = %ld", height);
height = 75;
};
// height2 = 65
NSLog(@"height2 = %ld", height);
height = 70;
// height3 = 70;
NSLog(@"height3 = %ld", height);
// height1 = 70
printHeightBlock();
// height4 = 75
NSLog(@"height4 = %ld", height);
}
上述代碼使用__block
定義變量height
,這樣定義以后編譯器會在block
定義的時候捕獲變量的引用而不是拷貝一個值。這樣,外部變量的修改就會影響到block
內部。
__block的使用原理:
- 在block的內部,訪問外部的變量時,block內部會對外部的變量進行一次拷貝,在block內部操作的是拷貝之后的副本,不會影響外部的變量,這個變量在堆區;
- 在block內部,修改外部變量,是不被允許的;
- 如果非要在block內部修改外部的變量,需要使用__block修飾外部變量;
- 一旦外部的int變量(在棧區)被__block標記了,如果block內部又修改了這個變量,那么這個變量的地址會永久的被修改在堆區 ;
- 如果外部變量是NSMutableString這樣本身就在堆區的,在block內部修改就不會報錯;
- 為什么在block的內部不能修改外部的變量? 因為block一般是需要傳遞給另外一個類里面,block內部的一些變量不能存儲在棧區,需要存在堆區,不然數據就容易丟失,這就是使用__block修飾的原因,這樣傳輸數據的時候,數據就不會丟失;
四. Block原理性知識
1. Block循環引用
(1.1)常見循環引用情況及解決辦法
使用block最常見的問題就是循環引用問題,循環引用也可能發生在delegate
或NSTimer
中,具體可以自行查閱。
前文講過如果block
內部訪問了外部變量會進行值捕獲,block
同樣是一個對象,也有引用計數,如果一個對象持有了一個block
而block
內部也捕獲了這個對象,那么就會產生循環引用。
@interface HBBlockViewController ()
{
void (^_cycleReferenceBlock)(void);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 循環引用
_cycleReferenceBlock = ^{
NSLog(@"%@",self); // 引發循環引用
};
}
遇到這種代碼編譯器只會告訴你存在警告,很多時候我們都是忽略警告的,這最后會導致內存泄露,兩者都無法釋放。跟普通變量存在__block
關鍵字一樣的,系統提供給我們__weak
的關鍵字用來修飾對象變量,聲明這是一個弱引用的對象,從而解決了循環引用的問題;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
__weak typeof(*&self)weakSelf = self;
_cycleReferenceBlock = ^{
NSLog(@"%@",weakSelf); //弱指針引用,不會造成循環引用
};
}
(1.2)嵌套block循環引用情況及解決辦法
比如block中有使用block,在調用self的時候又有延遲操作,延遲操作的waekSelf可能已經提前銷毀,輸出的為null。
__weak typeof(*&self)weakSelf = self;
// 嵌套block
_cycleReferenceBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf); // null
});
};
_cycleReferenceBlock();
優化方式,在內部在用__strong typeof修飾weakSelf:
__weak typeof(self)weakSelf = self;
_cycleReferenceBlock = ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf);
});
};
_cycleReferenceBlock();
對於block這種有趣的特性,在唐巧的談Objective-C block的實現有詳細介紹block的底層實現代碼,有興趣的可以去研究研究。
五. Block內存管理
后續補充!!!!
六. Block應用實例詳解
1. Block作為參數傳遞
定義一個block
塊然后作為C函數參數傳遞:
//使用typedef定義一個無返回值、有一個NSInteger類型的形參的block類型,該block名字為 HBNumberOperationBlock
typedef void (^HBNumberOperationBlock)(NSInteger);
//numberOperator函數,參數為一個numberArray數組和一個HBNumberOperationBlock塊類型
void numberOperator(NSArray *numberArray, HBNumberOperationBlock operationBlock) {
NSUInteger count = [numberArray count];
for (NSUInteger i = 0; i < count; i++) {
operationBlock([numberArray[i] integerValue]);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//定義一個數組
NSArray *numberArray = @[@1, @2, @3];
//直接調用numberOperator函數,實現一個匿名的block傳入成為“內聯塊”(inline block),在swift中成為“尾隨閉包”(Trailing closure)
// pow(number, 2) = 1
// pow(number, 2) = 4
// pow(number, 2) = 9
numberOperator(numberArray, ^(NSInteger number) {
NSLog(@"pow(number, 2) = %ld", number * number);
});
//定義一個HBNumberOperationBlock
HBNumberOperationBlock operationBlock = ^(NSInteger number) {
NSLog(@"add(number, number) = %ld", number + number);
};
//將上述定義的CJMNumberOperationBlock參數傳入
// add(number, number) = 2
// add(number, number) = 4
// add(number, number) = 6
numberOperator(numberArray, operationBlock);
}
return 0;
}
定義一個block
塊然后作為OC函數參數傳遞:
// NSArray+Category.h 文件
// 使用typedef定義一個無返回值、有一個NSInteger類型的形參的block類型,該block名字為 HBNumberOperationBlock
typedef void (^HBNumberOperationBlock)(NSInteger);
@interface NSArray (Category)
// numberOperator函數,參數為一個HBNumberOperationBlock塊類型。遍歷數組,並按照operationBlock處理數據
- (void)numberOperatorBlock:(HBNumberOperationBlock)operationBlock;
@end
// NSArray+Category.m 文件
@implementation NSArray (Category)
// 遍歷數組,並按照operationBlock處理數據
- (void)numberOperatorBlock:(HBNumberOperationBlock)operationBlock {
NSInteger count = [self count];
for (int i=0; i<count; i++) {
operationBlock([[self objectAtIndex:i] integerValue]);
};
}
@end
// 調用
// 007 - Block作為參數傳遞
- (void)blockParameterTest {
NSArray *number = @[@1, @2, @3];
[number numberOperatorBlock:^(NSInteger number) {
NSInteger pow = number * number;
NSLog(@"pow(number, 2) = %ld", pow);
// pow(number, 2) = 1
// pow(number, 2) = 4
// pow(number, 2) = 9
}];
HBNumberOperationBlock operationBlock = ^(NSInteger number) {
NSInteger add = number + number;
NSLog(@"add(number, number) = %ld", add);
// add(number, number) = 2
// add(number, number) = 4
// add(number, number) = 6
};
[number numberOperatorBlock:operationBlock];
}
2. 仿swift高階函數
其實上面的Block作為參數傳遞實例中就已經體現了用block仿swift高階函數的特性,接下來我們詳細研究下其應用。
用過swift的開發者都知道swift的函數調用很好的體現了鏈式編程的思想,即將多個操作通過.
連接起來,使得可讀性更強。這種編程方式的條件之一是每次函數調用必須有返回值。雖然在使用Objective-C開發的過程中,方法的調用是通過[target action]
的方式完成的,但是block本身的調用方式也是通過blockName(parameters)
的方式執行的,與這種鏈式函數有異曲同工之妙。
在swift中提供了包括map
、filter
、reduce
等十分簡潔優秀的高階函數供我們對數組數據進行操作,同樣情況下,遍歷一個數組並求和在使用oc(不使用kvc)和swift的環境下的代碼是這樣的:
#pragma mark - OC code
NSArray numbers = @[@10, @20, @30];
NSInteger totalNumber = 0;
for (NSNumber number in numbers) {
totalNumber += number.integerValue;
}
#pragma mark - swift code
let numbers = [10, 15, 99, 66, 25];
let totalNumber = numbers.reduce(0, { $0+$1 })
無論是代碼量還是簡潔性,此時的oc都比不上swift。那么接下來就要通過神奇的block來為oc添加這些高階函數的實現。為此我們需要新建一個NSArray
的分類擴展,命名為NSArray+HBExtension
// NSArray+HBExtension.h文件
// 數組元素轉換
typedef id(^HBItemMap)(id item);
typedef NSArray *(^HBArrayMap)(HBItemMap itemMap);
// 數組元素篩選
typedef BOOL(^HBItemFilter)(id item);
typedef NSArray *(^HBArrayFilter)(HBItemFilter itemFilter);
NS_ASSUME_NONNULL_BEGIN
/**
* 擴展數組高級方法仿swift調用
*/
@interface NSArray (HBExtension)
@property(nonatomic, copy, readonly)HBArrayMap map;
@property (nonatomic, copy, readonly) HBArrayFilter filter;
@end
前面說了為了實現鏈式編程,函數調用的前提是具有返回對象。因此我使用了typedef
聲明了幾個不同類型的block。雖然本質上HBArrayMap
和HBArrayFilter
兩個block是一樣的,但是為了區分它們的功能,還是建議這么做。其實現文件如下:
// NSArray+HBExtension.m文件
#import "NSArray+HBExtension.h"
@implementation NSArray (HBExtension)
- (HBArrayMap)map {
HBArrayMap map = ^id(HBItemMap itemMap) {
NSMutableArray *items = @[].mutableCopy;
for (id item in self) {
[items addObject:itemMap(item)];
}
return items;
};
return map;
}
- (HBArrayFilter)filter {
HBArrayFilter filter = ^(HBItemFilter itemFilter) {
NSMutableArray * items = @[].mutableCopy;
for (id item in self) {
if (itemFilter(item)) { [items addObject: item]; }
}
return items;
};
return filter;
}
- (void)setMap:(HBArrayMap _Nonnull)map {}
- (void)setFilter:(HBArrayFilter)filter {}
@end
我們通過重寫setter方法保證block不會被外部修改實現,並且在getter中遍歷數組的元素並調用傳入的執行代碼來實現map
和filter
等功能。對於這兩個功能的實現也很簡單,下面舉出兩個調用高階函數的例子:
#pragma mark - 篩選數組中大於20的數值並轉換成字符串
NSArray<NSNumber *> * numbers = @[@10, @15, @60, @22, @25, @28.1, @18.6, @11.2, @66.2];
NSArray * result = numbers.filter(^BOOL(NSNumber * item) {
return item.doubleValue > 20
}).map(^id(NSNumber * item) {
return [NSString stringWithFormat: @"string %g", item.doubleValue];
});
#pragma mark - 將數組中的字典轉換成對應的數據模型
NSArray<NSDictionary *> * jsons = @[@{ ... }, @{ ... }, @{ ... }];
NSArray<LXDModel *> * models = jsons.map(^id(id item) {
return [[LXDModel alloc] initWithJSON: item];
})
由於語法上的限制,雖然這樣的調用跟swift原生的調用對比起來還是復雜了,但通過block讓oc實現了函數鏈式調用的代碼看起來也清爽了很多
3. 作為回調函數的替代
在block出現之前,開發者實現回調基本都是通過代理的方式進行的。比如負責網絡請求的原生類NSURLConnection
類,通過多個協議方法實現請求中的事件處理。而在最新的環境下,使用的NSURLSession
已經采用block的方式處理任務請求了。各種第三方網絡請求框架也都在使用block進行回調處理。這種轉變很大一部分原因在於block使用簡單,邏輯清晰,靈活等原因。接下來我會完成一次網絡請求,然后通過block進行回調處理。
// HBDownloadManager.h文件
@interface HBDownloadManager : NSObject
typedef void(^HBDownloadHandler)(NSData *receiveData, NSError *error);
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(HBDownloadHandler)handler;
@end
// HBDownloadManager.m文件
@implementation HBDownloadManager
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(HBDownloadHandler)handler {
// 創建請求對象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]];
NSData *data = [NSJSONSerialization dataWithJSONObject:parameters options:NSJSONWritingPrettyPrinted error:nil];
[request setHTTPBody:data];
NSURLSession *session = [NSURLSession sharedSession];
// 執行請求任務
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(data, error);
});
}
}];
[task resume];
}
@end
上面通過封裝NSURLSession
的請求,傳入一個處理請求結果的block對象,就會自動將請求任務放到工作線程中執行實現,我們在網絡請求邏輯的代碼中調用如下:
[[HBDownloadManager alloc] downloadWithURL:@"" parameters:nil handler ^(NSData * receiveData, NSError * error) {
if (error) { NSLog(@"下載失敗:%@", error) }
else {
//處理下載數據
}
}
4. Block傳值
傳值分為兩種,順傳是給需要傳值的對象直接定義屬性就能傳值。逆傳可以用代理、block方式實現。具體代碼后期有時間再寫!!!
七. 最后
block捕獲變量、代碼傳遞、代碼內聯等特性賦予了它多於代理機制的功能和靈活性,盡管它也存在循環引用、不易調試追溯等缺陷,但無可置疑它的優點深受碼農們的喜愛。如何更加靈活的使用block需要我們對它不斷的使用、探究了解才能完成。