iOS.URLEncode,看看AFN和Facebook吧

別再使用stringByAddingPercentEscapesUsingEncoding

當(dāng)遇到發(fā)送網(wǎng)絡(luò)請求的參數(shù)中有漢字的情況,很多人一股腦地使用stringByAddingPercentEscapesUsingEncoding:進(jìn)行轉(zhuǎn)義,這樣帶有漢字的urlString就會將每個(gè)漢字轉(zhuǎn)成相應(yīng)的unicode編碼對應(yīng)的3個(gè)%形式,這叫urlEncode(每個(gè)能寫后端的語言都有的方法),但是蘋果的stringByAddingPercentEscapesUsingEncoding:卻不是urlEncode。實(shí)際上我們使用的參數(shù)值可能會包含一些特殊的字符,如&,?這樣的字符,而Percent轉(zhuǎn)義已經(jīng)不能滿足需求了,如下面的例子:

NSString*queryWord =@"漢字&ss";NSString*urlString = [NSStringstringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", queryWord];NSString*escapedString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSLog(@"%@", escapedString);// https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97&ss

這是一個(gè)非常常見的情景,(之前公司項(xiàng)目的搜索中,也遇到過這種情況),這種被轉(zhuǎn)義之后的URL,服務(wù)端接收到的參數(shù)會使這樣的

["ie":"UTF-8","wd":"漢字","ss":nil]

即使你做如下的改進(jìn):(在請求之前將每個(gè)參數(shù)都轉(zhuǎn)義,再使用&拼接參數(shù)也無濟(jì)于事)

NSString*queryWord =@"漢字&ss";NSString*escapedQueryWord = [queryWord stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSString*urlString = [NSStringstringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", escapedQueryWord];NSLog(@"%@", urlString);// https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97&ss

產(chǎn)生這種情況的原因是:百分號轉(zhuǎn)義不等于URLEncode

該編碼不同于URL編碼,由于不會對&字符編碼,因此不會改變URL參數(shù)的分隔。URL編碼會編碼&、?與其他標(biāo)點(diǎn)符號。如果查詢字符串包含了這些字符,那么需要實(shí)現(xiàn)一種更加徹底的編碼方法。

不過還好iOS7.0推出了stringByAddingPercentEncodingWithAllowedCharacters:方法,這個(gè)方法會對字符串進(jìn)行更徹底的轉(zhuǎn)義,但是需要傳遞一個(gè)參數(shù):這個(gè)參數(shù)是一個(gè)字符集,表示:在進(jìn)行轉(zhuǎn)義過程中,不會對這個(gè)字符集中包含的字符進(jìn)行轉(zhuǎn)義,而保持原樣保留下來。

這樣就可以使用它改造上面的代碼了:

NSString *queryWord =@"漢字&ss";NSString *escapedQueryWord = [queryWord stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]];NSLog(@"%@", escapedQueryWord);//%E6%B1%89%E5%AD%97%26ssNSString *urlString = [NSString stringWithFormat:@"https://www.baidu.com/s?ie=UTF-8&wd=%@", escapedQueryWord];NSLog(@"%@", urlString);// https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97%26ss

在上面的例子中傳遞參數(shù)[NSCharacterSet letterCharacterSet]來保證字母不被轉(zhuǎn)義。所以被轉(zhuǎn)義之后的參數(shù)值是:%E6%B1%89%E5%AD%97%26ss,這樣問題就解決了,但是有時(shí)候會遇到queryString中的表單域也需要轉(zhuǎn)義的情況,比如是一個(gè)表單數(shù)組如:

https://www.baidu.com/s?person[contact]=13801001234&person[address]=北京&habit[]=游泳&habit[]=騎行

這樣可以使用將key轉(zhuǎn)義,不過key中的[和]字符是不需要轉(zhuǎn)義的:可以自定義一個(gè)CharacterSet實(shí)現(xiàn)需求:

NSMutableCharacterSet*mutableCharSet = [[NSMutableCharacterSetalloc] init];[mutableCharSet addCharactersInString:@"[]"];// 允許'['和']'不被轉(zhuǎn)義NSCharacterSet*charSet = mutableCharSet.copy;NSMutableString*mutableString = [NSMutableStringstring];for(unitinqueryString) {NSString*escapedField = [unit.field stringByAddingPercentEncodingWithAllowedCharacters:charSet];NSString*escapedValue = [unit.value stringByAddingPercentEncodingWithAllowedCharacters:charSet];? ? [mutableString addFormat:@"%@=%@", escapedField, escapedValue];}

這樣問題已經(jīng)圓滿解決了,美中不足的是:當(dāng)queryString非常多的時(shí)候你如何保證從queryString正確地提取出來每個(gè)unit呢,這個(gè)牽扯到復(fù)雜的字符串解析的問題。先不做討論。實(shí)際上有一個(gè)好的方案是使用AFN將每個(gè)參數(shù)的URL和queryString在構(gòu)建的時(shí)候分離,使用URL和parameter(字典)分別傳入的方法,也就是說在使用AFN的時(shí)候避免使用:

GET:@"https://www.baidu.com/s?ie=UTF-8&wd=%E6%B1%89%E5%AD%97%26ss"parameters:nil

success:nil

failure:nil

而是盡量使用

GET:@"https://www.baidu.com/s"parameters:@{@"ie":@"UTF-8",@"wd":@"漢字&ss"};success:nilfailure:nil

為什么要這樣,翻看AFN的源碼會發(fā)現(xiàn),AFN對queryString的組裝是這樣進(jìn)行的:

AFN會將parameters的傳遞的字典通過將每個(gè)表單元素的field和value進(jìn)行urlcode之后拼接,然后再直接附加在傳遞的URLString后面(當(dāng)然,如果是POST方式就不是附加了,而是將拼好的串放到HTTP body中)。

那么如果要使用第一種方式,必須要確保自己在傳入的URLString是經(jīng)過完美轉(zhuǎn)義的,因?yàn)锳FN不會對你傳入的URLString進(jìn)行檢測有沒有進(jìn)行了轉(zhuǎn)義或者正確與否,但是AFN對上面方法中parameter參數(shù)的解析時(shí)非常徹底的,因此強(qiáng)烈建議使用第二種方式調(diào)用AFN的方法。那么AFN是如何完美解析parameter參數(shù)的呢,這剛好是一個(gè)可以將字典轉(zhuǎn)為queryString的模塊呀!??!,下面就來看一下:

對AFN urlEncode的研究

AFN將網(wǎng)絡(luò)訪問分割為三個(gè)過程模塊:

1.請求前:構(gòu)建request的header和queryString、uploadContent和配置(如超時(shí)等),這部分的功能在AFURLRequestSerialization中

2.請求中:分別有基于NSConnection的訪問(3.0移除)和基于NSURLSession的訪問模塊

3.請求后:1錯(cuò)誤處理2.成功處理:數(shù)據(jù)格式轉(zhuǎn)換和解析,主要在AFURLResponseSerialization中

requestSerialization就像過濾器一樣,每一個(gè)用于構(gòu)建網(wǎng)絡(luò)請求的URLRequest對象都會經(jīng)過requestSerialization配置,再返回一個(gè)NSMutableURLRequest對象(參見2.x版本的dataTaskWithHTTPMethod: URLString: parameters: success: failure方法,3.0版本dataTaskWithHTTPMethod URLString: parameters: uploadProgress: downloadProgress: success:方法),NSURLSession對象會使用這個(gè)NSMutableURLRequest對象創(chuàng)建task。而我們要討論的將parameter轉(zhuǎn)為queryString的功能全部在AFURLRequestSerialization中,它實(shí)際上使用了

NSMutableURLRequest *request = [self.requestSerializerrequestWithMethod:methodURLString:[[NSURLURLWithString:URLStringrelativeToURL:self.baseURL] absoluteString]parameters:parameterserror:&serializationError];

就完成了所有的請求前的配置功能,可以查看一下內(nèi)部的實(shí)現(xiàn),有一句關(guān)鍵性的代碼

mutableRequest = [[selfrequestBySerializingRequest:mutableRequestwithParameters:parameterserror:error] mutableCopy];// 這里的self是AFURLResponseSerialization對象

這句代碼用于對request對象設(shè)置requestHeader和轉(zhuǎn)義queryString,我們僅僅看一下對queryString進(jìn)行轉(zhuǎn)義的其內(nèi)部按照這樣的思路實(shí)現(xiàn):

1.如果傳遞過來的parameters不為空,就會判斷self.queryStringSerialization是否為空(self.queryStringSerialization屬性是一個(gè) AFQueryStringSerializationBlock類型的block,它是用來實(shí)現(xiàn)轉(zhuǎn)義的核心代碼塊)

2.如果self.queryStringSerialization不為空,使用self.queryStringSerialization(request, parameters, &serializationError);進(jìn)行轉(zhuǎn)義和組裝:

3.如果self.queryStringSerialization為空,使用一個(gè)內(nèi)部函數(shù)來執(zhí)行:AFQueryStringFromParameters(parameters),

實(shí)際上每一個(gè)AFURLResponseSerialization對象在創(chuàng)建的時(shí)候queryStringSerialization屬性都是空的,因此外部不傳遞block類型的值給queryStringSerialization屬性時(shí)都會走這條路線,也就是使用AFQueryStringFromParameters(parameters)來解析參數(shù)。

AFQueryStringFromParameters的實(shí)現(xiàn)是這樣的:

NSString* AFQueryStringFromParameters(NSDictionary*parameters) {NSMutableArray*mutablePairs = [NSMutableArrayarray];for(AFQueryStringPair *pairinAFQueryStringPairsFromDictionary(parameters)) {? ? ? ? [mutablePairs addObject:[pair URLEncodedStringValue]];? ? }return[mutablePairs componentsJoinedByString:@"&"];}

而其中使用到的AFQueryStringPairsFromDictionary函數(shù)是這樣實(shí)現(xiàn)的:

NSArray* AFQueryStringPairsFromDictionary(NSDictionary*dictionary) {returnAFQueryStringPairsFromKeyAndValue(nil, dictionary);}NSArray* AFQueryStringPairsFromKeyAndValue(NSString*key,idvalue);// 這個(gè)方法太長,只放置了原型

思路為:

1.利用AFQueryStringPairsFromKeyAndValue函數(shù)將parameters字典中的每個(gè)key-value對取出,將每個(gè)key-value對構(gòu)建為AFQueryStringPair對象,放到一個(gè)數(shù)組中。

2.在AFQueryStringFromParameters方法內(nèi)部遍歷這個(gè)數(shù)組(每個(gè)元素為AFQueryStringPair對象),使用AFQueryStringPair類的轉(zhuǎn)義方法URLEncodedStringValue將AFQueryStringPair轉(zhuǎn)為字符串,將這些字符串存入新的數(shù)組中。這樣新數(shù)組中的每個(gè)元素就是轉(zhuǎn)義之后的field=value字符串,最后用&將數(shù)組元素連接即可。

函數(shù)AFQueryStringPairsFromKeyAndValue是一個(gè)非常完美的算法,基本上考慮到了所有類型的表單域:包括表單數(shù)組的處理和對一個(gè)表單域賦值多個(gè)value的情況的處理,表單數(shù)組在html頁面經(jīng)常用到的:

瀏覽器自動轉(zhuǎn)義: habit%5B%5D=%E6%B8%B8%E6%B3%B3&habit%5B%5D=%E9%AA%91%E8%A1%8C

AFN傳遞parameter = @{@"habit":@[@"游泳", @"騎行"]}:

2.x版本:habit[]=%E6%B8%B8%E6%B3%B3&habit[]=%E9%AA%91%E8%A1%8C

3.0版本:habit%5B%5D=%E6%B8%B8%E6%B3%B3&habit%5B%5D=%E9%AA%91%E8%A1%8C (與瀏覽器相同)

php會將$_GET解析為:

array(1){["habit"]=>array(2){[0]=>string(6)"游泳"[1]=>string(6)"騎行"} }

如果是這種寫法:

瀏覽器自動轉(zhuǎn)義:person%5Bcontact%5D=13801001234&person%5Baddress%5D=%E5%8C%97%E4%BA%AC

AFN傳遞parameter = @{@"person":@{@"contact":@"13801001234", @"address":@"北京"}}:

2.x版本: person[address]=%E5%8C%97%E4%BA%AC&person[contact]=13801001234 (沒有將[]轉(zhuǎn)義)

3.0版本:person%5Baddress%5D=%E5%8C%97%E4%BA%AC&person%5Bcontact%5D=13801001234 (與瀏覽器相同,但進(jìn)行了排序)

結(jié)果為:

array(1){["person"]=>array(2){["contact"]=>string(11)"13801001234"["address"]=>string(6)"北京"} }

// 如果是對于一個(gè)field多參數(shù)的情況

游泳騎行

瀏覽器自動轉(zhuǎn)義:habit=%E6%B8%B8%E6%B3%B3&habit=%E9%AA%91%E8%A1%8C

AFN傳遞parameter = @{@"habit":set} 其中set為:NSSet *set = [NSSet setWithObjects:@"游泳", @"騎行", nil];:

2.x版本: habit=%E6%B8%B8%E6%B3%B3&habit=%E9%AA%91%E8%A1%8C (與瀏覽器相同)

3.0版本:habit=%E6%B8%B8%E6%B3%B3&habit=%E9%AA%91%E8%A1%8C (與瀏覽器相同)

以上各種類型的html表單域的處理,函數(shù)AFQueryStringPairsFromKeyAndValue都已經(jīng)很好地處理,不管如此,還對每個(gè)field按照NSString默認(rèn)排序規(guī)則(字母表順序)進(jìn)行了排序。

在接下來我會針對2.x版本的AFN剖析一下AFQueryStringPair的轉(zhuǎn)義方法- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding的實(shí)現(xiàn),中間會說到一些和3.0的差別:

我們看一下- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding的代碼

- (NSString*)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding {// AFN3.0的區(qū)別是換了個(gè)方法名,而且不用傳遞stringEncodingif(!self.value || [self.value isEqual:[NSNullnull]]) {// 如果value為空值,只轉(zhuǎn)義fieldreturnAFPercentEscapedQueryStringKeyFromStringWithEncoding([self.field description], stringEncoding);? ? }else{// 將field和value轉(zhuǎn)義后拼接return[NSStringstringWithFormat:@"%@=%@", AFPercentEscapedQueryStringKeyFromStringWithEncoding([self.field description], stringEncoding), AFPercentEscapedQueryStringValueFromStringWithEncoding([self.value description], stringEncoding)];? ? }}

而對于AFPercentEscapedQueryStringKeyFromStringWithEncoding和AFPercentEscapedQueryStringValueFromStringWithEncoding方法,它的實(shí)現(xiàn)是這樣的:

staticNSString*constkAFCharactersToBeEscapedInQueryString =@":/?&=;+!@#$()',*";// 在queryString進(jìn)行URLEncode時(shí)需要進(jìn)行轉(zhuǎn)義的字符staticNSString* AFPercentEscapedQueryStringKeyFromStringWithEncoding(NSString*string,NSStringEncodingencoding) {staticNSString*constkAFCharactersToLeaveUnescapedInQueryStringPairKey =@"[].";// 在urlencode時(shí)不需要轉(zhuǎn)義return(__bridge_transferNSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridgeCFStringRef)string, (__bridgeCFStringRef)kAFCharactersToLeaveUnescapedInQueryStringPairKey, (__bridgeCFStringRef)kAFCharactersToBeEscapedInQueryString,CFStringConvertNSStringEncodingToEncoding(encoding));}staticNSString* AFPercentEscapedQueryStringValueFromStringWithEncoding(NSString*string,NSStringEncodingencoding) {return(__bridge_transferNSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridgeCFStringRef)string,NULL, (__bridgeCFStringRef)kAFCharactersToBeEscapedInQueryString,CFStringConvertNSStringEncodingToEncoding(encoding));}

這里是使用了一個(gè)CoreFoundation中定義的函數(shù):CFURLCreateStringByAddingPercentEscapes這函數(shù)的參數(shù)解釋如下:

index參數(shù)名解釋

1allocator為新的CFString對象分配內(nèi)存的分配器,傳遞NULL或者kCFAllocatorDefault使用當(dāng)前默認(rèn)的分配器

2originalString要copy的CFString對象

3charactersToLeaveUnescaped在百分號轉(zhuǎn)義過程中要完好地留下的字符集,傳遞NULL指明所有非法的字符會被轉(zhuǎn)義

4legalURLCharactersToBeEscaped需要轉(zhuǎn)義的合法的字符集。傳遞NULL指明沒有合法的字符集要被替換(所有字符都不轉(zhuǎn)義)

5encoding轉(zhuǎn)化過程使用的編碼 如果你不關(guān)心正確的編碼,你應(yīng)該使用UTF-8 (kCFStringEncodingUTF8), 這是一個(gè)由RFC 3986設(shè)計(jì)的在URL使用中很合適的編碼

看了參數(shù)的說明應(yīng)該很容易理解為什么要那樣傳遞參數(shù),不過需要注意的是傳遞的字符串都是CFStringRef類型:因此要和NSString做一下橋接:

// NSString 轉(zhuǎn)為CFStringRef(__bridge CFStringRef)string// CFStringRef 轉(zhuǎn)為NSString(__bridge_transfer? NSString *)string

而對于3.0的AFN無論是對field的轉(zhuǎn)義還是對value的轉(zhuǎn)義都使用了相同的函數(shù)NSString * AFPercentEscapedStringFromString(NSString *string)其在內(nèi)部的實(shí)現(xiàn)就是使用了在本文第一部分提到提到的系統(tǒng)方法- (nullable NSString *)stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet *)allowedCharacters,而apple的文檔中指出這個(gè)方法內(nèi)部會按照UTF-8編碼進(jìn)行轉(zhuǎn)義,因此這里剛好解釋了為什么之前2.x版本需要傳遞編碼參數(shù),而3.0就不用了。

這里是NSString * AFPercentEscapedStringFromString(NSString *string)函數(shù)的一點(diǎn)核心代碼:

NSString* AFPercentEscapedStringFromString(NSString*string) {staticNSString*constkAFCharactersGeneralDelimitersToEncode =@":#[]@";// does not include "?" or "/" due to RFC 3986 - Section 3.4staticNSString*constkAFCharactersSubDelimitersToEncode =@"!$&'()*+,;=";NSMutableCharacterSet* allowedCharacterSet = [[NSCharacterSetURLQueryAllowedCharacterSet] mutableCopy];? ? [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];// ......// .....returneacapedString;}

可以看到允許轉(zhuǎn)義的字符集一開始是URLQueryAllowedCharacterSet,然后去掉了kAFCharactersGeneralDelimitersToEncode(@":#[]@")和kAFCharactersSubDelimitersToEncode(@"!$&'()*+,;=")包含的字符,也就是說這些字符最終都需要轉(zhuǎn)義,相比較2.x版本確實(shí)是多轉(zhuǎn)義了[,]和.。這也是剛才看到說的使用AFN2.x版本參數(shù)值和3.0版本轉(zhuǎn)義后參數(shù)值不同而3.0與瀏覽器中相同的原因。

費(fèi)了那么大的勁,終于把這部分梳理清晰了,然后來做一件有意義的事:(將字典轉(zhuǎn)為queryString的功能抽取)

將AFN字典轉(zhuǎn)queryString模塊抽取

這里我只是寫了一個(gè)NSDictionary的分類

@interfaceNSDictionary(ConvertToQueryString)- (NSString*)convertToQueryString;@end

#import "NSDictionary+ConvertToQueryString.h"#import "AFNetworking.h"@implementationNSDictionary (ConvertToQueryString)- (NSString *)convertToQueryString {if(!self||[selfisEqual:[NSNull null]]) {return@"";? ? }#if AFN Version < 3.0returnAFQueryStringFromParametersWithEncoding(self, NSUTF8StringEncoding);#elsereturnAFQueryStringFromParameters(self);#endif}@end

一切就是那么簡單,但是很有用處。接下來就是一點(diǎn)點(diǎn)擴(kuò)展了,我們知道AFN已經(jīng)封裝了字典轉(zhuǎn)為queryString的功能,那么有時(shí)候會有將queryString轉(zhuǎn)為字典的需求,雖然這種需求并不常見,但偶爾也會碰到。那么具體怎么做呢。我推薦一個(gè)比較優(yōu)秀的框架:Facebook的facebook-ios-sdk這是一個(gè)用于構(gòu)建iOS應(yīng)用的基礎(chǔ)框架:包含了facebook登錄和分享、處理應(yīng)用間跳轉(zhuǎn)的功能、一些繪圖API,應(yīng)用的數(shù)據(jù)統(tǒng)計(jì)模塊等功能。這個(gè)框架并不是多么龐大,源碼文件也比較少,但是其中一個(gè)網(wǎng)絡(luò)工具還是挺好用的:FBSDKUtility。在這個(gè)類的頭文件中只是聲明了這樣4個(gè)方法:

@interfaceFBSDKUtility:NSObject+ (NSDictionary*)dictionaryWithQueryString:(NSString*)queryString;+ (NSString*)queryStringWithDictionary:(NSDictionary*)dictionary error:(NSError*__autoreleasing *)errorRef;+ (NSString*)URLDecode:(NSString*)value;+ (NSString*)URLEncode:(NSString*)value;@end

在m文件中的實(shí)現(xiàn)并不復(fù)雜,單就URLEncode來說,它雖然對了一些對參數(shù)值的驗(yàn)空操作,但是沒有想AFN那樣將各種情況都充分考慮,因此若要完成queryStringWithDictionary:的功能還是建議使用AFN的功能,將AFN的方法加入到FBSDKUtility中,這種代碼的普適性降低了但是增加幾分可靠性。

至于dictionaryWithQueryString:方法,我相信我們都能寫出這樣的方法,但是說到底數(shù)據(jù)被轉(zhuǎn)換后是要拿給后臺使用的,排除各種后臺語言和框架的差異,我們應(yīng)該做到盡量使得傳遞的數(shù)據(jù)與后臺經(jīng)過解析之后獲取的數(shù)據(jù)一致。

+ (NSDictionary*)dictionaryWithQueryString:(NSString*)queryString{NSMutableDictionary*result = [[NSMutableDictionaryalloc] init];NSArray*parts = [queryString componentsSeparatedByString:@"&"];for(NSString*partinparts) {if([part length] ==0) {continue;? ? }NSRangeindex = [part rangeOfString:@"="];NSString*key;NSString*value;if(index.location ==NSNotFound) {? ? ? key = part;? ? ? value =@"";? ? }else{? ? ? key = [part substringToIndex:index.location];? ? ? value = [part substringFromIndex:index.location + index.length];? ? }? ? key = [selfURLDecode:key];? ? value = [selfURLDecode:value];if(key && value) {? ? ? result[key] = value;? ? }? }returnresult;}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容