GCD多线程
GCD由淺入深學(xué)習(xí)
由于GCD相關(guān)知識比較多,因此這里分成兩篇文章總結(jié)。
引言
雖然GCD使用很廣,而且在面試時(shí)也經(jīng)常問與GCD相關(guān)的問題,但是我相信深入理解關(guān)于GCD知識的人肯定不多,大部分都是人云亦云,只是使用過GCD完成一些很簡單的功能。當(dāng)然,使用GCD完成一些簡單的功能,通常已經(jīng)能夠滿足我們的需求了。不過,筆者比較喜歡刨根問底,因此在這里記錄下學(xué)習(xí)的過程。
Tips
?
高手可繞邊走!!!
簡介
iOS實(shí)現(xiàn)提供實(shí)現(xiàn)多線程的方案有:NSThread、NSOperation、GCD。
在iOS所有實(shí)現(xiàn)多線程的方案中,GCD應(yīng)該是最有魅力的,而且使用起來也是最方便的,因?yàn)镚CD是蘋果公司為多核的并行運(yùn)算提出的解決方案。
GCD是Grand Central Dispatch的簡稱,它是基于C語言的。使用GCD,我們不需要編寫線程代碼,其生命周期也不需要我們手動(dòng)管理,定義想要執(zhí)行的任務(wù),然后添加到適當(dāng)?shù)恼{(diào)度隊(duì)列,也就是dispatch queue。GCD會(huì)負(fù)責(zé)創(chuàng)建線程和調(diào)度任務(wù),系統(tǒng)直接提供線程管理。
由于GCD是基于C語言的,因此使用起來對于沒有學(xué)習(xí)過C語言的同學(xué)們,相對困難一些。不過,事實(shí)上使用是很簡單,只要注意死鎖等問題就好了。
概念:隊(duì)列(Queue)
我們需要了解隊(duì)列的概念,GCD提供了dispatch queues來處理代碼塊,這些隊(duì)列管理所提供給GCD的任務(wù)并用FIFO順序執(zhí)行這些任務(wù)。這樣才能保證第一個(gè)被添加到隊(duì)列里的任務(wù)會(huì)是隊(duì)列中第一個(gè)開始的任務(wù),而第二個(gè)被添加的任務(wù)將第二個(gè)開始,如此直到隊(duì)列的終點(diǎn)。
概念:調(diào)度隊(duì)列(dispath queue)
所有的調(diào)度隊(duì)列(dispatch queues)自身都是線程安全的,我們能從多個(gè)線程并行的訪問它們。?GCD的優(yōu)點(diǎn)是顯而易見的。我們需要了解調(diào)度隊(duì)列如何我們的代碼的不同部分提供線程安全,以決定使用何種隊(duì)列,在哪個(gè)線程上執(zhí)行等。
GCD將長期運(yùn)行的任務(wù)拆分成多個(gè)工作單元,并將這些單元添加到dispath queue中,系統(tǒng)會(huì)管理這些dispath queue,為我們在多個(gè)線程上執(zhí)行工作單元,我們不需要手動(dòng)啟動(dòng)和管理后臺(tái)線程。
?
系統(tǒng)提供了許多預(yù)定義的dispath queue,包括始終在主線程上執(zhí)行工作的dispath queue。我們可以創(chuàng)建自己的dispath queue,而且可以創(chuàng)建任意多個(gè)。GCD的dispath queue嚴(yán)格遵循FIFO(先進(jìn)先出)原則,添加到dispath queue的工作任務(wù)將按照加入dispath queue的順序啟動(dòng)。
概念:串行(Serial)
我們在學(xué)習(xí)操作系統(tǒng)這門課程的時(shí)候,經(jīng)常會(huì)提到串行。我們使用GCD,也會(huì)用到串行的概念。
所謂串行(Serial)執(zhí)行,指同一時(shí)間每次只能執(zhí)行一個(gè)任務(wù)。
概念:并發(fā)(Concurrent)
說到串行,自然會(huì)想到并發(fā)。在操作系統(tǒng)這門課程中,這個(gè)概念是非常重要的。
所謂并發(fā)(Concurrent),指同一時(shí)間可以同時(shí)執(zhí)行多個(gè)任務(wù)。
概念:死鎖(Deadlock)
操作系統(tǒng)這門課程中對死鎖的介紹說明有很多。在實(shí)際開發(fā)中,也經(jīng)常遇到死鎖的問題。
所謂死鎖(Deadlock)是指它們都卡住了,并等待對方完成或執(zhí)行其它操作。第一個(gè)不能完成是因?yàn)樗诘却诙€(gè)的完成。但第二個(gè)也不能完成,因?yàn)樗诘却谝粋€(gè)的完成。
概念:線程安全(Thread Safe)
還記得我們在寫單例的時(shí)候都加了哪些代碼嗎?我們應(yīng)該知道,既然要聲明為單例,說明這是共享資源區(qū),就會(huì)存在競態(tài)條件,因此,我們必須保證只創(chuàng)建一次。
像這樣添加了線程鎖的:
| 1 2 3 4 5 | @synchronized(<#token#>) { ??<#statements#> } |
還有這樣用于創(chuàng)建單例的,以確保只執(zhí)行一次:
| 1 2 3 4 5 6 | static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ??<#code to be executed once#> }); |
?
創(chuàng)建和管理dispatch queue
1.獲取全局并發(fā)調(diào)度隊(duì)列
并發(fā)的調(diào)度隊(duì)列可以同時(shí)并行地執(zhí)行多個(gè)任務(wù),但是并發(fā)隊(duì)列也是隊(duì)列,因此同樣遵循著FIFO的原則來啟動(dòng)任務(wù)。因?yàn)椴l(fā)執(zhí)行任務(wù)與系統(tǒng)有關(guān),其同時(shí)執(zhí)行任務(wù)的數(shù)量是由系統(tǒng)根據(jù)應(yīng)用和系統(tǒng)動(dòng)態(tài)變化決定的。
現(xiàn)在iOS系統(tǒng),為每個(gè)應(yīng)用提供了四種并發(fā)的全局共享的調(diào)度隊(duì)列,其區(qū)別在于優(yōu)先級不一樣。
| 1 2 3 4 5 6 7 8 9 10 11 | /* * The global concurrent queues may still be identified by their priority, * which map to the following QOS classes: * *??- DISPATCH_QUEUE_PRIORITY_HIGH:???????? QOS_CLASS_USER_INITIATED *??- DISPATCH_QUEUE_PRIORITY_DEFAULT:??????QOS_CLASS_DEFAULT *??- DISPATCH_QUEUE_PRIORITY_LOW:??????????QOS_CLASS_UTILITY *??- DISPATCH_QUEUE_PRIORITY_BACKGROUND:?? QOS_CLASS_BACKGROUND */?? |
我們不需要?jiǎng)?chuàng)建它,只需要直接獲取就可以了,因?yàn)檫@是系統(tǒng)為我們提供的,而且這個(gè)還是全局共享的:
| 1 2 3 | dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
第一個(gè)參數(shù)為優(yōu)先級,就是上面提供的這四種。第二個(gè)參數(shù)沒有使用到,這個(gè)參數(shù)是預(yù)留的,使用0即可,看官方說明:
| 1 2 3 4 5 | * @param flags * Reserved for future use. Passing any value other than zero may result in * a NULL return value. |
flags就是第二個(gè)參數(shù),也就是為未來預(yù)留的參數(shù)。看看蘋果想得真夠遠(yuǎn)的,為未來預(yù)留~~。
注意:雖然dispatch queue是引用計(jì)數(shù)的對象,但我們不需要retain和release這個(gè)全局的并發(fā)queue。因?yàn)檫@些queue對應(yīng)用是全局的,retain和release調(diào)用會(huì)被忽略。
2.創(chuàng)建串行調(diào)度隊(duì)列
當(dāng)任務(wù)需要按特定的順序執(zhí)行時(shí),就需要使用串行調(diào)度隊(duì)列(Dispatch Queue),串行調(diào)度隊(duì)列每次只能執(zhí)行一個(gè)任務(wù)。
我們可以使用串行隊(duì)列替代鎖,保護(hù)共享資源等。和鎖不一樣的是,串行隊(duì)列確保任務(wù)按指定的順序執(zhí)行,而且只要你異步地提交任務(wù)到串行隊(duì)列,就永遠(yuǎn)不會(huì)產(chǎn)生死鎖。
我們可以手動(dòng)創(chuàng)建和管理串行隊(duì)列,且可以創(chuàng)建很多個(gè),但是我們不要?jiǎng)?chuàng)建很多個(gè)串行隊(duì)列來執(zhí)行很多的任務(wù),當(dāng)需要執(zhí)行大量的任務(wù)時(shí),應(yīng)該交給全局并發(fā)隊(duì)列來完成。從操作系統(tǒng)方面思考,雖然允許應(yīng)用創(chuàng)建很多個(gè)串行隊(duì)列,但是其優(yōu)先級永遠(yuǎn)不會(huì)比系統(tǒng)級的高,因此當(dāng)任務(wù)很多時(shí),所要求的資源未必就可以提供。所以,任務(wù)量大時(shí),應(yīng)該交給系統(tǒng)提供的全局隊(duì)列來完成才是最佳的。
使用下面的方法來創(chuàng)建串行隊(duì)列,其中第一個(gè)參數(shù)是隊(duì)列的名稱,通常使用公司的反域名,如com.company.project。第二個(gè)參數(shù)是隊(duì)列相關(guān)屬性,通常都傳NULL:
| 1 2 3 | dispatch_queue_t sequalQueue = dispatch_queue_create("com.huangyibiao.helloworld", NULL); |
?
3.獲取公共隊(duì)列
應(yīng)用提供了幾下幾種獲取公共隊(duì)列的方法:
- dispatch_get_current_queue:在iOS 6.0之后已經(jīng)廢棄,用于獲取當(dāng)前正在執(zhí)行任務(wù)的隊(duì)列,主要用于調(diào)試
- dispatch_get_main_queue: 最常用的,用于獲取應(yīng)用主線程關(guān)聯(lián)的串行調(diào)度隊(duì)列
- dispatch_get_global_queue:最常用的,用于獲取應(yīng)用全局共享的并發(fā)隊(duì)列
對于后面這兩個(gè)分別獲取主線程的串行隊(duì)列和獲取應(yīng)用全局共享的并發(fā)隊(duì)列是非常常用的,當(dāng)我們需要開一個(gè)線程并發(fā)地異步執(zhí)行任務(wù)時(shí),我們就會(huì)放到全局隊(duì)列中。當(dāng)我們在異步執(zhí)行完成時(shí),通常需要回到主線程更新UI顯示。
4.調(diào)度隊(duì)列(Dispatch Queue)的內(nèi)存管理
調(diào)度隊(duì)列,即Dispatch Queue與其它類型的dispatch對象是引用計(jì)數(shù)的數(shù)據(jù)類型。當(dāng)創(chuàng)建一個(gè)串行dispatch queue時(shí),初始引用計(jì)數(shù)為1,我們可用dispatch_retain和dispatch_release函數(shù)來增加和減少引用計(jì)數(shù)。當(dāng)引用計(jì)數(shù)為0時(shí),系統(tǒng)會(huì)異步地銷毀這個(gè)queue。
以上是對于普通創(chuàng)建的調(diào)度隊(duì)列有用,但對于系統(tǒng)本身提供的全局并發(fā)隊(duì)列和主線程串行隊(duì)列則不需要我們手動(dòng)內(nèi)管其內(nèi)存,系統(tǒng)會(huì)自動(dòng)管理。
在使用全局并發(fā)隊(duì)列時(shí),我們只通過dispatch_get_global_queue方法來獲取即可,我們不需要管理其引用。 在使用主線程串行隊(duì)列時(shí),我們只通過dispatch_get_main_queue方法來獲取即可,我們也不需要管理其內(nèi)存問題。
添加任務(wù)到調(diào)度隊(duì)列
要想讓調(diào)度隊(duì)列執(zhí)行任務(wù),那么我們就需要將任務(wù)添加到適當(dāng)?shù)恼{(diào)度隊(duì)列中。在實(shí)際iOS開發(fā)中,我們通常配合block的使用,將任務(wù)封裝到一個(gè)block中。
我們可以異步或者同步添加任務(wù)到隊(duì)列中,但是我們應(yīng)該盡可能地使用dispatch_async或dispatch_async_f。前者是提交一個(gè)block任務(wù)到隊(duì)列中,后者是提供一個(gè)函數(shù)任務(wù)到隊(duì)列中。基本上都是直接使用dispatch_async提交一個(gè)block到隊(duì)列中,這代碼寫起來更加地簡潔。
當(dāng)然,我們也可以同步添加任務(wù)。有時(shí)候我們可能希望同步地調(diào)度任務(wù),以避免競爭條件或其它同步錯(cuò)誤。使用dispatch_sync或dispatch_sync_f函數(shù)同步地添加任務(wù)到Queue,這兩個(gè)函數(shù)會(huì)阻塞當(dāng)前調(diào)用線程,直到相應(yīng)任務(wù)完成執(zhí)行。在實(shí)際開發(fā)中,當(dāng)需要同步執(zhí)行任務(wù)時(shí),大多是直接使用dispatch_sync這個(gè)提交block任務(wù)的方法,使用起來更簡潔。
注意:當(dāng)隊(duì)列中有任務(wù)正在同步執(zhí)行時(shí),我們不能使用dispatch_sync或dispatch_sync_f同步調(diào)度新任務(wù)到當(dāng)前正在執(zhí)行的queue中。對于串行queue肯定會(huì)導(dǎo)致死鎖,而對于并發(fā)queue也應(yīng)該避免這么使用。原來我接手的項(xiàng)目中,有一個(gè)同步任務(wù)正在執(zhí)行數(shù)據(jù)庫操作,可是當(dāng)我也需要操作數(shù)據(jù)時(shí),調(diào)用其所提供的api,使用dispatch_sync將我的任務(wù)添加到隊(duì)列中,結(jié)果導(dǎo)致了死鎖,每次都crash。
為什么盡可能地添加異步執(zhí)行的任務(wù)呢?因此同步任務(wù)會(huì)阻塞主線程,很可能導(dǎo)致事件響應(yīng)不了。
我們看看如何簡單地創(chuàng)建隊(duì)列、異步、同步任務(wù)添加到隊(duì)列:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | dispatch_queue_t queue = dispatch_queue_create("com.huangyibiao.helloworld", NULL); dispatch_async(queue, ^{?? ????NSLog(@"開啟了一個(gè)異步任務(wù),當(dāng)前線程:%@", [NSThread currentThread]);?? });?? ?? dispatch_sync(queue, ^{?? ????NSLog(@"開啟了一個(gè)同步任務(wù),當(dāng)前線程:%@", [NSThread currentThread]);?? });?? // MRC下才能調(diào)用,對于ARC就不能添加這行代碼了。 dispatch_release(queue); |
由于這個(gè)串行調(diào)度隊(duì)列是我們自己創(chuàng)建的,我們需要管理其內(nèi)存。不過在實(shí)際開發(fā)中,使用自己創(chuàng)建創(chuàng)建的方式是比較少見的,通常都是直接使用系統(tǒng)為每個(gè)應(yīng)用提供的全局共享并發(fā)隊(duì)列異步執(zhí)行任務(wù),然后使用主線程串行隊(duì)列更新界面。
控制并發(fā)數(shù)
太多并發(fā)是會(huì)帶來很多的風(fēng)險(xiǎn)的。在實(shí)際開發(fā)中,并不是并發(fā)數(shù)越多就越好,往往是需要控制其并發(fā)數(shù)量的。比如,在處理網(wǎng)絡(luò)請求并發(fā)數(shù)時(shí),通常會(huì)設(shè)置限制最大并發(fā)數(shù)為4左右。當(dāng)并發(fā)數(shù)量大了,開銷也會(huì)很大。學(xué)過操作系統(tǒng)應(yīng)該清楚,并發(fā)量大了,臨界資源訪問操作就很難控制,控制不好就會(huì)導(dǎo)致死鎖等。當(dāng)我們需要執(zhí)行循環(huán)異步處理任務(wù)時(shí),可以考慮使用dispatch_apply來替代。請看下一節(jié)!
并發(fā)地循環(huán)迭代任務(wù)
如果迭代執(zhí)行的任務(wù)與其它迭代任務(wù)是獨(dú)立無關(guān)的,而且循環(huán)迭代執(zhí)行順序也無關(guān)緊要的話,我們可以調(diào)用dispatch_apply或dispatch_apply_f函數(shù)來替換循環(huán)。前者是提交block任務(wù),后者是提交函數(shù)任務(wù)到隊(duì)列中。比如,我們需要上傳多張圖片,這些圖片的上傳是互不干擾的,迭代執(zhí)行的順序是不重要的,那么我們就可以使用dispatch_apply來替換掉for循環(huán)。
下面代碼使用dispatch_apply替換了for循環(huán),所傳遞的block必須包含一個(gè)size_t類型的參數(shù),用來標(biāo)識當(dāng)前循環(huán)迭代。第一次迭代這個(gè)參數(shù)值為0,最后一次值為count - 1:
| 1 2 3 4 5 6 7 8 9 10 11 12 | // 獲得全局并發(fā)queue dispatch_queue_t gqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); size_t gcount = 10; dispatch_apply(gcount, gqueue, ^(size_t i) { ????[self uploadImageWithIndex:(NSUInteger)(i)]; }); - (void)uploadImageWithIndex:(NSUInteger)imageIndex { ??NSLog(@"上傳索引為%lu的圖片", imageIndex); } |
打印結(jié)果說明順序是不確定的,可看得出來這是并發(fā)執(zhí)行的:
| 1 2 3 4 5 6 7 8 9 10 11 12 | 2015-11-24 00:06:11.692 TestGCD[27714:2678984] 上傳索引為0的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2679067] 上傳索引為3的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2678984] 上傳索引為4的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2679064] 上傳索引為2的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2678984] 上傳索引為6的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2679065] 上傳索引為1的圖片 2015-11-24 00:06:11.693 TestGCD[27714:2678984] 上傳索引為8的圖片 2015-11-24 00:06:11.692 TestGCD[27714:2679067] 上傳索引為5的圖片 2015-11-24 00:06:11.693 TestGCD[27714:2679064] 上傳索引為7的圖片 2015-11-24 00:06:11.693 TestGCD[27714:2679065] 上傳索引為9的圖片 |
?
注意:dispatch_apply或dispatch_apply_f函數(shù)也是在所有迭代完成之后才會(huì)返回,因此這兩個(gè)函數(shù)會(huì)阻塞當(dāng)前線程。當(dāng)我們在主線程中使用時(shí),一定要小心,很容易造成事件無法響應(yīng),所以如果循環(huán)
代碼需要一定的時(shí)間執(zhí)行,可考慮在另一個(gè)線程中調(diào)用這兩個(gè)函數(shù)。如果所傳遞的參數(shù)是串行queue,而且正是執(zhí)行當(dāng)前代碼的queue,就會(huì)產(chǎn)生死鎖。
主線程中執(zhí)行任務(wù)
看看下面很常用的異步下載圖片的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 | // 異步下載圖片?? dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{?? ????NSURL *url = [NSURL URLWithString:@"圖片的URL"];?? ????UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];?? ?????? ????// 回到主線程顯示圖片?? ????dispatch_async(dispatch_get_main_queue(), ^{?? ????????self.imageView.image = image;?? ????});?? }); |
這里先將異步下載圖片的任務(wù)放到dispatch_get_global_queue全局共享并發(fā)隊(duì)列中執(zhí)行,在完成以后,需要放在dispatch_get_main_queue回到主線程更新UI。
暫停和繼續(xù)queue
我們可以使用·dispatch_suspend·函數(shù)暫停一個(gè)queue以阻止它執(zhí)行block對象;使用dispatch_resume函數(shù)繼續(xù)dispatch queue。調(diào)用dispatch_suspend會(huì)增加queue的引用計(jì)數(shù),調(diào)用dispatch_resume則減少queue的引用計(jì)數(shù)。當(dāng)引用計(jì)數(shù)大于0時(shí),queue就保持掛起狀態(tài)。因此你必須對應(yīng)地調(diào)用dispatch_suspend和dispatch_resume函數(shù)。掛起和繼續(xù)是異步的,而且只在執(zhí)行block之間生效,掛起一個(gè)queue不會(huì)導(dǎo)致正在執(zhí)行的block停止。
| 1 2 3 4 | dispatch_suspend(gqueue); dispatch_resume(gqueue); |
?
注意:dispatch_suspend和dispatch_resume是成對出現(xiàn)的。
調(diào)度組(Dispatch Group)的使用
當(dāng)我們需要下載多張圖片并且圖片要求這幾張圖片都下載完成以后才能更新UI,那么這種情況下,我們就需要使用dispatch_group_t來完成了。
像這樣:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 異步下載圖片?? dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{?? ????// 下載第一張圖片?? ????UIImage *image1 = [self imageWithURLString:url1];?? ?????? ????// 下載第二張圖片?? ????UIImage *image2 = [self imageWithURLString:url2];?? ?????? ????// 回到主線程顯示圖片?? ????dispatch_async(dispatch_get_main_queue(), ^{?? ????????self.imageView1.image = image1;?? ????????self.imageView2.image = image2;?? ????});?? }); |
這段代碼是不能做到的,但是,我們還是有辦法做到的。dispatch_group_t就是很好的選擇。對于調(diào)度組,所添加的任務(wù)可以是同步的,也可以是異步的,在最近任務(wù)全部完成后都會(huì)有回調(diào)。
首先,我們通過dispatch_group_create創(chuàng)建一個(gè)組,然后通過dispatch_group_async將任務(wù)分別添加到該組中。當(dāng)組中的所有任務(wù)都完成以后,我們可以通過dispatch_group_notify得到回調(diào),然后在主線程更新UI。
代碼寫法像下面這樣:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 異步下載圖片 dispatch_async(queue, ^{ ??// 創(chuàng)建一個(gè)組 ??dispatch_group_t group = dispatch_group_create(); ?? ??__block UIImage *image1 = nil; ??__block UIImage *image2 = nil; ?? ??// 分別將任務(wù)添加到組中 ??dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ????image1 = [self downloadImage:url1]; ??}); ?? ??dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ????image2 = [self downloadImage:url2]; ??}); ?? ??// 等待組中的任務(wù)執(zhí)行完畢,回到主線程執(zhí)行block回調(diào) ??dispatch_group_notify(group, dispatch_get_main_queue(), ^{ ????self.imageView1.image = image1; ????self.imageView2.image = image2; ??}); }); |
?
延遲執(zhí)行
我們常見的延遲執(zhí)行方法有:
方法一:使用NSObject的api,同步執(zhí)行:
| 1 2 3 | [self performSelector:@selector(myFunction) withObject:nil afterDelay:5.0]; |
方法二:使用NSTimer定時(shí)器,不過這種方法沒必要。
方法三:使用dispatch_after方法異步延遲執(zhí)行:
| 1 2 3 4 5 6 7 8 | CGFloat time = 5.0f; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), ???????????? dispatch_get_main_queue(), ^{ ????// time秒后異步執(zhí)行這里的代碼... ???? }); |
?
結(jié)尾
對于在實(shí)際開發(fā)中常用的差不多全了,其它比較偏的API就不說了,在開發(fā)中比較少用。
?
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/zylin/p/5138892.html
總結(jié)
- 上一篇: socket套接字选项
- 下一篇: IE兼容性