無埋點SDK實現方案(一)— 網路篇(NSURLSession)

ppsheep發表於2019-03-03

網路層的資料的收集

網路層的資料,一般要收集的是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…

歡迎關注:

無埋點SDK實現方案(一)— 網路篇(NSURLSession)

聯絡我: ppsheep.qian@gmail.com

相關文章