Round 1
最近公司的文件服務(wù)器進(jìn)行了改造,即使是圖片的加載請(qǐng)求也要攜帶token,否則無(wú)法加載,而我們項(xiàng)目中圖片加載用的是SDWebImage,當(dāng)時(shí)聽(tīng)到這個(gè)需求我內(nèi)心毫無(wú)波動(dòng),心里已經(jīng)....你懂得,不過(guò)該做還是要做,三下五除二寫(xiě)完了代碼如下:
[[SDWebImageDownloader sharedDownloader] setValue:@"你的token" forHTTPHeaderField:@"Authorization"];
SDWebImage的下載處理是由SDWebImageDownloader單例類(lèi)實(shí)現(xiàn),所以在你項(xiàng)目中合適的地方加上這句代碼,項(xiàng)目中所有用SDWebImage做圖片加載的地方就都會(huì)攜帶上token了
Round 2
這樣修改完后確實(shí)本來(lái)不能加載的地方變得正常了,直到那一天,那是一個(gè)春天...
項(xiàng)目中要添加一個(gè)需求,需要引用公司的一個(gè)私有Pod功能庫(kù),又是一頓操作,集成完畢,邏輯編寫(xiě)完成,run,誒,圖片居然沒(méi)加載出來(lái),我草這什么情況,我再次確認(rèn)了一下,我的token設(shè)置完成了的
我去詢(xún)問(wèn)編寫(xiě)這個(gè)Pod的同事,是不是我哪里沒(méi)配置完成,他略微沉思一下兩秒鐘后說(shuō),你添加完token還要在SDWebImageDownloader修改下源碼,我:???,隨后他找到了這個(gè)源碼,代碼如下:
- (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);
}
}
}
他解釋說(shuō)道,這個(gè)功能模塊里的一些圖片鏈接中攜帶了一些參數(shù),并不是直接指向資源,所以請(qǐng)求會(huì)進(jìn)行重定向,所以需要在這里進(jìn)行處理,處理后的代碼如下:
- (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,確實(shí),問(wèn)題解決了,但是不對(duì)啊,我們的SDWebImage是通過(guò)Pod的方式集成的,這樣直接在Pod文件夾下修改三方的源碼,那么下次更新后,豈不是被覆蓋了?這是一個(gè)新的問(wèn)題,于是我開(kāi)始思考怎么解決重定向問(wèn)題的同事不修改三方庫(kù)的源碼,腦海中瞬間就想到了AOP
iOS開(kāi)發(fā)中優(yōu)秀的AOP庫(kù)那必須有Aspects名字,接下來(lái)我開(kāi)始思考具體步驟
首先,通過(guò)同事提供的解決問(wèn)題的代碼來(lái)看,他是把參數(shù)request給改為了一個(gè)自定義的customRequest,這兩個(gè)的區(qū)別,然后重新設(shè)置了allHTTPHeaderFields
NSMutableURLRequest *customRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL];
customRequest.allHTTPHeaderFields = self.HTTPHeaders;
那么我想,問(wèn)題主要就是在allHTTPHeaderFields這里了,我打印了request和customRequest的allHTTPHeaderFields后發(fā)現(xiàn),前者比后者少了token,怪不得無(wú)法加載,這里要提一下下邊這個(gè)方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler
這其實(shí)不是SDWebImageDownloader的方法,是NSURLSessionTaskDelegate的里的協(xié)議方法,SDWebImageDownloader實(shí)現(xiàn)協(xié)議方法后在里邊做了自己的重定向處理
那么說(shuō)一下我一開(kāi)始的想法,既然問(wèn)題出在重定向時(shí)request里未攜帶我們手動(dòng)添加的token,并且重定向這里肯定是要做處理的,那我們直接把相關(guān)參數(shù)設(shè)置給request,沒(méi)必要?jiǎng)?chuàng)建一個(gè)新的customRequest實(shí)例
[request setValue:self.HTTPHeaders forKey:@"allHTTPHeaderFields"];
因?yàn)?code>request是一個(gè)NSURLRequest對(duì)象,它的allHTTPHeaderFields是一個(gè)readonly屬性,我們不能直接修改,所以我想當(dāng)然的用KVC去操作,
然后 run ,然后 我草crash了,報(bào)錯(cuò)信息如下
"[<NSURLRequest 0x2800efa50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key allHTTPHeaderFields."
看描述信息是說(shuō)NSURLRequest沒(méi)有對(duì)應(yīng)的allHTTPHeaderFields這個(gè)key,有那么一瞬間我愣了下,這不科學(xué)啊怎么可能沒(méi)有,我點(diǎn)進(jìn)NSURLRequest類(lèi)確認(rèn)了下,有啊,什么情況,但是本著求知的態(tài)度,我心想是不是NSURLRequest內(nèi)部使用的是不是不叫allHTTPHeaderFields,但是不對(duì)啊,這個(gè)屬性明明在的啊,即使是別的也應(yīng)該是內(nèi)部重新賦值我這里不應(yīng)該報(bào)錯(cuò)啊,不過(guò)我還是用通過(guò)runtime將他的屬性列表打印了一下,再次確認(rèn)了,他確實(shí)有allHTTPHeaderFields這個(gè)屬性,于是我搜索了下相關(guān)問(wèn)題,發(fā)現(xiàn)了一個(gè)關(guān)鍵詞
+ (BOOL)accessInstanceVariablesDirectly
詳細(xì)信息自行檢索,我這里說(shuō)下結(jié)果,這個(gè)是針對(duì)KVC的,總的來(lái)說(shuō),當(dāng)一個(gè)類(lèi)實(shí)現(xiàn)了這個(gè)方法并且返回了YES他就可以通過(guò)KVC(這個(gè)說(shuō)法不完全準(zhǔn)確,因?yàn)楸疚牟皇轻槍?duì)KVC的故簡(jiǎn)要說(shuō)明)賦值,如果返回NO就不可以用KVC賦值
看到這里后我猜測(cè)NSURLRequest里這個(gè)方法應(yīng)該是返回了NO,那完了,走不通了,還是要實(shí)例化一個(gè)新的對(duì)象
Round 3
搞了半天想省事看來(lái)不行啊,那拉倒,直接開(kāi)始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方法的信息,可以通過(guò)它取到原方法的參數(shù),調(diào)用對(duì)象等等,我們這里要添加我們的token,因此取出參數(shù)進(jìn)行修改
- arguments是原方法的入?yún)⒘斜?,是一個(gè)數(shù)組
- invocation是一個(gè)消息對(duì)象,包含了一個(gè)方法的所有信息
通過(guò)URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler:方法我們可以知道request的索引是3,task的索引是1(取出它是因?yàn)槲覀円@取原h(huán)eader信息,這個(gè)不能丟棄),之后對(duì)request重新進(jìn)行賦值,完成修改,然后重新調(diào)用方法
[invocation setArgument:&request atIndex:5];
[invocation invoke];
因?yàn)槲覀冎恍枰薷?code>request一個(gè)對(duì)象,因此只重新設(shè)置這一個(gè)方法入?yún)ⅲ劣谶@里為什么在賦值的時(shí)候索引是5,因?yàn)榍皟蓚€(gè)分別被該方法的self與_cmd占用,所以我們?cè)O(shè)置參數(shù)的時(shí)候索引是從2開(kāi)始
再次run,嗯,圖片順利加載,問(wèn)題解決。
這樣一來(lái),我們就不需要修改三方的源碼就解決了問(wèn)題,否則修改源碼的話每次更新Pod我們的修改就會(huì)被覆蓋掉,如果哪次發(fā)版沒(méi)注意,測(cè)試也沒(méi)回歸覆蓋,很容易將問(wèn)題帶到線上