網路層的資料的收集
網路層的資料,一般要收集的是API的請求頻率、API請求時間、成功率等等資訊。如果通過無埋的方式收集網路資訊,肯定是通過AOP的方式,hook相應的方法和相應的delegate方法,來實現這一需求。
針對NSURLSession進行網路資料的抓取
首先來分析一下通過NSURLSession發起的網路請求的流程:NSURLSession實際發起網路請求,是根據響應生成的[task resume]來開始網路請求的。
然後NSURLSession提供了兩種方式來對請求的回撥進行處理,一種是通過delegate來進行處理,還有一種就是通過block的方式,直接回撥請求結果。
delegate回撥方式
通過delegate回撥方式來進行網路請求回撥的處理,AFNetWorking通過NSURLSession發起的網路請求就是通過delegate來處理的,只是對外暴露的我們經常使用的是block的方式。
看一下NSURLSession的初始化方式和設定delegate的方式
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
複製程式碼
提供的是兩個類構造器,從上面兩個構造的引數,我們能夠猜出來,其實sessionWithConfiguration:最終也是呼叫sessionWithConfiguration:delegate:delegateQueu:方法,來初始化。一般我們把sessionWithConfiguration:delegate:delegateQueu:叫做工廠類方法。
還有一個方法,我們也經常用來獲取session的例項,就是sharedSession,那這個獲取的session和兩個類構造器獲取的session有什麼不同呢?
其實我們在初始化session的時候,無論呼叫哪一個類構造器初始化session時,sharedSession都會呼叫sessionWithConfiguration:方法初始化一個單例session,但是這個單例的session有許多的限制,比如cookie、cache等,具體的說明,詳見developer.apple.com/documentati…
什麼意思呢?上面這麼長一句。意思就是說,如果我們初始化了一個session,通過方法sessionWithConfiguration:,其實在NSURLSession內部會呼叫兩次這個方法,第一次是我們主動呼叫生成一個session,返回給我們,另外一次就是sharedSession呼叫,生成一個系統預設的單例session,注意:因為這個sharedSession是一個單例的session,所以也就只有在首次生成session的時候,sharedSession會主動呼叫。當然,通過方法sessionWithConfiguration:delegate:delegateQueu:初始化session也是一樣的。
為啥要說這麼多,因為我們需要在session初始化的時候,做hook delegate的操作,因為NSURLSession的delegate是一個只讀的屬性,我們只能在初始化的時候來做hook處理
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
複製程式碼
hook delegate
首先考慮一下,我們有三個方法能夠獲取到session的例項,其實真正有delegate的只有一個構造方法,其他兩個方法都沒有delegate,那怎麼做呢?
沒有delegate的session是通過block回撥方式拿到請求結果的,所以我們可以將session的含有block回撥的方法hook掉,然後通過傳入我們自己的block就能夠拿到網路的回撥結果了。
**注意:**如果一個session同時有delegate和block回撥,那麼delegate是不會被觸發的,會直接回撥到block裡面,因為如果沒有通過block回撥來發起的請求,在session內部,實際上也是呼叫的含block的方法。這個在後面會詳細介紹
還是看一下程式碼吧
首先介紹hook類構造器,達到hook delegate的效果。因為需要通過delegate拿到網路回撥的類構造器只有sessionWithConfiguration:delegate:delegateQueue:方法,所以只需要將這個構造器hook掉,然後拿到delegate,然後再將delegate的對應的delegate方法hook掉就行
在NSURLSession的一個分類中,在load方法中,我們將sessionWithConfiguration:delegate:delegateQueue: hook
Hook_Method(cls, @selector(sessionWithConfiguration:delegate:delegateQueue:), cls, @selector(hook_sessionWithConfiguration:delegate:delegateQueue:),YES);
複製程式碼
具體的hook實現方法,這個方法把hook類方法和hook例項方法都放在裡面了,因為待會我們還要hook session的例項方法
static void Hook_Method(Class originalClass, SEL originalSel, Class replaceClass, SEL replaceSel, BOOL isHookClassMethod) {
Method originalMethod = NULL;
Method replaceMethod = NULL;
if (isHookClassMethod) {
originalMethod = class_getClassMethod(originalClass, originalSel);
replaceMethod = class_getClassMethod(replaceClass, replaceSel);
} else {
originalMethod = class_getInstanceMethod(originalClass, originalSel);
replaceMethod = class_getInstanceMethod(replaceClass, replaceSel);
}
if (!originalMethod || !replaceMethod) {
return;
}
IMP originalIMP = method_getImplementation(originalMethod);
IMP replaceIMP = method_getImplementation(replaceMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *replaceType = method_getTypeEncoding(replaceMethod);
//注意這裡的class_replaceMethod方法,一定要先將替換方法的實現指向原實現,然後再將原實現指向替換方法,否則如果先替換原方法指向替換實現,那麼如果在執行完這一句瞬間,原方法被呼叫,這時候,替換方法的實現還沒有指向原實現,那麼現在就造成了死迴圈
if (isHookClassMethod) {
Class originalMetaClass = objc_getMetaClass(class_getName(originalClass));
Class replaceMetaClass = objc_getMetaClass(class_getName(replaceClass));
class_replaceMethod(replaceMetaClass,replaceSel,originalIMP,originalType);
class_replaceMethod(originalMetaClass,originalSel,replaceIMP,replaceType);
} else {
class_replaceMethod(replaceClass,replaceSel,originalIMP,originalType);
class_replaceMethod(originalClass,originalSel,replaceIMP,replaceType);
}
複製程式碼
然後在我們的hook實現方法中
+ (NSURLSession *)hook_sessionWithConfiguration: (NSURLSessionConfiguration *)configuration delegate: (id<NSURLSessionDelegate>)delegate delegateQueue: (NSOperationQueue *)queue {
if (delegate) {
Hook_Delegate_Method([delegate class], @selector(URLSession:dataTask:didReceiveData:), [self class], @selector(hook_URLSession:dataTask:didReceiveData:), @selector(none_URLSession:dataTask:didReceiveData:));
}
return [self hook_sessionWithConfiguration: configuration delegate: delegate delegateQueue: queue];
}
複製程式碼
同樣的,hook delegate的方法
//hook delegate方法
static void Hook_Delegate_Method(Class originalClass, SEL originalSel, Class replaceClass, SEL replaceSel, SEL noneSel) {
Method originalMethod = class_getInstanceMethod(originalClass, originalSel);
Method replaceMethod = class_getInstanceMethod(replaceClass, replaceSel);
if (!originalMethod) {//沒有實現delegate 方法
Method noneMethod = class_getInstanceMethod(replaceClass, noneSel);
BOOL didAddNoneMethod = class_addMethod(originalClass, originalSel, method_getImplementation(noneMethod), method_getTypeEncoding(noneMethod));
if (didAddNoneMethod) {
NSLog(@"沒有實現的delegate方法新增成功");
}
return;
}
BOOL didAddReplaceMethod = class_addMethod(originalClass, replaceSel, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddReplaceMethod) {
NSLog(@"hook 方法新增成功");
Method newMethod = class_getInstanceMethod(originalClass, replaceSel);
method_exchangeImplementations(originalMethod, newMethod);
}
}
複製程式碼
注意 這裡有一個地方需要注意,如果我們要hook的delegate有些方法沒有實現,但是我們又想要hook掉這個方法,那麼就需要先將delegate沒有實現的方法 將它先新增進去,然後再將這個方法替換掉
然後在我們的替換類中實現相應的替換方法即可
- (void)hook_URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[self hook_URLSession:session dataTask:dataTask didReceiveData:data];
}
- (void)none_URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSLog(@"11");
}
複製程式碼
替換block回撥
如果session沒有通過delegate去拿到回撥,那我們這時候需要怎麼做呢?
如果不通過delegate拿,那就是session中一系列的含block的請求方法了,這些被稱為 非同步便利請求方法,全部定義在NSURLSession的一個分類中
NSURLSession (NSURLSessionAsynchronousConvenience)
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
...
複製程式碼
這裡就舉一個例子,來展示一下怎麼hook 帶block 引數的方法,其實也就是構造一個和引數一樣的block,將自己的block傳進去
同樣的還是先將方法替換掉
Hook_Method(cls, @selector(dataTaskWithRequest:completionHandler:), cls, @selector(hook_dataTaskWithRequest:completionHandler:),NO);
複製程式碼
然後,在我們hook的方法中
- (NSURLSessionDataTask *)hook_dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler {
NSLog(@"33");
void (^customBlock)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (completionHandler) {
completionHandler(data,response,error);
}
//做自己的處理
};
if (completionHandler) {
return [self hook_dataTaskWithRequest:request completionHandler:customBlock];
} else {
return [self hook_dataTaskWithRequest:request completionHandler:nil];
}
}
複製程式碼
注意 這裡需要判斷當前的block是否存在,因為當我們將這個方法hook了以後,如果是當前的session是需要通過delegate來進行網路回撥的,但是請求還是會走到我們hook的方法中,因為在session內部實現,我猜測應該是做了類似工廠方法的處理
所以這裡判斷如果block回撥為空的時候,直接將nil傳進去,這樣就能夠通過delegate拿到回撥結果了
這裡就簡單舉了一個帶block引數的hook 其他的方法處理方式也是類似的,這裡就不再一一列舉了
這一篇主要講的是hook系統的預設的http的請求方法,因為NSURLConnection已經廢棄了,所以就沒有做這個的hook,不過實現方式也是類似的
下一篇,我們將講一下socket的hook,然後就再到view的圈選等等,這個系列會將無埋的一些主要的處理方式都分享出來。
另外:之前做這個hook的方式之前,也使用過NSURLProtocol來進行一些網路處理的攔截,但是因為涉及到多protocol的問題,因為目前專案中已經使用到了多個protocol,所以這種方式就拋棄了。而且,根據之前做的處理,NSURLProtocol要做的工作也不比這個少,所以就採用了AOP的方式
原始碼地址:github.com/yangqian111…
歡迎關注:
聯絡我:
ppsheep.qian@gmail.com