这两天遇到一个问题是,当我用AFNetWorking进行POST请求,并且POST参数需要GBK编码时,请求会有问题。
跟踪代码后发现问题出在 AFPercentEscapedStringFromString
函数里:
NSString * AFPercentEscapedStringFromString(NSString *string) {
static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];
// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
static NSUInteger const batchSize = 50;
NSUInteger index = 0;
NSMutableString *escaped = @"".mutableCopy;
while (index < string.length) {
NSUInteger length = MIN(string.length - index, batchSize);
NSRange range = NSMakeRange(index, length);
// To avoid breaking up character sequences such as 👴🏻👮🏽
range = [string rangeOfComposedCharacterSequencesForRange:range];
NSString *substring = [string substringWithRange:range];
NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
[escaped appendString:encoded];
index += range.length;
}
return escaped;
}
这个函数其实是做URL编码的,但是用的 stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet
编出来都是UTF8 格式的,有心的你会发现AF不是可以自定义请求编码格式的么,就是AFHTTPRequestSerializer
的 stringEncoding
属性,这里稍微理解下AF请求的参数转变流程为如下:
- 你发送请求的入参 paramDic 字典
- 你的入参paramDic会被解析为 key1=value1&key2=value2 格式的字符串
- 被解析好的 key1=value1&key2=value2 字符串被转换为data格式传入请求的 httpbody 里
在这个过程中:
- 1到2的解析过程用到的是上面说的有问题的方法
AFPercentEscapedStringFromString
,所以任何请求这部都用的UTF8编码 - 而2到3的时候的编码类型才用到
AFHTTPRequestSerializer
的stringEncoding
属性
所以很明显,如果你需要GBK编码格式编码请求参数,并且也设定了AFHTTPRequestSerializer
的 stringEncoding
为GBK ,在这个过程中你的参数从字典变为拼接字符串的时候,会先进行UTF编码,然后字符串又转成了GBK格式的data类型,很显然不对。
所以最终导致,如果你的POSTBody需要GBK或者别的编码格式编码,当用AFNetworking时,会编码错误,导致你的服务器解码是也解不对,导致请求出问题。
这个问题再AFNetworking的Github官方主页的issue下也有记录,目前还是open状态,即未解决。
那怎么解决这个问题呢?
这有两种解决方法,一种不侵入AF源码,一种是改AF源码。
1.通过AFHTTPRequestSerializer 的 setQueryStringSerializationWithBlock: 方法自定义,那么整个请求流程代码就类似如下:
NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
[manager.requestSerializer setQueryStringSerializationWithBlock:^NSString * _Nonnull(NSURLRequest * _Nonnull request, id _Nonnull parameters, NSError * _Nullable __autoreleasing * _Nullable error) {
NSDictionary *dic = parameters;
return [NSString stringWithFormat:@"%@=%@",[dic.allKeys[0] stringByAddingPercentEscapesUsingEncoding:gbkEncoding],[dic.allValues[0] stringByAddingPercentEscapesUsingEncoding:gbkEncoding]];
}];
manager.requestSerializer.stringEncoding = gbkEncoding;
[manager POST:searchHomeUrlStr parameters:@{@"key":@"GBK编码值"} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
上面我是举例写,代码不很严瑾,你能明白我主要要表达什么意思就行。
每个需要 非UTF8 的请求都需要定义下 setQueryStringSerializationWithBlock:
方法,在这个方法中你需要把你的参数字典解析成 key1=value1&key2=value2 格式,并且用你需要的编码方式编码。
2.直接改源码
改源码要改3处地方,都在AFHTTPRequestSerializer
里,第1处和第2处新增两个方法需要能接收编码方式,第3处改动,需要 requestBySerializingRequest: withParameters: error:
方法里判断编码方式是不是UTF8,如果不是,走你新增的那两个方法来达到用你自己编码方式编码。
新增1:旧AFQueryStringFromParametersWithEncoding
下新增一个下面这个函数,多一个编码方式入参
NSString * AFQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding encode) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue:encode]];
}
return [mutablePairs componentsJoinedByString:@"&"];
}
新增2: 旧URLEncodedStringValue
方法下新增一个下面这个方法,也是多一个编码方式入参
- (NSString *)URLEncodedStringValue:(NSStringEncoding)encode {
if (!self.value || [self.value isEqual:[NSNull null]]) {
return AFPercentEscapedStringFromString([self.field description]);
} else {
if (encode == NSUTF8StringEncoding) {
return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
} else {
return [NSString stringWithFormat:@"%@=%@", [self.field stringByAddingPercentEscapesUsingEncoding:encode], [self.value stringByAddingPercentEscapesUsingEncoding:encode]];
}
}
}
改动3,下面这个方法里,改动地方看下面代码块下面的图:
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
NSString *query = nil;
if (parameters) {
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
if (self.stringEncoding == NSUTF8StringEncoding) {
query = AFQueryStringFromParameters(parameters);
} else {
query = AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding);
}
break;
}
}
}
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query && query.length > 0) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
改动地方:这里做一个是否UTF8的判断,不是UTF8类型走你新增1里的那个方法,把编码方式作为入参传入即可:
做个记录,如果有更好的改进方法,请留言。谢谢。
主要还是要等官方修这个bug啊~