Round 1
最近公司的文件服務器進行了改造,即使是圖片的加載請求也要攜帶token,否則無法加載,而我們項目中圖片加載用的是SDWebImage
,當時聽到這個需求我內心毫無波動,心里已經....你懂得,不過該做還是要做,三下五除二寫完了代碼如下:
[[SDWebImageDownloader sharedDownloader] setValue:@"你的token" forHTTPHeaderField:@"Authorization"];
SDWebImage
的下載處理是由SDWebImageDownloader
單例類實現,所以在你項目中合適的地方加上這句代碼,項目中所有用SDWebImage
做圖片加載的地方就都會攜帶上token了
Round 2
這樣修改完后確實本來不能加載的地方變得正常了,直到那一天,那是一個春天... 項目中要添加一個需求,需要引用公司的一個私有Pod功能庫,又是一頓操作,集成完畢,邏輯編寫完成,run
,誒,圖片居然沒加載出來,我草這什么情況,我再次確認了一下,我的token設置完成了的
我去詢問編寫這個Pod
的同事,是不是我哪里沒配置完成,他略微沉思一下兩秒鐘后說,你添加完token還要在SDWebImageDownloader
修改下源碼,我:???,隨后他找到了這個源碼,代碼如下:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
[dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(request);
}
}
}
他解釋說道,這個功能模塊里的一些圖片鏈接中攜帶了一些參數,并不是直接指向資源,所以請求會進行重定向,所以需要在這里進行處理,處理后的代碼如下:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
customRequest.allHTTPHeaderFields = self.HTTPHeaders;
if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
[dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:customRequest completionHandler:completionHandler];
} else {
if (completionHandler) {
completionHandler(customRequest);
}
}
}
我將代碼修改后,run
,確實,問題解決了,但是不對啊,我們的SDWebImage
是通過Pod
的方式集成的,這樣直接在Pod
文件夾下修改三方的源碼,那么下次更新后,豈不是被覆蓋了?這是一個新的問題,于是我開始思考怎么解決重定向問題的同時不修改三方庫的源碼,腦海中瞬間就想到了AOP
iOS開發中優秀的AOP
庫那必須有Aspects
的名字,接下來我開始思考具體步驟 首先,通過同事提供的解決問題的代碼來看,他是把參數request給改為了一個自定義的customRequest
,這兩個的區別,然后重新設置了allHTTPHeaderFields
NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
customRequest.allHTTPHeaderFields = self.HTTPHeaders;
那么我想,問題主要就是在allHTTPHeaderFields
這里了,我打印了request
和customRequest
的allHTTPHeaderFields
后發現,前者比后者少了token,怪不得無法加載,這里要提一下下邊這個方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler
這其實不是SDWebImageDownloader
的方法,是NSURLSessionTaskDelegate
的里的協議方法,SDWebImageDownloader
實現協議方法后在里邊做了自己的重定向處理
那么說一下我一開始的想法,既然問題出在重定向時request
里未攜帶我們手動添加的token,并且重定向這里肯定是要做處理的,那我們直接把相關參數設置給request
,沒必要創建一個新的customRequest
實例
[request setValue:self.HTTPHeaders forKey:@"allHTTPHeaderFields"];
因為request是一個NSURLRequest
對象,它的allHTTPHeaderFields
是一個readonly
屬性,我們不能直接修改,所以我想當然的用KVC
去操作, 然后 run
,然后 我草crash
了,報錯信息如下
"[<NSURLRequest 0x2800efa50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key
allHTTPHeaderFields."
看描述信息是說NSURLRequest
沒有對應的allHTTPHeaderFields
這個key
,有那么一瞬間我愣了下,這不科學啊怎么可能沒有,我點進NSURLRequest
類確認了下,有啊,什么情況,但是本著求知的態度,我心想是不是NSURLRequest
內部使用的是不是不叫allHTTPHeaderFields
,但是不對啊,這個屬性明明在的啊,即使是別的也應該是內部重新賦值我這里不應該報錯啊,不過我還是用通過runtime
將他的屬性列表打印了一下,再次確認了,他確實有allHTTPHeaderFields
這個屬性,于是我搜索了下相關問題,發現了一個關鍵詞
+ (BOOL)accessInstanceVariablesDirectly
詳細信息自行檢索,我這里說下結果,這個是針對KVC
的,總的來說,當一個類實現了這個方法并且返回了YES
他就可以通過KVC
(這個說法不完全準確,因為本文不是針對KVC
的故簡要說明)賦值,如果返回NO
就不可以用KVC
賦值
看到這里后我猜測NSURLRequest
里這個方法應該是返回了NO
,那完了,走不通了,還是要實例化一個新的對象
Round 3
搞了半天想省事看來不行啊,那拉倒,直接開始AOP
修改:
NSError * error ;
[[SDWebImageDownloader sharedDownloader] aspect_hookSelector:@selector(URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo, NSString *num){
NSArray * param = aspectInfo.arguments;
NSURLRequest * request = param[3];
NSURLSessionTask * task = param[1];
NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
customRequest.allHTTPHeaderFields = task.currentRequest.allHTTPHeaderFields;
request = customRequest;
NSInvocation * invocation = aspectInfo.originalInvocation;
[invocation setArgument:&request atIndex:5];
[invocation invoke];
} error:&error];
這里邊aspectInfo
就是被hook
方法的信息,可以通過它取到原方法的參數,調用對象等等,我們這里要添加我們的token,因此取出參數進行修改
arguments是原方法的入參列表,是一個數組
invocation是一個消息對象,包含了一個方法的所有信息
通過URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler:
方法我們可以知道request
的索引是3,task
的索引是1(取出它是因為我們要獲取原header信息,這個不能丟棄),之后對request
重新進行賦值,完成修改,然后重新調用方法
[invocation setArgument:&request atIndex:5];
[invocation invoke];
因為我們只需要修改request
一個對象,因此只重新設置這一個方法入參,至于這里為什么在賦值的時候索引是5,因為前兩個分別被該方法的self
與_cmd
占用,所以我們設置參數的時候索引是從2開始
再次run
,嗯,圖片順利加載,問題解決。
這樣一來,我們就不需要修改三方的源碼就解決了問題,否則修改源碼的話每次更新Pod我們的修改就會被覆蓋掉,如果哪次發版沒注意,測試也沒回歸覆蓋,很容易將問題帶到線上