AFURLRequestSerialization
AF一共实现了三种RequestSerialization:AFHTTPRequestSerializer、AFJSONRequestSerializer、AFPropertyListRequestSerializer。
AFHTTPRequestSerializer实现了AFURLRequestSerialization协议,协议里面只有一个方法:
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
AFJSONRequestSerializer和AFPropertyListRequestSerializer都继承自AFHTTPRequestSerializer。主要的差别是Content-Type的类型。
AFHTTPSessionManager里的requestSerializer:
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;
在发起请求的时候,会通过self.requestWidthMethod来生成request:
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
来到self.requestSerializer的requestWithMethod方法:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(URLString);
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
// 拼接请求参数
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
主要做了三件事:
1、用url创建NSMutableURLRequest;
2、设置NSMutableURLRequest的相关属性;
3、序列化NSMutableURLRequest。
在第二步中,AF用一个单例数组来保存NSMutableURLRequest一些属性的名称:
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
});
return _AFHTTPRequestSerializerObservedKeyPaths;
}
而AFHTTPRequestSerializer自己也实现了这些属性,在初始化的时候对这些属性进行监听:
static void *AFHTTPRequestSerializerObserverContext = &AFHTTPRequestSerializerObserverContext;
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
}
}
当设置了这些属性的值时,会把它们存到一个数组里, 当创建请求的时候再把这些属性设置到NSMutableURLRequest里去,避免了一个一个地去判断每个属性。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(__unused id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == AFHTTPRequestSerializerObserverContext) {
if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
[self.mutableObservedChangedKeyPaths removeObject:keyPath];
} else {
[self.mutableObservedChangedKeyPaths addObject:keyPath];
}
}
}
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
第二步的源码:
- (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:
query = AFQueryStringFromParameters(parameters);
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;
}
1、拷贝传进来的NSURLRequest为NSMutableURLRequest;
2、设置头部;
3、拼接queryString;
4、设置queryString;
拼接queryString的时候先判断queryStringSerialization是否为空,queryStringSerialization就是AFQueryStringSerializationBlock,可以自定义怎么拼接参数。如果为空则进入AFQueryStringFromParameters方法:
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
return [mutablePairs componentsJoinedByString:@"&"];
}
设置queryString的时候,会根据HTTPMethodsEncodingParametersInURI这个数组来判断应该把参数拼在地址后面还是设置到Body当中。
如果我们要发起一个Post请求,我们就要对我们要发送的数据进行编码。常见的三种编码的方式:
-
application/x-www-form-urlencoded
(默认): 就是常见的在Url后面直接拼接Query字符串; -
multipart/form-data
: 用来发送文件; -
application/json
: 用来告诉服务端消息主体是序列化后的 JSON 字符串。
至此一个NSMutableURLRequest就创建完成了。
除了requestWithMethod创造NSMutableURLRequest之外,AFHTTPRequestSerializer还有另外两个创建Request的方法:
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(void (^)(NSError *error))handler
先看multipartFormRequestWithMethod:
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
if (block) {
block(formData);
}
return [formData requestByFinalizingMultipartFormData];
}
1、根据method和ur创建一个NSMutableURLRequest;
2、创建AFStreamingMultipartFormData formData;
3、把参数设置到formData当中;
4、做最后的序列化。
先看AFStreamingMultipartFormData:@interface AFStreamingMultipartFormData : NSObject <AFMultipartFormData>
, 实现了AFMultipartFormData协议。
AFMultipartFormData里面的方法:
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * _Nullable __autoreleasing *)error;
- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream
name:(NSString *)name
fileName:(NSString *)fileName
length:(int64_t)length
mimeType:(NSString *)mimeType;
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType;
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name;
- (void)appendPartWithHeaders:(nullable NSDictionary <NSString *, NSString *> *)headers
body:(NSData *)body;
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
可以看到大部分是添加文件数据和头部的方法。
看看AFStreamingMultipartFormData的实现:
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSString *fileName = [fileURL lastPathComponent];
NSString *mimeType = AFContentTypeForPathExtension([fileURL pathExtension]);
return [self appendPartWithFileURL:fileURL name:name fileName:fileName mimeType:mimeType error:error];
}
通过AFContentTypeForPathExtension方法来获取mimeType:
static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
if (!contentType) {
return @"application/octet-stream";
} else {
return contentType;
}
}
然后继续:
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSParameterAssert(fileName);
NSParameterAssert(mimeType);
if (![fileURL isFileURL]) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
} else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
}
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error];
if (!fileAttributes) {
return NO;
}
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = mutableHeaders;
bodyPart.boundary = self.boundary;
bodyPart.body = fileURL;
bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
[self.bodyStream appendHTTPBodyPart:bodyPart];
return YES;
}
1、通过[fileURL isFileURL]来判断是不是文件的路径;
2、通过[fileURL checkResourceIsReachableAndReturnError:error]判断该文件是否存在且可读;
3、设置头部;
4、创建AFHTTPBodyPart;
5、把bodyPart拼接到bodyStream中去。
bodyStream就是AFMultipartBodyStream,它是AFStreamingMultipartFormData的一个属性。AFMultipartBodyStream继承自NSInputStream:@interface AFMultipartBodyStream : NSInputStream <NSStreamDelegate>
,它的appendHTTPBodyPart方法其实就是把bodyPart加到一个数组中:
- (void)appendHTTPBodyPart:(AFHTTPBodyPart *)bodyPart {
[self.HTTPBodyParts addObject:bodyPart];
}
bodyStream最后会被设置到request当中:[self.request setHTTPBodyStream:self.bodyStream];
;
AFMultipartFormData还有一个特别的方法:throttleBandwidthWithPacketSize:。throttle的意思是节流。可以通过throttleBandwidthWithPacketSize来设置bodyStream每次写到Buffer的数据量和延迟时间来提高请求的成功率。
根据注释可以知道,但我们在比较弱的网络下上传文件时,有可能会遇到"request body stream exhausted"的错误,这时候我们可以在失败的回调里面通过设置throttled bandwidth进行重试。AF也提供了两个建议的值:kAFUploadStream3GSuggestedPacketSize
和 kAFUploadStream3GSuggestedDelay
。
我们通过TCP发送数据的时候,数据会被分成一个个小的数据包发送,这些Packet存在一个SendBuffer中的。在弱网络环境下,一个Packet的传输失败率会提高,但因为TCP提供可靠传输,一次失败TCP不会马上任务请求失败,而是会重试一段时间,同时TCP还要保证有序传输,这就导致后面的Packet继续等待。如果一次发送的数据比较大,那后面的Packet传输失败的可能性也会变高,也就意味着我们HTTP请求失败的几率会变大。
可以看看AFMultipartBodyStream实现的方法:
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
if ([self streamStatus] == NSStreamStatusClosed) {
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
break;
}
} else {
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
if (numberOfBytesRead == -1) {
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
totalNumberOfBytesRead += numberOfBytesRead;
if (self.delay > 0.0f) {
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
return totalNumberOfBytesRead;
}
它是在read方法里面限制TCP的Packet大小的,而且通过[NSThread sleepForTimeInterval:self.delay];来暂停读操作从而延迟请求。
事实上,iOS中的Throttle还有好几种。比如我们快速点击某个按钮时,如果每次点击都执行某些操作,那就有可能导致页面卡顿。我们可以把某次点击之后一段时间间隔以内的点击都忽略掉,这也是一种Throttle。除此之外,iOS中Global Queue的DISPATCH_QUEUE_PRIORITY_BACKGROUND优先级也涉及到Disk I/O Throttle。
iOS编程中throttle那些事
一次磁盘读写操作涉及到的硬件资源主要有两个,CPU和磁盘。任务本身由CPU触发和调度,读操作发生时,CPU告知Disk去获取某个地址的数据,此时由于Disk的读操作存在寻址延迟,CPU是处于I/O wait状态,一直维持到Disk返回数据为止。处于I/O wait状态的CPU,此时并不能把这部分等待的时间用来处理其他任务,也就是说这一段等待的CPU时间被“浪费”了。而CPU是公共的系统资源,这部分资源的损耗自然会对系统的整体表现产生负面影响。即使Global Queue使用的是子线程,也会造成CPU资源的消耗。
如果把任务的Priority调整为DISPATCH_QUEUE_PRIORITY_BACKGROUND,那么这些任务中的I/O操作就被被控制,部分I/O操作的启动时间很有可能被适当延迟,把更多的CPU资源腾出来处理其他任务(比如说一些系统资源的调度任务),这样可以让我们的系统更加稳定高效。简而言之,对于重度磁盘I/O依赖的后台任务,如果对实时性要求不高,放到DISPATCH_QUEUE_PRIORITY_BACKGROUND Queue中是个好习惯,对系统更友好。
AFURLResponseSerialization数据解析
AF实现了以下七种数据解析的类:
- AFHTTPResponseSerializer
- AFJSONResponseSerializer
- AFXMLParserResponseSerializer
- AFXMLDocumentResponseSerializer
- AFPropertyListResponseSerializer
- AFImageResponseSerializer
这些类都继承了同一个协议:AFURLResponseSerialization
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
当任务完成后会来到- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
方法,然后调用解析类来解析数据,拿到结果:
responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
responseSerializer在AFURLSessionManage初始化的时候创建,默认为AFJSONResponseSerializier:
self.responseSerializer = [AFJSONResponseSerializer serializer];
AFJSONResponseSerializer继承自AFHTTPResponseSerializer。
AFJSONResponseSerializer解析数据时会先调用AFHTTPResponseSerializer的- (BOOL)validateResponse:(NSHTTPURLResponse *)response data:(NSData *)data error:(NSError * __autoreleasing *)error
方法来判断返回的数据是否可用,主要有两个条件:
1、返回的结果类型是可接受的类型;
2、状态码是可接受的状态码。
可接受的数据类型保存在self.acceptableContentTypes,AFJSONResponseSerializer初始化时设置了值:
self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
返回结果可用之后,就进入数据解析的环节:
id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
另外一段移除值为空的字段的代码:
id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
if ([JSONObject isKindOfClass:[NSArray class]]) {
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
for (id value in (NSArray *)JSONObject) {
if (![value isEqual:[NSNull null]]) {
[mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
}
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
} else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
id value = (NSDictionary *)JSONObject[key];
if (!value || [value isEqual:[NSNull null]]) {
[mutableDictionary removeObjectForKey:key];
} else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
}
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
}
return JSONObject;
}
判断值是父为空:!value || [value isEqual:[NSNull null]]
_AFURLSessionTaskSwizzling
Method Swizzling
Method Swizzling指的是改变一个已存在的选择器对应的实现的过程,就是可以在运行的过程中改变某个方法的实现。它和类别的区别在于,类别只能增加新的方法或覆盖有的方法,而无法调用原来的。
一个完整的Method Swizzling示例如下:
@implementation UIViewController (Logging)
+ (void)load {
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
- (void)swizzled_viewDidAppear:(BOOL)animated {
// call original implementation
[self swizzled_viewDidAppear:animated];
// Logging
[Logging logWithEventName:NSStringFromClass([self class])];
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
替换的主要流程为:
1、通过class_getInstanceMethod
方法拿到原有的方法(后面用Old方法表示)和即将要替换成的方法(后面用New方法表示)
2、利用class_addMethod
来判断该类有没有实现Old方法,因为class_getInstanceMethod
拿到的方法可能是父类的,而我们并不想替换父类的方法。如果class_addMethod
添加失败,则说明该类已经有此方法了,否则原本就没有实现;
3、如果第2步的添加是成功的,则说明已经添加了Old方法,而这个Old方法的实现是New方法的,此时只需要将New方法的实现替换为Old方法的实现即可;如果步骤2是添加失败的,则说明Old方法原本就实现了,这样就需要调用method_exchangeImplementations
来进行方法替换。
替换的步骤会放在+load:中执行,是因为一般来说类别的方法会覆盖掉主类中原有的方法,而+load:
是个特例。当一个类被读到内存时,runtime会给这个类和他的每个类别都发送一个+load:
消息,这样就保证了 +load:
方法一定会执行到。
替换完成之后,外部调用viewDidAppear:方法时,实际上执行的是swizzled_viewDidAppear:方法的实现,而在swizzled_viewDidAppear:的实现里面又调用了swizzled_viewDidAppear:方法,看似是递归,但此时swizzled_viewDidAppear:对应的实现已经被替换成viewDidAppear:方法的实现了,因此就做到了方法的替换。
最终实际逻辑如以下代码:
- (void)viewDidAppear:(BOOL)animated {
// call original implementation
[self swizzled_viewDidAppear:animated];
// Logging
[Logging logWithEventName:NSStringFromClass([self class])];
}
- (void)swizzled_viewDidAppear:(BOOL)animated {
// 原有的实现
}
AF中Method Swizzling的应用
以下为_AFURLSessionTaskSwizzling的源码:
/**
* A workaround for issues related to key-value observing the `state` of an `NSURLSessionTask`.
*
* See:
* - https://github.com/AFNetworking/AFNetworking/issues/1477
* - https://github.com/AFNetworking/AFNetworking/issues/2638
* - https://github.com/AFNetworking/AFNetworking/pull/2702
*/
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method));
}
static NSString * const AFNSURLSessionTaskDidResumeNotification = @"com.alamofire.networking.nsurlsessiontask.resume";
static NSString * const AFNSURLSessionTaskDidSuspendNotification = @"com.alamofire.networking.nsurlsessiontask.suspend";
@interface _AFURLSessionTaskSwizzling : NSObject
@end
@implementation _AFURLSessionTaskSwizzling
+ (void)load {
/**
WARNING: Trouble Ahead
https://github.com/AFNetworking/AFNetworking/pull/2702
*/
if (NSClassFromString(@"NSURLSessionTask")) {
/**
iOS 7 and iOS 8 differ in NSURLSessionTask implementation, which makes the next bit of code a bit tricky.
Many Unit Tests have been built to validate as much of this behavior has possible.
Here is what we know:
- NSURLSessionTasks are implemented with class clusters, meaning the class you request from the API isn't actually the type of class you will get back.
- Simply referencing `[NSURLSessionTask class]` will not work. You need to ask an `NSURLSession` to actually create an object, and grab the class from there.
- On iOS 7, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `__NSCFURLSessionTask`.
- On iOS 8, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `NSURLSessionTask`.
- On iOS 7, `__NSCFLocalSessionTask` and `__NSCFURLSessionTask` are the only two classes that have their own implementations of `resume` and `suspend`, and `__NSCFLocalSessionTask` DOES NOT CALL SUPER. This means both classes need to be swizzled.
- On iOS 8, `NSURLSessionTask` is the only class that implements `resume` and `suspend`. This means this is the only class that needs to be swizzled.
- Because `NSURLSessionTask` is not involved in the class hierarchy for every version of iOS, its easier to add the swizzled methods to a dummy class and manage them there.
Some Assumptions:
- No implementations of `resume` or `suspend` call super. If this were to change in a future version of iOS, we'd need to handle it.
- No background task classes override `resume` or `suspend`
The current solution:
1) Grab an instance of `__NSCFLocalDataTask` by asking an instance of `NSURLSession` for a data task.
2) Grab a pointer to the original implementation of `af_resume`
3) Check to see if the current class has an implementation of resume. If so, continue to step 4.
4) Grab the super class of the current class.
5) Grab a pointer for the current class to the current implementation of `resume`.
6) Grab a pointer for the super class to the current implementation of `resume`.
7) If the current class implementation of `resume` is not equal to the super class implementation of `resume` AND the current implementation of `resume` is not equal to the original implementation of `af_resume`, THEN swizzle the methods
8) Set the current class to the super class, and repeat steps 3-8
*/
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
Class currentClass = [localDataTask class];
while (class_getInstanceMethod(currentClass, @selector(resume))) {
Class superClass = [currentClass superclass];
IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
if (classResumeIMP != superclassResumeIMP &&
originalAFResumeIMP != classResumeIMP) {
[self swizzleResumeAndSuspendMethodForClass:currentClass];
}
currentClass = [currentClass superclass];
}
[localDataTask cancel];
[session finishTasksAndInvalidate];
}
}
+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));
if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
}
if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
}
}
- (NSURLSessionTaskState)state {
NSAssert(NO, @"State method should never be called in the actual dummy class");
return NSURLSessionTaskStateCanceling;
}
- (void)af_resume {
NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
NSURLSessionTaskState state = [self state];
[self af_resume];
if (state != NSURLSessionTaskStateRunning) {
[[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
}
}
- (void)af_suspend {
NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
NSURLSessionTaskState state = [self state];
[self af_suspend];
if (state != NSURLSessionTaskStateSuspended) {
[[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
}
}
@end
从注释中可以知道这个类主要是为了解决NSURLSessionTask
的state
的监听的问题。因为直接对属性进行KOV监听会出现闪退的情况。而AF的解决思路是替换NSURLSessionTask
的resume
和suspend
方法,这样在这两个方法调用时就知道任务的开始和暂停,然后再发出相应的通知。
为了替换方法,首先要拿到方法,而拿到方法又要先去的当前的类。前面的示例中我们是通过[self class]来拿到类的,但在这里情况却有一点复杂。
注释中说,iOS 7 和 iOS 8中NSURLSessionTask的实现是不一样的:
- 1、NSURLSessionTasks are implemented with class clusters,meaning the class you request from the API isn't actually the type of class you will get back。通过API拿到的类并不是真正想要的。
- 2、调用
[NSURLSessionTask class]
并没有用,你必须要利用NSURLSession
创建一个NSURLSessionTask
示例并从该实例拿到class。 - 3、在iOS 7 中,
localDataTask
是一个__NSCFLocalDataTask
,继承关系为__NSCFLocalDataTask
->__NSCFLocalSessionTask
->__NSCFURLSessionTask
。 - 4、在iOS 8 中,
localDataTask
是一个__NSCFLocalDataTask
,继承关系为__NSCFLocalDataTask
->__NSCFLocalSessionTask
->NSURLSessionTask
。 - 5、在iOS 7 中,只有
__NSCFLocalSessionTask
和__NSCFURLSessionTask
这两个类实现了resume
和suspend
方法,而且__NSCFLocalSessionTask
没有调用父类的方法,所以这两个类的方法都要被分别替换。 - 6、在iOS 8 中,只要
NSURLSessionTask
实现了这两个方法,所以也只有它需要被替换。
实现的步骤:
- 1、利用
NSURLSession
来创建一个任务从而拿到__NSCFLocalDataTask
的一个实例; - 2、拿到指向
af_resume
实现的指针originalAFResumeIMP; - 3、判断当前的NSURLSessionDataTask类是否实现了
resume
方法; - 4、拿到当前类的父类;
- 5、拿到当前类的resume方法的实现的指针classResumeIMP;
- 6、拿到父类的resume方法的实现的指针superclassResumeIMP;
- 7、如果classResumeIMP != superclassResumeIMP && originalAFResumeIMP != classResumeIMP,那么就开始交换方法;
- 8、令当前类等于父类,再重复3-8。
AF交换方法时没有利用class_addMethod
来判断拿到的方法是不是父类的方法,是因为它在进入交换之前就已经做了判断:classResumeIMP != superclassResumeIMP && originalAFResumeIMP != classResumeIMP
,这样保证了拿到的方法不是父类的方法,所以它直接添加了ad_resume
方法,然后进行交换。
参考:
网友评论