iOS17 NSURL解析异常处理

iOS 17 中 Apple 对 NSURL 进行了隐式升级,影响比较广,在此分享其中一种修复方式

1. iOS 17 中 NSURL的改动

本次 Apple iOS 17 升级,对 NSURL 类的 URLWithString 进行了隐式升级( 点名批评  ,应用甚广的API竟然硬升级)

对齐了NSURLNSURLComponents 的执行标准,统一为 RFC 3986 (各执行标准感兴趣可以自行查阅,RFC 1808、RFC 1738、RFC 2732、RFC 3986

  • iOS 16:URLWithString 判断参数 urlString 如有非法字符(包含中文字符)就返回 nil
  • iOS 17:URLWithString 默认对非法字符(包含中文字符)进行%转义

直接看文档,iOS 17 以后URL中如果出现中文字符也可以直接进行编码,这看起来很美好,实际测试时发现有以下问题:

  1. 如果URL中没有非法字符,那URL中原有的转义字符(%)不会再次转义
  2. 如果URL中含有非法字符,那URL中原有的转义字符(%)会再次转义 (本次遇到的BUG)
iOS 17 NSURL的改动:.png

2. 修复代码

/// NSURL+XYAdapter.h 文件
#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSURL (XYAdapter)

@end

NS_ASSUME_NONNULL_END



/// NSURL+XYAdapter.m 文件
#import "NSURL+XYAdapter.h"
#import 

#pragma mark - URL encode
static NSString * XYAdapterPercentEscapedStringFromString(NSString *string) {
    static NSString * const kXYAdapterCharactersGeneralDelimitersToEncode = @":#[]@/?";
    static NSString * const kXYAdapterCharactersSubDelimitersToEncode = @"!$&'()*+,;=";

    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    [allowedCharacterSet removeCharactersInString:[kXYAdapterCharactersGeneralDelimitersToEncode stringByAppendingString:kXYAdapterCharactersSubDelimitersToEncode]];
    
    static NSUInteger const batchSize = 50;

    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    while (index < string.length) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wgnu"
        NSUInteger length = MIN(string.length - index, batchSize);
#pragma GCC diagnostic pop
        NSRange range = NSMakeRange(index, length);
        range = [string rangeOfComposedCharacterSequencesForRange:range];
        NSString *substring = [string substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];
        index += range.length;
    }

    return escaped;
}

@interface XYAdapterQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;

- (id)initWithField:(id)field value:(id)value;

- (NSString *)URLEncodedStringValue;
@end

@implementation XYAdapterQueryStringPair

- (id)initWithField:(id)field value:(id)value {
    self = [super init];
    if (self) {
        self.field = field;
        self.value = value;
    }
    return self;
}

- (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return XYAdapterPercentEscapedStringFromString([self.field description]);
    } else {
        return [NSString stringWithFormat:@"%@=%@", XYAdapterPercentEscapedStringFromString([self.field description]), XYAdapterPercentEscapedStringFromString([self.value description])];
    }
}
@end

FOUNDATION_EXPORT NSArray * XYAdapterQueryStringPairsFromDictionary(NSDictionary *dictionary);
FOUNDATION_EXPORT NSArray * XYAdapterQueryStringPairsFromKeyAndValue(NSString *key, id value);

static NSString * XYAdapterQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    for (XYAdapterQueryStringPair *pair in XYAdapterQueryStringPairsFromDictionary(parameters)) {
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }

    return [mutablePairs componentsJoinedByString:@"&"];
}

NSArray * XYAdapterQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return XYAdapterQueryStringPairsFromKeyAndValue(nil, dictionary);
}

NSArray * XYAdapterQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];

    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];

    if ([value isKindOfClass:[NSDictionary class]]) {
        NSDictionary *dictionary = value;
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[sortDescriptor]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                [mutableQueryStringComponents addObjectsFromArray:XYAdapterQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }
    } else if ([value isKindOfClass:[NSArray class]]) {
        NSArray *array = value;
        for (id nestedValue in array) {
            [mutableQueryStringComponents addObjectsFromArray:XYAdapterQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
    } else if ([value isKindOfClass:[NSSet class]]) {
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            [mutableQueryStringComponents addObjectsFromArray:XYAdapterQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        [mutableQueryStringComponents addObject:[[XYAdapterQueryStringPair alloc] initWithField:key value:value]];
    }

    return mutableQueryStringComponents;
}

#pragma mark - iOS 17 NSURL Bug
@implementation NSURL (XYAdapter)

+(void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (@available(iOS 17.0, *)) {
            /*
             [NSURL URLWithString:@"A"];
             最终调用 [NSURL alloc] initWithString:@"A" relativeToURL:nil];
             
             [[NSURL alloc] initWithString:@"A"];
             最终调用 [[NSURL alloc] initWithString:@"A" relativeToURL:nil];
             */
            Method oriMethod = class_getInstanceMethod(self, @selector(initWithString:relativeToURL:));
            Method newMethod = class_getInstanceMethod(self, @selector(XYAdapter_initWithString:relativeToURL:));
            method_exchangeImplementations(oriMethod, newMethod);
        }
    });
}

- (nullable instancetype)XYAdapter_initWithString:(NSString *)URLString relativeToURL:(nullable NSURL *)baseURL {
    
    if (@available(iOS 17.0, *)) {
        /*
         不改动 scheme、host、path、params、fragment 以及 baseURL
         只对 query 部分重新编码
         
         NSURL 文档:
         https://developer.apple.com/documentation/foundation/nsurl?language=objc
         
         URL 标准文档:
         RFC 1808: https://datatracker.ietf.org/doc/html/rfc1808
         :///;?#
         each of which, except , may be absent from a particular URL.
         These components are defined as follows (a complete BNF is provided
         in Section 2.2):
          
         scheme ":"   ::= scheme name, as per Section 2.1 of RFC 1738 [2].
        
         "//" net_loc ::= network location and login information, as per
                             Section 3.1 of RFC 1738 [2].
        
         "/" path     ::= URL path, as per Section 3.1 of RFC 1738 [2].
        
         ";" params   ::= object parameters (e.g., ";type=a" as in
                             Section 3.2.2 of RFC 1738 [2]).
          
         "?" query    ::= query information, as per Section 3.3 of
                             RFC 1738 [2].
         
         "#" fragment ::= fragment identifier.
         
        */
        
        NSString *host = nil; // scheme + net_loc + host + path + params
        NSString *query = nil;
        NSString *fragment = nil;
        NSString *url = URLString ?: @"";
        
        // scheme、net_loc、host、path、params、query
        NSRange hostRange = [url rangeOfString:@"?" options:NSBackwardsSearch];
        // NSBackwardsSearch 反向查,规避存在多个 ? 的情况
        if (hostRange.location != NSNotFound && hostRange.length > 0) {
            host = [url substringToIndex:hostRange.location + 1];
            if (hostRange.location + 1 < url.length) {
                url = [url substringFromIndex:hostRange.location + 1];
            } else {
                url = @"";
            }
        } else {
            // 无 query
            NSRange paramsRange = [url rangeOfString:@";" options:NSBackwardsSearch];
            if (paramsRange.location != NSNotFound && paramsRange.length > 0) {
                host = [url substringToIndex:paramsRange.location + 1];
                if (paramsRange.location + 1 < url.length) {
                    url = [url substringFromIndex:paramsRange.location + 1];
                } else {
                    url = @"";
                }
            } else {
                // 无 params
                NSRange lastPathRange = [url rangeOfString:@"/" options:NSBackwardsSearch];
                if (lastPathRange.location != NSNotFound && lastPathRange.length > 0) {
                    host = [url substringToIndex:lastPathRange.location + 1];
                    if (lastPathRange.location + 1 < url.length) {
                        url = [url substringFromIndex:lastPathRange.location + 1];
                    } else {
                        url = @"";
                    }
                }
            }
        }
        
        // fragment
        NSRange fragmentRange = [url rangeOfString:@"#"];
        if (fragmentRange.location != NSNotFound && fragmentRange.length > 0) {
            fragment = [url substringFromIndex:fragmentRange.location];
            url = [url substringToIndex:fragmentRange.location];
        }
        
        // query
        NSRange queryRange1 = [url rangeOfString:@"&"];
        NSRange queryRange2 = [url rangeOfString:@"="];
        if ((queryRange1.location != NSNotFound && queryRange1.length > 0)
            || (queryRange2.location != NSNotFound && queryRange2.length > 0)) {
            NSArray *params = [url componentsSeparatedByString:@"&"];
            NSMutableDictionary *kvs = [NSMutableDictionary new];
            for (NSString *param in params) {
                NSArray *sup = [param componentsSeparatedByString:@"="];
                NSString *key = [sup.firstObject stringByRemovingPercentEncoding];
                NSString *value = [sup.lastObject stringByRemovingPercentEncoding];
                if (sup.lastObject && sup.firstObject) {
                    [kvs setValue:value forKey:key];
                }
            }
            // encode query
            if (kvs.allKeys.count > 0) {
                NSString *enodeStr = XYAdapterQueryStringFromParameters(kvs);
                if ([enodeStr isKindOfClass:[NSString class]]
                    && enodeStr.length > 0) {
                    query = enodeStr;
                    url = @"";
                }
            }
        }
        
        NSString *encodeUrl = [NSString stringWithFormat:@"%@%@%@%@", host ?: @"", url?: @"", query?:@"", fragment ?: @""];
        if (encodeUrl.length > 0) {
            return [self XYAdapter_initWithString:encodeUrl relativeToURL:baseURL];
        } else {
            return [self XYAdapter_initWithString:URLString relativeToURL:baseURL];
        }
    } else {
        return [self XYAdapter_initWithString:URLString relativeToURL:baseURL];
    }
}

@end

你可能感兴趣的:(iOS17 NSURL解析异常处理)