1 理解"塊"這一概念
- 塊的基礎(chǔ)知識
塊與函數(shù)類似,用"^"符號來表示,后面跟著一對花括號.括號里面是塊的實(shí)現(xiàn)代碼.如
^{ //Block implementation here }
塊其實(shí)就是一個(gè)值,而且自由其相關(guān)類型,與int,float或者OC對象一樣,也可以把塊賦值給變量,然后向其他變量那樣使用
塊類型的語法與函數(shù)指針類似,如
void (^someBlock)() = { //Block implementation here}
塊類型的語法結(jié)構(gòu)如下:
return_type (^block_name)(parameters)
定義一個(gè)接受2個(gè)int類型參數(shù),返回int類型的參數(shù)的block
int (^addBlock)(int a,int b) = ^(int a,int b){
return a+b;
};
定義好后,可以向函數(shù)一樣調(diào)用
int add = addBlock(2,5);//add = 7
塊的強(qiáng)大之處:在聲明它的范圍里,所有變量都可以為其所捕獲.也就是說,那個(gè)范圍內(nèi)的全部變量都可以在塊里使用
int additional = 5; int (^addBlock)(int a,int b) = ^(int a,int b){
return a+b+additional;
}; int add = addBlock(2,5);//add = 12
默認(rèn)情況下,為塊所捕獲的變量,是不可以在塊里修改的,如上代碼,如果在塊里修改了addition變量,那么編譯器會報(bào)錯.不過什么變量的時(shí)候如果加上__block修飾符,就可以在塊里修改了(自己理解為:不加__block捕獲的是變量的值,加了__block之后捕獲的是變量的地址)
如果塊所捕獲的變量是對象類型,那么就會自動保留它.系統(tǒng)在釋放這個(gè)快的時(shí)候,也會將其一并釋放.塊本身可視為對象.與其他對象一樣,有引用計(jì)數(shù).當(dāng)最后一個(gè)指向塊的引用移走之后,塊就回收了.回收時(shí)也會釋放塊所捕獲的變量,以便平衡捕獲時(shí)所執(zhí)行的保留操作.
如果將塊定義在OC類的實(shí)例方法中,那么除了可以訪問類的所有實(shí)例變量之外, 還可以使用self變量(當(dāng)前類的實(shí)例).塊總能修改實(shí)例變量,所以在聲明時(shí)無須加上__block.不過如果讀取或?qū)懭氩僮鞑东@了實(shí)例變量,那么也會自動把self變量一并捕獲,因?yàn)閷?shí)例變量是與self所指代的實(shí)例關(guān)聯(lián)在一起的.例如
#import "EOCClass.h" @implementation EOCClass{
NSString *_anInstanceVariable;
} - (void)anInstanceMethod{
//... void(^SomeBlock)() = ^{
_anInstanceVariable = @"something";
NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
};
//...
} @end
如果某個(gè)EOCClass實(shí)例在執(zhí)行anInstanceMethod方法,那么self變量就指向此實(shí)例,由于塊里沒有明確使用self變量,所以很容易忘記self變量其實(shí)也為塊所捕獲了.直接訪問變量和通過self來訪問是等效的:
self->_anInstanceVariable = @"something";
之所以要捕獲self變量,原因正在于此.我們經(jīng)常通過屬性來訪問實(shí)例變量,在這種情況下就需要指明self了:
self.aProperty = @"something";
然而,一定要記住:self也是個(gè)對象,因而塊在捕獲它時(shí)也會將其保留,如果self所指代的哪個(gè)對象同時(shí)也保留了塊,那么這種情況通常會導(dǎo)致"保留環(huán)".
2 塊的內(nèi)部結(jié)構(gòu)
每個(gè)OC對象都占據(jù)著某個(gè)內(nèi)存區(qū)域.塊本事也是對象,在存放塊對象的內(nèi)存區(qū)域中,首個(gè)變量是指向Class對象的指針,該指針叫做isa.其余內(nèi)存里含有塊對象正常運(yùn)轉(zhuǎn)所需的各種信息.如下圖:

在內(nèi)存布局中,最重要的就是invoke變量,這是個(gè)函數(shù)指針,指向塊的實(shí)現(xiàn)代碼,函數(shù)原型至少要接受一個(gè)void *型的參數(shù),此參數(shù)代表塊.塊其實(shí)就是一種代替函數(shù)指針的語法結(jié)構(gòu).
descriptor變量是指向結(jié)構(gòu)體的指針,每個(gè)塊里都包含此結(jié)構(gòu)體,其中聲明了塊對象的總體大小,還聲明了copy,dispose兩個(gè)輔助函數(shù)對應(yīng)的函數(shù)指針,輔助函數(shù)在拷貝丟棄塊對象時(shí)運(yùn)行,其中執(zhí)行一些操作,前者要保留捕獲的對象,后者則將之銷毀
塊還會把他所捕獲的所有變量都拷貝一份,這些拷貝放在descriptor變量后面,捕獲了多少個(gè)變量,就要占據(jù)多少內(nèi)存空間,拷貝的并不是變量本身,而是指向這些對象的指針變量.
invoke函數(shù)為何需要把塊對象作為參數(shù)傳進(jìn)來呢?原因在于,執(zhí)行塊時(shí),要從內(nèi)存中把這些捕獲到的變量讀出來.
3 全局塊,棧塊,堆塊
- 定義塊的時(shí)候,其所占的內(nèi)存區(qū)域是分配在棧中的,就是說,塊只在定義它的那個(gè)范圍有效.當(dāng)給塊對象發(fā)送copy消息.
可以把塊從棧賦值到堆. - 除"堆塊"與"棧塊"之外,還有一類叫做"全局塊"(global block).這種快不會捕捉任何狀態(tài)(比如外圍的常量等),運(yùn)行時(shí)也無需有狀態(tài)來參與.塊所使用的整個(gè)內(nèi)出去,在編譯器已經(jīng)完全確定了.因此全局塊可以聲明在全局內(nèi)存里.而不需要每次用到的時(shí)候于棧中創(chuàng)建.另外全局塊的拷貝操作是個(gè)空操作,因?yàn)槿謮K不可能為系統(tǒng)所回收,這種快實(shí)際上相當(dāng)于單例.
4 為常用的塊類型創(chuàng)建typedef
每個(gè)塊都具備其"固有類型"(inherent type).因而可以將其賦給適當(dāng)類型的變量.這個(gè)快有所接受的參數(shù)及其返回值組成
int (variableName)(BOOL flag,int value) = ^(BOOL flag,int value){
return someInt;
}
以上block接受2個(gè)類型分別為BOOL及int類型的參數(shù),返回int的值.在定義塊變量的時(shí)候,要把變量名放在類型之中.
為了隱藏負(fù)責(zé)的塊類型,可以使用C語言的"類型定義"(type definition)
typedef int(^EOCSomeBlock)(BOOL flag,int value); EOCSomeBlock block = ^(BOOL flag,int value){
return someInt;
}
5 用handler塊降低代碼分散程度
- 在創(chuàng)建對象時(shí),可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務(wù)邏輯一并聲明
- 在有多個(gè)實(shí)例需要監(jiān)控時(shí),如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對象來切換,而若該用handler塊來實(shí)現(xiàn),則可以直接將塊與相關(guān)對象放在一起
- 設(shè)計(jì)API時(shí),如果用到了handler,那么可以增加一個(gè)參數(shù),使調(diào)用者通過此參數(shù)來決定應(yīng)該把塊安排在哪個(gè)隊(duì)列上執(zhí)行
6 用塊引用其所屬對象時(shí)候不要出現(xiàn)保留環(huán)
- 如果塊所捕獲的對象直接或者間接地保留了塊本事,那么就的擔(dān)心保留換問題
- 一定要找適當(dāng)?shù)臅r(shí)機(jī)接觸保留環(huán),而不能把責(zé)任推給API調(diào)用者
7 多用派發(fā)隊(duì)列,少用同步鎖
1>在OC中,如果有多個(gè)線程要執(zhí)行同一份代碼,那么有可能會出問題,這種情況下,通常要使用鎖來實(shí)現(xiàn)某種同步機(jī)制.
在GCD之前,有2種辦法:
"同步塊"(synchronization block)
- (void)synchronizationMethod{
@synchronized(self){
//safe
}
}
然而濫用@synchronization(self)會降低代碼效率,因?yàn)楣餐靡粋€(gè)鎖的那些代碼塊,都必須按順序執(zhí)行,若是在self對象上加鎖,那么程序可能要等另外一段與此無關(guān)的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當(dāng)前代碼
另外一種直接使用NSLock對象
_lock = [[NSLock alloc] init]; - (void)synchronizedMethod{
[_lock lock];
//safe
[_lock unlock];
}
也可以使用NSRecursiveLock這種"遞歸鎖"(recursive lock),線程能夠多次持有該鎖,而不會出現(xiàn)死鎖(deadlock)現(xiàn)象
簡單而高效的辦法可以替代同步塊或鎖對象,那就是GCD的"串行同步隊(duì)列"(serial synchronization queue).將讀取操作和寫入操作都安排在同一個(gè)隊(duì)列里,即可保證數(shù)據(jù)同步.
@implementation EOCClass{
dispatch_queue_t _syncQueue;
} - (instancetype)init{
self = [super init];
if (self) {
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}
return self;
} - (void)setSomeString:(NSString *)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
} - (NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
8 多用GCD,少用performSelector系列方法
1 延時(shí)操作
//using performSelector:withObject:afterDelay: [self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0]; //using dispatch_after dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0*NSEC_PER_SEC)); dispatch_after(time, dispatch_get_main_queue(), ^{
[self doSomething];
});
2 回到主線程操作
//suing performSelectorOnMainThread:withObject:waitUntilDone: [self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO]; //using dispatch_async dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
對比:
- performSelector系列方法在內(nèi)存管理方法容易有疏失,他無法確定將要執(zhí)行的選擇器具體是什么,因而ARC編譯器也就無法插入適當(dāng)?shù)膬?nèi)存管理方法(他是在運(yùn)行期才確定選擇器的,這也是他的強(qiáng)大之處)
- performSelector系列方法所能處理的選擇器太過局限了,選擇器的返回值類型及發(fā)送給方法的參數(shù)個(gè)數(shù)都收到限制
- 如果想把任務(wù)放在另一個(gè)線程上執(zhí)行,那么最好不要用performSelector系列方法,而是應(yīng)該把任務(wù)封裝在塊里,然后調(diào)用GCD的相關(guān)方法實(shí)現(xiàn)
9 掌握GCD及操作隊(duì)列的使用時(shí)機(jī)
該章節(jié)列出了NSOperation與GCD的對比,在以下情況使用NSOperation比GCD更方便
- 取消某個(gè)操作,可以調(diào)用NSOperation對象的cancel方法,不過已經(jīng)啟動的任務(wù)無法取消
- 指定操作間的依耐
- 通過鍵值觀察NSOperation對象的屬性,如isCancelled, isFinished
- 指定操作的優(yōu)先級
10 通過Dispatch Group機(jī)制,根據(jù)系統(tǒng)執(zhí)行狀況來執(zhí)行任務(wù)
dispatch group是GCD的一項(xiàng)特性,能夠把任務(wù)分組.調(diào)用者可以等待這組任務(wù)執(zhí)行完畢,收到通知.最主要用處是把將要并發(fā)執(zhí)行的多個(gè)任務(wù)合為一組.調(diào)用著可以知道這些任務(wù)何時(shí)才能全部執(zhí)行完畢.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{
//do something
});
dispatch_group_async(group, queue, ^{
//do something else
});
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(group, notifyQueue, ^{
//continue processing after completing tasks
});
在向并發(fā)隊(duì)列派發(fā)任務(wù)的例子中,為了執(zhí)行隊(duì)列中塊,GCD對在適當(dāng)?shù)膶?shí)際自動創(chuàng)建新線程或復(fù)用舊線程.如果使用并發(fā)隊(duì)列,那么其中有可能會有多個(gè)線程,這 也就意味著多個(gè)塊可以并發(fā)執(zhí)行.在并發(fā)動隊(duì)列中,執(zhí)行任務(wù)所用的并發(fā)線程數(shù)量,取決于各個(gè)因素,而GCD主要是根據(jù)系統(tǒng)資源狀況來判斷這些因素的,開發(fā)者可以只用關(guān)系業(yè)務(wù)邏輯代碼,無需再為處理并發(fā)任務(wù)而編寫負(fù)責(zé)的調(diào)度器.
11 使用dispatch_once來執(zhí)行只需要運(yùn)行一次的線程安全代碼
+ (instancetype)sharedInstance{
staticEOCClass *instance = nil;
staticdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[selfalloc] init];
});
return instance; }
- 經(jīng)常需要編寫"只需要運(yùn)行一次的線程安全代碼",通過GCD的dispatch_once很容易實(shí)現(xiàn)次功能
- 標(biāo)記應(yīng)該聲明在static或global作用域中,這樣的話,在把只需要ahix一次的塊傳給dispatch_once函數(shù)時(shí),傳進(jìn)去的標(biāo)記也是相同的
12 不要使用dispatch_get_current_queue
dispatch_get_current_queue函數(shù)的行為常常與開發(fā)者所預(yù)期的不同,此函數(shù)已經(jīng)廢棄,可以使用NSThread替代
[NSThreadcurrentThread]