黑魔法——“低版本中使用高版本中出现的类”之技术实现原理详解
NSURLComponents的一些属性在高版本才可以使用,例如:
@property (nullable, readonly, copy) NSString *string NS_AVAILABLE(10_10, 8_0);
@property (readonly) NSRange rangeOfScheme NS_AVAILABLE(10_11, 9_0);
@property (nullable, copy) NSArray<NSURLQueryItem *> *queryItems NS_AVAILABLE(10_10, 8_0);
等等。这些属性在较低版本中是无法使用的,此库可以在低版本中也能使用这些属性。
使用方法:直接拷贝导入
直接将XAURLComponents文件下的NSURLComponents+XAMatch.h/.m,XAURLQueryItem.h/.m拷贝到工程中即可,无需改动代码。
Method Resolution
首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
当低版本调用高版本的方法时,因为找不到方法实现,会进入resolveInstanceMethod:方法,所以我们就可以在resolveInstanceMethod:方法中为低版本动态添加高版本的方法实现。
NSURLQueryItem是iOS8才有的类,如果想在iOS7中使用,我们可以用黑魔法——“低版本中使用高版本中出现的类”的技术来实现。
NSURLComponents+XAMatch.h
#import <Foundation/Foundation.h>
@interface NSURLComponents (XAMatch)
@end
NSURLComponents+XAMatch.m
#import "NSURLComponents+XAMatch.h"
#import <objc/runtime.h>
@implementation NSURLComponents (XAMatch)
#pragma mark - string
- (NSString *)xa_string {
return self.URL.absoluteString;
}
#pragma mark - queryItems
- (NSArray *)xa_queryItems {
NSMutableArray *queryItems = [NSMutableArray array];
NSDictionary *params = [self paramsWithUrlString:self.string];
if (params && [params isKindOfClass:[NSDictionary class]]) {
for (NSString *key in params.allKeys) {
NSURLQueryItem *item = [NSURLQueryItem queryItemWithName:key value:params[key]];
[queryItems addObject:item];
}
}
return queryItems;
}
- (NSDictionary *)paramsWithUrlString:(NSString *)urlStr {
if (urlStr && urlStr.length && [urlStr rangeOfString:@"?"].length == 1) {
NSArray *array = [urlStr componentsSeparatedByString:@"?"];
if (array && array.count == 2) {
NSString *paramsStr = array.lastObject;
if (paramsStr.length) {
NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary];
NSArray *paramArray = [paramsStr componentsSeparatedByString:@"&"];
for (NSString *param in paramArray) {
if (param && param.length) {
NSArray *parArr = [param componentsSeparatedByString:@"="];
if (parArr.count == 2) {
paramsDict[parArr[0]] = parArr[1];
}
}
}
return paramsDict;
}
}
}
return nil;
}
#pragma mark - rangeOf
- (NSRange)xa_rangeOfScheme {
return [self xa_rangeOfString:self.scheme];
}
- (NSRange)xa_rangeOfUser {
return [self xa_rangeOfString:self.percentEncodedUser];
}
- (NSRange)xa_rangeOfPassword {
return [self xa_rangeOfString:self.percentEncodedPassword];
}
- (NSRange)xa_rangeOfHost {
return [self xa_rangeOfString:self.percentEncodedHost];
}
- (NSRange)xa_rangeOfPort {
return [self xa_rangeOfString:self.port.stringValue];
}
- (NSRange)xa_rangeOfPath {
return [self xa_rangeOfString:self.percentEncodedPath];
}
- (NSRange)xa_rangeOfQuery {
return [self xa_rangeOfString:self.percentEncodedQuery];
}
- (NSRange)xa_rangeOfFragment {
return [self xa_rangeOfString:self.percentEncodedFragment];
}
- (NSRange)xa_rangeOfString:(NSString *)string {
if (string && [string isKindOfClass:[NSString class]] && string.length) {
return [self.string rangeOfString:string];
} else {
return NSMakeRange(NSNotFound, 0);
}
}
#pragma mark - Method Resolution
+ (BOOL)resolveInstanceMethod:(SEL)sel {
Class class = [self class];
SEL swizzledSelector = NSSelectorFromString([NSString stringWithFormat:@"xa_%@", NSStringFromSelector(sel)]);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
class_addMethod(class, sel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
return YES;
}
@end
XAURLQueryItem.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XAURLQueryItem : NSObject <NSSecureCoding, NSCopying> {
@private
NSString *_name;
NSString *_value;
}
- (instancetype)initWithName:(NSString *)name value:(nullable NSString *)value;
+ (instancetype)queryItemWithName:(NSString *)name value:(nullable NSString *)value;
@property (readonly) NSString *name;
@property (nullable, readonly) NSString *value;
@end
NS_ASSUME_NONNULL_END
XAURLQueryItem.m
#import "XAURLQueryItem.h"
#import <objc/runtime.h>
@implementation XAURLQueryItem
- (instancetype)initWithName:(NSString *)name value:(NSString *)value {
if ((self = [super init])) {
_name = name;
_value = value;
}
return self;
}
+ (instancetype)queryItemWithName:(NSString *)name value:(NSString *)value {
return [[XAURLQueryItem alloc] initWithName:name value:value];
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super init])) {
_name = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"name"];
_value = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"value"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeObject:_value forKey:@"value"];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
return [XAURLQueryItem allocWithZone:zone];
}
#pragma mark - Runtime Injection
__asm(
".section __DATA,__objc_classrefs,regular,no_dead_strip\n"
#if TARGET_RT_64_BIT
".align 3\n"
"L_OBJC_CLASS_NSURLQueryItem:\n"
".quad _OBJC_CLASS_$_NSURLQueryItem\n"
#else
".align 2\n"
"_OBJC_CLASS_NSURLQueryItem:\n"
".long _OBJC_CLASS_$_NSURLQueryItem\n"
#endif
".weak_reference _OBJC_CLASS_$_NSURLQueryItem\n"
);
__attribute__((constructor)) static void XAURLQueryItemPatchEntry(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
// >= iOS8
if (objc_getClass("NSURLQueryItem")) {
return;
}
Class *urlQueryItem = NULL;
#if TARGET_CPU_ARM
__asm("movw %0, :lower16:(_OBJC_CLASS_NSURLQueryItem-(LPC0+4))\n"
"movt %0, :upper16:(_OBJC_CLASS_NSURLQueryItem-(LPC0+4))\n"
"LPC0: add %0, pc" : "=r"(urlQueryItem));
#elif TARGET_CPU_ARM64
__asm("adrp %0, L_OBJC_CLASS_NSURLQueryItem@PAGE\n"
"add %0, %0, L_OBJC_CLASS_NSURLQueryItem@PAGEOFF" : "=r"(urlQueryItem));
#elif TARGET_CPU_X86_64
__asm("leaq L_OBJC_CLASS_NSURLQueryItem(%%rip), %0" : "=r"(urlQueryItem));
#elif TARGET_CPU_X86
void *pc = NULL;
__asm("calll L0\n"
"L0: popl %0\n"
"leal _OBJC_CLASS_NSURLQueryItem-L0(%0), %1" : "=r"(pc), "=r"(urlQueryItem));
#else
#error Unsupported CPU
#endif
if (urlQueryItem && !*urlQueryItem) {
Class class = objc_allocateClassPair([XAURLQueryItem class], "NSURLQueryItem", 0);
*urlQueryItem = class;
}
}
});
}
@end