别再使用stringByAddingPercentEscapesUsingEncoding

当遇到发送网络请求的参数中有汉字的情况,很多人一股脑地使用stringByAddingPercentEscapesUsingEncoding:进行转义,这样带有汉字的urlString就会将每个汉字转成相应的unicode编码对应的3个%形式,这叫urlEncode(每个能写后端的语言都有的方法),但是苹果的stringByAddingPercentEscapesUsingEncoding:却不是urlEncode。实际上我们使用的参数值可能会包含一些特殊的字符,如&?这样的字符,而Percent转义已经不能满足需求了,如下面的例子:

NSString *queryWord = @"汉字&ss";
NSString *urlString = [NSString stringWithFormat:@"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

这是一个非常常见的情景,(之前公司项目的搜索中,也遇到过这种情况),这种被转义之后的URL,服务端接收到的参数会使这样的

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

即使你做如下的改进:(在请求之前将每个参数都转义,再使用&拼接参数也无济于事)

NSString *queryWord = @"汉字&ss";
NSString *escapedQueryWord = [queryWord stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *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&ss

产生这种情况的原因是:百分号转义不等于URLEncode

该编码不同于URL编码,由于不会对&字符编码,因此不会改变URL参数的分隔。URL编码会编码&?与其他标点符号。如果查询字符串包含了这些字符,那么需要实现一种更加彻底的编码方法。

不过还好iOS7.0推出了stringByAddingPercentEncodingWithAllowedCharacters:方法,这个方法会对字符串进行更彻底的转义,但是需要传递一个参数:这个参数是一个字符集,表示:在进行转义过程中,不会对这个字符集中包含的字符进行转义,而保持原样保留下来。

这样就可以使用它改造上面的代码了:

NSString *queryWord = @"汉字&ss";
NSString *escapedQueryWord = [queryWord stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]];
NSLog(@"%@", escapedQueryWord); // %E6%B1%89%E5%AD%97%26ss
NSString *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

在上面的例子中传递参数[NSCharacterSet letterCharacterSet]来保证字母不被转义。所以被转义之后的参数值是:%E6%B1%89%E5%AD%97%26ss,这样问题就解决了,但是有时候会遇到queryString中的表单域也需要转义的情况,比如是一个表单数组如:

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

这样可以使用将key转义,不过key中的[]字符是不需要转义的:可以自定义一个CharacterSet实现需求:

NSMutableCharacterSet *mutableCharSet = [[NSMutableCharacterSet alloc] init];
[mutableCharSet addCharactersInString:@"[]"]; // 允许'['和']'不被转义
NSCharacterSet *charSet = mutableCharSet.copy; NSMutableString *mutableString = [NSMutableString string];
for (unit in queryString) {
NSString *escapedField = [unit.field stringByAddingPercentEncodingWithAllowedCharacters:charSet];
NSString *escapedValue = [unit.value stringByAddingPercentEncodingWithAllowedCharacters:charSet];
[mutableString addFormat:@"%@=%@", escapedField, escapedValue];
}

这样问题已经圆满解决了,美中不足的是:当queryString非常多的时候你如何保证从queryString正确地提取出来每个unit呢,这个牵扯到复杂的字符串解析的问题。先不做讨论。实际上有一个好的方案是使用AFN将每个参数的URL和queryString在构建的时候分离,使用URL和parameter(字典)分别传入的方法,也就是说在使用AFN的时候避免使用:

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:nil
failure:nil

为什么要这样,翻看AFN的源码会发现,AFN对queryString的组装是这样进行的:

AFN会将parameters的传递的字典通过将每个表单元素的field和value进行urlcode之后拼接,然后再直接附加在传递的URLString后面(当然,如果是POST方式就不是附加了,而是将拼好的串放到HTTP body中)。

那么如果要使用第一种方式,必须要确保自己在传入的URLString是经过完美转义的,因为AFN不会对你传入的URLString进行检测有没有进行了转义或者正确与否,但是AFN对上面方法中parameter参数的解析时非常彻底的,因此强烈建议使用第二种方式调用AFN的方法。那么AFN是如何完美解析parameter参数的呢,这刚好是一个可以将字典转为queryString的模块呀!!!,下面就来看一下:

对AFN urlEncode的研究

AFN将网络访问分割为三个过程模块

1.请求前:构建request的header和queryString、uploadContent和配置(如超时等),这部分的功能在AFURLRequestSerialization中

2.请求中:分别有基于NSConnection的访问(3.0移除)和基于NSURLSession的访问模块

3.请求后:1错误处理2.成功处理:数据格式转换和解析,主要在AFURLResponseSerialization中

requestSerialization就像过滤器一样,每一个用于构建网络请求的URLRequest对象都会经过requestSerialization配置,再返回一个NSMutableURLRequest对象(参见2.x版本的dataTaskWithHTTPMethod: URLString: parameters: success: failure方法,3.0版本dataTaskWithHTTPMethod URLString: parameters: uploadProgress: downloadProgress: success:方法),NSURLSession对象会使用这个NSMutableURLRequest对象创建task。而我们要讨论的将parameter转为queryString的功能全部在AFURLRequestSerialization中,它实际上使用了

NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];

就完成了所有的请求前的配置功能,可以查看一下内部的实现,有一句关键性的代码

mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
// 这里的self是AFURLResponseSerialization对象

这句代码用于对request对象设置requestHeader和转义queryString,我们仅仅看一下对queryString进行转义的

其内部按照这样的思路实现:

1.如果传递过来的parameters不为空,就会判断self.queryStringSerialization是否为空(self.queryStringSerialization属性是一个 AFQueryStringSerializationBlock类型的block,它是用来实现转义的核心代码块)

2.如果self.queryStringSerialization不为空,使用self.queryStringSerialization(request, parameters, &serializationError);进行转义和组装:

3.如果self.queryStringSerialization为空,使用一个内部函数来执行:AFQueryStringFromParameters(parameters),实际上每一个AFURLResponseSerialization对象在创建的时候queryStringSerialization属性都是空的,因此外部不传递block类型的值给queryStringSerialization属性时都会走这条路线,也就是使用AFQueryStringFromParameters(parameters)来解析参数。

AFQueryStringFromParameters的实现是这样的:

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

而其中使用到的AFQueryStringPairsFromDictionary函数是这样实现的:

NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
} NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value); // 这个方法太长,只放置了原型

思路为:

1.利用AFQueryStringPairsFromKeyAndValue函数将parameters字典中的每个key-value对取出,将每个key-value对构建为AFQueryStringPair对象,放到一个数组中。

2.在AFQueryStringFromParameters方法内部遍历这个数组(每个元素为AFQueryStringPair对象),使用AFQueryStringPair类的转义方法URLEncodedStringValue将AFQueryStringPair转为字符串,将这些字符串存入新的数组中。这样新数组中的每个元素就是转义之后的field=value字符串,最后用&将数组元素连接即可。

函数AFQueryStringPairsFromKeyAndValue是一个非常完美的算法,基本上考虑到了所有类型的表单域:包括表单数组的处理和对一个表单域赋值多个value的情况的处理,表单数组在html页面经常用到的:

<form method="GET" action="http://127.0.0.1/test.php">
<input name="habit[]" value="游泳" />
<input name="habit[]" value="骑行" />
<input type="submit" value="提交">
</form>

浏览器自动转义: habit%5B%5D=%E6%B8%B8%E6%B3%B3&habit%5B%5D=%E9%AA%91%E8%A1%8C

AFN传递parameter = @{@"habit"

iOS. PercentEscape是错用的URLEncode,看看AFN和Facebook吧的更多相关文章

  1. [iOS Xcode8报错]dyld: Library not loaded: /System/Library/Frameworks/UserNotifications.framework/UserN

    [iOS Xcode8报错]dyld: Library not loaded: /System/Library/Frameworks/UserNotifications.framework/UserN ...

  2. ios UIImageWriteToSavedPhotosAlbum报错 NSPhotoLibraryAddUsageDescription

    最近学习IOS相关知识. 视频课程[UIImage](https://www.imooc.com/video/12718) 相关知识点: 存储一张本地图片到系统相册中. API: UIImageWri ...

  3. 【小程序】微信小程序iOS苹果报错“协议错误”

    遇到问题 目前正在开发一个小程序,然后苹果真机测试时发现无法授权并提示,errMsg:"request:fail 未能完成该操作.协议错误" 开发环境下测试没问题,安卓机真机测试没 ...

  4. ios上架报错90080,90087,90209,90125 解决办法

    ERROR ITMS-90087: "Unsupported Architectures. The executable for yht.temp_caseinsensitive_renam ...

  5. 运行百度语音识别官方iOS demo报错: load offline engine failed: 4001

    运行官方BDVRClientSample这个demo(ios版的),demo可以安到手机上,但是点“识别UI”那个按钮后“授权验证失败”.如果点“语音识别”那个按钮,控制台输出:2015-10-23 ...

  6. IOS开发报错之Undefined symbols for architecture armv6

    本文转载至  http://blog.csdn.net/sanpintian/article/details/7575434 今天在项目中引入SVSegmentedControl.h/.my以及SVS ...

  7. IOS 空字符串报错 解决办法

    NSScanner: nil string argument  NSScanner: nil string argument libc++abi.dylib: terminate_handler un ...

  8. [ios] 定位报错Error Domain=kCLErrorDomain Code=0 "The operation couldn’t be completed. (kCLErrorDomain error 0.)"

    Error Domain=kCLErrorDomain Code=0 "The operation couldn’t be completed. (kCLErrorDomain error ...

  9. iOS CFNetwork报错

    2016-11-16 10:05:35.082 天天送[46197:11758717] 46197: CFNetwork internal error (0xc01a:/BuildRoot/Libra ...

随机推荐

  1. powershell批量设置权限

    批量设置权限 $acl=get-acl .\demo Get-ChildItem .\Documents -Recurse -Force|Set-Acl -AclObject $acl

  2. Go简介

    Go是Google开发的一种编译型,並發型,并具有垃圾回收功能的编程语言. 罗伯特·格瑞史莫(Robert Griesemer),罗勃·派克(Rob Pike)及肯·汤普逊于2007年9月开始设计Go ...

  3. Google Code Jam 2016 Round 1C C

    题意:三种物品分别有a b c个(a<=b<=c),现在每种物品各选一个进行组合.要求每种最和最多出现一次.且要求任意两个物品的组合在所有三个物品组合中的出现总次数不能超过n. 要求给出一 ...

  4. MacBook安装双系统(Windows多分区)

    分区 ---------- 启动电脑,放入mac os安装盘,按alt选择光盘启动. 1. 在工具菜单里选择磁盘工具对整个硬盘进行分区: ----------- 第一个是  exFAT.Msdos 格 ...

  5. Swift 定义函数 参数 返回值

    定义多参数函数 - 用func声明函数  func name(parameters) -> return type { function body } func halfOpenRangeLen ...

  6. 修改linux的系统时间和时区

    时间: date命令将日期设置为2016年12月16日 ----   date -s 12/16/16 将时间设置为9点28分50秒 ----   date -s 09:28:50 时区: tzsel ...

  7. MFC---给按钮加上快捷键

    现在快捷键的使用已经很频繁了.快捷键可以使我们的操作变得更简单,更快捷.如何给自己的按钮加一个快捷键呢.    如下图:我们希望给我们的参照按钮加一个快捷键CTR + F. 不要以为在按钮的标题上加上 ...

  8. windows下安装easy_install, pip 及whl文件安装方法

    转:http://www.cnblogs.com/wu-wenmin/p/4250330.html 写在前面的话 最近在看"Computer Vision with Python" ...

  9. 【Java EE 学习 75 上】【数据采集系统第七天】【二进制运算实现权限管理】【权限分析和设计】

    一.权限计算相关分析 1.如何存储权限 首先说一下权限保存的问题,一个系统中最多有多少权限呢?一个大的系统中可能有成百上千个权限需要管理.怎么保存这么多的权限?首先,我们使用一个数字中的一位保存一种权 ...

  10. 大数据项目实践:基于hadoop+spark+mongodb+mysql+c#开发医院临床知识库系统

    一.前言 从20世纪90年代数字化医院概念提出到至今的20多年时间,数字化医院(Digital Hospital)在国内各大医院飞速的普及推广发展,并取得骄人成绩.不但有数字化医院管理信息系统(HIS ...