一、问题背景
相信很多ios开发者在项目中都需要用到uiwebview,那就离不开url了,一般符合网络标准的url是没啥问题的,那么当遇到一些特殊的url时,你就会踩坑了。。。举几个栗子:
顺带说一下一个完整的url格式:
协议://域名:端口号/路径?参数1&参数2#路由锚点(浏览器使用)
url1=https://www.jianshu.com/这是中文/notebooks&123/3521954/notes(路径含中文和&)
url2=https://www.jianshu.com/notebooks/352195?location=%&23!&userid=8957(参数含%&!特殊字符)
url3=https://www.jianshu.com/notebooks/352195?username=逗比&userid=89757(参数含中文)
url4=https://www.jianshu.com/writer?name=ha#ha#/notebooks/35214/notes(参数带#,路径后面包含#锚点)
当你遇上以上几种url,你如果上来就粗暴的搞个[NSURL URLWithString:url],然后用webview开始loadrequest,那你就会惊喜的发现:根本打不开,404错误赫然出现在眼前。此时,你就需要对url先进行转码了,下面我们就url转码的问题好好说说。
二、url转码的具体原因
在iOS程序中,访问一些HTTP/HTTPS的资源服务时,如果url中存在中文或者特殊字符时,会导致无法正常的访问到资源或服务,想要解决这个问题,需要对url进行编码。
网络标准RFC 1738规定url中只能包含英文字母和阿拉伯数字,以及一些特殊字符:
"...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."
“只有字母和数字[0-9a-zA-Z]、和特殊符号”$-_.+!*’(),”[不包括双引号]、及某些保留字,才可以不经过编码直接用于URL。”
此时如果url中包含如汉字或者其他特殊字符则需要对它进行编码,编码的意义在于,假如url的参数中的中文或特殊字符在发送到服务端时,服务端无法解析它的真正意义,会导致服务端不能理解客户端的请求,如前面列举的几个特殊的url。
三、转码coding
1. 使用CoreFoundation对url参数进行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:@"我是参数8957"]; // p2=我是参数8957
NSString *encodeUrl = [NSString stringWithFormat:@"https://www.jianshu.com?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;
}
打印结果如下:
H5ViewController.m:59 https://www.jianshu.com?p1=%25%2623&p2=%E6%88%91%E6%98%AF%E5%8F%82%E6%95%B08957
可以看到转码对象中,除了中文正常转码外,特殊字符只要包含在!*'();:@&=+$,/?%#[]这些字符范围内的都进行了转码。
此方法适用于,url前缀不包含中文以及其它非法字符的情况,只需要对参数进行编码即可。
2.使用Foundation框架对完整url进行encode
这里有两个方法都用于url转码:
方法一.- (nullable NSString *)stringByAddingPercentEscapesUsingEncoding:(NSStringEncoding)encode;(已废弃)
调用代码示例如下:
- (void)encodeUrl {
NSString *url = [@"https://www.jianshu.com/这是中文/notebooks&123/3521954/notes" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSLog(@"%@",url);
}
打印结果如下:
2018-06-12 14:03:57.864836+0800 haha[10930:15910902] https://www.jianshu.com/%E8%BF%99%E6%98%AF%E4%B8%AD%E6%96%87/notebooks&123/3521954/notes
可以看到原先url带的中文都被转码了,这里需要说明的是,该方法已经被苹果废弃,因为该方支持的字符比较少,只对`#%^{}[]|\"<> 加空格共14个字符编码,不包括&?等符号
方法二.- (nullable NSString *)stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet *)allowedCharacters;(推荐)
说到方法二,虽说拓展性更强了,允许你自定义字符集,也是有坑的呀,下面看一下苹果的官方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这个参数指定。
到此坑就来了,有的同学可能通过各种文章了解这个方法就是上文说的意思,其实不是的,测试代码如下:
- (void)encodeUrl {
NSString *url = @"https://www.jianshu.com/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字符(字母,数字等)和字符集中的%不会被编码,但是结果却吓了我一跳:
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可能有点长,看不出来,但从第二条log可以看出只有#没有被编码),按理说正常的字符也不会被编码啊,怎么常见的字母和数字都被编码了呢?
由此可以看出不是我们之前所理解的“allowedCharacters里面的字符跟7-bit ASCII字符不会被编码”,而是只有allowedCharacters里的字符才不会被编码!!!
那么怎么才能正确忽略部分字符对url正常转码呢?
invertedSet的作用就来了,代码如下:
- (void)encodeUrl {
NSString *url = @"https://www.jianshu.com/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);
}
打印结果如下:
2018-06-12 15:13:05.725613+0800 haha[11629:15956002] https://www.jianshu.com/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
可以看到,通过集合反转之后得到的结果才是我们想要的,但是此处的意思是反的,就是对集合进行invertedSet,表示集合内的字符和非7-bit ASCII字符是需要转码的,所以我们以后使用这个方法进行转码的时候要从反面进行转码,把想要进行转码的特殊字符写在集合里就好了,注意这里说的是想要转码的特殊字符(!*'();:@&=+$,/?%#[]),像中文会被认为是非7-bit ASCII字符会自动转码的,所以中文你就不用操心了。
举个例子,我想对url中的%不转码,其他的特殊字符都转码,那你在集合中写上想转码的字符就好了,不要写%。补充说一下,用这个方法转码的时候不用担心ios系统适配的问题,这个方法支持ios7之后的机器,目前苹果机型基本上没有低于ios7之后的机器了。
四.关于url含有#的问题
项目中H5给的url中总是含有符号#,每次有#的时候的时候我们的webview就打不开了,但是粘贴到safri能打开,后来才发现我们之前使用的转码方式是用的老的stringByAddingPercentEscapesUsingEncoding进行转码的,默认会把#转成%23,但是后来了解到#是url中的一个重要组成部分,是跟在url参数之后的的最后一部分,作为一个url的锚点,用于浏览器的定位,且#之后的部分是不会传到服务器的,仅供浏览器使用。我们之前之所以打不开是因为#被转成%23了,浏览器找不到#这个location定位,所以就打不开了。
那么此时的#是不能转码的,于是我就对现有的url转码写了个分类:
- (NSString *)dfStringByAddingPercentEncoding{
NSString *encodeStr = @"";
if (self.length > 0) {
//针对中文和`%^{}\"[]|\\<> 进行转义,#作为H5路由标志,不处理
encodeStr = [self stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"`%^{}\"[]|\\<> "].invertedSet]; }
return encodeStr;
}
这里对所有#都不转码,其实也是有风险的,比如一个url当中不仅参数中含#,而且也有#作为锚点符号,比如第一部分中的url4,
url4=https://www.jianshu.com/writer?name=ha#ha#/notebooks/35214/notes(参数带#,路径后面包含#锚点)
对于这个url,如果你不对#转码,那么#之后的所有内容都不会传到服务器,也就是仅仅只有https://www.jianshu.com/writer?name=ha,而这个url的真正意图是,第一个#是作为参数中的一个字符出现的,这个#是服务器需要的一个参数,第二个#才是浏览器的锚点定位,此时就有问题了。
由此我们可以看到,#作为url中前面的路径或者参数出现的时候,这部分是需要传到服务器的,是需要转码的,而后面的#作为锚点又是不能转码的,这就矛盾了啊。
可能有的同学会说,我们转码的时候只转url的参数部分,也就是?和#之间的部分,不就行了?
还是有问题,比如#作为参数的一部分,你通过?和#是无法正确分割的。再比如有一些不和规范的url,路径上就有#出现,这个#也是需要转码的,你只转参数部分也是不行的。
综上,我们转码还是需要对整个url全部转码,至于参数中出现的#时,就需要我们对参数中这种特殊的#进行先转码然后再拼到url当中,整个url转码时忽略掉#(就是我写的分类的处理方式)。
五.拓展一下NSCharacterSet中几个url集合
URLFragmentAllowedCharacterSet "#%<>[\]^`{|}
URLHostAllowedCharacterSet "#%/<>?@\^`{|}
URLPasswordAllowedCharacterSet "#%/:<>?@[\]^`{|}
URLPathAllowedCharacterSet "#%;<>?[\]^`{|}
URLQueryAllowedCharacterSet "#%<>[\]^`{|}
URLUserAllowedCharacterSet "#%/:<>?@[\]^`