當(dāng)前多線程編程的核心就是“塊”(block)與“大中樞派發(fā)”(Grand Central Dispatch,GCD)。
37.理解“塊”這一概念
1.塊的基礎(chǔ)知識
塊與函數(shù)類似,只不過是直接定義在另一個(gè)函數(shù)里的,和定義它的那個(gè)函數(shù)共享同一個(gè)范圍內(nèi)的東西洗做。塊用“^”符號來表示,后面跟著一對花括號,括號里面是塊的實(shí)現(xiàn)代碼义郑。
^{
//Block implementation
}
//塊類型的語法結(jié)構(gòu)及事例
return_type (^block_name)(parameters)
//使用
block_name(parameters);
eg:
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
}
//使用
int result = addBlock(3,5);
塊的強(qiáng)大之處在于:在聲明它的范圍里,所有變量都可以為其所捕獲丈钙。這就是說非驮,塊所在的范圍里的全部變量,在塊里依然可用雏赦。默認(rèn)情況下劫笙,為塊所捕獲的變量,是不可以在塊里修改的喉誊,聲明變量的時(shí)候可以加上__block修飾符邀摆,這樣就可以在塊內(nèi)修改了。對于實(shí)例變量伍茄,塊總是能夠修改的栋盹,所以對于要修改的實(shí)例變量則無需加__block.
塊的保留環(huán):塊里面使用了實(shí)例變量或self,self也是個(gè)對象敷矫,因而塊在捕獲它時(shí)也會將其保留例获。如果self所指代的那個(gè)對象同時(shí)也保留了塊汉额,那么這種情況通常就會導(dǎo)致保留環(huán)。
2.全局塊榨汤、棧塊及堆塊
定義塊的時(shí)候蠕搜,其所占的內(nèi)存區(qū)域是分配在棧中的。這就是說,塊只在定義它的那個(gè)范圍內(nèi)有效换况。
void (^block)();
if(/* some condition */){
block = ^{
NSLog(@"Block A");
}
}else{
block = ^{
NSLog(@"Block B");
}
}
block();
定義在if及else語句中的兩個(gè)塊都分配在棧內(nèi)存中你画。編譯器會給每個(gè)塊分配好棧內(nèi)存,然而等離開了相應(yīng)的范圍之后虫埂,編譯器有可能把分配給塊的內(nèi)存覆寫掉。于是圃验,這兩個(gè)塊只能保證在對于的if或else語句范圍內(nèi)有效掉伏。這樣寫出來的代碼可以編譯,但是運(yùn)行起來時(shí)而正確澳窑,時(shí)而錯誤斧散。若編譯器未覆寫待執(zhí)行的塊,則程序照常運(yùn)行摊聋,若覆寫鸡捐,則程序崩潰。
為解決此問題栗精,可給塊對象發(fā)送copy消息來拷貝之闯参。這樣的話,就可以把塊從棧復(fù)制到堆了悲立÷拐拷貝后的塊,可以在定義它的那個(gè)范圍之外使用薪夕。而且脚草,一旦復(fù)制到堆上,塊就成了帶引用計(jì)數(shù)的對象了原献。
//全局塊
void (^blocks)(void) = ^{
// self.propert = @"string";//會報(bào)錯馏慨,不會捕捉任何狀態(tài)
};
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
除了“棧塊”和“堆塊”之外,還有一類塊叫做“全局塊”(global block)姑隅。這種塊不會捕捉任何狀態(tài)(比如外圍的變量等)写隶,運(yùn)行時(shí)也無需有狀態(tài)來參與。塊所使用的整個(gè)內(nèi)存區(qū)域讲仰,在編譯期已經(jīng)完全確定了慕趴,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時(shí)候于棧中創(chuàng)建冕房。另外躏啰,全局塊的拷貝操作是空操作,因?yàn)槿謮K決不可能為系統(tǒng)所回收耙册。
要點(diǎn):
- 塊是C给僵、C++、Objective-C中的詞法閉包详拙。
- 塊可接受參數(shù)帝际,也可返回值
- 塊可以分配在棧或堆上溪厘,也可以是全局的胡本。分配在棧上的塊可拷貝到堆里,這樣的話畸悬,就和標(biāo)準(zhǔn)的Objective-C對象一樣,具備引用計(jì)數(shù)了珊佣。
38.為常用的塊類型創(chuàng)建typedef
與其他類型的變量不同蹋宦,在定義塊變量時(shí),要把變量名放在類型之中咒锻,而不是放在右側(cè)冷冗。鑒于此,我們應(yīng)該為常用的塊類型起個(gè)別名惑艇。為了隱藏復(fù)雜的塊類型蒿辙,需要用到C語言中名為“類型定義”(type definition)的特性。typedif關(guān)鍵字用于給類型起個(gè)易讀的別名滨巴。
typedef int(^BlockName)(BOOL flag, int value);
BlockName block = ^(BOOL flag, int value){
// block implementation
}
要點(diǎn):
- 以typedef重新定義塊類型思灌,可令塊變量用起來更加簡單
- 定義新類型時(shí)應(yīng)遵循現(xiàn)有的命名習(xí)慣,勿使用其名稱與別的類型相沖突
- 不妨為同一個(gè)塊簽名定義多個(gè)類型別名恭取。如果要重構(gòu)的代碼使用了塊類型的某個(gè)別名泰偿,那么只需要修改相應(yīng)typedef中的塊簽名即可,無需改動其他typedef蜈垮。
39.用handler塊降低代碼分散程度
設(shè)計(jì)API時(shí)耗跛,對于回調(diào)的選擇有多種,選用合適的回調(diào)方式能夠讓我們的代碼更加清晰整潔攒发。
要點(diǎn):
- 在創(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í)行唉擂。
40.用塊引用其所屬對象時(shí)不要出現(xiàn)保留環(huán)
要點(diǎn):
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問題
- 一定要找個(gè)適當(dāng)?shù)臅r(shí)機(jī)解除保留環(huán)檀葛,而不能把責(zé)任推給API的調(diào)用者玩祟。
41.多用派發(fā)隊(duì)列,少用同步鎖
在Objective-C中屿聋,如果有多個(gè)線程要執(zhí)行同一份代碼空扎,那么有時(shí)可能會出現(xiàn)問題。這種情況下润讥,通常要使用鎖來實(shí)現(xiàn)某種同步機(jī)制转锈,在GCD出現(xiàn)之前,有兩種辦法楚殿,第一種是采用內(nèi)置的“同步塊”(synchronization block)撮慨;第二種是直接使用NSLock對象;
//同步塊(synchronization block)
- (void)synchronizedMehtod{
@synchronized(self){
// safe 安全的執(zhí)行代碼
}
}
這種寫法會根據(jù)給定的對象脆粥,自動創(chuàng)建一個(gè)鎖砌溺,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處变隔,鎖就釋放了规伐。這么寫通常沒錯,因?yàn)樗梢员WC每個(gè)對象實(shí)例都能不受干擾地運(yùn)行其synchronizationMehtod方法匣缘。然而猖闪,濫用@synchronized(self)則會降低代碼效率,因?yàn)楣灿猛粋€(gè)鎖的那些同步塊肌厨,都必須按照順序執(zhí)行培慌。若是在self對象上頻繁加鎖,那么程序可能要等另一段與此無關(guān)的代碼執(zhí)行完畢夏哭,才能繼續(xù)執(zhí)行當(dāng)前代碼检柬,這樣做其實(shí)并沒有必要。
//NSLock對象
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod{
[_lock lock];
//safe code
[_lock unlock];
}
也可以使用NSRecursiveLock這種“遞歸鎖”(recursive lock),線程能夠多次持有該鎖竖配,而不會出現(xiàn)死鎖(deadlock)現(xiàn)象何址。這兩種方法都很好,不過也有其缺陷进胯。比方說用爪,在極端情況下,同步塊會導(dǎo)致死鎖胁镐,另外偎血,效率也不見得很高诸衔,而如果直接使用鎖對象的話,一旦遇到死鎖颇玷,就會非常麻煩笨农。
有種簡單而高效的辦法可以代替同步塊或鎖對象,那就是使用“串行同步隊(duì)列”帖渠。將讀取操作及寫入操作都安排在同一個(gè)隊(duì)列里谒亦,即可保證數(shù)據(jù)同步。
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue",NULL);
- (NSString *)someString{
__block NSString *localString;
dispatch_sync(_syncQueue,^{
localString = _someString;
});
return localString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});
}
此模式的思路是:把設(shè)置操作與獲取操作都安排在序列化的隊(duì)列里執(zhí)行空郊,這樣的話份招,所有針對屬性的訪問操作就都同步了。為了shi塊代碼能夠設(shè)置局部變量狞甚,獲取方法中用到了__block語法锁摔,若是拋開這一點(diǎn),那么這種寫法要比前面那些更為整潔哼审。全部加鎖任務(wù)都在GCD中處理谐腰,而GCD是在相當(dāng)深的底層來實(shí)現(xiàn)的,于是能夠做許多優(yōu)化涩盾。因此怔蚌,開發(fā)者無需擔(dān)心那些事,只要專心把訪問方法寫好就行旁赊。
要點(diǎn):
- 派發(fā)隊(duì)列可用來表述同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對象更簡單
- 將同步與異步派發(fā)結(jié)合起來椅野,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為终畅,而這么做卻不會阻塞執(zhí)行異步派發(fā)的形成
- 使用同步隊(duì)列及柵欄塊,可以令同步行為更加高效
42.多用GCD,少用performSelector系列方法
SEL selector = @selector(test);
[self performSelector:selector];
報(bào)警告:PerformSelector may cause a leak because its selector is unknown
原因在于:編譯器并不知道將要調(diào)用的選擇子是什么竟闪,因此离福,也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚炼蛤。而且妖爷,由于編譯器不知道方法名,所以就沒辦法運(yùn)用ARC的內(nèi)存管理規(guī)則來判定返回值是不是應(yīng)該釋放理朋。鑒于此絮识,ARC采用了比較謹(jǐn)慎的做法,就是不添加釋放操作嗽上。然而這么做可能導(dǎo)致內(nèi)存泄漏次舌,因?yàn)榉椒ㄔ诜祷貙ο髸r(shí)可能已經(jīng)將其保留了。
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
傳入的參數(shù)都是id類型兽愤,所以傳入的參數(shù)必須是對象才行彼念,基本數(shù)據(jù)類型不行挪圾;再者,返回值也是id逐沙。還有一個(gè)問題就是哲思,多個(gè)參數(shù)的傳遞,我們可能需要使用字典等集合來進(jìn)行封裝再進(jìn)行傳遞吩案。
如果改為其他替代方案棚赔,那就不受這些限制了。最主要的替代方案就是使用塊务热。
//using performSelector
[self performSelector:@selector(doSomeThing) withObject:nil afterDelay:5.0];
//using GCD
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];
});
//using performSelector
[self performSelectorOnMainThread:@selector(doSomeThing) withObject:nil waitUntilDone:NO];
//using GCD
//if waitUntilDone is YES,then dispatch_sync
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomeThing];
});
要點(diǎn):
- performSelector系列方法在內(nèi)存管理方法容易有疏失忆嗜。它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無法插入適當(dāng)?shù)膬?nèi)存管理方法崎岂。
- performSelector系列方法所能處理的選擇子太多局限了捆毫,選擇子的返回值類型及發(fā)送給方法的參數(shù)個(gè)數(shù)都受到限制
- 如果想把任務(wù)放另一個(gè)線程上執(zhí)行,那么最好不要用performSelector系列方法冲甘,而是應(yīng)該把任務(wù)封裝到塊里绩卤,然后調(diào)用大中樞派發(fā)機(jī)制的相關(guān)方法來實(shí)現(xiàn)。
43.掌握GCD及操作隊(duì)列的使用時(shí)機(jī)
出了GCD之外江醇,還有一種技術(shù)叫做NSOperationQueue,它雖然與GCD不同濒憋,但是卻與之相關(guān),開發(fā)者可以把操作以NSOperation子類的形式放在隊(duì)列中陶夜,而這些操作也能并發(fā)執(zhí)行凛驮。區(qū)別:GCD是純C的API,而操作隊(duì)列則是Objective-C的對象条辟。在GCD中黔夭,任務(wù)用塊來表示。用NSOperationQueue類的“addOperationWithBlock:”方法搭配NSBlockOperation類來使用操作隊(duì)列羽嫡,其語法與GCD方式類似本姥。使用NSOperation及NSOperationQueue的好處如下:
- 可以取消某個(gè)操作。
- 指定操作間的依賴關(guān)系杭棵。
- 通過鍵值觀測機(jī)制監(jiān)控NSOperation對象的屬性婚惫。
- 指定操作的優(yōu)先級。
- 重用NSOperation對象
NSNotificationCeter使用的就是操作隊(duì)列而非派發(fā)隊(duì)列
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
[[NSNotificationCenter defaultCenter] addObserverForName:(nullable NSNotificationName) object:(nullable id) queue:(nullable NSOperationQueue *) usingBlock:^(NSNotification * _Nonnull note) {
}];
要點(diǎn):
- 在解決多線程與任務(wù)管理問題時(shí)魂爪,派發(fā)隊(duì)列并非唯一方案先舷。
- 操作隊(duì)列提供了一套高層的Objective-C API,能實(shí)現(xiàn)純GCD所具備的絕大部分功能甫窟,而且還能完成一些更為復(fù)雜的操作密浑,那些操作若改用GCD來實(shí)現(xiàn),則需另外編寫代碼
44.通過Dispatch Group機(jī)制粗井,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務(wù)
GCD常見方法
//創(chuàng)建隊(duì)列組
dispatch_group_t group = dispatch_group_create();
//任務(wù)編組
//方式一:把待執(zhí)行的任務(wù)塊歸屬某個(gè)組
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//方式二:進(jìn)組 與 出組 成對出現(xiàn)
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
//等待dispatch_group執(zhí)行完畢
//arg0:等待的隊(duì)列組 arg1:等待時(shí)間 DISPATCH_TIME_FOREVER表示一直等著dispatch_group執(zhí)行完
//返回類型long,如果執(zhí)行g(shù)roup所需的時(shí)間小于timeout尔破,則返回0街图,否則返回非0值
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
//通知隊(duì)列組執(zhí)行完后在指定的隊(duì)列進(jìn)行回調(diào) 與上面的方法相比,在非主隊(duì)列中不會阻塞
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//GCD遍歷集合 該方法會持續(xù)阻塞懒构,從0開始餐济,直至iterations - 1
void dispatch_apply(size_t iterations, dispatch_queue_t queue,DISPATCH_NOESCAPE void (^block)(size_t));
要點(diǎn):
- 一系列任務(wù)可歸入一個(gè)dispatch group中。開發(fā)者可以在這組執(zhí)行完畢時(shí)獲得通知胆剧。
- 通過dispatch group,可以在并發(fā)式派發(fā)隊(duì)列中同時(shí)執(zhí)行多項(xiàng)任務(wù)絮姆。此時(shí)GCD會根據(jù)系統(tǒng)資源來調(diào)度這些并發(fā)執(zhí)行的任務(wù)。開發(fā)者若自己來實(shí)現(xiàn)此功能秩霍,則需要編寫大量代碼篙悯。
45.使用dispatch_once來執(zhí)行只需運(yùn)行一次的線程安全代碼
//單例中的dispatch_once使用
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
要點(diǎn):
- 經(jīng)常需要編寫“只需要執(zhí)行一次的線程安全代碼”。通過GCD所提供的dispatch_once函數(shù)铃绒,很容易就能實(shí)現(xiàn)此功能鸽照。
- 標(biāo)記應(yīng)該聲明在static或global作用域中,這樣的話颠悬,在把只需執(zhí)行一次的塊傳給dispatch_once函數(shù)時(shí)矮燎,傳進(jìn)去的標(biāo)記也是相同的。
46.不要使用dispatch_get_current_queue
要點(diǎn):
- dispatch_get_current_queue函數(shù)的行為常常與開發(fā)者所預(yù)期的不同赔癌。此函數(shù)已經(jīng)廢棄诞外,只應(yīng)做調(diào)試使用
- 由于派發(fā)隊(duì)列是按層級來組織的,所以無法單用某個(gè)隊(duì)列對象來描述“當(dāng)前隊(duì)列”這一概念
- dispatch_get_current_queue函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖灾票,然而能用此函數(shù)解決的問題峡谊,通常也能改用“隊(duì)列特定數(shù)據(jù)”來解決
PDF格式的資料來自iOS開發(fā)交流群、感覺作者的貢獻(xiàn)刊苍,對于知識的系統(tǒng)歸納總結(jié)很有幫助靖苇。
編寫高質(zhì)量代碼的52個(gè)有效方法
編寫高質(zhì)量代碼的52個(gè)有效方法(一)—熟悉OC
編寫高質(zhì)量代碼的52個(gè)有效方法(二)—對象、消息班缰、運(yùn)行期
編寫高質(zhì)量代碼的52個(gè)有效方法(三)—接口與API設(shè)計(jì)
編寫高質(zhì)量代碼的52個(gè)有效方法(四)—協(xié)議與分類
編寫高質(zhì)量代碼的52個(gè)有效方法(五)—內(nèi)存管理
編寫高質(zhì)量代碼的52個(gè)有效方法(六)—塊與大中樞派發(fā)
編寫高質(zhì)量代碼的52個(gè)有效方法(七)---系統(tǒng)框架