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

網絡層的數據的收集

網絡層的數據,通常要收集的是API的請求頻率、API請求時間、成功率等等信息。若是經過無埋的方式收集網絡信息,確定是經過AOP的方式,hook相應的方法和相應的delegate方法,來實現這一需求。git

針對NSURLSession進行網絡數據的抓取

首先來分析一下經過NSURLSession發起的網絡請求的流程:NSURLSession實際發起網絡請求,是根據響應生成的[task resume]來開始網絡請求的。github

而後NSURLSession提供了兩種方式來對請求的回調進行處理,一種是經過delegate來進行處理,還有一種就是經過block的方式,直接回調請求結果。cookie

delegate回調方式

經過delegate回調方式來進行網絡請求回調的處理,AFNetWorking經過NSURLSession發起的網絡請求就是經過delegate來處理的,只是對外暴露的咱們常用的是block的方式。網絡

看一下NSURLSession的初始化方式和設置delegate的方式session

+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
複製代碼

提供的是兩個類構造器,從上面兩個構造的參數,咱們可以猜出來,其實sessionWithConfiguration:最終也是調用sessionWithConfiguration:delegate:delegateQueu:方法,來初始化。通常咱們把sessionWithConfiguration:delegate:delegateQueu:叫作工廠類方法。app

還有一個方法,咱們也常常用來獲取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也是同樣的。socket

爲啥要說這麼多,由於咱們須要在session初始化的時候,作hook delegate的操做,由於NSURLSession的delegate是一個只讀的屬性,咱們只能在初始化的時候來作hook處理ui

@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
複製代碼

hook delegate

首先考慮一下,咱們有三個方法可以獲取到session的實例,其實真正有delegate的只有一個構造方法,其餘兩個方法都沒有delegate,那怎麼作呢?url

沒有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

相關文章
相關標籤/搜索