版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
iOS程序員面試分類模擬13簡(jiǎn)答題1.
如何使用NSURLConnection進(jìn)行網(wǎng)絡(luò)請(qǐng)求?正確答案:NSURLConnection是iOS中最經(jīng)典的網(wǎng)絡(luò)請(qǐng)求方案。雖然在蘋(píng)果公司推出NSURLSe(江南博哥)ssion后已經(jīng)不推薦使用NSURLConnection了(NSURLConnection在iOS9被宣布棄用),但是在一些早先構(gòu)建的項(xiàng)目和框架中可能仍然使用了NSURLConnection的技術(shù),所以了解NSURLConnection的基本操作仍然是有必要的。使用NSURLConnection發(fā)送請(qǐng)求通常需要使用以下類:
1)NSURL,主要用于創(chuàng)建網(wǎng)絡(luò)請(qǐng)求地址。一個(gè)NSURL對(duì)象代表了一個(gè)表示遠(yuǎn)程服務(wù)器資源或者本地文件的URL。開(kāi)發(fā)者可以直接使用經(jīng)過(guò)UTF-8編碼后的字符串創(chuàng)建一個(gè)NSURL:
+URLWithString:
-initWithString:
2)NSURLRequest,代表網(wǎng)絡(luò)請(qǐng)求對(duì)象。它包含了發(fā)送一個(gè)請(qǐng)求所需要的一系列信息,如NSURL對(duì)象、請(qǐng)求方式、請(qǐng)求體、請(qǐng)求頭等。使用NSURL對(duì)象創(chuàng)建NSURLRequest對(duì)象:
+(instancetype)requestWithURL:(NSURL*)URL
+(instancetype)requestWithURL:(NSURL*)URLcachePolicy:
(NSURLRequestCachePolicy)cachePolicytimeoutInterval:(NSTimeInterval)timeoutInterval
3)NSURLConnection,作為CoreFoundation/CFNetwork框架的API之上的一個(gè)抽象,在2003年隨著第一版的Safari就發(fā)布了。它作為iOS7之前的網(wǎng)絡(luò)基礎(chǔ)架構(gòu),可以發(fā)送同步或異步請(qǐng)求,可以直接接收數(shù)據(jù)也可以使用代理監(jiān)聽(tīng)請(qǐng)求。
NSURLConnection、NSURL、NSURLRequest之間的關(guān)系如圖所示。
三者關(guān)系
NSURLConnection的使用步驟如下:
1)創(chuàng)建一個(gè)NSURL對(duì)象,用于設(shè)置請(qǐng)求路徑。
2)創(chuàng)建一個(gè)NSURLRequest對(duì)象,并設(shè)置請(qǐng)求頭、請(qǐng)求體等請(qǐng)求參數(shù)。
3)創(chuàng)建一個(gè)NSURLResponse對(duì)象用于接收響應(yīng)數(shù)據(jù),一般使用NSURLResponse的子類NSHTTPURLResponse。
4)使用NSURLConnection發(fā)送同步或異步請(qǐng)求。
下面的代碼將演示如何使用NSURLConnection發(fā)送請(qǐng)求并獲得數(shù)據(jù)。
(1)同步GET請(qǐng)求方法
sendSynchronousRequest:requestretumingResponse:&responseerror:
示例代碼如下:
-(void)connectionSyncGet{
/*創(chuàng)建NSURL對(duì)象*/
NSURL*url=[NSURLURLVCithString:IMAGEURL];
/*創(chuàng)建請(qǐng)求對(duì)象,默認(rèn)為GET請(qǐng)求*/
NSURLRequest*request=[NSURLRequestrequestWithURL:url];
/*創(chuàng)建請(qǐng)求響應(yīng)對(duì)象*/
NSHTTPURLResponse*response=nil;
NSError*error=nil;
/*發(fā)送請(qǐng)求,同步請(qǐng)求會(huì)阻塞當(dāng)前線程*/
NSData*data=[NSURLConnectionsendSynchronousRequest:requestreturningResponse:&responseerror:&error];
/*解析返回的數(shù)據(jù)*/
_image=[UIImageimageWithData:data];
/*顯示圖片*/
[selfchangeBg];
}
-(void)changeBg{
self.view.layer.contents=(id)_image.CGImage;
}
(2)異步POST請(qǐng)求方法
+(void)sendAsynchronousRequest:(NSURLRequest*)request
queue:(NSOperationQueue*)queue
completionHandler:(void(^)fNSURLResponse*_Nullableresponse,NSData*_Nullabledata,NSError*_NuliableconnectionError))handler
示例代碼如下:
-(void)connectionAsyncPost{
NSURL*url=[NSURLURLWithString:IMAGEURL];
/*創(chuàng)建請(qǐng)求對(duì)象*/
NSMutableURLRequest*request=[NSMutableURLRequestrequestWithURL:url];
/*設(shè)置請(qǐng)求方式為POST*/
request.HTTPMethod=@"POST";
/*設(shè)置請(qǐng)求體,在請(qǐng)求體中設(shè)置參數(shù)*/
request.HTTPBody=[@"usemame=520it&pwd=520&type=JSON"
dataUsingEncoding:NSUTF8StringEncoding];
/*設(shè)置清求超時(shí)*/
request.timeoutInterval=15;
/*設(shè)置請(qǐng)求頭*/
[requestsetValue:@"iOS"forHTTPHeaderField:@"User-Agent"];
/*發(fā)送請(qǐng)求*/
[NSURLConnectionsendAsynchronousRequest:requestqueue:[NSOperationQueue
mainQueue]completionHandler:^(NSURLResponse*_Nullableresponse,NSData*
_Nullabledata,NSError*NullableconnectionError){
NSLog(@"NSThread=%@",[NSThreadcurrentThread]);
if(connectionError){
NSLog(@"error=%@",connectionError.userInfo);
}else{
/*解析返回的數(shù)據(jù)*/
_image=[UIImageimageWithData:data];
/*顯示圖片*/
[selfchangeBg];
}
}];
}
(3)NSURLConnectiOnDelegate的使用
可以使用NSURLConnectionDelegate監(jiān)聽(tīng)網(wǎng)絡(luò)請(qǐng)求的響應(yīng)。示例代碼如下:
-(void)connectGETDelegate{
/*創(chuàng)建NSURL對(duì)象*/
NSURL*url=[NSURLURLWithString:IMAGEURL];
/*創(chuàng)建請(qǐng)求對(duì)緣,默認(rèn)為GET請(qǐng)求*/
NSURLRequest*request=[NSURLRequestrequestWithURL:url];
/*設(shè)置代理*/
[NSURLConnectionconnectionWithRequest:requestdelegate:self];
}
/*當(dāng)接收到服務(wù)器響應(yīng)的時(shí)候調(diào)用。第一個(gè)參數(shù)connection:監(jiān)聽(tīng)的是哪個(gè)
NSURLCormection對(duì)象;第二個(gè)參數(shù)response:接收到的服務(wù)器返回的響應(yīng)頭信息*/
-(void)connection:(nonnullNSURLConnection*)connectiondidReceiveResponse:(nonnullNSURLResponse*)response{
_imageData=[NSMutableDatadata];
}
/*當(dāng)接收到數(shù)據(jù)的時(shí)候調(diào)用,該方法會(huì)被調(diào)用多次。第一個(gè)參數(shù)connection:監(jiān)聽(tīng)的
是哪個(gè)NSURLConnection對(duì)象;第二個(gè)參數(shù)data:本次接收到的服務(wù)端返回的二進(jìn)制數(shù)
據(jù)(可能是片段)*/
-(void)connection:(nonnullNSURLConnection*)connectiondidReceiveData:(nonnullNSData*)data{
[_imageDataappendData:data];
}
/*服務(wù)端返回的數(shù)據(jù)接收完畢之后會(huì)調(diào)用。通常在該方法中解析服務(wù)器返回的數(shù)據(jù)*/
-(void)IconnectionDidFinishLoading:(nonnullNSURLConnection*)connection{
/*解析返回的數(shù)據(jù)*/
_image=[UIImageimageWithData:_imageData];
/*顯示圖片*/
[selfchangeBg];
}
/*當(dāng)請(qǐng)求錯(cuò)誤的時(shí)候調(diào)用(例如請(qǐng)求超時(shí))。第一個(gè)參數(shù)connection:NSURLCormection
對(duì)象。第二個(gè)參數(shù):網(wǎng)絡(luò)請(qǐng)求的錯(cuò)誤信息,如果請(qǐng)求失敗,那么error有值*/
-(void)connection:(nonnullNSURLConnection*)connectiondidFailWithError:(nonnullNSError*)error{
}
2.
如何使用NSURLSession進(jìn)行網(wǎng)絡(luò)請(qǐng)求?正確答案:在2013年的WWDC上,蘋(píng)果公司推出了NSURLConnection的替代方案:NSURLSession。和NSURLConnection一樣,NSURLSession指的也不僅是同名類NSURLSession,它還包括一系列相關(guān)聯(lián)的類。NSURLSession包括了與之前相同的組件:NSURLRequest與NSURLCache,但是將NSURLConnection替換成了NSURLSession、NSURLSessionConfiguration及NSURLSessionTask的3個(gè)子類:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask。
與NSURLConnection相比,NSURLSession最直接的改進(jìn)就是可以配置每個(gè)session的緩存、協(xié)議、cookie,以及證書(shū)策略(CredentialPolicy),甚至跨進(jìn)程共享這些信息。這將允許程序和網(wǎng)絡(luò)基礎(chǔ)框架之間相互獨(dú)立,不會(huì)發(fā)生干擾。每個(gè)NSURLSession對(duì)象都由一個(gè)NSURLSessionConfiguration對(duì)象進(jìn)行初始化,后者指定了剛才提到的那些策略以及一些用來(lái)增強(qiáng)移動(dòng)設(shè)備上性能的新選項(xiàng)。
NSURLSessionTask負(fù)責(zé)處理數(shù)據(jù)的加載以及文件的數(shù)據(jù)在客戶端與服務(wù)器之間的上傳和下載。它是一個(gè)抽象類,一般使用其子類:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask。這3個(gè)子類封裝了現(xiàn)代程序3個(gè)最基本的網(wǎng)絡(luò)任務(wù):獲取數(shù)據(jù)(如JSON或者XML),上傳文件和下載文件。
NSURLSession相關(guān)類的關(guān)系如圖所示。
NSURLSession相關(guān)類的關(guān)系
如何使用NSURLSession像NSURLConnection那樣發(fā)送一個(gè)請(qǐng)求呢?基本步驟如下:
1)創(chuàng)建NSURLSessionConfiguration對(duì)象對(duì)NSURLSession進(jìn)行配置。
2)創(chuàng)建NSURLSession對(duì)象。
3)利用上一步創(chuàng)建好的NSURLSession對(duì)象創(chuàng)建NSURLSessionTask的子類對(duì)象。
4)執(zhí)行請(qǐng)求任務(wù)。
下面的示例展示了NSURLSession的基本用法,代碼如下:
-(void)sessionGet{
/*創(chuàng)建NSURL對(duì)象*/
NSURL*url=[NSURLURLWithString:IMAGEURL];
/*創(chuàng)建請(qǐng)求對(duì)象,默認(rèn)為GET請(qǐng)求*/
NSURLRequest*request=[NSURLRequestrequestWithURL:url];
/*創(chuàng)建配置*/
NSURLSessionConfiguration*config=[NSURLSessionConfigurationdefaultSessionConfigtwation];
/*創(chuàng)建NSURLSession對(duì)象*/
NSURLSession*session=[NSURLSessionsessionWithConfiguration:config];
/*創(chuàng)建任務(wù)*/
NSURLSessionDataTask*task=[sessiondataTaskWithRequest:requestcompletionHandler:^(NSData*_Nullabledata,NSURLResponse*_Nullableresponse,NSError*_Nullableerror){
if(error){
return;
}
/*解析返回的數(shù)據(jù)*/
_image=[UIImageimageWithData:data];
/*顯示圖片,注意!此時(shí)是異步線程需要在主線程中顯示圖片*/
[selfchangeBg];
}];
[taskresume];
}
3.
block有哪幾種定義的方式?正確答案:在Objective-C中,block定義包含了block的類型聲明和實(shí)現(xiàn),基本形式如下:
返回值類型(^block名稱)(參數(shù)類型)=^(參數(shù)類型和參數(shù)名){};
其中,返回值類型和參數(shù)可以是空。如果有參數(shù),那么在定義block的時(shí)候,必須要標(biāo)明參數(shù)的類型和參數(shù)名。所以,block大致有3種細(xì)分的定義方式。
1)沒(méi)有返回值,沒(méi)有參數(shù)的定義方式。
void(^myBlock)()=^{
//代碼
};
2)有返回值,有參數(shù)的定義方式。
int(^myBlock)(int)=^(inta){
returna;
};
3)有返回值,沒(méi)有參數(shù)的定義方式。
int(^myBlock)()=^{
return100;
};
當(dāng)然,block也有屬于自己的類型,就像在Objective-C中,字符串對(duì)象屬于NSString類型一樣。block類型的格式就是:
返回值類型(^)(參數(shù)類型)
也就是說(shuō),上面第一種定義方式的block類型就是void(^)(),myBlock不是變量名,而是這種block類型的別名。在Objective-C中,可以使用typedef關(guān)鍵字定義block類型,也可以直接使用inline提示符來(lái)自動(dòng)生成block格式。示例代碼如下:
/*使用typedef關(guān)鍵字定義block類型*/
typedefvoid(^myBloek)();
myBlockblock=^{
};
/*使用inline提示符來(lái)自動(dòng)生成block格式*/
<#retumType#>(^<#blockName#>)(<#parameterTypes#>)=^(<#parameters#>){
<#statements#>
};
4.
在ARC環(huán)境下,是否需要使用copy關(guān)鍵字來(lái)修飾block?正確答案:先要明確的是,block其實(shí)包含兩個(gè)組成部分,一部分是block所執(zhí)行的代碼,這一部分在編譯的時(shí)候已經(jīng)確定;另一部分是block執(zhí)行時(shí)所需要的外部變量值的數(shù)據(jù)結(jié)構(gòu)。根據(jù)block在內(nèi)存中的位置,系統(tǒng)將block分為3類。
1)NSGlobalBlock:該類型的block類似函數(shù),內(nèi)存地址位于內(nèi)存全局區(qū)。只要block沒(méi)有對(duì)作用域中局部變量進(jìn)行引用,此block會(huì)被系統(tǒng)設(shè)置為該類型。示例代碼如下:
-(void)test{
void(^gBlock1)(int,int)=^(inta,intb){
NSLog(@"a+b=%d",a+b);
};
NSLog(@"%@",gBlock1);
}
以上代碼的輸出結(jié)果是:<__NSGlobalBlock__:0x1025e8110>
事實(shí)上,對(duì)于NSGlobalBlock類型的block,無(wú)需做更多的處理,不需要使用retain和copy進(jìn)行修飾。即使使用了copy,系統(tǒng)也不會(huì)改變block的內(nèi)存地址,操作是無(wú)效的。
2)NSStackBlock:該類型的block內(nèi)存位于棧,其生命周期由函數(shù)決定,函數(shù)返回后block將無(wú)效。
在MRC環(huán)境下,若block內(nèi)部引用了局部變量,此block就會(huì)被系統(tǒng)設(shè)置為該類型。對(duì)于NSStackBlock類型的block,使用retain和release操作都是無(wú)效的,必須調(diào)用Block_copy()方法,或者使用copy進(jìn)行修飾,其作用就是將block的內(nèi)存從棧轉(zhuǎn)移到堆,此時(shí)block就會(huì)轉(zhuǎn)變?yōu)镹SMallocBlock類型,這也是一直使用copy修飾block的原因。
在ARC環(huán)境下,若block內(nèi)部引用了局部變量,系統(tǒng)默認(rèn)使用了copy對(duì)block進(jìn)行修飾,使其變成NSMallocBlock類型。所以在ARC環(huán)境下,不需要手動(dòng)使用copy關(guān)鍵字來(lái)修飾block。
3)NSMallocBlock:當(dāng)對(duì)NSStackBlock類型的block進(jìn)行copy操作后,block就會(huì)轉(zhuǎn)為此類型。在MRC環(huán)境下,可以使用retain、release等方法手動(dòng)管理此類型block的生命周期。在ARC環(huán)境下,系統(tǒng)會(huì)幫助管理此類型block的生命周期。
5.
在block內(nèi)如何修改block外部變量?正確答案:在block內(nèi)部修改block外部變量會(huì)造成編譯錯(cuò)誤,提示變量缺少block修飾,不可賦值。要想在block內(nèi)部修改block外部變量,則必須在外部定義變量時(shí),前面加上block修飾符。示例代碼如下:
/*block外部變量*/
__blockintvar1=0;
intvar2=0;
/*定義block*/
void(^block)(void)=^{
/*試圖修改block外部變量*/
var1=100;
/*編譯錯(cuò)誤,在block內(nèi)部不可對(duì)var2賦值*/
//var2=1:
};
/*執(zhí)行block*/
block();
NSLog(@"修改后的var1:%d",var1);
//修改后的var1:100
block內(nèi)部為何不能直接修改外部變量呢?因?yàn)楫?dāng)外部變量沒(méi)有使用__block修飾符修飾時(shí),block在截獲外部的自動(dòng)變量時(shí)會(huì)在內(nèi)部新創(chuàng)建一個(gè)新的變量val來(lái)保存所截獲的外部變量的瞬時(shí)值,新變量val成為block的成員變量(Objective-C中block也是對(duì)象),之后在block代碼中修改的值是成員變量val的值,而不是截獲的外部變量的值,所以外部變量的值不會(huì)受影響。此時(shí),修改外部變量是先取值并賦值給成員變量val,然后修改val的值。可用下而的代碼模擬其原理,假設(shè)block對(duì)外部變量var進(jìn)行了加1操作,block使用一個(gè)名為block的函數(shù)來(lái)表示。
intvar=1;
voidblock(){
intval=var;
val+=1;
}
當(dāng)外部變量使用了__block修飾符進(jìn)行修飾的時(shí)候則是另外一種情形了,此時(shí)block并不是截獲外部自動(dòng)變量的瞬時(shí)值并保存到自己的新成員變量中,而是保存了對(duì)外部變量的指引引用,因此對(duì)指針變量的修改會(huì)直接影響外部變量的值。此時(shí)使用代碼模擬其原理如下,依然是block對(duì)外部變量var進(jìn)行加1操作。
__blockintVar=1;
voidblock(){
int*ptr=&vat;
*ptr+=1;
}
因此,block內(nèi)部不可以直接修改外部變量,如果要修改外部變量,那么該外部變量必須使用__block修飾符進(jìn)行修飾,否則編譯器會(huì)直接進(jìn)行報(bào)錯(cuò)提示。
需要注意的是,此處討論的是自動(dòng)變量,而靜態(tài)變量由于默認(rèn)傳給block的就是地址值,所以是可以直接修改的。另外,全局變量和靜態(tài)全局變量由于作用域很廣,也是可以在block中直接被修改的,編譯器也不會(huì)報(bào)錯(cuò)。
6.
在block中使用self關(guān)鍵字是否一定導(dǎo)致循環(huán)引用?正確答案:在block中使用self關(guān)鍵字并不總會(huì)引起循環(huán)引用。事實(shí)上,只有當(dāng)block和self相互持有時(shí),才會(huì)導(dǎo)致循環(huán)引用。由于block會(huì)對(duì)block中的對(duì)象進(jìn)行持有操作,就相當(dāng)于持有了其中的對(duì)象,此時(shí)如果block中的對(duì)象又持有了該block,那么就會(huì)造成循環(huán)引用。典型的場(chǎng)景就是當(dāng)block作為self的屬性使用時(shí),又在block內(nèi)部調(diào)用了self的屬性或者方法。示例代碼如下:
typedefvoid(^block)();
@property(copy,nonatomic)blockmyBlock;
@property(copy,nonatomic)NSString*blockString;
-(void)testBlock{
self.myBlock=^(){
/*其實(shí)注釋中的代碼,同樣會(huì)造成循環(huán)引用*/
NSString*localString=self.blockString;
};
}
在上面這個(gè)例子中,myBlock和self相互引用了對(duì)方。此時(shí),self的銷毀依賴于myBlock的銷毀,而myBlock的銷毀又依賴self的銷毀,這樣就造成了循環(huán)引用,即使在外界已經(jīng)沒(méi)有任何指針能夠訪問(wèn)到它們了,它們也無(wú)法被釋放,如圖所示。
block循環(huán)引用
解決循環(huán)引用的關(guān)鍵是斷開(kāi)引用鏈。在實(shí)際開(kāi)發(fā)中,主要使用弱引用(weakreference)的方法來(lái)避免循環(huán)引用的產(chǎn)生。在ARC環(huán)境下,使用__weak修飾符定義一個(gè)__weakself的引用,并且在里面使用這個(gè)弱引用。使用這種方式對(duì)示例代碼修改如下:
typedefvoid(^block)();
@property(copy,nonatomie)blockmyBlock;
@property(copy,nonatomic)NSString*blockString;
-(void)testBlock{
__weaktypeOf(self)weakSelf=self;
self.myBlock=^(){
/*其實(shí)注釋中的代碼,同樣會(huì)造成循環(huán)引用*/
NSString*localString=weakSelf.blockString;
};
}
當(dāng)使用__weak修飾的弱類型self時(shí),block便不會(huì)再持有self的引用了,也就不會(huì)再產(chǎn)生循環(huán)引用了。
下面是不會(huì)造成循環(huán)引用的幾種情況:
1)大部分GCD方法。示例代碼如下:
/*使用GCD異步執(zhí)行主隊(duì)列任務(wù)*/
dispatch_async(dispatch_get_main_queue(),^{
[selfdoSomething];
});
在例子中,因?yàn)閟elf并沒(méi)有對(duì)GCD的block進(jìn)行持有,只有block持有了self的引用,所以不會(huì)造成循環(huán)引用。
2)block作為臨時(shí)變量。在這種情況下,同樣self并沒(méi)有持有block,所以也不會(huì)造成循環(huán)引用。
3)block執(zhí)行過(guò)程中self對(duì)象被釋放。事實(shí)上,block的具體執(zhí)行時(shí)間不確定,當(dāng)block被執(zhí)行的時(shí)候block中被__weak修飾的self對(duì)象有可能已經(jīng)被釋放了(例如,控制器對(duì)象已經(jīng)被POP了)。當(dāng)在并發(fā)執(zhí)行,涉及異步服務(wù)的時(shí)候,這種情況有可能會(huì)出現(xiàn)。
對(duì)于這種情況,應(yīng)該在block中使用__strong修飾符修飾self對(duì)象,使得在block期間對(duì)對(duì)象持有,當(dāng)block執(zhí)行結(jié)束后,解除其持有。示例代碼如下:
-(void)testBlock{
__weaktypeOf(self)weakSelf=self;
self.myBlock=^(){
__strongtypeOf(self)strongSelf=weakSelf;
NSString*localString=strongSelf.blockString;
};
}
7.
GCD中有哪幾種隊(duì)列?正確答案:在GCD中,派發(fā)隊(duì)列(DispatchQueue)是最重要的概念之一。派發(fā)隊(duì)列是一個(gè)對(duì)象,它可以接受任務(wù),并將任務(wù)以FIFO(先進(jìn)先出)的順序來(lái)執(zhí)行。派發(fā)隊(duì)列可以是并發(fā)的或串行的。并發(fā)隊(duì)列可以執(zhí)行多任務(wù),串行隊(duì)列同一時(shí)間只執(zhí)行單一任務(wù)。在GCD中,有3種類型的派發(fā)隊(duì)列。
1)串行隊(duì)列。串行隊(duì)列中的任務(wù)按先后順序逐個(gè)執(zhí)行,通常用于同步訪問(wèn)一個(gè)特定的資源。使用dispatch_queue_create函數(shù),可以創(chuàng)建串行隊(duì)列。
2)并發(fā)隊(duì)列。在GCD中也稱為全局并發(fā)隊(duì)列,可以并發(fā)地執(zhí)行一個(gè)或者多個(gè)任務(wù)。并發(fā)隊(duì)列有高、中、低、后臺(tái)4個(gè)優(yōu)先級(jí)別,中級(jí)是默認(rèn)級(jí)別??梢允褂胐ispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)函數(shù)來(lái)獲取全局并發(fā)隊(duì)列對(duì)象。串行隊(duì)列和異步隊(duì)列的區(qū)別在于同步執(zhí)行和異步執(zhí)行時(shí)的表現(xiàn)(見(jiàn)表)。串行隊(duì)列和異步隊(duì)列的區(qū)別
同步執(zhí)行異步執(zhí)行串行隊(duì)列在當(dāng)前線程中,F(xiàn)IFO執(zhí)行在其它線程,F(xiàn)IFO執(zhí)行并發(fā)隊(duì)列在當(dāng)前線程中,F(xiàn)IFO執(zhí)行多條線程,同步執(zhí)行
3)主隊(duì)列。它是一種特殊的串行隊(duì)列。它在應(yīng)用程序的主線程中用于更新UI。其他的兩種隊(duì)列不能更新UI。使用dispatch_get_main_queue函數(shù),可以獲得主隊(duì)列對(duì)象。
8.
如何理解GCD死鎖?正確答案:所謂死鎖,通常指兩個(gè)操作相互等待對(duì)方完成,造成死循環(huán),于是兩個(gè)操作都無(wú)法完成,就產(chǎn)生了死鎖。下面是一個(gè)死鎖的代碼示例。
intmain(intargc,constchar*argv[]){
@autoreleasepool{
dispatch_sync(dispatch_get_main_queue(),^(void){
NSLog(@"這里死鎖了");
});
}
return0;
}
這個(gè)程序就是典型的死鎖。程序?qū)⒅麝?duì)列和一個(gè)block傳入GCD的同步函數(shù)dispatch_sync中,等待同步函數(shù)執(zhí)行,直到同步函數(shù)返回。但是事實(shí)上,這個(gè)block永遠(yuǎn)不會(huì)被執(zhí)行。因?yàn)閙ain函數(shù)是在主隊(duì)列中的,它是正在被執(zhí)行的任務(wù),而主隊(duì)列中同時(shí)只能有一個(gè)任務(wù)在執(zhí)行,也就是說(shuō)只有隊(duì)頭的任務(wù)才能被執(zhí)行。由于主隊(duì)列是一個(gè)特殊的串行隊(duì)列,它嚴(yán)格遵循FIFO的原則,所以block中的任務(wù)必須等到main函數(shù)執(zhí)行完,才能被執(zhí)行。另外,dispatch_sync函數(shù)的特性是,只有block中的任務(wù)被執(zhí)行完畢,才會(huì)返回。因此,只要block不被執(zhí)行,它就不會(huì)返回。所以,在這段代碼中,main函數(shù)等待dispatch_sync函數(shù)返回,而dispatch_sync的返回又依賴block執(zhí)行完畢,block的執(zhí)行又需要等待main函數(shù)的執(zhí)行結(jié)束。這樣就造成了三方循環(huán)等待,即死鎖。
可以總結(jié)出GCD死鎖的原因大體有以下兩點(diǎn):
1)GCD函數(shù)未返回,會(huì)阻塞正在執(zhí)行的任務(wù)。這里需要強(qiáng)調(diào)的是,阻塞(blocking)和死鎖(deadlock)是不同的意思。阻塞表示A任務(wù)的執(zhí)行需要等待B任務(wù)的完成,稱作B會(huì)阻塞A,通俗來(lái)講就是強(qiáng)制等待的意思。而死鎖表示A任務(wù)和B任務(wù)相互等待,形成阻塞閉環(huán)。
2)隊(duì)列中的任務(wù)無(wú)法并發(fā)執(zhí)行。
以上兩點(diǎn),如果同時(shí)出現(xiàn),那么就會(huì)產(chǎn)生阻塞閉環(huán),形成死鎖。所以針對(duì)以上情況,只需要消除其中任何一個(gè)因素,就可以打破這個(gè)閉環(huán),避免死鎖。
解決GCD死鎖的方法有以下幾種方式:
1)使用dispatch_async函數(shù)。dispatch_async函數(shù)是異步函數(shù),具備開(kāi)啟新線程的能力,但是不一定會(huì)開(kāi)啟新線程。如果傳入的隊(duì)列參數(shù)是主隊(duì)列,那么任務(wù)仍然會(huì)在主線程中等待執(zhí)行,函數(shù)不會(huì)立即返回。如果傳入的隊(duì)列是普通的串行隊(duì)列或者并發(fā)隊(duì)列,那么該函數(shù)就會(huì)立即返回。
2)將有可能形成阻塞閉環(huán)的任務(wù)分別放到不同的隊(duì)列中執(zhí)行。如案例中,可以新建一個(gè)串行隊(duì)列,將block放入自己的串行隊(duì)列中,不再和main函數(shù)除以一個(gè)隊(duì)列,就能夠解決隊(duì)列阻塞,因此避免了死鎖問(wèn)題。示例代碼如下:
intmain(intargc,constchar*argv[]){
@autoreleasepool{
dispatch_queueserialQueue=dispatch_queue_create("這是一個(gè)串行隊(duì)列",DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue,^(void){
NSLog(@"這里不會(huì)死鎖了");
});
}
return0;
}
另外,有些面試題,如“是否在主線程使用sync函數(shù)就會(huì)造成死鎖”或者“是否在主線程使用sync函數(shù),同時(shí)傳入串行隊(duì)列就會(huì)死鎖”,答案都是否定的,只要能夠真正了解GCD死鎖的原理,就能很好地回答類似問(wèn)題了。
9.
如何使用GCD實(shí)現(xiàn)線程之間的通信?正確答案:在iOS應(yīng)用程序的開(kāi)發(fā)中,一般需要在主線程中進(jìn)行UI刷新。例如,響應(yīng)單擊、滾動(dòng)或者拖曳等事件,所以主線程一般也被稱為UI線程。在主線程中,應(yīng)該盡量避免在主線程中執(zhí)行一些耗時(shí)的操作,如文件的上傳和下載等。應(yīng)該將這些耗時(shí)操作放到子線程中執(zhí)行,等子線程的耗時(shí)操作執(zhí)行完成后,再通知主線程更新相應(yīng)的UI。要完成這樣的操作,就必須實(shí)現(xiàn)線程之間的通信,GCD是實(shí)現(xiàn)線程之間通信的常用方式。示例代碼如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
/*執(zhí)行耗時(shí)操作*/
for(inti=0;i<10000;i++){
NSLog(@"i=%i",i);
}
/*回到主線程*/
dispatch_async(dispatch_get_main_queue(),^{
//更新UI
}):
});
先將需要執(zhí)行的耗時(shí)操作放入全局并發(fā)隊(duì)列中,再使用dispatch_async異步函數(shù)執(zhí)行并發(fā)隊(duì)列,這樣就會(huì)開(kāi)啟新的線程執(zhí)行耗時(shí)操作,不會(huì)阻塞主線程的任務(wù)。當(dāng)耗時(shí)任務(wù)完成后,通過(guò)主隊(duì)列回到主線程執(zhí)行相應(yīng)的UI更新操作。需要強(qiáng)調(diào)的是,當(dāng)使用主隊(duì)列時(shí),無(wú)論是使用dispatch_async異步函數(shù),還是使用dispatch_sync同步函數(shù),執(zhí)行的結(jié)果是一樣的。因?yàn)橹麝?duì)列是一種特殊的串行隊(duì)列,在主隊(duì)列中任務(wù)總會(huì)在主線程中執(zhí)行。
10.
GCD如何實(shí)現(xiàn)線程同步?正確答案:NSOperation可以通過(guò)使用addDependency函數(shù)直接設(shè)置操作之間的依賴關(guān)系來(lái)調(diào)整操作之間的執(zhí)行順序從而實(shí)現(xiàn)線程同步,還可以使用setMaxConcurrentOperationcount函數(shù)來(lái)直接設(shè)置并控制最大并發(fā)數(shù)量,那么在GCD中如何實(shí)現(xiàn)呢?
GCD實(shí)現(xiàn)線程同步的方法有以下3種:
1)組隊(duì)列(dispatch_group)。
2)阻塞任務(wù)(dispatch_barrier_(a)sync)。
3)信號(hào)量機(jī)制(dispatch_semaphore)
信號(hào)量機(jī)制主要是通過(guò)設(shè)置有限的資源數(shù)量來(lái)控制線程的最大并發(fā)數(shù)量及阻塞線程實(shí)現(xiàn)線程同步等。
GCD中使用信號(hào)量需要用到3個(gè)函數(shù):
1)dispatch_semaphore_create用來(lái)創(chuàng)建一個(gè)semaphore信號(hào)量并設(shè)置初始信號(hào)量的值。
2)dispatch_semaphore_signal發(fā)送一個(gè)信號(hào)讓信號(hào)量增加1(對(duì)應(yīng)PV操作的V操作)。
3)dispatch_semaphore_wait等待信號(hào)使信號(hào)量減1(對(duì)應(yīng)PV操作的P操作)。
11.
GCD多線程編程中什么時(shí)候會(huì)創(chuàng)建新線程?正確答案:對(duì)于是否會(huì)開(kāi)啟新線程的情景主要有如下幾種情況:串行隊(duì)列中提交異步任務(wù)、串行隊(duì)列中提交同步任務(wù)、并發(fā)隊(duì)列中提交異步任務(wù)、并發(fā)隊(duì)列中提交同步任務(wù)。
(其中主隊(duì)列是典型的串行隊(duì)列,全局隊(duì)列是典型的并發(fā)隊(duì)列)
/*創(chuàng)建一個(gè)串行隊(duì)列*/
dispatch_queue_tserialQueue=dispatch_queue_create("serial.queue".DISPATCH_QUEUE_SERIAL);
/*創(chuàng)建一個(gè)并發(fā)隊(duì)列*/
dispatch_queue_tconcurrentQueue=dispatch_queue_create("concurrent.queue",DISPATCH_QUEUE_CONCURRENT);
1)串行隊(duì)列中提交同步任務(wù):不會(huì)開(kāi)啟新線程,直接在當(dāng)前線程同步地串行執(zhí)行這些任務(wù)。
/*1串行隊(duì)列添加同步任務(wù):沒(méi)有開(kāi)肩新線程,全部在主線程串行執(zhí)行*/
dispatch_sync(serialQueue,^{
NSLog(@"SERIAL_SYN_A%@",[NSThreadcurrentThread]);
});
dispatch_sync(serialQueue,^{
NSLog(@"SERIAL_SYN_B%@",[NSThreadcurrentThread]);
});
dispatch_sync(serialQueue,^{
NSLog(@"SERIAL_SYN_C%@",[NSThreadcurrentThread]);
});
2)串行隊(duì)列中提交異步任務(wù):會(huì)開(kāi)啟一個(gè)新線程,在新子線程異步地串行執(zhí)行這些任務(wù)。
/*2串行隊(duì)列添加異步任務(wù):開(kāi)啟了一個(gè)新子線程并共用,串行執(zhí)行*/
dispatch_async(serialQueue,^{
NSLog(@"SERIAL_ASYN_A%@",[NSThreadcurrentThread]);
});
dispatch_async(serialQueue,^{
NSLog(@"SERfAL_ASYN_B%@",[NSThreadcurrentThread]);
});
dispatch_async(serialQueue,^{
NSLog(@"SERIAL_ASYN_C%@",[NSThreadcurrentThread]);
});
3)并發(fā)隊(duì)列中提交同步任務(wù):不會(huì)開(kāi)啟新線程,效果和“串行隊(duì)列中提交同步任務(wù)”一樣,直接在當(dāng)前線程同步地串行執(zhí)行這些任務(wù)。
/*3并發(fā)隊(duì)列添加同步任務(wù):沒(méi)有開(kāi)啟新線程,全部在主線程串行執(zhí)行*/
dispatch_sync(concurrentQueue,^{
NSLog(@"CONCURRENT_SYN_A%@",[NSThreadcurrentThread]);
});
dispatch_sync(concurrentQueue,^{
NSLog(@"CONCURRENT_SYN_B%@",(NSThreadcurrentThread]);
});
dispatch_sync(concurrentQueue,^{
NSLog(@"CONCURRENT_SYN_C%@",[NSThreadcurrentThread]);
});
4)并發(fā)隊(duì)列中提交異步任務(wù):會(huì)開(kāi)啟多個(gè)子線程,在子線程異步地并發(fā)執(zhí)行這些任務(wù)。
/*4并發(fā)隊(duì)列添加異步任務(wù):開(kāi)啟多個(gè)子線程,并發(fā)執(zhí)行多個(gè)任務(wù)*/
dispatch_async(concurrentQueue,^{
NSLog(@"CONCURRENT_ASYN_A%@",[NSThreadcurrentThread]);
});
dispatch_async(concurrentQueue,^{
NSLog(@"CONCURRENT_ASYN_B%@",[NSThreadcurrentThread]);
});
dispatch_async(concurrentQueue,^{
NSLog(@"CONCURRENT_ASYN_C%@",[NSThreadcurrentThread]);
});
下圖展示了上面例子中線程的執(zhí)行順序。
線程執(zhí)行順序
總結(jié):
只有異步提交任務(wù)時(shí)才會(huì)開(kāi)啟新線程,異步提交到串行隊(duì)列會(huì)開(kāi)啟一個(gè)新線程,異步提交到并發(fā)隊(duì)列可能會(huì)開(kāi)啟多個(gè)線程。
同步提交任務(wù)無(wú)論提交到并發(fā)隊(duì)列還是串行隊(duì)列,都不會(huì)開(kāi)啟新線程,都會(huì)直接在當(dāng)前線程依次同步執(zhí)行。
注意,如果當(dāng)前線程是主線程,那么不可在當(dāng)前線程提交同步任務(wù),否則會(huì)造成線程死鎖而報(bào)錯(cuò)。
12.
iOS中如何觸發(fā)定時(shí)任務(wù)或延時(shí)任務(wù)?正確答案:定時(shí)任務(wù)指周期性地調(diào)用某個(gè)方法,實(shí)現(xiàn)任務(wù)的反復(fù)執(zhí)行,如倒計(jì)時(shí)等;延時(shí)任務(wù)指等待一定的時(shí)間后再執(zhí)行某個(gè)任務(wù),如頁(yè)面的延時(shí)跳轉(zhuǎn)等。iOS中控制任務(wù)的延時(shí)或定時(shí)執(zhí)行的方法有很多,使用中要注意是同步還是異步,是否會(huì)阻塞主線程等問(wèn)題。延時(shí)和定時(shí)的實(shí)現(xiàn)方法依次如下。
1.performSelector實(shí)現(xiàn)延時(shí)任務(wù)
延時(shí)任務(wù)可以通過(guò)當(dāng)前UIViewController的perfonnSelector隱式創(chuàng)建子線程實(shí)現(xiàn),不會(huì)阻塞主線程。
/*延遲10s執(zhí)行任務(wù)*/
[selfperformSelector:@selector(task)withObject:nilafterDelay:10];
-(void)task
{
//delaytask
}
2.利用sleep實(shí)現(xiàn)后面任務(wù)的等待
慎用,會(huì)阻塞主線程N(yùn)SThreadsleepForTimeInterval:10.0];
3.GCD實(shí)現(xiàn)延時(shí)或定時(shí)任務(wù)
通過(guò)GCD實(shí)現(xiàn)block代碼塊的延時(shí)執(zhí)行。
dispatch_time_tdelay=dispatch_time(DiSPATCH_TIME_NOW,10*NSEC_PER_SEC);
dispatch_after(delay,dispatch_get_main_queue0,^{
//delaytask
});
GCD還可以用來(lái)實(shí)現(xiàn)定時(shí)器功能,還能設(shè)置延時(shí)開(kāi)啟計(jì)時(shí)器,使用中注意一定要定義強(qiáng)引用指針來(lái)指向計(jì)時(shí)器對(duì)象才可讓計(jì)時(shí)器生效。
/*必須要用強(qiáng)引用指針,計(jì)時(shí)器才會(huì)生效*/
@property(nonatomic,strong)dispatch_source_ttimer;
/*在指定線程上定義計(jì)時(shí)器*/
dispatch_queue_tqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatc_souree_t_timer=dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0,queue);
/*開(kāi)始的時(shí)間*/
dispatch_time_twhen=dispatch_time(DISPATCH_TIME_NOW,(int64_t)(1.0*NSEC_PER_SEC));
/*設(shè)置計(jì)時(shí)器*/
dispatch_source_set_timer(_timer,when,1.0*NSEC_PER_SEC,0);
/*計(jì)時(shí)器回調(diào)block*/
dispatch_source_set_event_handler(_timer,^{
NSLog(@"dispatch_source_set_timerisworking!");
});
/*開(kāi)啟計(jì)時(shí)器*/
dispatch_resume(_timer);
/*強(qiáng)引用計(jì)時(shí)器對(duì)象*/
self.timer=_timer;
4.NSTimer實(shí)現(xiàn)定時(shí)任務(wù)
NSTimer主要用于開(kāi)啟定時(shí)任務(wù),但要正確使用才能保證它能夠正常有效地運(yùn)行。尤其要注意以下兩點(diǎn):
1)確保NSTimer已經(jīng)添加到當(dāng)前RunLoop。
2)確保當(dāng)前RunLoop已經(jīng)啟動(dòng)。
創(chuàng)建NSTimer有兩種方法,代碼如下:
+(NSTimer*)timerWithTimeInterval:(NSTimeInterval)titarget:(id)aTargetselector:(SEL)aSelectoruserInfo:(nullableid)userInforepeats:(BOOL)yesOrNo;
+(NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)titarget:(id)aTargetselector:(SEL)aSelectoruserInfo:(nullableid)userInforepeats:(BOOL)yesOrNo;
這兩種方法的主要區(qū)別為,使用timerWithTimeInterval創(chuàng)建的timer不會(huì)自動(dòng)添加到當(dāng)前RunLoop中,需要手動(dòng)添加并指定RunLoop的模式:[[NSRunLoopcurrentRunLoop]addTimer:mainThreadTimerforMode:NSDefaultRtmLoopMode];而使用scheduledTimerWithTimeInterval創(chuàng)建的RunLoop會(huì)默認(rèn)添加到當(dāng)前RunLoop中。
NSTimer可能在主線程中創(chuàng)建,也可能在子線程中創(chuàng)建。主線程中的RunLoop默認(rèn)是啟動(dòng)的,所以timer只要添加到主線程RunLoop中就會(huì)被執(zhí)行;而子線程中的RunLoop默認(rèn)是不啟動(dòng)的,所以timer添加到子線程RunLoop中后,還要手動(dòng)啟動(dòng)RunLoop才能使timer被執(zhí)行。
NSTimer只有添加到啟動(dòng)起來(lái)的RunLoop中才會(huì)正常運(yùn)行。NSTimer通常不建議添加到主線程中執(zhí)行,因?yàn)榻缑娴母略谥骶€程中進(jìn)行,這會(huì)影響NSTimer的準(zhǔn)確性。
以下代碼為4種情形下NSTimer的正確使用方法。
-(void)viewDidLoad{
[superviewDidLoad];
/*第一種,主線程中創(chuàng)建timer,需要手動(dòng)添加到RunLoop中*/
NSTimer*mainThreadTimer=[NSTimertimerWithTimeInterval:10.0target:selfselector:@selector(mainThreadTimer_SEL)userInfo:nilrepeats:YES];
/*第二種,主線程中創(chuàng)建timer,不需要手動(dòng)添加到RunLoop中*/
NSTimer*mainThreadSchduledTimer=[NSTimer
scheduledTimerWithTimeInterval:10.0target:selfselector:@selector(mainThreadSchduledTimer_SEL)userInfo:nilrepeats:YES];
/*將mainThreadTimer添加到主線程runloop*/
[[NSRunLoopcurrentRunLoop]addTimer:mainThreadTimer
forMode:NSDefaultRunLoopMode];
dispatch_asyne(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
/*第三種,子線程中創(chuàng)建timer,需要手動(dòng)添加到RunLoop中*/
NSTimer*subThreadTimer=[NSTimertimerWithTimeInterval:10.0target:selfselector:@selector(subThreadTimer_SEL)userInfo:nilrepeats:YES];
/*第三種,子線程中創(chuàng)建timer,不需要手動(dòng)添加到RunLoop中*/
NSTimer*subThreadSchduledTimer=[NSTimerscheduledTimerWithTimeInterval:10.0target:selfselector:@selector(subThreadSchduledTimer_SEL)userInfo:nilrepeats:YES];
/*將subThreadTimer添加到子線程runloop*/
[[NSRunLoopcurrentRunLoop]addTimer:subThreadTimerforMode:NSDefaultRunLoopMode];
/*啟動(dòng)子線程mnloop*/
[[NSRunLoopcurrentRunLoop]runMode:NSDefaultRunLoopModebeforeDate:[NSDatedistantFuture]];
});
}
-(void)mainThreadTimer_SEL{
NSLog(@"mainThreadTimerisworking!");
}
-(void)mainThreadSchduledTimer_SEL{
NSLog(@"mainThreadSchduledTimerisworking!");
}
-(void)su
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年大學(xué)安全工程(安全系統(tǒng)工程)試題及答案
- 2025年高職農(nóng)產(chǎn)品加工與質(zhì)量檢測(cè)(質(zhì)量檢測(cè)技術(shù))試題及答案
- 2025年大學(xué)大四(宴會(huì)設(shè)計(jì))菜單定制專項(xiàng)測(cè)試題及答案
- 新能源鋰電光伏復(fù)合涂層材料生產(chǎn)項(xiàng)目可行性研究報(bào)告模板-立項(xiàng)備案
- 2026年如何降低電氣設(shè)備故障率
- 2025四川自貢市第一人民醫(yī)院招聘醫(yī)療輔助崗人員11人備考題庫(kù)及完整答案詳解1套
- 2025福建廈門(mén)市翔發(fā)集團(tuán)有限公司招聘3人備考題庫(kù)(第三期)及1套完整答案詳解
- 2026浙江杭州市建德市大同鎮(zhèn)中心衛(wèi)生院編外人員招聘3人備考題庫(kù)及答案詳解(奪冠系列)
- 2025財(cái)達(dá)證券股份有限公司資產(chǎn)管理業(yè)務(wù)委員會(huì)招聘2人備考題庫(kù)(北京)及一套參考答案詳解
- 2025新疆阿勒泰布喀公路建設(shè)開(kāi)發(fā)有限公司招聘1人備考題庫(kù)及一套參考答案詳解
- 美術(shù)教學(xué)中的跨學(xué)科教學(xué)策略
- mc尼龍澆鑄工藝
- 旅居養(yǎng)老可行性方案
- 燈謎大全及答案1000個(gè)
- 老年健康與醫(yī)養(yǎng)結(jié)合服務(wù)管理
- 中國(guó)焦慮障礙防治指南
- 1到六年級(jí)古詩(shī)全部打印
- 心包積液及心包填塞
- GB/T 40222-2021智能水電廠技術(shù)導(dǎo)則
- 兩片罐生產(chǎn)工藝流程XXXX1226
- 第十章-孤獨(dú)癥及其遺傳學(xué)研究課件
評(píng)論
0/150
提交評(píng)論