一、問題背景
? ? 相信很多ios開發(fā)者在項(xiàng)目中都需要用到uiwebview,那就離不開url了,一般符合網(wǎng)絡(luò)標(biāo)準(zhǔn)的url是沒啥問題的,那么當(dāng)遇到一些特殊的url時,你就會踩坑了。。。舉幾個栗子:
順帶說一下一個完整的url格式:
協(xié)議://域名:端口號/路徑?參數(shù)1&參數(shù)2#路由錨點(diǎn)(瀏覽器使用)
url1=http://www.itdecent.cn/這是中文/notebooks&123/3521954/notes(路徑含中文和&)
url2=http://www.itdecent.cn/notebooks/352195?location=%&23!&userid=8957(參數(shù)含%&!特殊字符)
url3=http://www.itdecent.cn/notebooks/352195?username=逗比&userid=89757(參數(shù)含中文)
url4=http://www.itdecent.cn/writer?name=ha#ha#/notebooks/35214/notes(參數(shù)帶#,路徑后面包含#錨點(diǎn))
當(dāng)你遇上以上幾種url,你如果上來就粗暴的搞個[NSURL URLWithString:url],然后用webview開始loadrequest,那你就會驚喜的發(fā)現(xiàn):根本打不開,404錯誤赫然出現(xiàn)在眼前。此時,你就需要對url先進(jìn)行轉(zhuǎn)碼了,下面我們就url轉(zhuǎn)碼的問題好好說說。
二、url轉(zhuǎn)碼的具體原因
????在iOS程序中,訪問一些HTTP/HTTPS的資源服務(wù)時,如果url中存在中文或者特殊字符時,會導(dǎo)致無法正常的訪問到資源或服務(wù),想要解決這個問題,需要對url進(jìn)行編碼。
網(wǎng)絡(luò)標(biāo)準(zhǔn)RFC 1738規(guī)定url中只能包含英文字母和阿拉伯?dāng)?shù)字,以及一些特殊字符:
"...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."
“只有字母和數(shù)字[0-9a-zA-Z]、和特殊符號”$-_.+!*’(),”[不包括雙引號]、及某些保留字,才可以不經(jīng)過編碼直接用于URL?!?/p>
此時如果url中包含如漢字或者其他特殊字符則需要對它進(jìn)行編碼,編碼的意義在于,假如url的參數(shù)中的中文或特殊字符在發(fā)送到服務(wù)端時,服務(wù)端無法解析它的真正意義,會導(dǎo)致服務(wù)端不能理解客戶端的請求,如前面列舉的幾個特殊的url。
三、轉(zhuǎn)碼coding
? ??1. 使用CoreFoundation對url參數(shù)進(jìn)行encode
使用API:
CFStringRef CFURLCreateStringByAddingPercentEscapes(CFAllocatorRef allocator, CFStringRef originalString, CFStringRef charactersToLeaveUnescaped, CFStringRef legalURLCharactersToBeEscaped, CFStringEncoding encoding)
- (void)encodeUrl { ? ?
NSString *para1 = [self encodeParameter:@"%&23"];? ?// p1=%&23 ? ? ?
NSString *para2 = [self encodeParameter:@"我是參數(shù)8957"];? ?// p2=我是參數(shù)8957
NSString *encodeUrl = [NSString stringWithFormat:@"http://www.itdecent.cn?p1=%@&p2=%@", para1, para2];? ?
NSLog(@"%@", encodeUrl);
}
- (NSString *)encodeParameter:(NSString *)originalPara {? ?
CFStringRef encodeParaCf = CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)originalPara, NULL, CFSTR("!*'();:@&=+$,/?%#[]"), kCFStringEncodingUTF8);? ?
NSString *encodePara = (__bridge NSString *)(encodeParaCf);? ? CFRelease(encodeParaCf);? ?
return encodePara;
}
打印結(jié)果如下:
H5ViewController.m:59 http://www.itdecent.cn?p1=%25%2623&p2=%E6%88%91%E6%98%AF%E5%8F%82%E6%95%B08957
可以看到轉(zhuǎn)碼對象中,除了中文正常轉(zhuǎn)碼外,特殊字符只要包含在!*'();:@&=+$,/?%#[]這些字符范圍內(nèi)的都進(jìn)行了轉(zhuǎn)碼。
此方法適用于,url前綴不包含中文以及其它非法字符的情況,只需要對參數(shù)進(jìn)行編碼即可。
2.使用Foundation框架對完整url進(jìn)行encode
這里有兩個方法都用于url轉(zhuǎn)碼:
方法一.- (nullable NSString *)stringByAddingPercentEscapesUsingEncoding:(NSStringEncoding)encode;(已廢棄)
調(diào)用代碼示例如下:
- (void)encodeUrl {? ?
NSString *url = [@"http://www.itdecent.cn/這是中文/notebooks&123/3521954/notes" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];? ? NSLog(@"%@",url);
}
打印結(jié)果如下:
2018-06-12 14:03:57.864836+0800 haha[10930:15910902] http://www.itdecent.cn/%E8%BF%99%E6%98%AF%E4%B8%AD%E6%96%87/notebooks&123/3521954/notes
可以看到原先url帶的中文都被轉(zhuǎn)碼了,這里需要說明的是,該方法已經(jīng)被蘋果廢棄,因?yàn)樵摲街С值淖址容^少,只對`#%^{}[]|\"<> 加空格共14個字符編碼,不包括&?等符號
方法二.- (nullable NSString *)stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet *)allowedCharacters;(推薦)
說到方法二,雖說拓展性更強(qiáng)了,允許你自定義字符集,也是有坑的呀,下面看一下蘋果的官方api:
// Returns a new string made from the receiver by replacing all characters not in the allowedCharacters set with percent encoded characters. UTF-8 encoding is used to determine the correct percent encoded characters. Entire URL strings cannot be percent-encoded. This method is intended to percent-encode an URL component or subcomponent string, NOT the entire URL string. Any characters in allowedCharacters outside of the 7-bit ASCII range are ignored. ?
最后一句Any characters in allowedCharacters outside of the 7-bit ASCII range are ignored.,意思就是說,任何非7-bit ASCII字符擱到allowedCharacters里面也將被忽略,也就是allowedCharacters里面的字符跟7-bit ASCII字符不會被編碼。
換句話說,上面方法在處理的時候會編碼url的中的非7-bit ASCII字符,如這些【`#%^{}"[]|\<>】,如果需要忽略之,需要通過(NSCharacterSet *)allowedCharacters這個參數(shù)指定。
到此坑就來了,有的同學(xué)可能通過各種文章了解這個方法就是上文說的意思,其實(shí)不是的,測試代碼如下:
- (void)encodeUrl {? ?
NSString *url = @"http://www.itdecent.cn/notebooks/352195?location=%&23!&userid=8957";? ?
NSString *encodeUrl = [url stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "]];? ? NSLog(@"%@",encodeUrl);?? ?? ?
NSString *testStr = @"2#&!@測試";? ?
NSString *testEncodeUrl = [testStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "]];? ? NSLog(@"%@",testEncodeUrl);?? ?
}
按照上面的解釋,字符集合[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "]包含的字符都不會編碼,也就是代碼中url中的正常的7-bit ASCII字符(字母,數(shù)字等)和字符集中的%不會被編碼,但是結(jié)果卻嚇了我一跳:
2018-06-12 14:32:46.432905+0800 haha[11266:15930653] %68%74%74%70%73%3A%2F%2F%77%77%77%2E%6A%69%61%6E%73%68%75%2E%63%6F%6D%2F%6E%6F%74%65%62%6F%6F%6B%73%2F%33%35%32%31%39%35%3F%6C%6F%63%61%74%69%6F%6E%3D%%26%32%33%21%26%75%73%65%72%69%64%3D%38%39%35%37
2018-06-12 14:32:46.433076+0800 haha[11266:15930653] %32#%26%21%40%E6%B5%8B%E8%AF%95
可以看到第一個url被全部編碼了,僅僅除了一個%沒有被編碼(第一條log可能有點(diǎn)長,看不出來,但從第二條log可以看出只有#沒有被編碼),按理說正常的字符也不會被編碼啊,怎么常見的字母和數(shù)字都被編碼了呢?
由此可以看出不是我們之前所理解的“allowedCharacters里面的字符跟7-bit ASCII字符不會被編碼”,而是只有allowedCharacters里的字符才不會被編碼?。。?/b>
那么怎么才能正確忽略部分字符對url正常轉(zhuǎn)碼呢?
invertedSet的作用就來了,代碼如下:
- (void)encodeUrl {? ?
NSString *url = @"http://www.itdecent.cn/notebooks/352195?location=%&23!&userid=8957";? ?
NSString *encodeUrl = [url stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];? ? NSLog(@"%@",encodeUrl);?? ?? ?
NSString *testStr = @"2#&!@測試";? ?
NSString *testEncodeUrl = [testStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`#%^{}\"[]|\\<> "].invertedSet];? ? NSLog(@"%@",testEncodeUrl);?? ?
}
打印結(jié)果如下:
2018-06-12 15:13:05.725613+0800 haha[11629:15956002] http://www.itdecent.cn/notebooks/352195?location=%25&23!&userid=89572018-06-12 15:13:05.725997+0800 haha[11629:15956002] 2%23&!@%E6%B5%8B%E8%AF%95
可以看到,通過集合反轉(zhuǎn)之后得到的結(jié)果才是我們想要的,但是此處的意思是反的,就是對集合進(jìn)行invertedSet,表示集合內(nèi)的字符和非7-bit ASCII字符是需要轉(zhuǎn)碼的,所以我們以后使用這個方法進(jìn)行轉(zhuǎn)碼的時候要從反面進(jìn)行轉(zhuǎn)碼,把想要進(jìn)行轉(zhuǎn)碼的特殊字符寫在集合里就好了,注意這里說的是想要轉(zhuǎn)碼的特殊字符(!*'();:@&=+$,/?%#[]),像中文會被認(rèn)為是非7-bit ASCII字符會自動轉(zhuǎn)碼的,所以中文你就不用操心了。
舉個例子,我想對url中的%不轉(zhuǎn)碼,其他的特殊字符都轉(zhuǎn)碼,那你在集合中寫上想轉(zhuǎn)碼的字符就好了,不要寫%。補(bǔ)充說一下,用這個方法轉(zhuǎn)碼的時候不用擔(dān)心ios系統(tǒng)適配的問題,這個方法支持ios7之后的機(jī)器,目前蘋果機(jī)型基本上沒有低于ios7之后的機(jī)器了。
四.關(guān)于url含有#的問題
項(xiàng)目中H5給的url中總是含有符號#,每次有#的時候的時候我們的webview就打不開了,但是粘貼到safri能打開,后來才發(fā)現(xiàn)我們之前使用的轉(zhuǎn)碼方式是用的老的stringByAddingPercentEscapesUsingEncoding進(jìn)行轉(zhuǎn)碼的,默認(rèn)會把#轉(zhuǎn)成%23,但是后來了解到#是url中的一個重要組成部分,是跟在url參數(shù)之后的的最后一部分,作為一個url的錨點(diǎn),用于瀏覽器的定位,且#之后的部分是不會傳到服務(wù)器的,僅供瀏覽器使用。我們之前之所以打不開是因?yàn)?被轉(zhuǎn)成%23了,瀏覽器找不到#這個location定位,所以就打不開了。
那么此時的#是不能轉(zhuǎn)碼的,于是我就對現(xiàn)有的url轉(zhuǎn)碼寫了個分類:
- (NSString *)dfStringByAddingPercentEncoding{? ?
NSString *encodeStr = @"";? ?
if (self.length > 0) {? ? ? ?
//針對中文和`%^{}\"[]|\\<> 進(jìn)行轉(zhuǎn)義,#作為H5路由標(biāo)志,不處理? ? ? ?
encodeStr = [self stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`%^{}\"[]|\\<> "].invertedSet]; ? ?} ? ?
return encodeStr;
}
這里對所有#都不轉(zhuǎn)碼,其實(shí)也是有風(fēng)險的,比如一個url當(dāng)中不僅參數(shù)中含#,而且也有#作為錨點(diǎn)符號,比如第一部分中的url4,
url4=http://www.itdecent.cn/writer?name=ha#ha#/notebooks/35214/notes(參數(shù)帶#,路徑后面包含#錨點(diǎn))
對于這個url,如果你不對#轉(zhuǎn)碼,那么#之后的所有內(nèi)容都不會傳到服務(wù)器,也就是僅僅只有http://www.itdecent.cn/writer?name=ha,而這個url的真正意圖是,第一個#是作為參數(shù)中的一個字符出現(xiàn)的,這個#是服務(wù)器需要的一個參數(shù),第二個#才是瀏覽器的錨點(diǎn)定位,此時就有問題了。
由此我們可以看到,#作為url中前面的路徑或者參數(shù)出現(xiàn)的時候,這部分是需要傳到服務(wù)器的,是需要轉(zhuǎn)碼的,而后面的#作為錨點(diǎn)又是不能轉(zhuǎn)碼的,這就矛盾了啊。
可能有的同學(xué)會說,我們轉(zhuǎn)碼的時候只轉(zhuǎn)url的參數(shù)部分,也就是?和#之間的部分,不就行了?
還是有問題,比如#作為參數(shù)的一部分,你通過?和#是無法正確分割的。再比如有一些不和規(guī)范的url,路徑上就有#出現(xiàn),這個#也是需要轉(zhuǎn)碼的,你只轉(zhuǎn)參數(shù)部分也是不行的。
綜上,我們轉(zhuǎn)碼還是需要對整個url全部轉(zhuǎn)碼,至于參數(shù)中出現(xiàn)的#時,就需要我們對參數(shù)中這種特殊的#進(jìn)行先轉(zhuǎn)碼然后再拼到url當(dāng)中,整個url轉(zhuǎn)碼時忽略掉#(就是我寫的分類的處理方式)。
五.拓展一下NSCharacterSet中幾個url集合
????URLFragmentAllowedCharacterSet "#%<>[\]^`{|}
? ? URLHostAllowedCharacterSet? ? ? "#%/<>?@\^`{|}
? ? URLPasswordAllowedCharacterSet? "#%/:<>?@[\]^`{|}
? ? URLPathAllowedCharacterSet? ? ? "#%;<>?[\]^`{|}
? ? URLQueryAllowedCharacterSet? ? "#%<>[\]^`{|}
? ? URLUserAllowedCharacterSet? ? ? "#%/:<>?@[\]^`